From 82dc2cbe9fec3e0b7dcdd516b4b0b12aae371eb2 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 10 Dec 2024 12:10:01 +0100 Subject: [PATCH 01/11] feat(grpc)!: enable celestia-grpc usage within wasm --- .github/workflows/ci.yml | 3 + Cargo.lock | 79 +++++++++++++++++++++------ ci/Dockerfile.grpcwebproxy | 10 ++++ ci/credentials/.gitignore | 5 +- ci/credentials/bridge-0.addr | 1 + ci/credentials/bridge-0.key | 9 +++ ci/credentials/bridge-0.plaintext-key | 1 + ci/docker-compose.yml | 10 ++++ grpc/Cargo.toml | 16 +++++- grpc/grpc-macros/src/lib.rs | 6 +- grpc/src/client.rs | 54 ++++++++++++------ grpc/src/lib.rs | 1 - grpc/tests/tonic.rs | 40 ++++++++------ grpc/tests/utils/mod.rs | 78 +++++++++++--------------- proto/Cargo.toml | 4 -- proto/build.rs | 1 - 16 files changed, 208 insertions(+), 110 deletions(-) create mode 100644 ci/Dockerfile.grpcwebproxy create mode 100644 ci/credentials/bridge-0.addr create mode 100644 ci/credentials/bridge-0.key create mode 100644 ci/credentials/bridge-0.plaintext-key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b58e09f..7fd337a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,9 @@ jobs: - name: Run rpc wasm test run: wasm-pack test --headless --chrome rpc --features=wasm-bindgen + - name: Run grpc wasm test + run: wasm-pack test --headless --chrome grpc + - name: Test node-wasm crate # We're running node-wasm tests in release mode to get around a failing debug assertion # https://github.com/libp2p/rust-libp2p/issues/5618 diff --git a/Cargo.lock b/Cargo.lock index 7d42555e..b2f1335f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,11 +751,15 @@ name = "celestia-grpc" version = "0.1.0" dependencies = [ "anyhow", + "bytes", "celestia-grpc-macros", "celestia-proto", "celestia-types", "dotenvy", + "getrandom", + "gloo-timers 0.3.0", "hex", + "http-body 1.0.0", "k256", "prost", "serde", @@ -764,6 +768,8 @@ dependencies = [ "thiserror", "tokio", "tonic", + "tonic-web-wasm-client", + "wasm-bindgen-test", ] [[package]] @@ -2477,10 +2483,11 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -5687,6 +5694,31 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tonic-web-wasm-client" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5ca6e7bdd0042c440d36b6df97c1436f1d45871ce18298091f114004b1beb4" +dependencies = [ + "base64", + "byteorder", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "httparse", + "js-sys", + "pin-project", + "thiserror", + "tonic", + "tower-service", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "tower" version = "0.4.13" @@ -5993,9 +6025,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -6004,13 +6036,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.87", @@ -6019,21 +6050,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6041,9 +6073,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -6054,9 +6086,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-bindgen-test" @@ -6084,11 +6116,24 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/ci/Dockerfile.grpcwebproxy b/ci/Dockerfile.grpcwebproxy new file mode 100644 index 00000000..c623e4d4 --- /dev/null +++ b/ci/Dockerfile.grpcwebproxy @@ -0,0 +1,10 @@ +FROM docker.io/alpine:3.19.1 + +RUN apk update && apk add --no-cache wget unzip + +RUN wget -O grpcwebproxy.zip https://github.com/improbable-eng/grpc-web/releases/download/v0.15.0/grpcwebproxy-v0.15.0-linux-x86_64.zip && \ + unzip grpcwebproxy.zip && \ + mv dist/grpcwebproxy* /usr/local/bin/grpcwebproxy && \ + rm grpcwebproxy.zip + +ENTRYPOINT ["/usr/local/bin/grpcwebproxy"] diff --git a/ci/credentials/.gitignore b/ci/credentials/.gitignore index a68d087b..2f542319 100644 --- a/ci/credentials/.gitignore +++ b/ci/credentials/.gitignore @@ -1,2 +1,3 @@ -/* -!/.gitignore +* +!/bridge-0.* +/bridge-0.jwt diff --git a/ci/credentials/bridge-0.addr b/ci/credentials/bridge-0.addr new file mode 100644 index 00000000..340fdd62 --- /dev/null +++ b/ci/credentials/bridge-0.addr @@ -0,0 +1 @@ +celestia1t52q7uqgnjfzdh3wx5m5phvma3umrq8k6tq2p9 diff --git a/ci/credentials/bridge-0.key b/ci/credentials/bridge-0.key new file mode 100644 index 00000000..f2ec8840 --- /dev/null +++ b/ci/credentials/bridge-0.key @@ -0,0 +1,9 @@ +-----BEGIN TENDERMINT PRIVATE KEY----- +type: secp256k1 +kdf: bcrypt +salt: 2D070635EBDE45AD8845CE82FB6D5A89 + +PboW9MooV09RX733cy55wuciTKhveZdY2H5NhJ0DIhfHxfyR11viqxy4wJ917rkG +OfsQph8JPYp315ZRYq7vUIsbTreMgnlRSdqPmL0= +=SLpn +-----END TENDERMINT PRIVATE KEY----- diff --git a/ci/credentials/bridge-0.plaintext-key b/ci/credentials/bridge-0.plaintext-key new file mode 100644 index 00000000..81e9bfdb --- /dev/null +++ b/ci/credentials/bridge-0.plaintext-key @@ -0,0 +1 @@ +393fdb5def075819de55756b45c9e2c8531a8c78dd6eede483d3440e9457d839 diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 19fe82a7..d8ae5ea4 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -14,6 +14,16 @@ services: - credentials:/credentials - genesis:/genesis + grpcwebproxy: + image: grpcwebproxy + platform: "linux/amd64" + build: + context: . + dockerfile: Dockerfile.grpcwebproxy + command: --backend_addr=validator:9090 --run_tls_server=false --allow_all_origins + ports: + - 18080:8080 + bridge-0: image: bridge platform: "linux/amd64" diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index b2504b87..65809faa 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -26,7 +26,9 @@ prost.workspace = true tendermint-proto.workspace = true tendermint.workspace = true +bytes = "1.8" hex = "0.4.3" +http-body = "1" k256 = "0.13.4" serde = "1.0.215" thiserror = "1.0.61" @@ -35,7 +37,19 @@ tonic = { version = "0.12.3", default-features = false, features = [ ]} [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tonic = { version = "0.12.3", default-features = false, features = [ "transport" ] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2.15", features = ["js"] } +tonic-web-wasm-client = "0.6" + +[dev-dependencies] anyhow = "1.0.86" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] dotenvy = "0.15.7" tokio = { version = "1.38.0", features = ["rt", "macros"] } -tonic = { version = "0.12.3", optional = true, default-features = false, features = [ "transport" ] } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +gloo-timers = { version = "0.3.0", features = ["futures"] } +wasm-bindgen-test = "0.3.42" diff --git a/grpc/grpc-macros/src/lib.rs b/grpc/grpc-macros/src/lib.rs index 1f648dfa..ebe93b94 100644 --- a/grpc/grpc-macros/src/lib.rs +++ b/grpc/grpc-macros/src/lib.rs @@ -58,10 +58,10 @@ impl GrpcMethod { let method = quote! { #doc_hash #doc_group pub #signature { - let mut client = #grpc_client_struct :: with_interceptor( - self.grpc_channel.clone(), - self.auth_interceptor.clone(), + let mut client = #grpc_client_struct :: new( + self.transport.clone(), ); + let request = ::tonic::Request::new(( #( #params ),* ).into_parameter()); let response = client. #grpc_method_name (request).await; response?.into_inner().try_from_response() diff --git a/grpc/src/client.rs b/grpc/src/client.rs index 80ce1a4b..550bb756 100644 --- a/grpc/src/client.rs +++ b/grpc/src/client.rs @@ -1,7 +1,4 @@ -use prost::Message; -use tonic::service::Interceptor; -use tonic::transport::Channel; - +use bytes::Bytes; use celestia_grpc_macros::grpc_method; use celestia_proto::celestia::blob::v1::query_client::QueryClient as BlobQueryClient; use celestia_proto::cosmos::auth::v1beta1::query_client::QueryClient as AuthQueryClient; @@ -13,6 +10,10 @@ use celestia_types::blob::{Blob, BlobParams, RawBlobTx}; use celestia_types::block::Block; use celestia_types::state::auth::AuthParams; use celestia_types::state::{Address, TxResponse}; +use http_body::Body; +use prost::Message; +use tonic::body::BoxBody; +use tonic::client::GrpcService; use crate::types::auth::Account; use crate::types::tx::GetTxResponse; @@ -21,25 +22,23 @@ use crate::Error; pub use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; +type StdError = Box; + /// Struct wrapping all the tonic types and doing type conversion behind the scenes. -pub struct GrpcClient -where - I: Interceptor, -{ - grpc_channel: Channel, - auth_interceptor: I, +pub struct GrpcClient { + transport: T, } -impl GrpcClient +impl GrpcClient where - I: Interceptor + Clone, + T: GrpcService + Clone, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, { /// Create a new client out of channel and optional auth - pub fn new(grpc_channel: Channel, auth_interceptor: I) -> Self { - Self { - grpc_channel, - auth_interceptor, - } + pub fn new(transport: T) -> Self { + Self { transport } } /// Get Minimum Gas price @@ -107,3 +106,24 @@ where #[grpc_method(TxServiceClient::get_tx)] async fn get_tx(&mut self, hash: String) -> Result; } + +#[cfg(not(target_arch = "wasm32"))] +impl GrpcClient { + /// Create a new client connected to the given `url` with default + /// settings of [`tonic::transport::Channel`]. + pub fn with_url(url: impl Into) -> Result { + let channel = tonic::transport::Endpoint::from_shared(url.into())?.connect_lazy(); + Ok(Self { transport: channel }) + } +} + +#[cfg(target_arch = "wasm32")] +impl GrpcClient { + /// Create a new client connected to the given `url` with default + /// settings of [`tonic_web_wasm_client::Client`]. + pub fn with_grpcweb_url(url: impl Into) -> Self { + Self { + transport: tonic_web_wasm_client::Client::new(url.into()), + } + } +} diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index 9e68cbfa..5a8484ca 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -1,5 +1,4 @@ #![doc = include_str!("../README.md")] -#![cfg(not(target_arch = "wasm32"))] mod client; mod error; diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 07d1387a..cc1226d6 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,4 +1,4 @@ -#![cfg(not(target_arch = "wasm32"))] +use std::time::Duration; use celestia_grpc::types::auth::Account; use celestia_grpc::types::tx::sign_tx; @@ -9,28 +9,34 @@ use celestia_types::{AppVersion, Blob}; pub mod utils; -use crate::utils::{load_account, new_test_client}; +use crate::utils::{load_account, new_test_client, sleep}; -const BRIDGE_0_ACCOUNT_DATA: &str = "../ci/credentials/bridge-0"; +#[cfg(not(target_arch = "wasm32"))] +use tokio::test as async_test; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test as async_test; -#[tokio::test] +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[async_test] async fn get_min_gas_price() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); let gas_price = client.get_min_gas_price().await.unwrap(); assert!(gas_price > 0.0); } -#[tokio::test] +#[async_test] async fn get_blob_params() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); let params = client.get_blob_params().await.unwrap(); assert!(params.gas_per_blob_byte > 0); assert!(params.gov_max_square_size > 0); } -#[tokio::test] +#[async_test] async fn get_auth_params() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); let params = client.get_auth_params().await.unwrap(); assert!(params.max_memo_characters > 0); assert!(params.tx_sig_limit > 0); @@ -39,9 +45,9 @@ async fn get_auth_params() { assert!(params.sig_verify_cost_secp256k1 > 0); } -#[tokio::test] +#[async_test] async fn get_block() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); let latest_block = client.get_latest_block().await.unwrap(); let height = latest_block.header.height.value() as i64; @@ -50,9 +56,9 @@ async fn get_block() { assert_eq!(block.header, latest_block.header); } -#[tokio::test] +#[async_test] async fn get_account() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); let accounts = client.get_accounts().await.unwrap(); @@ -68,11 +74,11 @@ async fn get_account() { assert_eq!(&account, first_account); } -#[tokio::test] +#[async_test] async fn submit_blob() { - let mut client = new_test_client().await.unwrap(); + let mut client = new_test_client().unwrap(); - let account_credentials = load_account(BRIDGE_0_ACCOUNT_DATA); + let account_credentials = load_account(); let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); let blobs = vec![Blob::new(namespace, "Hello, World!".into(), AppVersion::V3).unwrap()]; let chain_id = "private".to_string(); @@ -101,7 +107,7 @@ async fn submit_blob() { .await .unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; + sleep(Duration::from_secs(3)).await; let _submitted_tx = client .get_tx(response.txhash) diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 0ac2acdf..5b660df9 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -1,19 +1,15 @@ -#![cfg(not(target_arch = "wasm32"))] - -use std::{env, fs}; +use std::{env, time::Duration}; use anyhow::Result; -use tonic::metadata::{Ascii, MetadataValue}; -use tonic::service::Interceptor; -use tonic::transport::Channel; -use tonic::{Request, Status}; - use celestia_grpc::GrpcClient; use celestia_types::state::Address; use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; use tendermint::public_key::Secp256k1 as VerifyingKey; +#[cfg(not(target_arch = "wasm32"))] const CELESTIA_GRPC_URL: &str = "http://localhost:19090"; +#[cfg(target_arch = "wasm32")] +const CELESTIA_GRPCWEB_PROXY_URL: &str = "http://localhost:18080"; /// [`TestAccount`] stores celestia account credentials and information, for cases where we don't /// mind jusk keeping the plaintext secret key in memory @@ -27,58 +23,46 @@ pub struct TestAccount { pub signing_key: SigningKey, } -// -#[derive(Clone)] -pub struct TestAuthInterceptor { - token: Option>, -} - -impl Interceptor for TestAuthInterceptor { - fn call(&mut self, mut request: Request<()>) -> Result, Status> { - if let Some(token) = &self.token { - request - .metadata_mut() - .insert("authorization", token.clone()); - } - Ok(request) - } -} - -impl TestAuthInterceptor { - pub fn new(bearer_token: Option) -> Result { - let token = bearer_token.map(|token| token.parse()).transpose()?; - Ok(Self { token }) - } -} - -pub fn env_or(var_name: &str, or_value: &str) -> String { +fn env_or(var_name: &str, or_value: &str) -> String { env::var(var_name).unwrap_or_else(|_| or_value.to_owned()) } -pub async fn new_test_client() -> Result> { +#[cfg(not(target_arch = "wasm32"))] +pub fn new_test_client() -> Result> { let _ = dotenvy::dotenv(); let url = env_or("CELESTIA_GRPC_URL", CELESTIA_GRPC_URL); - let grpc_channel = Channel::from_shared(url)?.connect().await?; - let auth_interceptor = TestAuthInterceptor::new(None)?; - Ok(GrpcClient::new(grpc_channel, auth_interceptor)) + Ok(GrpcClient::with_url(url)?) } -pub fn load_account(path: &str) -> TestAccount { - let account_file = format!("{path}.addr"); - let key_file = format!("{path}.plaintext-key"); +#[cfg(target_arch = "wasm32")] +pub fn new_test_client() -> Result> { + Ok(GrpcClient::with_grpcweb_url(CELESTIA_GRPCWEB_PROXY_URL)) +} - let account = fs::read_to_string(account_file).expect("file with account name to exists"); - let hex_encoded_key = fs::read_to_string(key_file).expect("file with plaintext key to exists"); +pub fn load_account() -> TestAccount { + let address = include_str!("../../../ci/credentials/bridge-0.addr"); + let hex_key = include_str!("../../../ci/credentials/bridge-0.plaintext-key"); - let signing_key = SigningKey::from_slice( - &hex::decode(hex_encoded_key.trim()).expect("valid hex representation"), - ) - .expect("valid key material"); + let signing_key = + SigningKey::from_slice(&hex::decode(hex_key.trim()).expect("valid hex representation")) + .expect("valid key material"); TestAccount { - address: account.trim().parse().expect("valid address"), + address: address.trim().parse().expect("valid address"), verifying_key: *signing_key.verifying_key(), signing_key, } } + +#[cfg(not(target_arch = "wasm32"))] +pub async fn sleep(duration: Duration) { + tokio::time::sleep(duration).await; +} + +#[cfg(target_arch = "wasm32")] +pub async fn sleep(duration: Duration) { + let millis = u32::try_from(duration.as_millis().max(1)).unwrap_or(u32::MAX); + let delay = gloo_timers::future::TimeoutFuture::new(millis); + delay.await; +} diff --git a/proto/Cargo.toml b/proto/Cargo.toml index bb8d735b..3cdb3c12 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -30,10 +30,6 @@ prost-types.workspace = true protox = "0.7.1" tonic-build = { version = "0.12.3", default-features = false, optional = true, features = [ "prost" ]} -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tonic = { version = "0.12.3", optional = true, default-features = false, features = [ "transport" ] } -tonic-build = { version = "0.12.3", optional = true, default-features = false, features = ["transport"] } - [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.42" diff --git a/proto/build.rs b/proto/build.rs index 77980156..77b101de 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -194,7 +194,6 @@ fn tonic_build(fds: FileDescriptorSet) { .include_file("mod.rs") .build_client(true) .build_server(false) - .client_mod_attribute(".", "#[cfg(not(target_arch=\"wasm32\"))]") .use_arc_self(true) .compile_well_known_types(true) .skip_protoc_run() From 99b3af1b9863f3d85b93c688a64ccd884c64fe4f Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 10 Dec 2024 15:19:01 +0100 Subject: [PATCH 02/11] remove env_or helper --- grpc/tests/utils/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 5b660df9..6ec320a2 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -23,14 +23,10 @@ pub struct TestAccount { pub signing_key: SigningKey, } -fn env_or(var_name: &str, or_value: &str) -> String { - env::var(var_name).unwrap_or_else(|_| or_value.to_owned()) -} - #[cfg(not(target_arch = "wasm32"))] pub fn new_test_client() -> Result> { let _ = dotenvy::dotenv(); - let url = env_or("CELESTIA_GRPC_URL", CELESTIA_GRPC_URL); + let url = std::env::var("CELESTIA_GRPC_URL").unwrap_or_else(|_| CELESTIA_GRPC_URL.into()); Ok(GrpcClient::with_url(url)?) } From e83598da88a3e8010ea471b704c22fce9734d909 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 10 Dec 2024 15:35:57 +0100 Subject: [PATCH 03/11] remove unused import --- grpc/tests/utils/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 6ec320a2..32a45b4b 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -1,4 +1,4 @@ -use std::{env, time::Duration}; +use std::time::Duration; use anyhow::Result; use celestia_grpc::GrpcClient; From 5cebaf8936296246b679e99a85345bd0bf5155dc Mon Sep 17 00:00:00 2001 From: zvolin Date: Wed, 11 Dec 2024 16:46:42 +0100 Subject: [PATCH 04/11] remove anyhow in tests --- Cargo.lock | 1 - grpc/Cargo.toml | 3 --- grpc/tests/tonic.rs | 12 ++++++------ grpc/tests/utils/mod.rs | 9 ++++----- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2f1335f..9c6cb141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,6 @@ checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" name = "celestia-grpc" version = "0.1.0" dependencies = [ - "anyhow", "bytes", "celestia-grpc-macros", "celestia-proto", diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 65809faa..02172ad8 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -43,9 +43,6 @@ tonic = { version = "0.12.3", default-features = false, features = [ "transport" getrandom = { version = "0.2.15", features = ["js"] } tonic-web-wasm-client = "0.6" -[dev-dependencies] -anyhow = "1.0.86" - [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] dotenvy = "0.15.7" tokio = { version = "1.38.0", features = ["rt", "macros"] } diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index cc1226d6..61f76a37 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -21,14 +21,14 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[async_test] async fn get_min_gas_price() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let gas_price = client.get_min_gas_price().await.unwrap(); assert!(gas_price > 0.0); } #[async_test] async fn get_blob_params() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let params = client.get_blob_params().await.unwrap(); assert!(params.gas_per_blob_byte > 0); assert!(params.gov_max_square_size > 0); @@ -36,7 +36,7 @@ async fn get_blob_params() { #[async_test] async fn get_auth_params() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let params = client.get_auth_params().await.unwrap(); assert!(params.max_memo_characters > 0); assert!(params.tx_sig_limit > 0); @@ -47,7 +47,7 @@ async fn get_auth_params() { #[async_test] async fn get_block() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let latest_block = client.get_latest_block().await.unwrap(); let height = latest_block.header.height.value() as i64; @@ -58,7 +58,7 @@ async fn get_block() { #[async_test] async fn get_account() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let accounts = client.get_accounts().await.unwrap(); @@ -76,7 +76,7 @@ async fn get_account() { #[async_test] async fn submit_blob() { - let mut client = new_test_client().unwrap(); + let mut client = new_test_client(); let account_credentials = load_account(); let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 32a45b4b..9aca9770 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use anyhow::Result; use celestia_grpc::GrpcClient; use celestia_types::state::Address; use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; @@ -24,16 +23,16 @@ pub struct TestAccount { } #[cfg(not(target_arch = "wasm32"))] -pub fn new_test_client() -> Result> { +pub fn new_test_client() -> GrpcClient { let _ = dotenvy::dotenv(); let url = std::env::var("CELESTIA_GRPC_URL").unwrap_or_else(|_| CELESTIA_GRPC_URL.into()); - Ok(GrpcClient::with_url(url)?) + GrpcClient::with_url(url).expect("creating client failed") } #[cfg(target_arch = "wasm32")] -pub fn new_test_client() -> Result> { - Ok(GrpcClient::with_grpcweb_url(CELESTIA_GRPCWEB_PROXY_URL)) +pub fn new_test_client() -> GrpcClient { + GrpcClient::with_grpcweb_url(CELESTIA_GRPCWEB_PROXY_URL) } pub fn load_account() -> TestAccount { From 7079948827b46e566afc7ac5994237135578bd76 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 13 Dec 2024 12:06:03 +0100 Subject: [PATCH 05/11] add transaction client --- Cargo.lock | 37 +- grpc/Cargo.toml | 9 +- grpc/grpc-macros/src/lib.rs | 5 +- grpc/src/client.rs | 129 ---- grpc/src/error.rs | 35 +- grpc/src/grpc.rs | 184 ++++++ grpc/src/{types => grpc}/auth.rs | 33 +- grpc/src/grpc/bank.rs | 84 +++ grpc/src/grpc/blob.rs | 19 + grpc/src/grpc/celestia_tx.rs | 96 +++ grpc/src/grpc/cosmos_tx.rs | 91 +++ grpc/src/grpc/node.rs | 22 + grpc/src/grpc/tendermint.rs | 28 + grpc/src/lib.rs | 8 +- grpc/src/tx.rs | 564 ++++++++++++++++++ grpc/src/types.rs | 87 --- grpc/src/types/tx.rs | 130 ---- grpc/src/utils.rs | 49 ++ grpc/tests/tonic.rs | 269 +++++++-- grpc/tests/utils/mod.rs | 114 +++- proto/build.rs | 5 + proto/vendor/cosmos/bank/v1beta1/authz.proto | 19 + proto/vendor/cosmos/bank/v1beta1/bank.proto | 108 ++++ .../vendor/cosmos/bank/v1beta1/genesis.proto | 40 ++ proto/vendor/cosmos/bank/v1beta1/query.proto | 243 ++++++++ proto/vendor/cosmos/bank/v1beta1/tx.proto | 48 ++ proto/vendor/cosmos/msg/v1/msg.proto | 22 + tools/update-proto-vendor.sh | 2 +- types/Cargo.toml | 5 +- types/src/blob.rs | 27 + types/src/blob/msg_pay_for_blobs.rs | 20 +- types/src/consts.rs | 14 +- types/src/state.rs | 4 +- types/src/state/auth.rs | 9 +- types/src/state/tx.rs | 214 ++++++- 35 files changed, 2256 insertions(+), 517 deletions(-) delete mode 100644 grpc/src/client.rs create mode 100644 grpc/src/grpc.rs rename grpc/src/{types => grpc}/auth.rs (76%) create mode 100644 grpc/src/grpc/bank.rs create mode 100644 grpc/src/grpc/blob.rs create mode 100644 grpc/src/grpc/celestia_tx.rs create mode 100644 grpc/src/grpc/cosmos_tx.rs create mode 100644 grpc/src/grpc/node.rs create mode 100644 grpc/src/grpc/tendermint.rs create mode 100644 grpc/src/tx.rs delete mode 100644 grpc/src/types.rs delete mode 100644 grpc/src/types/tx.rs create mode 100644 grpc/src/utils.rs create mode 100644 proto/vendor/cosmos/bank/v1beta1/authz.proto create mode 100644 proto/vendor/cosmos/bank/v1beta1/bank.proto create mode 100644 proto/vendor/cosmos/bank/v1beta1/genesis.proto create mode 100644 proto/vendor/cosmos/bank/v1beta1/query.proto create mode 100644 proto/vendor/cosmos/bank/v1beta1/tx.proto create mode 100644 proto/vendor/cosmos/msg/v1/msg.proto diff --git a/Cargo.lock b/Cargo.lock index 9c6cb141..20e69024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,6 +761,9 @@ dependencies = [ "http-body 1.0.0", "k256", "prost", + "rand_core", + "regex", + "send_wrapper 0.6.0", "serde", "tendermint", "tendermint-proto", @@ -835,6 +838,7 @@ dependencies = [ "const_format", "ed25519-consensus", "enum_dispatch", + "enumn", "getrandom", "indoc", "leopard-codec", @@ -1431,6 +1435,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -3323,7 +3338,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "syn 2.0.87", ] @@ -4243,7 +4258,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "unarray", ] @@ -4513,14 +4528,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -4534,13 +4549,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4551,9 +4566,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 02172ad8..1c8f2554 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -30,23 +30,30 @@ bytes = "1.8" hex = "0.4.3" http-body = "1" k256 = "0.13.4" +regex = { version = "1.11", default-features = false } serde = "1.0.215" thiserror = "1.0.61" +tokio = { version = "1.38.0", features = ["sync"] } tonic = { version = "0.12.3", default-features = false, features = [ "codegen", "prost" ]} [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.38.0", features = ["time"] } tonic = { version = "0.12.3", default-features = false, features = [ "transport" ] } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.15", features = ["js"] } +gloo-timers = { version = "0.3.0", features = ["futures"] } +send_wrapper = { version = "0.6.0", features = ["futures"] } tonic-web-wasm-client = "0.6" +[dev-dependencies] +rand_core = "0.6.4" + [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] dotenvy = "0.15.7" tokio = { version = "1.38.0", features = ["rt", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -gloo-timers = { version = "0.3.0", features = ["futures"] } wasm-bindgen-test = "0.3.42" diff --git a/grpc/grpc-macros/src/lib.rs b/grpc/grpc-macros/src/lib.rs index ebe93b94..5640d2af 100644 --- a/grpc/grpc-macros/src/lib.rs +++ b/grpc/grpc-macros/src/lib.rs @@ -62,9 +62,10 @@ impl GrpcMethod { self.transport.clone(), ); - let request = ::tonic::Request::new(( #( #params ),* ).into_parameter()); + let param = crate::grpc::IntoGrpcParam::into_parameter(( #( #params ),* )); + let request = ::tonic::Request::new(param); let response = client. #grpc_method_name (request).await; - response?.into_inner().try_from_response() + crate::grpc::FromGrpcResponse::try_from_response(response?.into_inner()) } }; diff --git a/grpc/src/client.rs b/grpc/src/client.rs deleted file mode 100644 index 550bb756..00000000 --- a/grpc/src/client.rs +++ /dev/null @@ -1,129 +0,0 @@ -use bytes::Bytes; -use celestia_grpc_macros::grpc_method; -use celestia_proto::celestia::blob::v1::query_client::QueryClient as BlobQueryClient; -use celestia_proto::cosmos::auth::v1beta1::query_client::QueryClient as AuthQueryClient; -use celestia_proto::cosmos::base::node::v1beta1::service_client::ServiceClient as ConfigServiceClient; -use celestia_proto::cosmos::base::tendermint::v1beta1::service_client::ServiceClient as TendermintServiceClient; -use celestia_proto::cosmos::tx::v1beta1::service_client::ServiceClient as TxServiceClient; -use celestia_proto::cosmos::tx::v1beta1::Tx as RawTx; -use celestia_types::blob::{Blob, BlobParams, RawBlobTx}; -use celestia_types::block::Block; -use celestia_types::state::auth::AuthParams; -use celestia_types::state::{Address, TxResponse}; -use http_body::Body; -use prost::Message; -use tonic::body::BoxBody; -use tonic::client::GrpcService; - -use crate::types::auth::Account; -use crate::types::tx::GetTxResponse; -use crate::types::{FromGrpcResponse, IntoGrpcParam}; -use crate::Error; - -pub use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; - -type StdError = Box; - -/// Struct wrapping all the tonic types and doing type conversion behind the scenes. -pub struct GrpcClient { - transport: T, -} - -impl GrpcClient -where - T: GrpcService + Clone, - T::Error: Into, - T::ResponseBody: Body + Send + 'static, - ::Error: Into + Send, -{ - /// Create a new client out of channel and optional auth - pub fn new(transport: T) -> Self { - Self { transport } - } - - /// Get Minimum Gas price - #[grpc_method(ConfigServiceClient::config)] - async fn get_min_gas_price(&mut self) -> Result; - - /// Get latest block - #[grpc_method(TendermintServiceClient::get_latest_block)] - async fn get_latest_block(&mut self) -> Result; - - /// Get block by height - #[grpc_method(TendermintServiceClient::get_block_by_height)] - async fn get_block_by_height(&mut self, height: i64) -> Result; - - /// Get blob params - #[grpc_method(BlobQueryClient::params)] - async fn get_blob_params(&mut self) -> Result; - - /// Get auth params - #[grpc_method(AuthQueryClient::params)] - async fn get_auth_params(&mut self) -> Result; - - /// Get account - #[grpc_method(AuthQueryClient::account)] - async fn get_account(&mut self, account: &Address) -> Result; - - // TODO: pagination? - /// Get accounts - #[grpc_method(AuthQueryClient::accounts)] - async fn get_accounts(&mut self) -> Result, Error>; - - /// Broadcast prepared and serialised transaction - #[grpc_method(TxServiceClient::broadcast_tx)] - async fn broadcast_tx( - &mut self, - tx_bytes: Vec, - mode: BroadcastMode, - ) -> Result; - - /// Broadcast blob transaction - pub async fn broadcast_blob_tx( - &mut self, - tx: RawTx, - blobs: Vec, - mode: BroadcastMode, - ) -> Result { - // From https://github.com/celestiaorg/celestia-core/blob/v1.43.0-tm-v0.34.35/pkg/consts/consts.go#L19 - const BLOB_TX_TYPE_ID: &str = "BLOB"; - - if blobs.is_empty() { - return Err(Error::TxEmptyBlobList); - } - - let blobs = blobs.into_iter().map(Into::into).collect(); - let blob_tx = RawBlobTx { - tx: tx.encode_to_vec(), - blobs, - type_id: BLOB_TX_TYPE_ID.to_string(), - }; - - self.broadcast_tx(blob_tx.encode_to_vec(), mode).await - } - - /// Get Tx - #[grpc_method(TxServiceClient::get_tx)] - async fn get_tx(&mut self, hash: String) -> Result; -} - -#[cfg(not(target_arch = "wasm32"))] -impl GrpcClient { - /// Create a new client connected to the given `url` with default - /// settings of [`tonic::transport::Channel`]. - pub fn with_url(url: impl Into) -> Result { - let channel = tonic::transport::Endpoint::from_shared(url.into())?.connect_lazy(); - Ok(Self { transport: channel }) - } -} - -#[cfg(target_arch = "wasm32")] -impl GrpcClient { - /// Create a new client connected to the given `url` with default - /// settings of [`tonic_web_wasm_client::Client`]. - pub fn with_grpcweb_url(url: impl Into) -> Self { - Self { - transport: tonic_web_wasm_client::Client::new(url.into()), - } - } -} diff --git a/grpc/src/error.rs b/grpc/src/error.rs index a3f35cb7..39b4cd82 100644 --- a/grpc/src/error.rs +++ b/grpc/src/error.rs @@ -1,9 +1,10 @@ +use celestia_types::{hash::Hash, state::ErrorCode}; use tonic::Status; /// Alias for a `Result` with the error type [`celestia_tonic::Error`]. /// /// [`celestia_tonic::Error`]: crate::Error -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// Representation of all the errors that can occur when interacting with [`celestia_tonic`]. /// @@ -37,4 +38,36 @@ pub enum Error { /// Empty blob submission list #[error("Attempted to submit blob transaction with empty blob list")] TxEmptyBlobList, + + /// Broadcasting transaction failed + #[error("Broadcasting transaction {0} failed; code: {1}, error: {2}, gas limit: {3}")] + TxBroadcastFailed(Hash, ErrorCode, String, u64), + + /// Executing transaction failed + #[error("Transaction {0} execution failed; code: {1}, error: {2}")] + TxExecutionFailed(Hash, ErrorCode, String), + + /// Transaction was evicted from the mempool + #[error("Transaction {0} was evicted from the mempool")] + TxEvicted(Hash), + + /// Transaction wasn't found, it was likely rejected + #[error("Transaction {0} wasn't found, it was likely rejected")] + TxNotFound(Hash), + + /// Unsupported key algorithm + #[error("Key algorithm not supported")] + KeyAlgorithmNotSupported, + + /// Provided public key differs from one associated with account + #[error("Provided public key differs from one associated with account")] + PublicKeyMismatch, + + /// Public key not found in account and not provided + #[error("Public key not found in account and not provided")] + PublicKeyMissing, + + /// Updating gas price failed + #[error("Updating gas price failed: {0}")] + UpdatingGasPriceFailed(String), } diff --git a/grpc/src/grpc.rs b/grpc/src/grpc.rs new file mode 100644 index 00000000..cb243245 --- /dev/null +++ b/grpc/src/grpc.rs @@ -0,0 +1,184 @@ +//! Types and client for the celestia grpc + +use bytes::Bytes; +use celestia_grpc_macros::grpc_method; +use celestia_proto::celestia::blob::v1::query_client::QueryClient as BlobQueryClient; +use celestia_proto::celestia::core::v1::tx::tx_client::TxClient as TxStatusClient; +use celestia_proto::cosmos::auth::v1beta1::query_client::QueryClient as AuthQueryClient; +use celestia_proto::cosmos::bank::v1beta1::query_client::QueryClient as BankQueryClient; +use celestia_proto::cosmos::base::abci::v1beta1::GasInfo; +use celestia_proto::cosmos::base::node::v1beta1::service_client::ServiceClient as ConfigServiceClient; +use celestia_proto::cosmos::base::tendermint::v1beta1::service_client::ServiceClient as TendermintServiceClient; +use celestia_proto::cosmos::tx::v1beta1::service_client::ServiceClient as TxServiceClient; +use celestia_types::blob::BlobParams; +use celestia_types::block::Block; +use celestia_types::hash::Hash; +use celestia_types::state::auth::AuthParams; +use celestia_types::state::{Address, Coin, TxResponse}; +use http_body::Body; +use tonic::body::BoxBody; +use tonic::client::GrpcService; + +use crate::Result; + +// cosmos.auth +mod auth; +// cosmos.bank +mod bank; +// cosmos.base.node +mod node; +// cosmos.base.tendermint +mod tendermint; +// celestia.core.tx +mod celestia_tx; +// celestia.blob +mod blob; +// cosmos.tx +mod cosmos_tx; + +pub use crate::grpc::auth::Account; +pub use crate::grpc::celestia_tx::{TxStatus, TxStatusResponse}; +pub use crate::grpc::cosmos_tx::{BroadcastMode, GetTxResponse}; + +/// Error convertible to std, used by grpc transports +pub type StdError = Box; + +/// Struct wrapping all the tonic types and doing type conversion behind the scenes. +pub struct GrpcClient { + transport: T, +} + +impl GrpcClient { + /// Get the underlying transport. + pub fn into_inner(self) -> T { + self.transport + } +} + +impl GrpcClient +where + T: GrpcService + Clone, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, +{ + /// Create a new client wrapping given transport + pub fn new(transport: T) -> Self { + Self { transport } + } + + // cosmos.auth + + /// Get auth params + #[grpc_method(AuthQueryClient::params)] + async fn get_auth_params(&self) -> Result; + + /// Get account + #[grpc_method(AuthQueryClient::account)] + async fn get_account(&self, account: &Address) -> Result; + + /// Get accounts + #[grpc_method(AuthQueryClient::accounts)] + async fn get_accounts(&self) -> Result>; + + // cosmos.bank + + /// Get balance of coins with given denom + #[grpc_method(BankQueryClient::balance)] + async fn get_balance(&self, address: &Address, denom: impl Into) -> Result; + + /// Get balance of all coins + #[grpc_method(BankQueryClient::all_balances)] + async fn get_all_balances(&self, address: &Address) -> Result>; + + /// Get balance of all spendable coins + #[grpc_method(BankQueryClient::spendable_balances)] + async fn get_spendable_balances(&self, address: &Address) -> Result>; + + /// Get total supply + #[grpc_method(BankQueryClient::total_supply)] + async fn get_total_supply(&self) -> Result>; + + // cosmos.base.node + + /// Get Minimum Gas price + #[grpc_method(ConfigServiceClient::config)] + async fn get_min_gas_price(&self) -> Result; + + // cosmos.base.tendermint + + /// Get latest block + #[grpc_method(TendermintServiceClient::get_latest_block)] + async fn get_latest_block(&self) -> Result; + + /// Get block by height + #[grpc_method(TendermintServiceClient::get_block_by_height)] + async fn get_block_by_height(&self, height: i64) -> Result; + + // cosmos.tx + + /// Broadcast prepared and serialised transaction + #[grpc_method(TxServiceClient::broadcast_tx)] + async fn broadcast_tx(&self, tx_bytes: Vec, mode: BroadcastMode) -> Result; + + /// Get Tx + #[grpc_method(TxServiceClient::get_tx)] + async fn get_tx(&self, hash: Hash) -> Result; + + /// Broadcast prepared and serialised transaction + #[grpc_method(TxServiceClient::simulate)] + async fn simulate(&self, tx_bytes: Vec) -> Result; + + // celestia.blob + + /// Get blob params + #[grpc_method(BlobQueryClient::params)] + async fn get_blob_params(&self) -> Result; + + // celestia.core.tx + + /// Get status of the transaction + #[grpc_method(TxStatusClient::tx_status)] + async fn tx_status(&self, hash: Hash) -> Result; +} + +#[cfg(not(target_arch = "wasm32"))] +impl GrpcClient { + /// Create a new client connected to the given `url` with default + /// settings of [`tonic::transport::Channel`]. + pub fn with_url(url: impl Into) -> Result { + let channel = tonic::transport::Endpoint::from_shared(url.into())?.connect_lazy(); + Ok(Self { transport: channel }) + } +} + +#[cfg(target_arch = "wasm32")] +impl GrpcClient { + /// Create a new client connected to the given `url` with default + /// settings of [`tonic_web_wasm_client::Client`]. + pub fn with_grpcweb_url(url: impl Into) -> Self { + Self { + transport: tonic_web_wasm_client::Client::new(url.into()), + } + } +} + +pub(crate) trait FromGrpcResponse { + fn try_from_response(self) -> Result; +} + +pub(crate) trait IntoGrpcParam { + fn into_parameter(self) -> T; +} + +macro_rules! make_empty_params { + ($request_type:ident) => { + impl crate::grpc::IntoGrpcParam<$request_type> for () { + fn into_parameter(self) -> $request_type { + $request_type {} + } + } + }; +} + +pub(crate) use make_empty_params; diff --git a/grpc/src/types/auth.rs b/grpc/src/grpc/auth.rs similarity index 76% rename from grpc/src/types/auth.rs rename to grpc/src/grpc/auth.rs index da7aecb8..b8afcf36 100644 --- a/grpc/src/types/auth.rs +++ b/grpc/src/grpc/auth.rs @@ -10,9 +10,8 @@ use celestia_types::state::auth::{ use celestia_types::state::Address; use tendermint_proto::google::protobuf::Any; -use crate::types::make_empty_params; -use crate::types::{FromGrpcResponse, IntoGrpcParam}; -use crate::Error; +use crate::grpc::{make_empty_params, FromGrpcResponse, IntoGrpcParam}; +use crate::{Error, Result}; /// Enum representing different types of account #[derive(Debug, PartialEq)] @@ -23,18 +22,28 @@ pub enum Account { Module(ModuleAccount), } -impl Account { - /// Return [`BaseAccount`] reference, if it exists, from either Base or Module account - pub fn base_account_ref(&self) -> Option<&BaseAccount> { +impl std::ops::Deref for Account { + type Target = BaseAccount; + + fn deref(&self) -> &Self::Target { + match self { + Account::Base(base) => base, + Account::Module(module) => &module.base_account, + } + } +} + +impl std::ops::DerefMut for Account { + fn deref_mut(&mut self) -> &mut Self::Target { match self { - Account::Base(acct) => Some(acct), - Account::Module(acct) => acct.base_account.as_ref(), + Account::Base(base) => base, + Account::Module(module) => &mut module.base_account, } } } impl FromGrpcResponse for QueryAuthParamsResponse { - fn try_from_response(self) -> Result { + fn try_from_response(self) -> Result { let params = self.params.ok_or(Error::FailedToParseResponse)?; Ok(AuthParams { max_memo_characters: params.max_memo_characters, @@ -47,13 +56,13 @@ impl FromGrpcResponse for QueryAuthParamsResponse { } impl FromGrpcResponse for QueryAccountResponse { - fn try_from_response(self) -> Result { + fn try_from_response(self) -> Result { account_from_any(self.account.ok_or(Error::FailedToParseResponse)?) } } impl FromGrpcResponse> for QueryAccountsResponse { - fn try_from_response(self) -> Result, Error> { + fn try_from_response(self) -> Result> { self.accounts.into_iter().map(account_from_any).collect() } } @@ -74,7 +83,7 @@ impl IntoGrpcParam for () { } } -fn account_from_any(any: Any) -> Result { +fn account_from_any(any: Any) -> Result { let account = if any.type_url == RawBaseAccount::type_url() { let base_account = RawBaseAccount::decode(&*any.value).map_err(|_| Error::FailedToParseResponse)?; diff --git a/grpc/src/grpc/bank.rs b/grpc/src/grpc/bank.rs new file mode 100644 index 00000000..7a198943 --- /dev/null +++ b/grpc/src/grpc/bank.rs @@ -0,0 +1,84 @@ +use celestia_proto::cosmos::bank::v1beta1::{ + QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse, + QuerySpendableBalancesRequest, QuerySpendableBalancesResponse, QueryTotalSupplyRequest, + QueryTotalSupplyResponse, +}; +use celestia_types::state::{Address, Coin}; + +use crate::grpc::{FromGrpcResponse, IntoGrpcParam}; +use crate::{Error, Result}; + +impl IntoGrpcParam for (&Address, I) +where + I: Into, +{ + fn into_parameter(self) -> QueryBalanceRequest { + QueryBalanceRequest { + address: self.0.to_string(), + denom: self.1.into(), + } + } +} + +impl FromGrpcResponse for QueryBalanceResponse { + fn try_from_response(self) -> Result { + Ok(self + .balance + .ok_or(Error::FailedToParseResponse)? + .try_into()?) + } +} + +impl IntoGrpcParam for &Address { + fn into_parameter(self) -> QueryAllBalancesRequest { + QueryAllBalancesRequest { + address: self.to_string(), + pagination: None, + } + } +} + +impl FromGrpcResponse> for QueryAllBalancesResponse { + fn try_from_response(self) -> Result> { + Ok(self + .balances + .into_iter() + .map(|coin| coin.try_into()) + .collect::>()?) + } +} + +impl IntoGrpcParam for &Address { + fn into_parameter(self) -> QuerySpendableBalancesRequest { + QuerySpendableBalancesRequest { + address: self.to_string(), + pagination: None, + } + } +} + +impl FromGrpcResponse> for QuerySpendableBalancesResponse { + fn try_from_response(self) -> Result> { + Ok(self + .balances + .into_iter() + .map(|coin| coin.try_into()) + .collect::>()?) + } +} + +impl IntoGrpcParam for () { + fn into_parameter(self) -> QueryTotalSupplyRequest { + QueryTotalSupplyRequest { pagination: None } + } +} + +impl FromGrpcResponse> for QueryTotalSupplyResponse { + fn try_from_response(self) -> Result> { + Ok(self + .supply + .into_iter() + .map(|coin| coin.try_into()) + .collect::>()?) + } +} diff --git a/grpc/src/grpc/blob.rs b/grpc/src/grpc/blob.rs new file mode 100644 index 00000000..e708ca7e --- /dev/null +++ b/grpc/src/grpc/blob.rs @@ -0,0 +1,19 @@ +use celestia_proto::celestia::blob::v1::{ + QueryParamsRequest as QueryBlobParamsRequest, QueryParamsResponse as QueryBlobParamsResponse, +}; +use celestia_types::blob::BlobParams; + +use crate::grpc::{make_empty_params, FromGrpcResponse}; +use crate::{Error, Result}; + +impl FromGrpcResponse for QueryBlobParamsResponse { + fn try_from_response(self) -> Result { + let params = self.params.ok_or(Error::FailedToParseResponse)?; + Ok(BlobParams { + gas_per_blob_byte: params.gas_per_blob_byte, + gov_max_square_size: params.gov_max_square_size, + }) + } +} + +make_empty_params!(QueryBlobParamsRequest); diff --git a/grpc/src/grpc/celestia_tx.rs b/grpc/src/grpc/celestia_tx.rs new file mode 100644 index 00000000..7ac953d8 --- /dev/null +++ b/grpc/src/grpc/celestia_tx.rs @@ -0,0 +1,96 @@ +use std::fmt; +use std::str::FromStr; + +use celestia_proto::celestia::core::v1::tx::{ + TxStatusRequest as RawTxStatusRequest, TxStatusResponse as RawTxStatusResponse, +}; +use celestia_types::hash::Hash; +use celestia_types::state::ErrorCode; +use celestia_types::Height; + +use crate::grpc::{FromGrpcResponse, IntoGrpcParam}; +use crate::{Error, Result}; + +/// Response to a tx status query +#[derive(Debug, Clone)] +pub struct TxStatusResponse { + /// Height of the block in which the transaction was committed. + pub height: Height, + /// Index of the transaction in block. + pub index: u32, + /// Execution_code is returned when the transaction has been committed + /// and returns whether it was successful or errored. A non zero + /// execution code indicated an error. + pub execution_code: ErrorCode, + /// Error log, if transaction failed. + pub error: String, + /// Status of the transaction. + pub status: TxStatus, +} + +/// Represents state of the transaction in the mempool +#[derive(Debug, Copy, Clone)] +pub enum TxStatus { + /// The transaction is not known to the node, it could be never sent. + Unknown, + /// The transaction is still pending. + Pending, + /// The transaction was evicted from the mempool. + Evicted, + /// The transaction was committed into the block. + Committed, +} + +impl fmt::Display for TxStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + TxStatus::Unknown => "UNKNOWN", + TxStatus::Pending => "PENDING", + TxStatus::Evicted => "EVICTED", + TxStatus::Committed => "COMMITTED", + }; + write!(f, "{s}") + } +} + +impl FromStr for TxStatus { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "UNKNOWN" => Ok(TxStatus::Unknown), + "PENDING" => Ok(TxStatus::Pending), + "EVICTED" => Ok(TxStatus::Evicted), + "COMMITTED" => Ok(TxStatus::Committed), + _ => Err(Error::FailedToParseResponse), + } + } +} + +impl TryFrom for TxStatusResponse { + type Error = Error; + + fn try_from(value: RawTxStatusResponse) -> Result { + Ok(TxStatusResponse { + height: value.height.try_into()?, + index: value.index, + execution_code: value.execution_code.try_into()?, + error: value.error, + status: value.status.parse()?, + }) + } +} + +impl IntoGrpcParam for Hash { + fn into_parameter(self) -> RawTxStatusRequest { + RawTxStatusRequest { + tx_id: self.to_string(), + } + } +} + +impl FromGrpcResponse for RawTxStatusResponse { + fn try_from_response(self) -> Result { + self.try_into() + } +} diff --git a/grpc/src/grpc/cosmos_tx.rs b/grpc/src/grpc/cosmos_tx.rs new file mode 100644 index 00000000..7d977072 --- /dev/null +++ b/grpc/src/grpc/cosmos_tx.rs @@ -0,0 +1,91 @@ +use celestia_proto::cosmos::base::abci::v1beta1::GasInfo; +use celestia_types::hash::Hash; + +use celestia_proto::cosmos::tx::v1beta1::{ + BroadcastTxRequest, BroadcastTxResponse, GetTxRequest as RawGetTxRequest, + GetTxResponse as RawGetTxResponse, SimulateRequest, SimulateResponse, +}; +use celestia_types::state::{Tx, TxResponse}; + +use crate::grpc::{FromGrpcResponse, IntoGrpcParam}; +use crate::{Error, Result}; + +pub use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; + +/// Response to GetTx +#[derive(Debug)] +pub struct GetTxResponse { + /// Response Transaction + pub tx: Tx, + + /// TxResponse to a Query + pub tx_response: TxResponse, +} + +impl IntoGrpcParam for (Vec, BroadcastMode) { + fn into_parameter(self) -> BroadcastTxRequest { + let (tx_bytes, mode) = self; + + BroadcastTxRequest { + tx_bytes, + mode: mode.into(), + } + } +} + +impl FromGrpcResponse for BroadcastTxResponse { + fn try_from_response(self) -> Result { + Ok(self + .tx_response + .ok_or(Error::FailedToParseResponse)? + .try_into()?) + } +} + +impl IntoGrpcParam for Hash { + fn into_parameter(self) -> RawGetTxRequest { + RawGetTxRequest { + hash: self.to_string(), + } + } +} + +impl FromGrpcResponse for RawGetTxResponse { + fn try_from_response(self) -> Result { + let tx_response = self + .tx_response + .ok_or(Error::FailedToParseResponse)? + .try_into()?; + + let tx = self.tx.ok_or(Error::FailedToParseResponse)?; + + let cosmos_tx = Tx { + body: tx.body.ok_or(Error::FailedToParseResponse)?.try_into()?, + auth_info: tx + .auth_info + .ok_or(Error::FailedToParseResponse)? + .try_into()?, + signatures: tx.signatures, + }; + + Ok(GetTxResponse { + tx: cosmos_tx, + tx_response, + }) + } +} + +impl IntoGrpcParam for Vec { + fn into_parameter(self) -> SimulateRequest { + SimulateRequest { + tx_bytes: self, + ..SimulateRequest::default() + } + } +} + +impl FromGrpcResponse for SimulateResponse { + fn try_from_response(self) -> Result { + self.gas_info.ok_or(Error::FailedToParseResponse) + } +} diff --git a/grpc/src/grpc/node.rs b/grpc/src/grpc/node.rs new file mode 100644 index 00000000..7fd0451e --- /dev/null +++ b/grpc/src/grpc/node.rs @@ -0,0 +1,22 @@ +use celestia_proto::cosmos::base::node::v1beta1::{ConfigRequest, ConfigResponse}; + +use crate::grpc::{make_empty_params, FromGrpcResponse}; +use crate::{Error, Result}; + +impl FromGrpcResponse for ConfigResponse { + fn try_from_response(self) -> Result { + const UNITS_SUFFIX: &str = "utia"; + + let min_gas_price_with_suffix = self.minimum_gas_price; + let min_gas_price_str = min_gas_price_with_suffix + .strip_suffix(UNITS_SUFFIX) + .ok_or(Error::FailedToParseResponse)?; + let min_gas_price = min_gas_price_str + .parse::() + .map_err(|_| Error::FailedToParseResponse)?; + + Ok(min_gas_price) + } +} + +make_empty_params!(ConfigRequest); diff --git a/grpc/src/grpc/tendermint.rs b/grpc/src/grpc/tendermint.rs new file mode 100644 index 00000000..48191f27 --- /dev/null +++ b/grpc/src/grpc/tendermint.rs @@ -0,0 +1,28 @@ +use celestia_proto::cosmos::base::tendermint::v1beta1::{ + GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, + GetLatestBlockResponse, +}; +use celestia_types::block::Block; + +use crate::grpc::{make_empty_params, FromGrpcResponse, IntoGrpcParam}; +use crate::{Error, Result}; + +impl FromGrpcResponse for GetBlockByHeightResponse { + fn try_from_response(self) -> Result { + Ok(self.block.ok_or(Error::FailedToParseResponse)?.try_into()?) + } +} + +impl FromGrpcResponse for GetLatestBlockResponse { + fn try_from_response(self) -> Result { + Ok(self.block.ok_or(Error::FailedToParseResponse)?.try_into()?) + } +} + +impl IntoGrpcParam for i64 { + fn into_parameter(self) -> GetBlockByHeightRequest { + GetBlockByHeightRequest { height: self } + } +} + +make_empty_params!(GetLatestBlockRequest); diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index 5a8484ca..a58084a6 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -1,8 +1,10 @@ #![doc = include_str!("../README.md")] -mod client; mod error; -pub mod types; +pub mod grpc; +mod tx; +mod utils; -pub use crate::client::GrpcClient; pub use crate::error::{Error, Result}; +pub use crate::grpc::GrpcClient; +pub use crate::tx::{TxClient, TxConfig}; diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs new file mode 100644 index 00000000..b533ef45 --- /dev/null +++ b/grpc/src/tx.rs @@ -0,0 +1,564 @@ +use std::ops::Deref; +use std::sync::{LazyLock, RwLock}; +use std::time::Duration; + +use bytes::Bytes; +use celestia_proto::cosmos::crypto::secp256k1; +use celestia_proto::cosmos::tx::v1beta1::SignDoc; +use celestia_types::blob::{Blob, MsgPayForBlobs, RawBlobTx, RawMsgPayForBlobs}; +use celestia_types::consts::appconsts; +use celestia_types::hash::Hash; +use celestia_types::state::auth::BaseAccount; +use celestia_types::state::{ + Address, AuthInfo, ErrorCode, Fee, ModeInfo, RawTx, RawTxBody, SignerInfo, Sum, +}; +use celestia_types::{AppVersion, Height}; +use http_body::Body; +use k256::ecdsa::signature::Signer; +use k256::ecdsa::{Signature, VerifyingKey}; +use prost::{Message, Name}; +use regex::Regex; +use tendermint::chain::Id; +use tendermint::PublicKey; +use tendermint_proto::google::protobuf::Any; +use tendermint_proto::Protobuf; +use tokio::sync::{Mutex, MutexGuard}; +use tonic::body::BoxBody; +use tonic::client::GrpcService; + +use crate::grpc::Account; +use crate::grpc::TxStatus; +use crate::grpc::{GrpcClient, StdError}; +use crate::utils::Interval; +use crate::{Error, Result}; + +pub use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; + +// source https://github.com/celestiaorg/celestia-app/blob/v3.0.2/x/blob/types/payforblob.go#L21 +// PFBGasFixedCost is a rough estimate for the "fixed cost" in the gas cost +// formula: gas cost = gas per byte * bytes per share * shares occupied by +// blob + "fixed cost". In this context, "fixed cost" accounts for the gas +// consumed by operations outside the blob's GasToConsume function (i.e. +// signature verification, tx size, read access to accounts). +// +// Since the gas cost of these operations is not easy to calculate, linear +// regression was performed on a set of observed data points to derive an +// approximate formula for gas cost. Assuming gas per byte = 8 and bytes per +// share = 512, we can solve for "fixed cost" and arrive at 65,000. gas cost +// = 8 * 512 * number of shares occupied by the blob + 65,000 has a +// correlation coefficient of 0.996. To be conservative, we round up "fixed +// cost" to 75,000 because the first tx always takes up 10,000 more gas than +// subsequent txs. +const PFB_GAS_FIXED_COST: u64 = 75000; +// BytesPerBlobInfo is a rough estimation for the amount of extra bytes in +// information a blob adds to the size of the underlying transaction. +const BYTES_PER_BLOB_INFO: u64 = 70; +// source https://github.com/celestiaorg/celestia-app/blob/v3.0.2/pkg/appconsts/initial_consts.go#L20 +// DefaultMinGasPrice is the default min gas price that gets set in the app.toml file. +// The min gas price acts as a filter. Transactions below that limit will not pass +// a nodes `CheckTx` and thus not be proposed by that node. +const DEFAULT_MIN_GAS_PRICE: f64 = 0.002; // utia +const DEFAULT_GAS_MULTIPLIER: f64 = 1.1; +// source https://github.com/celestiaorg/celestia-core/blob/v1.43.0-tm-v0.34.35/pkg/consts/consts.go#L19 +const BLOB_TX_TYPE_ID: &str = "BLOB"; + +/// A result of correctly submitted transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxInfo { + /// Hash of the transaction. + pub hash: Hash, + /// Height at which transaction was submitted. + pub height: Height, +} + +/// Configuration for the transaction. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct TxConfig { + /// Custom gas limit for the transaction. + pub gas_limit: Option, + /// Custom gas price for fee calculation. + pub gas_price: Option, +} + +impl TxConfig { + /// Attach gas limit to this config. + pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + /// Attach gas price to this config. + pub fn with_gas_price(mut self, gas_price: f64) -> Self { + self.gas_price = Some(gas_price); + self + } +} + +/// A client for submitting messages and transactions to celestia. +/// +/// Client handles management of the accounts sequence (nonce), thus +/// it should be the only party submitting transactions signed with +/// given account. Using e.g. two distinct clients with the same account +/// will make them invalidate each others nonces. +pub struct TxClient { + client: GrpcClient, + + // NOTE: in future we might want a map of accounts + // and something like .add_account() + account: Mutex, + pubkey: VerifyingKey, + signer: S, + + app_version: AppVersion, + chain_id: Id, + gas_price: RwLock, +} + +impl TxClient +where + T: GrpcService + Clone, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + S: Signer, +{ + /// Create a new transaction client. + /// + /// Public key is optional if it can be retrieved from the account. + pub async fn new( + client: GrpcClient, + signer: S, + account_address: &Address, + account_pubkey: Option, + ) -> Result { + let account = client.get_account(account_address).await?; + let pubkey = match (account.pub_key, account_pubkey) { + (Some(fetched), Some(provided)) => { + if fetched != PublicKey::Secp256k1(provided) { + return Err(Error::PublicKeyMismatch); + } + provided + } + (Some(fetched), None) => { + if let PublicKey::Secp256k1(pubkey) = fetched { + pubkey + } else { + return Err(Error::KeyAlgorithmNotSupported); + } + } + (None, Some(provided)) => provided, + (None, None) => return Err(Error::PublicKeyMissing), + }; + let account = Mutex::new(account); + + let block = client.get_latest_block().await?; + let app_version = block.header.version.app; + let app_version = AppVersion::from_u64(app_version) + .ok_or(celestia_types::Error::UnsupportedAppVersion(app_version))?; + let chain_id = block.header.chain_id; + + Ok(Self { + client, + signer, + account, + pubkey, + app_version, + chain_id, + gas_price: RwLock::new(DEFAULT_MIN_GAS_PRICE), + }) + } + + /// Submit given message to celestia network. + /// + /// When no gas price is specified through config, it will automatically + /// handle updating client's gas price when consensus updates minimal + /// gas price. + /// + /// # Example + /// ```no_run + /// # async fn docs() { + /// use celestia_grpc::{GrpcClient, TxClient, TxConfig}; + /// use celestia_proto::cosmos::bank::v1beta1::MsgSend; + /// use celestia_types::state::{AccAddress, Coin}; + /// use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; + /// + /// let signing_key = SigningKey::random(&mut rand_core::OsRng); + /// let public_key = *signing_key.verifying_key(); + /// let address = AccAddress::new(public_key.into()).into(); + /// let grpc = GrpcClient::with_url("celestia-app-grpc-url:9090").unwrap(); + /// + /// let tx_client = TxClient::new(grpc, signing_key, &address, Some(public_key)) + /// .await + /// .unwrap(); + /// + /// let msg = MsgSend { + /// from_address: address.to_string(), + /// to_address: "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm".to_string(), + /// amount: vec![Coin::utia(12345).into()], + /// }; + /// + /// tx_client + /// .submit_message(msg.clone(), TxConfig::default()) + /// .await + /// .unwrap(); + /// # } + /// ``` + pub async fn submit_message(&self, message: M, cfg: TxConfig) -> Result + where + M: Name, + { + let tx_body = RawTxBody { + messages: vec![into_any(message)], + ..RawTxBody::default() + }; + + let mut is_retry = false; + let (tx_hash, sequence) = loop { + match self.sign_and_broadcast_tx(tx_body.clone(), cfg).await { + Ok(resp) => break resp, + Err(e) if !is_retry => { + if self.maybe_update_gas_price(&e, cfg)? { + is_retry = true; + continue; + } + return Err(e); + } + Err(e) => return Err(e), + } + }; + self.confirm_tx(tx_hash, sequence).await + } + + /// Submit given blobs to celestia network. + /// + /// When no gas price is specified through config, it will automatically + /// handle updating client's gas price when consensus updates minimal + /// gas price. + /// + /// # Example + /// ```no_run + /// # async fn docs() { + /// use celestia_grpc::{GrpcClient, TxClient, TxConfig}; + /// use celestia_types::state::{AccAddress, Coin}; + /// use celestia_types::{AppVersion, Blob}; + /// use celestia_types::nmt::Namespace; + /// use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; + /// + /// let signing_key = SigningKey::random(&mut rand_core::OsRng); + /// let public_key = *signing_key.verifying_key(); + /// let address = AccAddress::new(public_key.into()).into(); + /// let grpc = GrpcClient::with_url("celestia-app-grpc-url:9090").unwrap(); + /// + /// let tx_client = TxClient::new(grpc, signing_key, &address, Some(public_key)) + /// .await + /// .unwrap(); + /// + /// let ns = Namespace::new_v0(b"abcd").unwrap(); + /// let blob = Blob::new(ns, "some data".into(), AppVersion::V3).unwrap(); + /// + /// tx_client + /// .submit_blobs(&[blob], TxConfig::default()) + /// .await + /// .unwrap(); + /// # } + /// ``` + pub async fn submit_blobs(&self, blobs: &[Blob], cfg: TxConfig) -> Result { + if blobs.is_empty() { + return Err(Error::TxEmptyBlobList); + } + for blob in blobs { + blob.validate(self.app_version)?; + } + + let mut is_retry = false; + let (tx_hash, sequence) = loop { + match self.sign_and_broadcast_blobs(blobs.to_vec(), cfg).await { + Ok(resp) => break resp, + Err(e) if !is_retry => { + if self.maybe_update_gas_price(&e, cfg)? { + is_retry = true; + continue; + } + return Err(e); + } + Err(e) => return Err(e), + } + }; + self.confirm_tx(tx_hash, sequence).await + } + + /// Get current gas price used by the client + pub fn gas_price(&self) -> f64 { + *self.gas_price.read().expect("lock poisoned") + } + + /// Set current gas price used by the client + pub fn set_gas_price(&self, gas_price: f64) { + *self.gas_price.write().expect("lock poisoned") = gas_price; + } + + async fn sign_and_broadcast_tx(&self, tx: RawTxBody, cfg: TxConfig) -> Result<(Hash, u64)> { + let account = self.account.lock().await; + let sign_tx = |tx, gas, fee| { + sign_tx( + tx, + self.chain_id.clone(), + &account, + &self.pubkey, + &self.signer, + gas, + fee, + ) + }; + + let gas_limit = if let Some(gas_limit) = cfg.gas_limit { + gas_limit + } else { + // simulate the gas that would be used by transaction + // fee should be at least 1 as it affects calculation + let tx = sign_tx(tx.clone(), 0, 1); + let gas_info = self.client.simulate(tx.encode_to_vec()).await?; + (gas_info.gas_used as f64 * DEFAULT_GAS_MULTIPLIER) as u64 + }; + + let gas_price = cfg.gas_price.unwrap_or(self.gas_price()); + let fee = (gas_limit as f64 * gas_price).ceil(); + let tx = sign_tx(tx, gas_limit, fee as u64); + + self.broadcast_tx_with_account(tx.encode_to_vec(), account, gas_limit) + .await + } + + async fn sign_and_broadcast_blobs( + &self, + blobs: Vec, + cfg: TxConfig, + ) -> Result<(Hash, u64)> { + // lock the account; tx signing and broadcast must be atomic + // because node requires all transactions to be sequenced by account.sequence + let account = self.account.lock().await; + + let pfb = MsgPayForBlobs::new(&blobs, account.address.clone())?; + let pfb = RawTxBody { + messages: vec![into_any(RawMsgPayForBlobs::from(pfb))], + ..RawTxBody::default() + }; + + let gas_limit = cfg + .gas_limit + .unwrap_or_else(|| estimate_gas(&blobs, self.app_version, DEFAULT_GAS_MULTIPLIER)); + let gas_price = cfg.gas_price.unwrap_or(self.gas_price()); + let fee = (gas_limit as f64 * gas_price).ceil() as u64; + let tx = sign_tx( + pfb, + self.chain_id.clone(), + &account, + &self.pubkey, + &self.signer, + gas_limit, + fee, + ); + + let blobs = blobs.into_iter().map(Into::into).collect(); + let blob_tx = RawBlobTx { + tx: tx.encode_to_vec(), + blobs, + type_id: BLOB_TX_TYPE_ID.to_string(), + }; + + self.broadcast_tx_with_account(blob_tx.encode_to_vec(), account, gas_limit) + .await + } + + async fn broadcast_tx_with_account( + &self, + tx: Vec, + mut account: MutexGuard<'_, Account>, + gas_limit: u64, + ) -> Result<(Hash, u64)> { + let resp = self.client.broadcast_tx(tx, BroadcastMode::Sync).await?; + + if resp.code != ErrorCode::Success { + return Err(Error::TxBroadcastFailed( + resp.txhash, + resp.code, + resp.raw_log, + gas_limit, + )); + } + + let tx_sequence = account.sequence; + account.sequence += 1; + + Ok((resp.txhash, tx_sequence)) + } + + async fn confirm_tx(&self, hash: Hash, sequence: u64) -> Result { + let mut interval = Interval::new(Duration::from_millis(500)).await; + + loop { + let tx_status = self.client.tx_status(hash).await?; + match tx_status.status { + TxStatus::Pending => interval.tick().await, + TxStatus::Unknown => return Err(Error::TxNotFound(hash)), + TxStatus::Evicted => { + // node will treat this transaction like if it never happened, so + // we need to revert the account's sequence to the one of evicted tx. + // all transactions that were already submitted after this one + // will fail due to incorrect sequence number. + let mut acc = self.account.lock().await; + acc.sequence = sequence; + return Err(Error::TxEvicted(hash)); + } + TxStatus::Committed => { + if tx_status.execution_code == ErrorCode::Success { + return Ok(TxInfo { + hash, + height: tx_status.height, + }); + } else { + return Err(Error::TxExecutionFailed( + hash, + tx_status.execution_code, + tx_status.error, + )); + } + } + } + } + } + + fn maybe_update_gas_price(&self, err: &Error, cfg: TxConfig) -> Result { + let Error::TxBroadcastFailed(_, code, error, gas_limit) = err else { + return Ok(false); + }; + // nothing to update if we didn't use our gas internal gas price + if cfg.gas_price.is_some() { + return Ok(false); + } + if *code != ErrorCode::InsufficientFee { + return Ok(false); + } + + let Some((got_fee, want_fee)) = parse_insufficient_gas_err(error) else { + return Err(Error::UpdatingGasPriceFailed(format!( + "Couldn't parse required fee from error: {err}" + ))); + }; + if want_fee == 0.0 { + return Err(Error::UpdatingGasPriceFailed(format!( + "Wanted fee is 0: {err}" + ))); + } + + let new_gas_price = if self.gas_price() == 0.0 || got_fee == 0.0 { + if *gas_limit == 0 { + return Err(Error::UpdatingGasPriceFailed(format!( + "Cannot update gas price when gas price and limit are 0: {err}" + ))); + } + want_fee / *gas_limit as f64 + } else { + want_fee / got_fee * self.gas_price() + }; + + self.set_gas_price(new_gas_price.ceil()); + Ok(true) + } +} + +impl Deref for TxClient { + type Target = GrpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +/// Sign `tx_body` and the transaction metadata as the `base_account` using `signer` +pub fn sign_tx( + tx_body: RawTxBody, + chain_id: Id, + base_account: &BaseAccount, + verifying_key: &VerifyingKey, + signer: &impl Signer, + gas_limit: u64, + fee: u64, +) -> RawTx { + // From https://github.com/celestiaorg/cosmos-sdk/blob/v1.25.0-sdk-v0.46.16/proto/cosmos/tx/signing/v1beta1/signing.proto#L24 + const SIGNING_MODE_INFO: ModeInfo = ModeInfo { + sum: Sum::Single { mode: 1 }, + }; + + let public_key = secp256k1::PubKey { + key: verifying_key.to_encoded_point(true).as_bytes().to_vec(), + }; + let public_key_as_any = Any { + type_url: secp256k1::PubKey::type_url(), + value: public_key.encode_to_vec(), + }; + + let mut fee = Fee::new(fee, gas_limit); + fee.payer = Some(base_account.address.clone()); + + let auth_info = AuthInfo { + signer_infos: vec![SignerInfo { + public_key: Some(public_key_as_any), + mode_info: SIGNING_MODE_INFO, + sequence: base_account.sequence, + }], + fee, + }; + + let bytes_to_sign = SignDoc { + body_bytes: tx_body.encode_to_vec(), + auth_info_bytes: auth_info.clone().encode_vec(), + chain_id: chain_id.into(), + account_number: base_account.account_number, + } + .encode_to_vec(); + + let signature = signer.sign(&bytes_to_sign); + + RawTx { + auth_info: Some(auth_info.into()), + body: Some(tx_body), + signatures: vec![signature.to_bytes().to_vec()], + } +} + +fn estimate_gas(blobs: &[Blob], app_version: AppVersion, gas_multiplier: f64) -> u64 { + let gas_per_blob_byte = appconsts::gas_per_blob_byte(app_version); + let tx_size_cost_per_byte = appconsts::tx_size_cost_per_byte(app_version); + + let blobs_bytes = + blobs.iter().map(Blob::shares_count).sum::() as u64 * appconsts::SHARE_SIZE as u64; + + let gas = blobs_bytes * gas_per_blob_byte + + (tx_size_cost_per_byte * BYTES_PER_BLOB_INFO * blobs.len() as u64) + + PFB_GAS_FIXED_COST; + (gas as f64 * gas_multiplier) as u64 +} + +// Any::from_msg is infallible, but it yet returns result +fn into_any(msg: M) -> Any +where + M: Name, +{ + Any { + type_url: M::type_url(), + value: msg.encode_to_vec(), + } +} + +fn parse_insufficient_gas_err(error: &str) -> Option<(f64, f64)> { + static RE: LazyLock = LazyLock::new(|| { + // insufficient minimum gas price for this node; got: 50 required at least: 199.000000000000000000: insufficient fee + Regex::new(r".*got.*?(?P[0-9.]+).*required.*?(?P[0-9.]+)").expect("valid regex") + }); + let caps = RE.captures(error)?; + let got: f64 = caps["got"].parse().ok()?; + let want: f64 = caps["want"].parse().ok()?; + + Some((got, want)) +} diff --git a/grpc/src/types.rs b/grpc/src/types.rs deleted file mode 100644 index 4a107ce1..00000000 --- a/grpc/src/types.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Custom types and wrappers needed by gRPC - -use celestia_proto::celestia::blob::v1::{ - QueryParamsRequest as QueryBlobParamsRequest, QueryParamsResponse as QueryBlobParamsResponse, -}; -use celestia_proto::cosmos::base::node::v1beta1::{ConfigRequest, ConfigResponse}; -use celestia_proto::cosmos::base::tendermint::v1beta1::{ - GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, - GetLatestBlockResponse, -}; -use celestia_types::blob::BlobParams; -use celestia_types::block::Block; - -use crate::Error; - -/// types related to authorisation -pub mod auth; -/// types related to transaction querying and submission -pub mod tx; - -macro_rules! make_empty_params { - ($request_type:ident) => { - impl IntoGrpcParam<$request_type> for () { - fn into_parameter(self) -> $request_type { - $request_type {} - } - } - }; -} - -pub(crate) use make_empty_params; - -pub(crate) trait FromGrpcResponse { - fn try_from_response(self) -> Result; -} - -pub(crate) trait IntoGrpcParam { - fn into_parameter(self) -> T; -} - -impl FromGrpcResponse for QueryBlobParamsResponse { - fn try_from_response(self) -> Result { - let params = self.params.ok_or(Error::FailedToParseResponse)?; - Ok(BlobParams { - gas_per_blob_byte: params.gas_per_blob_byte, - gov_max_square_size: params.gov_max_square_size, - }) - } -} - -impl FromGrpcResponse for GetBlockByHeightResponse { - fn try_from_response(self) -> Result { - Ok(self.block.ok_or(Error::FailedToParseResponse)?.try_into()?) - } -} - -impl FromGrpcResponse for GetLatestBlockResponse { - fn try_from_response(self) -> Result { - Ok(self.block.ok_or(Error::FailedToParseResponse)?.try_into()?) - } -} - -impl FromGrpcResponse for ConfigResponse { - fn try_from_response(self) -> Result { - const UNITS_SUFFIX: &str = "utia"; - - let min_gas_price_with_suffix = self.minimum_gas_price; - let min_gas_price_str = min_gas_price_with_suffix - .strip_suffix(UNITS_SUFFIX) - .ok_or(Error::FailedToParseResponse)?; - let min_gas_price = min_gas_price_str - .parse::() - .map_err(|_| Error::FailedToParseResponse)?; - - Ok(min_gas_price) - } -} - -impl IntoGrpcParam for i64 { - fn into_parameter(self) -> GetBlockByHeightRequest { - GetBlockByHeightRequest { height: self } - } -} - -make_empty_params!(GetLatestBlockRequest); -make_empty_params!(ConfigRequest); -make_empty_params!(QueryBlobParamsRequest); diff --git a/grpc/src/types/tx.rs b/grpc/src/types/tx.rs deleted file mode 100644 index aa66fcfc..00000000 --- a/grpc/src/types/tx.rs +++ /dev/null @@ -1,130 +0,0 @@ -use k256::ecdsa::{signature::Signer, Signature}; -use prost::{Message, Name}; - -use celestia_proto::cosmos::crypto::secp256k1; -use celestia_proto::cosmos::tx::v1beta1::{ - BroadcastTxRequest, BroadcastTxResponse, GetTxRequest as RawGetTxRequest, - GetTxResponse as RawGetTxResponse, SignDoc, -}; -use celestia_types::state::auth::BaseAccount; -use celestia_types::state::{ - AuthInfo, Fee, ModeInfo, RawTx, RawTxBody, SignerInfo, Sum, Tx, TxResponse, -}; -use tendermint::public_key::Secp256k1 as VerifyingKey; -use tendermint_proto::google::protobuf::Any; -use tendermint_proto::Protobuf; - -use crate::types::{FromGrpcResponse, IntoGrpcParam}; -use crate::Error; - -pub use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; - -/// Response to GetTx -#[derive(Debug)] -pub struct GetTxResponse { - /// Response Transaction - pub tx: Tx, - - /// TxResponse to a Query - pub tx_response: TxResponse, -} - -impl FromGrpcResponse for BroadcastTxResponse { - fn try_from_response(self) -> Result { - Ok(self - .tx_response - .ok_or(Error::FailedToParseResponse)? - .try_into()?) - } -} - -impl FromGrpcResponse for RawGetTxResponse { - fn try_from_response(self) -> Result { - let tx_response = self - .tx_response - .ok_or(Error::FailedToParseResponse)? - .try_into()?; - - let tx = self.tx.ok_or(Error::FailedToParseResponse)?; - - let cosmos_tx = Tx { - body: tx.body.ok_or(Error::FailedToParseResponse)?.try_into()?, - auth_info: tx - .auth_info - .ok_or(Error::FailedToParseResponse)? - .try_into()?, - signatures: tx.signatures, - }; - - Ok(GetTxResponse { - tx: cosmos_tx, - tx_response, - }) - } -} - -impl IntoGrpcParam for (Vec, BroadcastMode) { - fn into_parameter(self) -> BroadcastTxRequest { - let (tx_bytes, mode) = self; - - BroadcastTxRequest { - tx_bytes, - mode: mode.into(), - } - } -} - -impl IntoGrpcParam for String { - fn into_parameter(self) -> RawGetTxRequest { - RawGetTxRequest { hash: self } - } -} - -/// Sign `tx_body` and the transaction metadata as the `base_account` using `signer` -pub fn sign_tx( - tx_body: RawTxBody, - chain_id: String, - base_account: &BaseAccount, - verifying_key: VerifyingKey, - signer: impl Signer, - gas_limit: u64, - fee: u64, -) -> RawTx { - // From https://github.com/celestiaorg/cosmos-sdk/blob/v1.25.0-sdk-v0.46.16/proto/cosmos/tx/signing/v1beta1/signing.proto#L24 - const SIGNING_MODE_INFO: ModeInfo = ModeInfo { - sum: Sum::Single { mode: 1 }, - }; - - let public_key = secp256k1::PubKey { - key: verifying_key.to_encoded_point(true).as_bytes().to_vec(), - }; - let public_key_as_any = Any { - type_url: secp256k1::PubKey::type_url(), - value: public_key.encode_to_vec(), - }; - - let auth_info = AuthInfo { - signer_infos: vec![SignerInfo { - public_key: Some(public_key_as_any), - mode_info: SIGNING_MODE_INFO, - sequence: base_account.sequence, - }], - fee: Fee::new(fee, gas_limit), - }; - - let bytes_to_sign = SignDoc { - body_bytes: tx_body.encode_to_vec(), - auth_info_bytes: auth_info.clone().encode_vec(), - chain_id, - account_number: base_account.account_number, - } - .encode_to_vec(); - - let signature: Signature = signer.sign(&bytes_to_sign); - - RawTx { - auth_info: Some(auth_info.into()), - body: Some(tx_body), - signatures: vec![signature.to_bytes().to_vec()], - } -} diff --git a/grpc/src/utils.rs b/grpc/src/utils.rs new file mode 100644 index 00000000..5c05d974 --- /dev/null +++ b/grpc/src/utils.rs @@ -0,0 +1,49 @@ +pub(crate) use imp::*; + +#[cfg(not(target_arch = "wasm32"))] +mod imp { + use std::time::Duration; + use tokio::time::interval; + pub(crate) struct Interval(tokio::time::Interval); + + #[cfg(not(target_arch = "wasm32"))] + impl Interval { + pub(crate) async fn new(dur: Duration) -> Self { + let mut inner = interval(dur); + + // In Tokio the first tick returns immediately, so we + // consume to it to create an identical cross-platform + // behavior. + inner.tick().await; + + Interval(inner) + } + + pub(crate) async fn tick(&mut self) { + self.0.tick().await; + } + } +} + +#[cfg(target_arch = "wasm32")] +mod imp { + use gloo_timers::future::{IntervalStream, TimeoutFuture}; + use send_wrapper::SendWrapper; + use std::time::Duration; + + pub(crate) struct Interval(SendWrapper); + + impl Interval { + pub(crate) async fn new(dur: Duration) -> Self { + // If duration was less than a millisecond, then make + // it 1 millisecond. + let millis = u32::try_from(dur.as_millis().max(1)).unwrap_or(u32::MAX); + + Interval(SendWrapper::new(IntervalStream::new(millis))) + } + + pub(crate) async fn tick(&mut self) { + self.0.next().await; + } + } +} diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 61f76a37..c231b4a7 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,15 +1,15 @@ -use std::time::Duration; +use std::sync::Arc; -use celestia_grpc::types::auth::Account; -use celestia_grpc::types::tx::sign_tx; -use celestia_proto::cosmos::tx::v1beta1::BroadcastMode; -use celestia_types::blob::MsgPayForBlobs; +use celestia_grpc::TxConfig; +use celestia_proto::cosmos::bank::v1beta1::MsgSend; use celestia_types::nmt::Namespace; +use celestia_types::state::Coin; use celestia_types::{AppVersion, Blob}; +use utils::{load_account, TestAccount}; pub mod utils; -use crate::utils::{load_account, new_test_client, sleep}; +use crate::utils::{new_grpc_client, new_tx_client}; #[cfg(not(target_arch = "wasm32"))] use tokio::test as async_test; @@ -19,24 +19,9 @@ use wasm_bindgen_test::wasm_bindgen_test as async_test; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); -#[async_test] -async fn get_min_gas_price() { - let mut client = new_test_client(); - let gas_price = client.get_min_gas_price().await.unwrap(); - assert!(gas_price > 0.0); -} - -#[async_test] -async fn get_blob_params() { - let mut client = new_test_client(); - let params = client.get_blob_params().await.unwrap(); - assert!(params.gas_per_blob_byte > 0); - assert!(params.gov_max_square_size > 0); -} - #[async_test] async fn get_auth_params() { - let mut client = new_test_client(); + let client = new_grpc_client(); let params = client.get_auth_params().await.unwrap(); assert!(params.max_memo_characters > 0); assert!(params.tx_sig_limit > 0); @@ -45,9 +30,53 @@ async fn get_auth_params() { assert!(params.sig_verify_cost_secp256k1 > 0); } +#[async_test] +async fn get_account() { + let client = new_grpc_client(); + + let accounts = client.get_accounts().await.unwrap(); + + let first_account = accounts.first().expect("account to exist"); + let account = client.get_account(&first_account.address).await.unwrap(); + + assert_eq!(&account, first_account); +} + +#[async_test] +async fn get_balance() { + let account = load_account(); + let client = new_grpc_client(); + + let coin = client.get_balance(&account.address, "utia").await.unwrap(); + assert_eq!("utia", &coin.denom); + assert!(coin.amount > 0); + + let all_coins = client.get_all_balances(&account.address).await.unwrap(); + assert!(!all_coins.is_empty()); + assert!(all_coins.iter().map(|c| c.amount).sum::() > 0); + + let spendable_coins = client + .get_spendable_balances(&account.address) + .await + .unwrap(); + assert!(!spendable_coins.is_empty()); + assert!(spendable_coins.iter().map(|c| c.amount).sum::() > 0); + + let total_supply = client.get_total_supply().await.unwrap(); + assert!(!total_supply.is_empty()); + assert!(total_supply.iter().map(|c| c.amount).sum::() > 0); +} + +#[async_test] +async fn get_min_gas_price() { + let client = new_grpc_client(); + let gas_price = client.get_min_gas_price().await.unwrap(); + assert!(gas_price > 0.0); +} + #[async_test] async fn get_block() { - let mut client = new_test_client(); + let client = new_grpc_client(); let latest_block = client.get_latest_block().await.unwrap(); let height = latest_block.header.height.value() as i64; @@ -57,60 +86,176 @@ async fn get_block() { } #[async_test] -async fn get_account() { - let mut client = new_test_client(); +async fn get_blob_params() { + let client = new_grpc_client(); + let params = client.get_blob_params().await.unwrap(); + assert!(params.gas_per_blob_byte > 0); + assert!(params.gov_max_square_size > 0); +} - let accounts = client.get_accounts().await.unwrap(); +#[async_test] +async fn submit_and_get_tx() { + let (_lock, tx_client) = new_tx_client().await; - let first_account = accounts.first().expect("account to exist"); + let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); + let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; - let address = match first_account { - Account::Base(acct) => acct.address.clone(), - Account::Module(acct) => acct.base_account.as_ref().unwrap().address.clone(), - }; + let tx = tx_client + .submit_blobs(&blobs, TxConfig::default()) + .await + .unwrap(); + let tx2 = tx_client.get_tx(tx.hash).await.unwrap(); - let account = client.get_account(&address).await.unwrap(); + assert_eq!(tx.hash, tx2.tx_response.txhash); +} - assert_eq!(&account, first_account); +#[async_test] +async fn submit_blobs_parallel() { + let (_lock, tx_client) = new_tx_client().await; + let tx_client = Arc::new(tx_client); + + let futs = (0..100) + .map(|n| { + let tx_client = tx_client.clone(); + tokio::spawn(async move { + let namespace = Namespace::new_v0(&[1, 2, n]).unwrap(); + let blobs = + vec![Blob::new(namespace, format!("bleb{n}").into(), AppVersion::V3).unwrap()]; + + let response = tx_client + .submit_blobs(&blobs, TxConfig::default()) + .await + .unwrap(); + + assert!(response.height.value() > 3) + }) + }) + .collect::>(); + + for fut in futs { + fut.await.unwrap(); + } } #[async_test] -async fn submit_blob() { - let mut client = new_test_client(); +async fn submit_blobs_insufficient_gas_price_and_limit() { + let (_lock, tx_client) = new_tx_client().await; - let account_credentials = load_account(); let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); - let blobs = vec![Blob::new(namespace, "Hello, World!".into(), AppVersion::V3).unwrap()]; - let chain_id = "private".to_string(); - let account = client - .get_account(&account_credentials.address) + let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; + + tx_client + .submit_blobs(&blobs, TxConfig::default().with_gas_limit(10000)) + .await + .unwrap_err(); + + tx_client + .submit_blobs(&blobs, TxConfig::default().with_gas_price(0.0005)) + .await + .unwrap_err(); +} + +#[async_test] +async fn submit_blobs_gas_price_update() { + let (_lock, tx_client) = new_tx_client().await; + + let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); + let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; + + tx_client.set_gas_price(0.0005); + + // if user also set gas price, no update should happen + tx_client + .submit_blobs(&blobs, TxConfig::default().with_gas_limit(10000)) + .await + .unwrap_err(); + assert_eq!(tx_client.gas_price(), 0.0005); + + // with default config, gas price should be updated + tx_client + .submit_blobs(&blobs, TxConfig::default()) .await .unwrap(); - // gas and fees are overestimated for simplicity - let gas_limit = 100000; - let fee = 5000; - - let msg_pay_for_blobs = MsgPayForBlobs::new(&blobs, account_credentials.address).unwrap(); - - let tx = sign_tx( - msg_pay_for_blobs.into(), - chain_id, - account.base_account_ref().unwrap(), - account_credentials.verifying_key, - account_credentials.signing_key, - gas_limit, - fee, - ); - - let response = client - .broadcast_blob_tx(tx, blobs, BroadcastMode::Sync) + assert_ne!(tx_client.gas_price(), 0.0005); +} + +#[async_test] +async fn submit_message() { + let account = load_account(); + let other_account = TestAccount::random(); + let amount = Coin::utia(12345); + let (_lock, tx_client) = new_tx_client().await; + + let msg = MsgSend { + from_address: account.address.to_string(), + to_address: other_account.address.to_string(), + amount: vec![amount.clone().into()], + }; + + tx_client + .submit_message(msg, TxConfig::default()) + .await + .unwrap(); + + let coins = tx_client + .get_all_balances(&other_account.address) .await .unwrap(); - sleep(Duration::from_secs(3)).await; + assert_eq!(coins.len(), 1); + assert_eq!(amount, coins[0]); +} + +#[async_test] +async fn submit_message_insufficient_gas_price_and_limit() { + let account = load_account(); + let other_account = TestAccount::random(); + let amount = Coin::utia(12345); + let (_lock, tx_client) = new_tx_client().await; + + let msg = MsgSend { + from_address: account.address.to_string(), + to_address: other_account.address.to_string(), + amount: vec![amount.clone().into()], + }; + + tx_client + .submit_message(msg.clone(), TxConfig::default().with_gas_limit(10000)) + .await + .unwrap_err(); + + tx_client + .submit_message(msg, TxConfig::default().with_gas_price(0.0005)) + .await + .unwrap_err(); +} + +#[async_test] +async fn submit_message_gas_price_update() { + let account = load_account(); + let other_account = TestAccount::random(); + let amount = Coin::utia(12345); + let (_lock, tx_client) = new_tx_client().await; - let _submitted_tx = client - .get_tx(response.txhash) + let msg = MsgSend { + from_address: account.address.to_string(), + to_address: other_account.address.to_string(), + amount: vec![amount.clone().into()], + }; + + tx_client.set_gas_price(0.0005); + + // if user also set gas price, no update should happen + tx_client + .submit_message(msg.clone(), TxConfig::default().with_gas_limit(10000)) .await - .expect("get to be successful"); + .unwrap_err(); + assert_eq!(tx_client.gas_price(), 0.0005); + + // with default config, gas price should be updated + tx_client + .submit_message(msg, TxConfig::default()) + .await + .unwrap(); + assert_ne!(tx_client.gas_price(), 0.0005); } diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 9aca9770..f6d4c21e 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -1,14 +1,8 @@ -use std::time::Duration; - -use celestia_grpc::GrpcClient; -use celestia_types::state::Address; +use celestia_types::state::{AccAddress, Address}; use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; use tendermint::public_key::Secp256k1 as VerifyingKey; -#[cfg(not(target_arch = "wasm32"))] -const CELESTIA_GRPC_URL: &str = "http://localhost:19090"; -#[cfg(target_arch = "wasm32")] -const CELESTIA_GRPCWEB_PROXY_URL: &str = "http://localhost:18080"; +pub use imp::*; /// [`TestAccount`] stores celestia account credentials and information, for cases where we don't /// mind jusk keeping the plaintext secret key in memory @@ -22,17 +16,17 @@ pub struct TestAccount { pub signing_key: SigningKey, } -#[cfg(not(target_arch = "wasm32"))] -pub fn new_test_client() -> GrpcClient { - let _ = dotenvy::dotenv(); - let url = std::env::var("CELESTIA_GRPC_URL").unwrap_or_else(|_| CELESTIA_GRPC_URL.into()); - - GrpcClient::with_url(url).expect("creating client failed") -} +impl TestAccount { + pub fn random() -> Self { + let signing_key = SigningKey::random(&mut rand_core::OsRng); + let verifying_key = *signing_key.verifying_key(); -#[cfg(target_arch = "wasm32")] -pub fn new_test_client() -> GrpcClient { - GrpcClient::with_grpcweb_url(CELESTIA_GRPCWEB_PROXY_URL) + Self { + address: AccAddress::new(verifying_key.into()).into(), + verifying_key, + signing_key, + } + } } pub fn load_account() -> TestAccount { @@ -51,13 +45,85 @@ pub fn load_account() -> TestAccount { } #[cfg(not(target_arch = "wasm32"))] -pub async fn sleep(duration: Duration) { - tokio::time::sleep(duration).await; +mod imp { + use std::sync::OnceLock; + use std::time::Duration; + + use celestia_grpc::{GrpcClient, TxClient}; + use tokio::sync::{Mutex, MutexGuard}; + use tonic::transport::Channel; + + use super::*; + + pub const CELESTIA_GRPC_URL: &str = "http://localhost:19090"; + + pub fn new_grpc_client() -> GrpcClient { + let _ = dotenvy::dotenv(); + let url = std::env::var("CELESTIA_GRPC_URL").unwrap_or_else(|_| CELESTIA_GRPC_URL.into()); + + GrpcClient::with_url(url).expect("creating client failed") + } + + // we have to sequence the tests which submits transactions. + // multiple independent tx clients don't work well in parallel + // as they break each other's account.sequence + pub async fn new_tx_client() -> (MutexGuard<'static, ()>, TxClient) { + static LOCK: OnceLock> = OnceLock::new(); + let lock = LOCK.get_or_init(|| Mutex::new(())).lock().await; + + let creds = load_account(); + let grpc_client = new_grpc_client(); + let client = TxClient::new( + grpc_client, + creds.signing_key, + &creds.address, + Some(creds.verifying_key), + ) + .await + .unwrap(); + + (lock, client) + } + + pub async fn sleep(duration: Duration) { + tokio::time::sleep(duration).await; + } } #[cfg(target_arch = "wasm32")] -pub async fn sleep(duration: Duration) { - let millis = u32::try_from(duration.as_millis().max(1)).unwrap_or(u32::MAX); - let delay = gloo_timers::future::TimeoutFuture::new(millis); - delay.await; +mod imp { + use std::time::Duration; + + use celestia_grpc::{GrpcClient, TxClient}; + use gloo_timers::future::TimeoutFuture; + use tonic_web_wasm_client::Client; + + use super::*; + + const CELESTIA_GRPCWEB_PROXY_URL: &str = "http://localhost:18080"; + + pub fn new_grpc_client() -> GrpcClient { + GrpcClient::with_grpcweb_url(CELESTIA_GRPCWEB_PROXY_URL) + } + + pub async fn new_tx_client() -> ((), TxClient) { + let creds = load_account(); + let grpc_client = new_test_client(); + let client = TxClient::new( + grpc_client, + creds.signing_key, + &creds.address, + Some(creds.verifying_key), + ) + .await + .unwrap(); + + ((), client) + } + + pub async fn sleep(duration: Duration) { + let millis = u32::try_from(duration.as_millis().max(1)).unwrap_or(u32::MAX); + let delay = TimeoutFuture::new(millis); + delay.await; + } } diff --git a/proto/build.rs b/proto/build.rs index 77b101de..50014fe2 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -115,8 +115,13 @@ const PROTO_FILES: &[&str] = &[ "vendor/celestia/blob/v1/tx.proto", "vendor/celestia/core/v1/da/data_availability_header.proto", "vendor/celestia/core/v1/proof/proof.proto", + "vendor/celestia/core/v1/tx/tx.proto", "vendor/cosmos/auth/v1beta1/auth.proto", "vendor/cosmos/auth/v1beta1/query.proto", + "vendor/cosmos/bank/v1beta1/bank.proto", + "vendor/cosmos/bank/v1beta1/genesis.proto", + "vendor/cosmos/bank/v1beta1/query.proto", + "vendor/cosmos/bank/v1beta1/tx.proto", "vendor/cosmos/base/abci/v1beta1/abci.proto", "vendor/cosmos/base/node/v1beta1/query.proto", "vendor/cosmos/base/tendermint/v1beta1/query.proto", diff --git a/proto/vendor/cosmos/bank/v1beta1/authz.proto b/proto/vendor/cosmos/bank/v1beta1/authz.proto new file mode 100644 index 00000000..4f58b15e --- /dev/null +++ b/proto/vendor/cosmos/bank/v1beta1/authz.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; +package cosmos.bank.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/base/v1beta1/coin.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types"; + +// SendAuthorization allows the grantee to spend up to spend_limit coins from +// the granter's account. +// +// Since: cosmos-sdk 0.43 +message SendAuthorization { + option (cosmos_proto.implements_interface) = "Authorization"; + + repeated cosmos.base.v1beta1.Coin spend_limit = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} diff --git a/proto/vendor/cosmos/bank/v1beta1/bank.proto b/proto/vendor/cosmos/bank/v1beta1/bank.proto new file mode 100644 index 00000000..7bc9819d --- /dev/null +++ b/proto/vendor/cosmos/bank/v1beta1/bank.proto @@ -0,0 +1,108 @@ +syntax = "proto3"; +package cosmos.bank.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "cosmos/msg/v1/msg.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types"; + +// Params defines the parameters for the bank module. +message Params { + option (gogoproto.goproto_stringer) = false; + repeated SendEnabled send_enabled = 1; + bool default_send_enabled = 2; +} + +// SendEnabled maps coin denom to a send_enabled status (whether a denom is +// sendable). +message SendEnabled { + option (gogoproto.equal) = true; + option (gogoproto.goproto_stringer) = false; + string denom = 1; + bool enabled = 2; +} + +// Input models transaction input. +message Input { + option (cosmos.msg.v1.signer) = "address"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + repeated cosmos.base.v1beta1.Coin coins = 2 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} + +// Output models transaction outputs. +message Output { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + repeated cosmos.base.v1beta1.Coin coins = 2 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} + +// Supply represents a struct that passively keeps track of the total supply +// amounts in the network. +// This message is deprecated now that supply is indexed by denom. +message Supply { + option deprecated = true; + + option (gogoproto.equal) = true; + option (gogoproto.goproto_getters) = false; + + option (cosmos_proto.implements_interface) = "*github.com/cosmos/cosmos-sdk/x/bank/migrations/v040.SupplyI"; + + repeated cosmos.base.v1beta1.Coin total = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} + +// DenomUnit represents a struct that describes a given +// denomination unit of the basic token. +message DenomUnit { + // denom represents the string name of the given denom unit (e.g uatom). + string denom = 1; + // exponent represents power of 10 exponent that one must + // raise the base_denom to in order to equal the given DenomUnit's denom + // 1 denom = 10^exponent base_denom + // (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with + // exponent = 6, thus: 1 atom = 10^6 uatom). + uint32 exponent = 2; + // aliases is a list of string aliases for the given denom + repeated string aliases = 3; +} + +// Metadata represents a struct that describes +// a basic token. +message Metadata { + string description = 1; + // denom_units represents the list of DenomUnit's for a given coin + repeated DenomUnit denom_units = 2; + // base represents the base denom (should be the DenomUnit with exponent = 0). + string base = 3; + // display indicates the suggested denom that should be + // displayed in clients. + string display = 4; + // name defines the name of the token (eg: Cosmos Atom) + // + // Since: cosmos-sdk 0.43 + string name = 5; + // symbol is the token symbol usually shown on exchanges (eg: ATOM). This can + // be the same as the display. + // + // Since: cosmos-sdk 0.43 + string symbol = 6; + // URI to a document (on or off-chain) that contains additional information. Optional. + // + // Since: cosmos-sdk 0.46 + string uri = 7 [(gogoproto.customname) = "URI"]; + // URIHash is a sha256 hash of a document pointed by URI. It's used to verify that + // the document didn't change. Optional. + // + // Since: cosmos-sdk 0.46 + string uri_hash = 8 [(gogoproto.customname) = "URIHash"]; +} diff --git a/proto/vendor/cosmos/bank/v1beta1/genesis.proto b/proto/vendor/cosmos/bank/v1beta1/genesis.proto new file mode 100644 index 00000000..aa35790b --- /dev/null +++ b/proto/vendor/cosmos/bank/v1beta1/genesis.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package cosmos.bank.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "cosmos/bank/v1beta1/bank.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types"; + +// GenesisState defines the bank module's genesis state. +message GenesisState { + // params defines all the paramaters of the module. + Params params = 1 [(gogoproto.nullable) = false]; + + // balances is an array containing the balances of all the accounts. + repeated Balance balances = 2 [(gogoproto.nullable) = false]; + + // supply represents the total supply. If it is left empty, then supply will be calculated based on the provided + // balances. Otherwise, it will be used to validate that the sum of the balances equals this amount. + repeated cosmos.base.v1beta1.Coin supply = 3 + [(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false]; + + // denom_metadata defines the metadata of the differents coins. + repeated Metadata denom_metadata = 4 [(gogoproto.nullable) = false]; +} + +// Balance defines an account address and balance pair used in the bank module's +// genesis state. +message Balance { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // address is the address of the balance holder. + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // coins defines the different coins this balance holds. + repeated cosmos.base.v1beta1.Coin coins = 2 + [(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false]; +} diff --git a/proto/vendor/cosmos/bank/v1beta1/query.proto b/proto/vendor/cosmos/bank/v1beta1/query.proto new file mode 100644 index 00000000..635471c4 --- /dev/null +++ b/proto/vendor/cosmos/bank/v1beta1/query.proto @@ -0,0 +1,243 @@ +syntax = "proto3"; +package cosmos.bank.v1beta1; + +import "cosmos/base/query/v1beta1/pagination.proto"; +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "cosmos/bank/v1beta1/bank.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types"; + +// Query defines the gRPC querier service. +service Query { + // Balance queries the balance of a single coin for a single account. + rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}/by_denom"; + } + + // AllBalances queries the balance of all coins for a single account. + rpc AllBalances(QueryAllBalancesRequest) returns (QueryAllBalancesResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}"; + } + + // SpendableBalances queries the spenable balance of all coins for a single + // account. + // + // Since: cosmos-sdk 0.46 + rpc SpendableBalances(QuerySpendableBalancesRequest) returns (QuerySpendableBalancesResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/spendable_balances/{address}"; + } + + // TotalSupply queries the total supply of all coins. + rpc TotalSupply(QueryTotalSupplyRequest) returns (QueryTotalSupplyResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/supply"; + } + + // SupplyOf queries the supply of a single coin. + rpc SupplyOf(QuerySupplyOfRequest) returns (QuerySupplyOfResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/supply/by_denom"; + } + + // Params queries the parameters of x/bank module. + rpc Params(QueryParamsRequest) returns (QueryParamsResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/params"; + } + + // DenomsMetadata queries the client metadata of a given coin denomination. + rpc DenomMetadata(QueryDenomMetadataRequest) returns (QueryDenomMetadataResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/denoms_metadata/{denom}"; + } + + // DenomsMetadata queries the client metadata for all registered coin + // denominations. + rpc DenomsMetadata(QueryDenomsMetadataRequest) returns (QueryDenomsMetadataResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/denoms_metadata"; + } + + // DenomOwners queries for all account addresses that own a particular token + // denomination. + // + // Since: cosmos-sdk 0.46 + rpc DenomOwners(QueryDenomOwnersRequest) returns (QueryDenomOwnersResponse) { + option (google.api.http).get = "/cosmos/bank/v1beta1/denom_owners/{denom}"; + } +} + +// QueryBalanceRequest is the request type for the Query/Balance RPC method. +message QueryBalanceRequest { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // address is the address to query balances for. + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // denom is the coin denom to query balances for. + string denom = 2; +} + +// QueryBalanceResponse is the response type for the Query/Balance RPC method. +message QueryBalanceResponse { + // balance is the balance of the coin. + cosmos.base.v1beta1.Coin balance = 1; +} + +// QueryBalanceRequest is the request type for the Query/AllBalances RPC method. +message QueryAllBalancesRequest { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // address is the address to query balances for. + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageRequest pagination = 2; +} + +// QueryAllBalancesResponse is the response type for the Query/AllBalances RPC +// method. +message QueryAllBalancesResponse { + // balances is the balances of all the coins. + repeated cosmos.base.v1beta1.Coin balances = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; + + // pagination defines the pagination in the response. + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QuerySpendableBalancesRequest defines the gRPC request structure for querying +// an account's spendable balances. +// +// Since: cosmos-sdk 0.46 +message QuerySpendableBalancesRequest { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // address is the address to query spendable balances for. + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageRequest pagination = 2; +} + +// QuerySpendableBalancesResponse defines the gRPC response structure for querying +// an account's spendable balances. +// +// Since: cosmos-sdk 0.46 +message QuerySpendableBalancesResponse { + // balances is the spendable balances of all the coins. + repeated cosmos.base.v1beta1.Coin balances = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; + + // pagination defines the pagination in the response. + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryTotalSupplyRequest is the request type for the Query/TotalSupply RPC +// method. +message QueryTotalSupplyRequest { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // pagination defines an optional pagination for the request. + // + // Since: cosmos-sdk 0.43 + cosmos.base.query.v1beta1.PageRequest pagination = 1; +} + +// QueryTotalSupplyResponse is the response type for the Query/TotalSupply RPC +// method +message QueryTotalSupplyResponse { + // supply is the supply of the coins + repeated cosmos.base.v1beta1.Coin supply = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; + + // pagination defines the pagination in the response. + // + // Since: cosmos-sdk 0.43 + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QuerySupplyOfRequest is the request type for the Query/SupplyOf RPC method. +message QuerySupplyOfRequest { + // denom is the coin denom to query balances for. + string denom = 1; +} + +// QuerySupplyOfResponse is the response type for the Query/SupplyOf RPC method. +message QuerySupplyOfResponse { + // amount is the supply of the coin. + cosmos.base.v1beta1.Coin amount = 1 [(gogoproto.nullable) = false]; +} + +// QueryParamsRequest defines the request type for querying x/bank parameters. +message QueryParamsRequest {} + +// QueryParamsResponse defines the response type for querying x/bank parameters. +message QueryParamsResponse { + Params params = 1 [(gogoproto.nullable) = false]; +} + +// QueryDenomsMetadataRequest is the request type for the Query/DenomsMetadata RPC method. +message QueryDenomsMetadataRequest { + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageRequest pagination = 1; +} + +// QueryDenomsMetadataResponse is the response type for the Query/DenomsMetadata RPC +// method. +message QueryDenomsMetadataResponse { + // metadata provides the client information for all the registered tokens. + repeated Metadata metadatas = 1 [(gogoproto.nullable) = false]; + + // pagination defines the pagination in the response. + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryDenomMetadataRequest is the request type for the Query/DenomMetadata RPC method. +message QueryDenomMetadataRequest { + // denom is the coin denom to query the metadata for. + string denom = 1; +} + +// QueryDenomMetadataResponse is the response type for the Query/DenomMetadata RPC +// method. +message QueryDenomMetadataResponse { + // metadata describes and provides all the client information for the requested token. + Metadata metadata = 1 [(gogoproto.nullable) = false]; +} + +// QueryDenomOwnersRequest defines the request type for the DenomOwners RPC query, +// which queries for a paginated set of all account holders of a particular +// denomination. +message QueryDenomOwnersRequest { + // denom defines the coin denomination to query all account holders for. + string denom = 1; + + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageRequest pagination = 2; +} + +// DenomOwner defines structure representing an account that owns or holds a +// particular denominated token. It contains the account address and account +// balance of the denominated token. +// +// Since: cosmos-sdk 0.46 +message DenomOwner { + // address defines the address that owns a particular denomination. + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // balance is the balance of the denominated coin for an account. + cosmos.base.v1beta1.Coin balance = 2 [(gogoproto.nullable) = false]; +} + +// QueryDenomOwnersResponse defines the RPC response of a DenomOwners RPC query. +// +// Since: cosmos-sdk 0.46 +message QueryDenomOwnersResponse { + repeated DenomOwner denom_owners = 1; + + // pagination defines the pagination in the response. + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} diff --git a/proto/vendor/cosmos/bank/v1beta1/tx.proto b/proto/vendor/cosmos/bank/v1beta1/tx.proto new file mode 100644 index 00000000..22e62cbf --- /dev/null +++ b/proto/vendor/cosmos/bank/v1beta1/tx.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package cosmos.bank.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "cosmos/bank/v1beta1/bank.proto"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/msg/v1/msg.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types"; + +// Msg defines the bank Msg service. +service Msg { + // Send defines a method for sending coins from one account to another account. + rpc Send(MsgSend) returns (MsgSendResponse); + + // MultiSend defines a method for sending coins from some accounts to other accounts. + rpc MultiSend(MsgMultiSend) returns (MsgMultiSendResponse); +} + +// MsgSend represents a message to send coins from one account to another. +message MsgSend { + option (cosmos.msg.v1.signer) = "from_address"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string to_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + repeated cosmos.base.v1beta1.Coin amount = 3 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} + +// MsgSendResponse defines the Msg/Send response type. +message MsgSendResponse {} + +// MsgMultiSend represents an arbitrary multi-in, multi-out send message. +message MsgMultiSend { + option (cosmos.msg.v1.signer) = "inputs"; + + option (gogoproto.equal) = false; + + repeated Input inputs = 1 [(gogoproto.nullable) = false]; + repeated Output outputs = 2 [(gogoproto.nullable) = false]; +} + +// MsgMultiSendResponse defines the Msg/MultiSend response type. +message MsgMultiSendResponse {} diff --git a/proto/vendor/cosmos/msg/v1/msg.proto b/proto/vendor/cosmos/msg/v1/msg.proto new file mode 100644 index 00000000..89bdf312 --- /dev/null +++ b/proto/vendor/cosmos/msg/v1/msg.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package cosmos.msg.v1; + +import "google/protobuf/descriptor.proto"; + +// TODO(fdymylja): once we fully migrate to protov2 the go_package needs to be updated. +// We need this right now because gogoproto codegen needs to import the extension. +option go_package = "github.com/cosmos/cosmos-sdk/types/msgservice"; + +extend google.protobuf.MessageOptions { + // signer must be used in cosmos messages in order + // to signal to external clients which fields in a + // given cosmos message must be filled with signer + // information (address). + // The field must be the protobuf name of the message + // field extended with this MessageOption. + // The field must either be of string kind, or of message + // kind in case the signer information is contained within + // a message inside the cosmos message. + repeated string signer = 11110000; +} \ No newline at end of file diff --git a/tools/update-proto-vendor.sh b/tools/update-proto-vendor.sh index bf518b6c..af9a0477 100755 --- a/tools/update-proto-vendor.sh +++ b/tools/update-proto-vendor.sh @@ -48,7 +48,7 @@ cp -r ../target/proto-vendor-src/go-square-main/proto vendor/go-square rm -rf vendor/cosmos mkdir -p vendor/cosmos -cp -r ../target/proto-vendor-src/cosmos-sdk-release-v0.46.x-celestia/proto/cosmos/{auth,base,staking,crypto,tx} vendor/cosmos +cp -r ../target/proto-vendor-src/cosmos-sdk-release-v0.46.x-celestia/proto/cosmos/{auth,bank,base,msg,staking,crypto,tx} vendor/cosmos rm -rf vendor/cosmos_proto cp -r ../target/proto-vendor-src/cosmos-proto-1.0.0-alpha7/proto/cosmos_proto vendor diff --git a/types/Cargo.toml b/types/Cargo.toml index 3084e80e..37da45ba 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -28,6 +28,7 @@ bytes = "1.6.0" cid = { version = "0.11.1", default-features = false, features = ["std"] } const_format = "0.2.32" ed25519-consensus = { version = "2.1.0", optional = true } +enumn = "0.1.14" enum_dispatch = "0.3.13" leopard-codec = "0.1.0" libp2p-identity = { version = "0.2.9", optional = true } @@ -36,7 +37,7 @@ multihash = "0.19.1" rand = { version = "0.8.5", optional = true } ruint = { version = "1.12.3", features = ["serde"] } serde = { version = "1.0.203", features = ["derive"] } -serde_repr = { version = "0.1.19", optional = true } +serde_repr = "0.1.19" sha2 = "0.10.6" thiserror = "1.0.61" time = { version = "0.3.36", default-features = false } @@ -55,7 +56,7 @@ wasm-bindgen-test = "0.3.42" [features] default = ["p2p"] -p2p = ["dep:libp2p-identity", "dep:multiaddr", "dep:serde_repr"] +p2p = ["dep:libp2p-identity", "dep:multiaddr"] test-utils = ["dep:ed25519-consensus", "dep:rand"] wasm-bindgen = ["time/wasm-bindgen"] diff --git a/types/src/blob.rs b/types/src/blob.rs index 697b3bed..232b2b43 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -307,6 +307,33 @@ impl Blob { Ok(blobs) } + + /// Get the amount of shares needed to encode this blob. + /// + /// # Example + /// + /// ``` + /// use celestia_types::{AppVersion, Blob}; + /// # use celestia_types::nmt::Namespace; + /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace"); + /// + /// let blob = Blob::new(namespace1, b"foo".to_vec(), AppVersion::V3).unwrap(), + /// let shares_count = blob.shares_count(); + /// + /// let blob_shares = blob.to_shares(); + /// + /// assert_eq!(shares_count, blob_shares.len()); + /// ``` + pub fn shares_count(&self) -> usize { + let Some(without_first_share) = self + .data + .len() + .checked_sub(appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE) + else { + return 1; + }; + 1 + without_first_share.div_ceil(appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE) + } } impl From for RawBlob { diff --git a/types/src/blob/msg_pay_for_blobs.rs b/types/src/blob/msg_pay_for_blobs.rs index d333cf70..eb08e74a 100644 --- a/types/src/blob/msg_pay_for_blobs.rs +++ b/types/src/blob/msg_pay_for_blobs.rs @@ -1,8 +1,4 @@ -use celestia_proto::celestia::blob::v1::MsgPayForBlobs as RawMsgPayForBlobs; -use celestia_proto::cosmos::tx::v1beta1::TxBody as RawTxBody; -use prost::Name; use serde::{Deserialize, Serialize}; -use tendermint_proto::google::protobuf::Any; use tendermint_proto::Protobuf; use crate::blob::{Blob, Commitment}; @@ -10,6 +6,8 @@ use crate::nmt::Namespace; use crate::state::Address; use crate::{Error, Result}; +pub use celestia_proto::celestia::blob::v1::MsgPayForBlobs as RawMsgPayForBlobs; + /// MsgPayForBlobs pays for the inclusion of a blob in the block. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MsgPayForBlobs { @@ -56,20 +54,6 @@ impl MsgPayForBlobs { } } -impl From for RawTxBody { - fn from(msg: MsgPayForBlobs) -> Self { - let msg_pay_for_blobs_as_any = Any { - type_url: RawMsgPayForBlobs::type_url(), - value: msg.encode_vec(), - }; - - RawTxBody { - messages: vec![msg_pay_for_blobs_as_any], - ..RawTxBody::default() - } - } -} - impl From for RawMsgPayForBlobs { fn from(msg: MsgPayForBlobs) -> Self { let namespaces = msg diff --git a/types/src/consts.rs b/types/src/consts.rs index 9bef0edc..3467db2e 100644 --- a/types/src/consts.rs +++ b/types/src/consts.rs @@ -122,18 +122,20 @@ pub mod appconsts { } /// Cost of each byte in a transaction (in units of gas). - pub const fn tx_size_cost_per_byte(app_version: AppVersion) -> Option { + pub const fn tx_size_cost_per_byte(app_version: AppVersion) -> u64 { + // v1 and v2 don't have this constant because it was taken from cosmos-sdk before. + // The value was the same as in v3 tho, so fall back to it. match app_version { - AppVersion::V1 | AppVersion::V2 => None, - AppVersion::V3 => Some(v3::TX_SIZE_COST_PER_BYTE), + AppVersion::V1 | AppVersion::V2 | AppVersion::V3 => v3::TX_SIZE_COST_PER_BYTE, } } /// Cost of each byte in blob (in units of gas). - pub const fn gas_per_blob_byte(app_version: AppVersion) -> Option { + pub const fn gas_per_blob_byte(app_version: AppVersion) -> u64 { + // In v1 and v2 this const was in appconsts/initial_consts.go rather than being versioned. + // The value was the same as in v3 tho, so fall back to it. match app_version { - AppVersion::V1 | AppVersion::V2 => None, - AppVersion::V3 => Some(v3::GAS_PER_BLOB_BYTE), + AppVersion::V1 | AppVersion::V2 | AppVersion::V3 => v3::GAS_PER_BLOB_BYTE, } } diff --git a/types/src/state.rs b/types/src/state.rs index 562e679b..90606f9b 100644 --- a/types/src/state.rs +++ b/types/src/state.rs @@ -13,8 +13,8 @@ pub use self::query_delegation::{ QueryDelegationResponse, QueryRedelegationsResponse, QueryUnbondingDelegationResponse, }; pub use self::tx::{ - AuthInfo, Coin, Fee, ModeInfo, RawTx, RawTxBody, RawTxResponse, SignerInfo, Sum, Tx, TxBody, - TxResponse, BOND_DENOM, + AuthInfo, Coin, ErrorCode, Fee, ModeInfo, RawTx, RawTxBody, RawTxResponse, SignerInfo, Sum, Tx, + TxBody, TxResponse, BOND_DENOM, }; /// A 256-bit unsigned integer. diff --git a/types/src/state/auth.rs b/types/src/state/auth.rs index cef72dfd..2d1ed7e6 100644 --- a/types/src/state/auth.rs +++ b/types/src/state/auth.rs @@ -8,6 +8,7 @@ use tendermint_proto::google::protobuf::Any; use tendermint_proto::Protobuf; use crate::state::Address; +use crate::validation_error; use crate::Error; pub use celestia_proto::cosmos::auth::v1beta1::BaseAccount as RawBaseAccount; @@ -40,7 +41,7 @@ pub struct BaseAccount { #[derive(Debug, Clone, PartialEq)] pub struct ModuleAccount { /// [`BaseAccount`] specification of this module account. - pub base_account: Option, + pub base_account: BaseAccount, /// Name of the module. pub name: String, /// Permissions associated with this module account. @@ -74,7 +75,7 @@ impl TryFrom for BaseAccount { impl From for RawModuleAccount { fn from(account: ModuleAccount) -> Self { - let base_account = account.base_account.map(BaseAccount::into); + let base_account = Some(account.base_account.into()); RawModuleAccount { base_account, name: account.name, @@ -89,8 +90,8 @@ impl TryFrom for ModuleAccount { fn try_from(account: RawModuleAccount) -> Result { let base_account = account .base_account - .map(RawBaseAccount::try_into) - .transpose()?; + .ok_or_else(|| validation_error!("base account missing"))? + .try_into()?; Ok(ModuleAccount { base_account, name: account.name, diff --git a/types/src/state/tx.rs b/types/src/state/tx.rs index 624fd69b..fd0bbd11 100644 --- a/types/src/state/tx.rs +++ b/types/src/state/tx.rs @@ -1,10 +1,17 @@ +use std::fmt; + use celestia_proto::cosmos::base::abci::v1beta1::AbciMessageLog; use serde::{Deserialize, Serialize}; +use serde_repr::Deserialize_repr; +use serde_repr::Serialize_repr; use tendermint_proto::google::protobuf::Any; use tendermint_proto::v0_34::abci::Event; use tendermint_proto::Protobuf; +use crate::bail_validation; +use crate::hash::Hash; use crate::state::bit_array::BitVector; +use crate::state::Address; use crate::Error; use crate::Height; @@ -76,13 +83,14 @@ pub struct TxResponse { pub height: Height, /// The transaction hash. - pub txhash: String, + #[serde(with = "crate::serializers::hash")] + pub txhash: Hash, /// Namespace for the Code pub codespace: String, /// Response code. - pub code: u32, + pub code: ErrorCode, /// Result bytes, if any. pub data: String, @@ -104,9 +112,6 @@ pub struct TxResponse { pub gas_used: i64, /// The request transaction bytes. - #[serde(skip)] - // caused by prost_types/pbjson_types::Any conditional compilation, should be - // removed once we align on tendermint::Any pub tx: Option, /// Time of the previous block. For heights > 1, it's the weighted median of @@ -197,20 +202,11 @@ pub struct Fee { /// if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees. /// the payer must be a tx signer (and thus have signed this field in AuthInfo). /// setting this field does *not* change the ordering of required signers for the transaction. - pub payer: String, + pub payer: Option
, /// if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used /// to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does /// not support fee grants, this will fail - pub granter: String, -} - -/// Coin defines a token with a denomination and an amount. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct Coin { - /// Coin denomination - pub denom: String, - /// Coin amount - pub amount: u64, + pub granter: Option
, } impl Fee { @@ -229,6 +225,175 @@ impl Fee { } } +/// Coin defines a token with a denomination and an amount. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct Coin { + /// Coin denomination + pub denom: String, + /// Coin amount + pub amount: u64, +} + +impl Coin { + /// Create a coin with given amount of `utia`. + pub fn utia(amount: u64) -> Self { + Self { + denom: "utia".into(), + amount, + } + } +} + +/// Error codes associated with transaction responses. +// source https://github.com/celestiaorg/cosmos-sdk/blob/v1.25.1-sdk-v0.46.16/types/errors/errors.go#L38 +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize_repr, Deserialize_repr)] +#[repr(u32)] +pub enum ErrorCode { + /// No error + Success = 0, + /// Cannot parse a transaction + TxDecode = 2, + /// Sequence number (nonce) is incorrect for the signature + InvalidSequence = 3, + /// Request without sufficient authorization is handled + Unauthorized = 4, + /// Account cannot pay requested amount + InsufficientFunds = 5, + /// Request is unknown + UnknownRequest = 6, + /// Address is invalid + InvalidAddress = 7, + /// Pubkey is invalid + InvalidPubKey = 8, + /// Address is unknown + UnknownAddress = 9, + /// Coin is invalid + InvalidCoins = 10, + /// Gas exceeded + OutOfGas = 11, + /// Memo too large + MemoTooLarge = 12, + /// Fee is insufficient + InsufficientFee = 13, + /// Too many signatures + TooManySignatures = 14, + /// No signatures in transaction + NoSignatures = 15, + /// Error converting to json + JSONMarshal = 16, + /// Error converting from json + JSONUnmarshal = 17, + /// Request contains invalid data + InvalidRequest = 18, + /// Tx already exists in the mempool + TxInMempoolCache = 19, + /// Mempool is full + MempoolIsFull = 20, + /// Tx is too large + TxTooLarge = 21, + /// Key doesn't exist + KeyNotFound = 22, + /// Key password is invalid + WrongPassword = 23, + /// Tx intended signer does not match the given signer + InvalidSigner = 24, + /// Invalid gas adjustment + InvalidGasAdjustment = 25, + /// Invalid height + InvalidHeight = 26, + /// Invalid version + InvalidVersion = 27, + /// Chain-id is invalid + InvalidChainID = 28, + /// Invalid type + InvalidType = 29, + /// Tx rejected due to an explicitly set timeout height + TxTimeoutHeight = 30, + /// Unknown extension options. + UnknownExtensionOptions = 31, + /// Account sequence defined in the signer info doesn't match the account's actual sequence + WrongSequence = 32, + /// Packing a protobuf message to Any failed + PackAny = 33, + /// Unpacking a protobuf message from Any failed + UnpackAny = 34, + /// Internal logic error, e.g. an invariant or assertion that is violated + Logic = 35, + /// Conflict error, e.g. when two goroutines try to access the same resource and one of them fails + Conflict = 36, + /// Called a branch of a code which is currently not supported + NotSupported = 37, + /// Requested entity doesn't exist in the state + NotFound = 38, + /// Internal errors caused by external operation + IO = 39, + /// Min-gas-prices field in BaseConfig is empty + AppConfig = 40, + /// Invalid GasWanted value is supplied + InvalidGasLimit = 41, + /// Node recovered from panic + Panic = 111222, +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{:?}", self) + } +} + +impl TryFrom for ErrorCode { + type Error = Error; + + fn try_from(value: u32) -> Result { + let error_code = match value { + 0 => ErrorCode::Success, + 2 => ErrorCode::TxDecode, + 3 => ErrorCode::InvalidSequence, + 4 => ErrorCode::Unauthorized, + 5 => ErrorCode::InsufficientFunds, + 6 => ErrorCode::UnknownRequest, + 7 => ErrorCode::InvalidAddress, + 8 => ErrorCode::InvalidPubKey, + 9 => ErrorCode::UnknownAddress, + 10 => ErrorCode::InvalidCoins, + 11 => ErrorCode::OutOfGas, + 12 => ErrorCode::MemoTooLarge, + 13 => ErrorCode::InsufficientFee, + 14 => ErrorCode::TooManySignatures, + 15 => ErrorCode::NoSignatures, + 16 => ErrorCode::JSONMarshal, + 17 => ErrorCode::JSONUnmarshal, + 18 => ErrorCode::InvalidRequest, + 19 => ErrorCode::TxInMempoolCache, + 20 => ErrorCode::MempoolIsFull, + 21 => ErrorCode::TxTooLarge, + 22 => ErrorCode::KeyNotFound, + 23 => ErrorCode::WrongPassword, + 24 => ErrorCode::InvalidSigner, + 25 => ErrorCode::InvalidGasAdjustment, + 26 => ErrorCode::InvalidHeight, + 27 => ErrorCode::InvalidVersion, + 28 => ErrorCode::InvalidChainID, + 29 => ErrorCode::InvalidType, + 30 => ErrorCode::TxTimeoutHeight, + 31 => ErrorCode::UnknownExtensionOptions, + 32 => ErrorCode::WrongSequence, + 33 => ErrorCode::PackAny, + 34 => ErrorCode::UnpackAny, + 35 => ErrorCode::Logic, + 36 => ErrorCode::Conflict, + 37 => ErrorCode::NotSupported, + 38 => ErrorCode::NotFound, + 39 => ErrorCode::IO, + 40 => ErrorCode::AppConfig, + 41 => ErrorCode::InvalidGasLimit, + 111222 => ErrorCode::Panic, + _ => bail_validation!("error code ({}) unknown", value), + }; + Ok(error_code) + } +} + impl TryFrom for TxBody { type Error = Error; @@ -289,9 +454,9 @@ impl TryFrom for TxResponse { fn try_from(response: RawTxResponse) -> Result { Ok(TxResponse { height: response.height.try_into()?, - txhash: response.txhash, + txhash: response.txhash.parse()?, codespace: response.codespace, - code: response.code, + code: response.code.try_into()?, data: response.data, raw_log: response.raw_log, logs: response.logs, @@ -336,11 +501,16 @@ impl TryFrom for Fee { .into_iter() .map(TryInto::try_into) .collect::>()?; + Ok(Fee { amount, gas_limit: value.gas_limit, - payer: value.payer, - granter: value.granter, + payer: (!value.payer.is_empty()) + .then(|| value.payer.parse()) + .transpose()?, + granter: (!value.granter.is_empty()) + .then(|| value.granter.parse()) + .transpose()?, }) } } @@ -351,8 +521,8 @@ impl From for RawFee { RawFee { amount, gas_limit: value.gas_limit, - payer: value.payer, - granter: value.granter, + payer: value.payer.map(|acc| acc.to_string()).unwrap_or_default(), + granter: value.granter.map(|acc| acc.to_string()).unwrap_or_default(), } } } From 74e461dbabe69cc226205ce38833833ef48ba042 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 19 Dec 2024 14:16:31 +0100 Subject: [PATCH 06/11] remove sleep from tests --- grpc/tests/utils/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 759a5b52..d86406e0 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -46,7 +46,6 @@ pub fn load_account() -> TestAccount { #[cfg(not(target_arch = "wasm32"))] mod imp { - use std::time::Duration; use std::{future::Future, sync::OnceLock}; use celestia_grpc::{GrpcClient, TxClient}; @@ -85,10 +84,6 @@ mod imp { (lock, client) } - pub async fn sleep(duration: Duration) { - tokio::time::sleep(duration).await; - } - pub fn spawn(future: F) -> tokio::task::JoinHandle<()> where F: Future + Send + 'static, @@ -100,7 +95,6 @@ mod imp { #[cfg(target_arch = "wasm32")] mod imp { use std::future::Future; - use std::time::Duration; use celestia_grpc::{GrpcClient, TxClient}; use tokio::sync::oneshot; From 3feb07f4e198fac7033ab0c237d5d909d6194237 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 19 Dec 2024 14:24:04 +0100 Subject: [PATCH 07/11] remove unused deps --- Cargo.lock | 12 ------------ grpc/Cargo.toml | 2 +- types/Cargo.toml | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef6d39ec..74b41054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,7 +840,6 @@ dependencies = [ "const_format", "ed25519-consensus", "enum_dispatch", - "enumn", "getrandom", "indoc", "leopard-codec", @@ -1437,17 +1436,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "equivalent" version = "1.0.1" diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 79eecbd1..bdde5c89 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -27,7 +27,6 @@ tendermint-proto.workspace = true tendermint.workspace = true bytes = "1.8" -futures = "0.3.30" hex = "0.4.3" http-body = "1" k256 = "0.13.4" @@ -44,6 +43,7 @@ tokio = { version = "1.38.0", features = ["time"] } tonic = { version = "0.12.3", default-features = false, features = [ "transport" ] } [target.'cfg(target_arch = "wasm32")'.dependencies] +futures = "0.3.30" getrandom = { version = "0.2.15", features = ["js"] } gloo-timers = { version = "0.3.0", features = ["futures"] } send_wrapper = { version = "0.6.0", features = ["futures"] } diff --git a/types/Cargo.toml b/types/Cargo.toml index 37da45ba..23ff0108 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -28,7 +28,6 @@ bytes = "1.6.0" cid = { version = "0.11.1", default-features = false, features = ["std"] } const_format = "0.2.32" ed25519-consensus = { version = "2.1.0", optional = true } -enumn = "0.1.14" enum_dispatch = "0.3.13" leopard-codec = "0.1.0" libp2p-identity = { version = "0.2.9", optional = true } From 66bef5b1fd4c9db27043e1cffd79ed1a7acf9579 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 19 Dec 2024 14:25:43 +0100 Subject: [PATCH 08/11] rename shares_count to shares_len --- grpc/src/tx.rs | 2 +- types/src/blob.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index b533ef45..20603929 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -532,7 +532,7 @@ fn estimate_gas(blobs: &[Blob], app_version: AppVersion, gas_multiplier: f64) -> let tx_size_cost_per_byte = appconsts::tx_size_cost_per_byte(app_version); let blobs_bytes = - blobs.iter().map(Blob::shares_count).sum::() as u64 * appconsts::SHARE_SIZE as u64; + blobs.iter().map(Blob::shares_len).sum::() as u64 * appconsts::SHARE_SIZE as u64; let gas = blobs_bytes * gas_per_blob_byte + (tx_size_cost_per_byte * BYTES_PER_BLOB_INFO * blobs.len() as u64) diff --git a/types/src/blob.rs b/types/src/blob.rs index 232b2b43..9ad2ff30 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -317,14 +317,14 @@ impl Blob { /// # use celestia_types::nmt::Namespace; /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace"); /// - /// let blob = Blob::new(namespace1, b"foo".to_vec(), AppVersion::V3).unwrap(), - /// let shares_count = blob.shares_count(); + /// let blob = Blob::new(namespace1, b"foo".to_vec(), AppVersion::V3).unwrap(); + /// let shares_len = blob.shares_len(); /// /// let blob_shares = blob.to_shares(); /// - /// assert_eq!(shares_count, blob_shares.len()); + /// assert_eq!(shares_len, blob_shares.len()); /// ``` - pub fn shares_count(&self) -> usize { + pub fn shares_len(&self) -> usize { let Some(without_first_share) = self .data .len() From 6b6b820552106c38b39e99694b85912bb28e9274 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 19 Dec 2024 14:30:46 +0100 Subject: [PATCH 09/11] add debug impls --- grpc/src/grpc.rs | 8 ++++++++ grpc/src/tx.rs | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/grpc/src/grpc.rs b/grpc/src/grpc.rs index cb243245..c774dbfc 100644 --- a/grpc/src/grpc.rs +++ b/grpc/src/grpc.rs @@ -1,5 +1,7 @@ //! Types and client for the celestia grpc +use std::fmt; + use bytes::Bytes; use celestia_grpc_macros::grpc_method; use celestia_proto::celestia::blob::v1::query_client::QueryClient as BlobQueryClient; @@ -163,6 +165,12 @@ impl GrpcClient { } } +impl fmt::Debug for GrpcClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("GrpcClient { .. }") + } +} + pub(crate) trait FromGrpcResponse { fn try_from_response(self) -> Result; } diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 20603929..8a4294c5 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::ops::Deref; use std::sync::{LazyLock, RwLock}; use std::time::Duration; @@ -475,6 +476,12 @@ impl Deref for TxClient { } } +impl fmt::Debug for TxClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("TxClient { .. }") + } +} + /// Sign `tx_body` and the transaction metadata as the `base_account` using `signer` pub fn sign_tx( tx_body: RawTxBody, From 201f091daa675b38ba53f8b0aad1115dc735bc77 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 19 Dec 2024 14:51:18 +0100 Subject: [PATCH 10/11] fix blob shares_len doctest --- types/src/blob.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/src/blob.rs b/types/src/blob.rs index 9ad2ff30..9dbdea7b 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -317,10 +317,10 @@ impl Blob { /// # use celestia_types::nmt::Namespace; /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace"); /// - /// let blob = Blob::new(namespace1, b"foo".to_vec(), AppVersion::V3).unwrap(); + /// let blob = Blob::new(namespace, b"foo".to_vec(), AppVersion::V3).unwrap(); /// let shares_len = blob.shares_len(); /// - /// let blob_shares = blob.to_shares(); + /// let blob_shares = blob.to_shares().unwrap(); /// /// assert_eq!(shares_len, blob_shares.len()); /// ``` From 138fec19fc3c0524a3a0ae4e2a6021beeadb90bc Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 20 Dec 2024 11:10:23 +0100 Subject: [PATCH 11/11] match errors in tests --- grpc/tests/tonic.rs | 48 +++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index d10315fc..483a5822 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use celestia_grpc::TxConfig; +use celestia_grpc::{Error, TxConfig}; use celestia_proto::cosmos::bank::v1beta1::MsgSend; use celestia_types::nmt::Namespace; -use celestia_types::state::Coin; +use celestia_types::state::{Coin, ErrorCode}; use celestia_types::{AppVersion, Blob}; use utils::{load_account, TestAccount}; @@ -144,15 +144,23 @@ async fn submit_blobs_insufficient_gas_price_and_limit() { let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; - tx_client + let err = tx_client .submit_blobs(&blobs, TxConfig::default().with_gas_limit(10000)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _, _) + )); - tx_client + let err = tx_client .submit_blobs(&blobs, TxConfig::default().with_gas_price(0.0005)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) + )); } #[async_test] @@ -165,10 +173,14 @@ async fn submit_blobs_gas_price_update() { tx_client.set_gas_price(0.0005); // if user also set gas price, no update should happen - tx_client - .submit_blobs(&blobs, TxConfig::default().with_gas_limit(10000)) + let err = tx_client + .submit_blobs(&blobs, TxConfig::default().with_gas_price(0.0006)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) + )); assert_eq!(tx_client.gas_price(), 0.0005); // with default config, gas price should be updated @@ -176,7 +188,7 @@ async fn submit_blobs_gas_price_update() { .submit_blobs(&blobs, TxConfig::default()) .await .unwrap(); - assert_ne!(tx_client.gas_price(), 0.0005); + assert!(tx_client.gas_price() > 0.0005); } #[async_test] @@ -219,15 +231,23 @@ async fn submit_message_insufficient_gas_price_and_limit() { amount: vec![amount.clone().into()], }; - tx_client + let err = tx_client .submit_message(msg.clone(), TxConfig::default().with_gas_limit(10000)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _, _) + )); - tx_client + let err = tx_client .submit_message(msg, TxConfig::default().with_gas_price(0.0005)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) + )); } #[async_test] @@ -246,10 +266,14 @@ async fn submit_message_gas_price_update() { tx_client.set_gas_price(0.0005); // if user also set gas price, no update should happen - tx_client - .submit_message(msg.clone(), TxConfig::default().with_gas_limit(10000)) + let err = tx_client + .submit_message(msg.clone(), TxConfig::default().with_gas_price(0.0006)) .await .unwrap_err(); + assert!(matches!( + err, + Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) + )); assert_eq!(tx_client.gas_price(), 0.0005); // with default config, gas price should be updated @@ -257,5 +281,5 @@ async fn submit_message_gas_price_update() { .submit_message(msg, TxConfig::default()) .await .unwrap(); - assert_ne!(tx_client.gas_price(), 0.0005); + assert!(tx_client.gas_price() > 0.0005); }