From 999138b06bfbbae2d6b8a9a59fada6f9e7739183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 28 Nov 2024 09:39:26 +0100 Subject: [PATCH 01/50] feat(node-wasm, types)!: Add blob service for wasm --- Cargo.lock | 208 ++++++++++++++++++++++++++-- node-wasm/Cargo.toml | 2 +- node-wasm/src/client.rs | 51 +++++++ node-wasm/src/commands.rs | 9 ++ node-wasm/src/worker.rs | 25 ++++ node/Cargo.toml | 2 + rpc/tests/blob.rs | 7 +- types/Cargo.toml | 3 +- types/src/blob.rs | 6 +- types/src/blob/commitment.rs | 21 ++- types/src/blob/msg_pay_for_blobs.rs | 8 +- types/src/nmt.rs | 3 + 12 files changed, 321 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f6544e4..bd898ba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,21 @@ dependencies = [ "bytes", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -498,6 +513,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "backon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" +dependencies = [ + "fastrand", + "gloo-timers 0.3.0", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -911,6 +937,7 @@ dependencies = [ "sha2 0.10.8", "thiserror", "time", + "wasm-bindgen", "wasm-bindgen-test", ] @@ -962,7 +989,10 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets 0.52.5", ] [[package]] @@ -1596,6 +1626,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flex-error" version = "0.4.4" @@ -2187,6 +2223,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 0.26.2", ] [[package]] @@ -2221,6 +2258,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2505,7 +2565,7 @@ dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -3486,6 +3546,7 @@ dependencies = [ "libp2p", "libp2p-websocket-websys 0.3.3", "lumina-node", + "opendal", "pin-project", "prost", "rand", @@ -3566,6 +3627,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -4011,6 +4082,34 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "opendal" +version = "0.50.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb28bb6c64e116ceaf8dd4e87099d3cfea4a58e85e62b104fef74c91afba0f44" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "base64 0.22.1", + "bytes", + "chrono", + "flagset", + "futures", + "getrandom", + "http 1.1.0", + "log", + "md-5", + "once_cell", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -4499,6 +4598,16 @@ dependencies = [ "unsigned-varint 0.8.0", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.2" @@ -4692,6 +4801,50 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.5.1", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.26.2", + "winreg 0.52.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -6040,6 +6193,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" @@ -6085,9 +6248,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -6096,9 +6259,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -6123,9 +6286,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6133,9 +6296,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -6146,9 +6309,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-bindgen-test" @@ -6176,6 +6339,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.70" @@ -6425,6 +6601,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 4c39e14e..37589407 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -25,7 +25,7 @@ crate-type = ["cdylib", "rlib"] [target.'cfg(target_arch = "wasm32")'.dependencies] blockstore.workspace = true celestia-tendermint.workspace = true -celestia-types.workspace = true +celestia-types = { workspace = true, features = ["wasm-bindgen"] } libp2p = { workspace = true, features = ["serde"] } lumina-node.workspace = true diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index d9eaafd5..e80000b6 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -2,6 +2,8 @@ use std::time::Duration; +use celestia_types::nmt::Namespace; +use celestia_types::Blob; use js_sys::Array; use libp2p::identity::Keypair; use serde::{Deserialize, Serialize}; @@ -257,6 +259,24 @@ impl NodeClient { headers.into() } + /// Request all blobs with provided namespace in the block corresponding to this header + /// using bitswap protocol. + #[wasm_bindgen(js_name = requestAllBlobs)] + pub async fn request_all_blobs( + &self, + header: JsValue, + namespace: Namespace, + timeout_secs: Option, + ) -> Result> { + let command = NodeCommand::RequestAllBlobs { + header, + namespace, + timeout_secs, + }; + let response = self.worker.exec(command).await?; + response.into_blobs().check_variant()? + } + /// Get current header syncing info. #[wasm_bindgen(js_name = syncerInfo)] pub async fn syncer_info(&self) -> Result { @@ -436,6 +456,7 @@ mod tests { use libp2p::{multiaddr::Protocol, Multiaddr}; use rexie::Rexie; use serde_wasm_bindgen::from_value; + use tracing::info; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::wasm_bindgen_test; use web_sys::MessageChannel; @@ -493,6 +514,36 @@ mod tests { .unwrap(); } + #[wasm_bindgen_test] + async fn get_blob() { + crate::utils::setup_logging(); + remove_database().await.expect("failed to clear db"); + let rpc_client = Client::new(WS_URL).await.unwrap(); + let bridge_ma = fetch_bridge_webtransport_multiaddr(&rpc_client).await; + let client = spawn_connected_node(vec![bridge_ma.to_string()]).await; + + let namespace = Namespace::new_v0(&[0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF]).unwrap(); + let mut found = None; + // TODO: once gRPC blob submission works from browser, submit blob and get its height that + // way. Otherwise, this looks for a blob submitted from the RPC tests `rpc/tests/blob.rs` + 'find_header: for h in 1..=500 { + info!("HH: {h}"); + let header = rpc_client.header_get_by_height(h).await.unwrap(); + for row in header.dah.row_roots() { + if row.min_namespace() < *namespace && *namespace < row.max_namespace() { + found = Some(header.clone()); + break 'find_header; + } + } + } + let header = found.expect("blob to exists"); + + let blob = client + .request_all_blobs(to_value(&header).unwrap(), namespace, None) + .await + .unwrap(); + } + async fn spawn_connected_node(bootnodes: Vec) -> NodeClient { let message_channel = MessageChannel::new().unwrap(); let mut worker = NodeWorker::new(message_channel.port1().into()); diff --git a/node-wasm/src/commands.rs b/node-wasm/src/commands.rs index 9f096aa0..04253e85 100644 --- a/node-wasm/src/commands.rs +++ b/node-wasm/src/commands.rs @@ -1,5 +1,7 @@ use std::fmt::Debug; +use celestia_types::nmt::Namespace; +use celestia_types::Blob; use enum_as_inner::EnumAsInner; use js_sys::Array; use libp2p::Multiaddr; @@ -53,6 +55,12 @@ pub(crate) enum NodeCommand { GetSamplingMetadata { height: u64, }, + RequestAllBlobs { + #[serde(with = "serde_wasm_bindgen::preserve")] + header: JsValue, + namespace: Namespace, + timeout_secs: Option, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -82,6 +90,7 @@ pub(crate) enum WorkerResponse { Headers(JsResult), LastSeenNetworkHead(JsResult), SamplingMetadata(Result>), + Blobs(Result>), } pub(crate) trait CheckableResponseExt { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index b855624d..f7b8d88c 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,5 +1,8 @@ use std::fmt::Debug; +use std::time::Duration; +use celestia_types::nmt::Namespace; +use celestia_types::Blob; use js_sys::Array; use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; @@ -246,6 +249,20 @@ impl NodeWorkerInstance { Ok(self.node.get_sampling_metadata(height).await?) } + async fn request_all_blobs( + &mut self, + header: JsValue, + namespace: Namespace, + timeout_secs: Option, + ) -> Result> { + let header = from_value(header)?; + let timeout = timeout_secs.map(Duration::from_secs_f64); + Ok(self + .node + .request_all_blobs(&header, namespace, timeout) + .await?) + } + async fn process_command(&mut self, command: NodeCommand) -> WorkerResponse { match command { NodeCommand::IsRunning => WorkerResponse::IsRunning(true), @@ -301,6 +318,14 @@ impl NodeWorkerInstance { NodeCommand::GetSamplingMetadata { height } => { WorkerResponse::SamplingMetadata(self.get_sampling_metadata(height).await) } + NodeCommand::RequestAllBlobs { + header, + namespace, + timeout_secs, + } => WorkerResponse::Blobs( + self.request_all_blobs(header, namespace, timeout_secs) + .await, + ), NodeCommand::InternalPing => WorkerResponse::InternalPong, } } diff --git a/node/Cargo.toml b/node/Cargo.toml index ed0910db..15ad609f 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -54,6 +54,8 @@ void = "1.0.2" web-time = "1.1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +opendal = "0.50.2" + backoff = { version = "0.4.0", features = ["tokio"] } blockstore = { workspace = true, features = ["redb"] } tokio = { version = "1.38.0", features = ["fs", "rt-multi-thread", "time"] } diff --git a/rpc/tests/blob.rs b/rpc/tests/blob.rs index 57577270..89abf0d7 100644 --- a/rpc/tests/blob.rs +++ b/rpc/tests/blob.rs @@ -6,6 +6,7 @@ use std::time::Duration; use celestia_rpc::blob::BlobsAtHeight; use celestia_rpc::prelude::*; use celestia_types::consts::appconsts::AppVersion; +use celestia_types::nmt::Namespace; use celestia_types::{Blob, Commitment}; use jsonrpsee::core::client::Subscription; @@ -17,7 +18,7 @@ use crate::utils::{random_bytes, random_bytes_array, random_ns}; #[tokio::test] async fn blob_submit_and_get() { let client = new_test_client(AuthLevel::Write).await.unwrap(); - let namespace = random_ns(); + let namespace = Namespace::new_v0(&[0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF]).unwrap(); let data = random_bytes(5); let blob = Blob::new(namespace, data, AppVersion::V2).unwrap(); @@ -194,7 +195,9 @@ async fn blob_get_get_proof_wrong_commitment() { let namespace = random_ns(); let data = random_bytes(5); let blob = Blob::new(namespace, data, AppVersion::V2).unwrap(); - let commitment = Commitment(random_bytes_array()); + let commitment = Commitment { + hash: random_bytes_array(), + }; let submitted_height = blob_submit(&client, &[blob.clone()]).await.unwrap(); diff --git a/types/Cargo.toml b/types/Cargo.toml index c2363c49..68050c6c 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -41,6 +41,7 @@ serde = { version = "1.0.203", features = ["derive"] } serde_repr = { version = "0.1.19", optional = true } sha2 = "0.10.6" thiserror = "1.0.61" +wasm-bindgen = { version = "0.2.95", optional = true } # `time` is a dependency of a dependency but we need to specify it # for fixing rust-lang/rust#125319. @@ -62,7 +63,7 @@ wasm-bindgen-test = "0.3.42" default = ["p2p"] p2p = ["dep:libp2p-identity", "dep:multiaddr", "dep:serde_repr"] test-utils = ["dep:ed25519-consensus", "dep:rand"] -wasm-bindgen = ["celestia-tendermint/wasm-bindgen"] +wasm-bindgen = ["celestia-tendermint/wasm-bindgen", "dep:wasm-bindgen"] tonic = ["dep:pbjson-types", "celestia-proto/tonic"] [package.metadata.docs.rs] diff --git a/types/src/blob.rs b/types/src/blob.rs index e0a63cf2..ecffb1b5 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -17,12 +17,15 @@ pub use self::msg_pay_for_blobs::MsgPayForBlobs; pub use celestia_proto::celestia::blob::v1::MsgPayForBlobs as RawMsgPayForBlobs; pub use celestia_tendermint_proto::v0_34::types::Blob as RawBlob; pub use celestia_tendermint_proto::v0_34::types::BlobTx as RawBlobTx; +#[cfg(feature = "wasm-bindgen")] +use wasm_bindgen::prelude::*; /// Arbitrary data that can be stored in the network within certain [`Namespace`]. // NOTE: We don't use the `serde(try_from)` pattern for this type // becase JSON representation needs to have `commitment` field but // Protobuf definition doesn't. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "wasm-bindgen", wasm_bindgen(getter_with_clone))] pub struct Blob { /// A [`Namespace`] the [`Blob`] belongs to. pub namespace: Namespace, @@ -35,6 +38,7 @@ pub struct Blob { pub share_version: u8, /// A [`Commitment`] computed from the [`Blob`]s data. pub commitment: Commitment, + /// Index of the blob's first share in the EDS. Only set for blobs retrieved from chain. // note: celestia supports deserializing blobs without index, so we should too #[serde(default, with = "index_serde")] @@ -394,7 +398,7 @@ mod tests { #[test] fn validate_blob_commitment_mismatch() { let mut blob = sample_blob(); - blob.commitment.0.fill(7); + blob.commitment.hash.fill(7); blob.validate(AppVersion::V2).unwrap_err(); } diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index a8242f51..ac393be9 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -8,6 +8,8 @@ use celestia_tendermint::{crypto, merkle}; use celestia_tendermint_proto::serializers::cow_str::CowStr; use nmt_rs::NamespaceMerkleHasher; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "wasm-bindgen")] +use wasm_bindgen::prelude::*; use crate::consts::appconsts; use crate::nmt::{Namespace, NamespacedHashExt, NamespacedSha2Hasher, Nmt, RawNamespacedHash}; @@ -50,8 +52,19 @@ use crate::{InfoByte, Share}; /// [`Nmt`]: crate::nmt::Nmt /// [`ExtendedDataSquare`]: crate::ExtendedDataSquare /// [`share commitment rules`]: https://github.com/celestiaorg/celestia-app/blob/main/specs/src/specs/data_square_layout.md#blob-share-commitment-rules + +#[cfg(not(feature = "wasm-bindgen"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Commitment { + pub hash: merkle::Hash, +} +#[cfg(feature = "wasm-bindgen")] +#[wasm_bindgen] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Commitment(pub merkle::Hash); +pub struct Commitment { + #[wasm_bindgen(skip)] + pub hash: merkle::Hash, +} impl Commitment { /// Generate the share commitment from the given blob data. @@ -101,7 +114,7 @@ impl Commitment { let hash = merkle::simple_hash_from_byte_vectors::(&subtree_roots); - Ok(Commitment(hash)) + Ok(Commitment { hash }) } } @@ -110,7 +123,7 @@ impl Serialize for Commitment { where S: Serializer, { - let s = BASE64_STANDARD.encode(self.0); + let s = BASE64_STANDARD.encode(self.hash); serializer.serialize_str(&s) } } @@ -133,7 +146,7 @@ impl<'de> Deserialize<'de> for Commitment { .try_into() .map_err(|_| serde::de::Error::custom("commitment is not a size of a sha256"))?; - Ok(Commitment(hash)) + Ok(Commitment { hash }) } } diff --git a/types/src/blob/msg_pay_for_blobs.rs b/types/src/blob/msg_pay_for_blobs.rs index 2fe76801..517ca4b2 100644 --- a/types/src/blob/msg_pay_for_blobs.rs +++ b/types/src/blob/msg_pay_for_blobs.rs @@ -94,7 +94,7 @@ impl From for RawMsgPayForBlobs { let share_commitments = msg .share_commitments .into_iter() - .map(|c| c.0.to_vec()) + .map(|c| c.hash.to_vec()) .collect(); RawMsgPayForBlobs { @@ -120,9 +120,9 @@ impl TryFrom for MsgPayForBlobs { .share_commitments .into_iter() .map(|c| { - Ok(Commitment( - c.try_into().map_err(|_| Error::InvalidComittmentLength)?, - )) + Ok(Commitment { + hash: c.try_into().map_err(|_| Error::InvalidComittmentLength)?, + }) }) .collect::>()?; diff --git a/types/src/nmt.rs b/types/src/nmt.rs index c3d8d0c2..37bf48f6 100644 --- a/types/src/nmt.rs +++ b/types/src/nmt.rs @@ -24,6 +24,8 @@ use celestia_tendermint_proto::serializers::cow_str::CowStr; use cid::CidGeneric; use multihash::Multihash; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "wasm-bindgen")] +use wasm_bindgen::prelude::*; mod namespace_proof; mod namespaced_hash; @@ -91,6 +93,7 @@ pub type Proof = nmt_rs::simple_merkle::proof::Proof; /// - secondary reserved namespaces - those use version `0xff` so they are always placed after /// user-submitted data. #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "wasm-bindgen", wasm_bindgen)] pub struct Namespace(nmt_rs::NamespaceId); impl Namespace { From a8a1b20329f1c16fb8e1b32514e98a6e707f87d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 29 Nov 2024 09:40:40 +0100 Subject: [PATCH 02/50] fix wasm ci --- Cargo.lock | 196 +------------------------- node/Cargo.toml | 2 - types/src/blob.rs | 7 +- types/src/blob/commitment.rs | 9 +- types/src/data_availability_header.rs | 2 +- types/src/nmt.rs | 4 +- 6 files changed, 14 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b319b74..068f7a55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,21 +89,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.14" @@ -513,17 +498,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backon" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" -dependencies = [ - "fastrand", - "gloo-timers 0.3.0", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.73" @@ -920,18 +894,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "windows-targets 0.52.5", -] - [[package]] name = "cid" version = "0.11.1" @@ -1553,12 +1515,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flagset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" - [[package]] name = "flex-error" version = "0.4.4" @@ -2149,7 +2105,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 0.26.2", ] [[package]] @@ -2184,29 +2139,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -2485,7 +2417,7 @@ dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] @@ -3464,7 +3396,6 @@ dependencies = [ "libp2p", "libp2p-websocket-websys 0.3.3", "lumina-node", - "opendal", "pin-project", "prost", "rand", @@ -3547,16 +3478,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "memchr" version = "2.7.4" @@ -3991,34 +3912,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "opendal" -version = "0.50.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb28bb6c64e116ceaf8dd4e87099d3cfea4a58e85e62b104fef74c91afba0f44" -dependencies = [ - "anyhow", - "async-trait", - "backon", - "base64", - "bytes", - "chrono", - "flagset", - "futures", - "getrandom", - "http 1.1.0", - "log", - "md-5", - "once_cell", - "percent-encoding", - "quick-xml", - "reqwest", - "serde", - "serde_json", - "tokio", - "uuid", -] - [[package]] name = "openssl-probe" version = "0.1.5" @@ -4470,16 +4363,6 @@ dependencies = [ "unsigned-varint 0.8.0", ] -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quinn" version = "0.11.2" @@ -4673,50 +4556,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "reqwest" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.5.1", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tokio-rustls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 0.26.2", - "winreg 0.52.0", -] - [[package]] name = "resolv-conf" version = "0.7.0" @@ -6110,16 +5949,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" -dependencies = [ - "getrandom", - "serde", -] - [[package]] name = "valuable" version = "0.1.0" @@ -6256,19 +6085,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "wasm-streams" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.70" @@ -6518,16 +6334,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "write16" version = "1.0.0" diff --git a/node/Cargo.toml b/node/Cargo.toml index b52b8662..38ba4d8a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -54,8 +54,6 @@ void = "1.0.2" web-time = "1.1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -opendal = "0.50.2" - backoff = { version = "0.4.0", features = ["tokio"] } blockstore = { workspace = true, features = ["redb"] } tokio = { version = "1.38.0", features = ["fs", "rt-multi-thread", "time"] } diff --git a/types/src/blob.rs b/types/src/blob.rs index 5313d714..184fcd2e 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -17,7 +17,7 @@ pub use self::msg_pay_for_blobs::MsgPayForBlobs; pub use celestia_proto::celestia::blob::v1::MsgPayForBlobs as RawMsgPayForBlobs; pub use celestia_proto::proto::blob::v1::BlobProto as RawBlob; pub use celestia_proto::proto::blob::v1::BlobTx as RawBlobTx; -#[cfg(feature = "wasm-bindgen")] +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] use wasm_bindgen::prelude::*; /// Arbitrary data that can be stored in the network within certain [`Namespace`]. @@ -25,7 +25,10 @@ use wasm_bindgen::prelude::*; // becase JSON representation needs to have `commitment` field but // Protobuf definition doesn't. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "wasm-bindgen", wasm_bindgen(getter_with_clone))] +#[cfg_attr( + all(feature = "wasm-bindgen", target_arch = "wasm32"), + wasm_bindgen(getter_with_clone) +)] pub struct Blob { /// A [`Namespace`] the [`Blob`] belongs to. pub namespace: Namespace, diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index 7bbf13e9..4fe317fb 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -8,7 +8,7 @@ use nmt_rs::NamespaceMerkleHasher; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tendermint::crypto::sha256::HASH_SIZE; use tendermint::{crypto, merkle}; -#[cfg(feature = "wasm-bindgen")] +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] use wasm_bindgen::prelude::*; use crate::consts::appconsts; @@ -52,16 +52,17 @@ use crate::{InfoByte, Share}; /// [`Nmt`]: crate::nmt::Nmt /// [`ExtendedDataSquare`]: crate::ExtendedDataSquare /// [`share commitment rules`]: https://github.com/celestiaorg/celestia-app/blob/main/specs/src/specs/data_square_layout.md#blob-share-commitment-rules - -#[cfg(not(feature = "wasm-bindgen"))] +#[cfg(not(all(feature = "wasm-bindgen", target_arch = "wasm32")))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { + /// hash of the commitment pub hash: merkle::Hash, } -#[cfg(feature = "wasm-bindgen")] +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[wasm_bindgen] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { + /// hash of the commitment #[wasm_bindgen(skip)] pub hash: merkle::Hash, } diff --git a/types/src/data_availability_header.rs b/types/src/data_availability_header.rs index dbe5e2ac..4d2343c5 100644 --- a/types/src/data_availability_header.rs +++ b/types/src/data_availability_header.rs @@ -208,7 +208,7 @@ impl DataAvailabilityHeader { proofs.push(MerkleProof::new(idx as usize, &all_roots)?.0); let row = self .row_root(idx) - .ok_or_else(|| Error::IndexOutOfRange(idx as usize, self.row_roots.len()))?; + .ok_or(Error::IndexOutOfRange(idx as usize, self.row_roots.len()))?; row_roots.push(row); } diff --git a/types/src/nmt.rs b/types/src/nmt.rs index 80aab40d..00a3a4fe 100644 --- a/types/src/nmt.rs +++ b/types/src/nmt.rs @@ -24,7 +24,7 @@ use cid::CidGeneric; use multihash::Multihash; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tendermint::hash::SHA256_HASH_SIZE; -#[cfg(feature = "wasm-bindgen")] +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] use wasm_bindgen::prelude::*; mod namespace_proof; @@ -93,7 +93,7 @@ pub type Proof = nmt_rs::simple_merkle::proof::Proof; /// - secondary reserved namespaces - those use version `0xff` so they are always placed after /// user-submitted data. #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -#[cfg_attr(feature = "wasm-bindgen", wasm_bindgen)] +#[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen)] pub struct Namespace(nmt_rs::NamespaceId); impl Namespace { From 8307ddcff6af8ad464c0519b6be8a203ff62cbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 29 Nov 2024 10:04:50 +0100 Subject: [PATCH 03/50] new rust, more CI --- grpc/tests/tonic.rs | 2 +- grpc/tests/utils.rs | 7 ++++--- node/src/block_ranges.rs | 2 +- node/src/store/utils.rs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 07d1387a..486de7cf 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -7,7 +7,7 @@ use celestia_types::blob::MsgPayForBlobs; use celestia_types::nmt::Namespace; use celestia_types::{AppVersion, Blob}; -pub mod utils; +mod utils; use crate::utils::{load_account, new_test_client}; diff --git a/grpc/tests/utils.rs b/grpc/tests/utils.rs index 0ac2acdf..8289c5d7 100644 --- a/grpc/tests/utils.rs +++ b/grpc/tests/utils.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +//! Utilities for grpc tests use std::{env, fs}; @@ -51,11 +52,11 @@ impl TestAuthInterceptor { } } -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> { +pub(crate) async 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?; @@ -64,7 +65,7 @@ pub async fn new_test_client() -> Result> { Ok(GrpcClient::new(grpc_channel, auth_interceptor)) } -pub fn load_account(path: &str) -> TestAccount { +pub(crate) fn load_account(path: &str) -> TestAccount { let account_file = format!("{path}.addr"); let key_file = format!("{path}.plaintext-key"); diff --git a/node/src/block_ranges.rs b/node/src/block_ranges.rs index 5d3c573b..7e208e3b 100644 --- a/node/src/block_ranges.rs +++ b/node/src/block_ranges.rs @@ -49,7 +49,7 @@ pub(crate) trait BlockRangeExt { pub(crate) struct BlockRangeDisplay<'a>(&'a RangeInclusive); -impl<'a> Display for BlockRangeDisplay<'a> { +impl Display for BlockRangeDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}-{}", self.0.start(), self.0.end()) } diff --git a/node/src/store/utils.rs b/node/src/store/utils.rs index 63beb893..ee40405b 100644 --- a/node/src/store/utils.rs +++ b/node/src/store/utils.rs @@ -55,7 +55,7 @@ impl From for VerifiedExtendedHeaders { } } -impl<'a> From<&'a ExtendedHeader> for VerifiedExtendedHeaders { +impl From<&ExtendedHeader> for VerifiedExtendedHeaders { fn from(value: &ExtendedHeader) -> Self { Self(vec![value.to_owned()]) } From 795e6acddce64889a6d0bc3ec9779ff19001cd5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 29 Nov 2024 10:12:25 +0100 Subject: [PATCH 04/50] fix unused --- grpc/tests/tonic.rs | 1 + grpc/tests/{utils.rs => utils/mod.rs} | 0 2 files changed, 1 insertion(+) rename grpc/tests/{utils.rs => utils/mod.rs} (100%) diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 486de7cf..82ff4f0e 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +//! gRPC tests use celestia_grpc::types::auth::Account; use celestia_grpc::types::tx::sign_tx; diff --git a/grpc/tests/utils.rs b/grpc/tests/utils/mod.rs similarity index 100% rename from grpc/tests/utils.rs rename to grpc/tests/utils/mod.rs From 4d2494cbd9284e9f2e998ff4077ef7a6e61c9215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 29 Nov 2024 15:23:44 +0100 Subject: [PATCH 05/50] increase timeout --- grpc/tests/tonic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 82ff4f0e..d17cea35 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -102,7 +102,7 @@ async fn submit_blob() { .await .unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; let _submitted_tx = client .get_tx(response.txhash) From bcfefcafa6481d8f4553fd453d28d504963f2e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Mon, 2 Dec 2024 09:33:08 +0100 Subject: [PATCH 06/50] better tests, et al --- node-wasm/src/client.rs | 40 ++++++++++++++++++++++------------------ rpc/tests/blob.rs | 3 +-- types/src/blob.rs | 1 - 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index 10f81f53..81d4a20e 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -451,7 +451,7 @@ mod tests { use celestia_rpc::{prelude::*, Client}; use celestia_types::p2p::PeerId; - use celestia_types::ExtendedHeader; + use celestia_types::{AppVersion, ExtendedHeader, TxConfig}; use gloo_timers::future::sleep; use libp2p::{multiaddr::Protocol, Multiaddr}; use rexie::Rexie; @@ -519,29 +519,33 @@ mod tests { crate::utils::setup_logging(); remove_database().await.expect("failed to clear db"); let rpc_client = Client::new(WS_URL).await.unwrap(); + let namespace = Namespace::new_v0(&[0xCD, 0xDC, 0xCD, 0xDC, 0xCD, 0xDC]).unwrap(); + let data = b"Hello, World"; + let blobs = vec![Blob::new(namespace, data.to_vec(), AppVersion::V3).unwrap()]; + info!("presubmit"); + let submitted_height = rpc_client + .blob_submit(&blobs, TxConfig::default()) + .await + .expect("successful submission"); + info!("preheader"); + let header = rpc_client + .header_get_by_height(submitted_height) + .await + .expect("header for blob"); + let bridge_ma = fetch_bridge_webtransport_multiaddr(&rpc_client).await; let client = spawn_connected_node(vec![bridge_ma.to_string()]).await; - let namespace = Namespace::new_v0(&[0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF]).unwrap(); - let mut found = None; - // TODO: once gRPC blob submission works from browser, submit blob and get its height that - // way. Otherwise, this looks for a blob submitted from the RPC tests `rpc/tests/blob.rs` - 'find_header: for h in 1..=500 { - info!("HH: {h}"); - let header = rpc_client.header_get_by_height(h).await.unwrap(); - for row in header.dah.row_roots() { - if row.min_namespace() < *namespace && *namespace < row.max_namespace() { - found = Some(header.clone()); - break 'find_header; - } - } - } - let header = found.expect("blob to exists"); + info!("pregetblobs"); - let _blob = client + let mut blobs = client .request_all_blobs(to_value(&header).unwrap(), namespace, None) .await - .unwrap(); + .expect("to fetch blob"); + assert_eq!(blobs.len(), 1); + let blob = blobs.pop().unwrap(); + assert_eq!(blob.data, data); + assert_eq!(blob.namespace, namespace); } async fn spawn_connected_node(bootnodes: Vec) -> NodeClient { diff --git a/rpc/tests/blob.rs b/rpc/tests/blob.rs index 89abf0d7..2ed0e3db 100644 --- a/rpc/tests/blob.rs +++ b/rpc/tests/blob.rs @@ -6,7 +6,6 @@ use std::time::Duration; use celestia_rpc::blob::BlobsAtHeight; use celestia_rpc::prelude::*; use celestia_types::consts::appconsts::AppVersion; -use celestia_types::nmt::Namespace; use celestia_types::{Blob, Commitment}; use jsonrpsee::core::client::Subscription; @@ -18,7 +17,7 @@ use crate::utils::{random_bytes, random_bytes_array, random_ns}; #[tokio::test] async fn blob_submit_and_get() { let client = new_test_client(AuthLevel::Write).await.unwrap(); - let namespace = Namespace::new_v0(&[0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF]).unwrap(); + let namespace = random_ns(); let data = random_bytes(5); let blob = Blob::new(namespace, data, AppVersion::V2).unwrap(); diff --git a/types/src/blob.rs b/types/src/blob.rs index 184fcd2e..58961c5b 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -41,7 +41,6 @@ pub struct Blob { pub share_version: u8, /// A [`Commitment`] computed from the [`Blob`]s data. pub commitment: Commitment, - /// Index of the blob's first share in the EDS. Only set for blobs retrieved from chain. // note: celestia supports deserializing blobs without index, so we should too #[serde(default, with = "index_serde")] From 69fb3e05a890f41f48bd83fd70af16cfc9c63cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Mon, 2 Dec 2024 10:24:56 +0100 Subject: [PATCH 07/50] missed --- node-wasm/src/client.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index 81d4a20e..baba5a2e 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -516,18 +516,17 @@ mod tests { #[wasm_bindgen_test] async fn get_blob() { - crate::utils::setup_logging(); remove_database().await.expect("failed to clear db"); let rpc_client = Client::new(WS_URL).await.unwrap(); let namespace = Namespace::new_v0(&[0xCD, 0xDC, 0xCD, 0xDC, 0xCD, 0xDC]).unwrap(); let data = b"Hello, World"; let blobs = vec![Blob::new(namespace, data.to_vec(), AppVersion::V3).unwrap()]; - info!("presubmit"); + let submitted_height = rpc_client .blob_submit(&blobs, TxConfig::default()) .await .expect("successful submission"); - info!("preheader"); + let header = rpc_client .header_get_by_height(submitted_height) .await @@ -536,12 +535,11 @@ mod tests { let bridge_ma = fetch_bridge_webtransport_multiaddr(&rpc_client).await; let client = spawn_connected_node(vec![bridge_ma.to_string()]).await; - info!("pregetblobs"); - let mut blobs = client .request_all_blobs(to_value(&header).unwrap(), namespace, None) .await .expect("to fetch blob"); + assert_eq!(blobs.len(), 1); let blob = blobs.pop().unwrap(); assert_eq!(blob.data, data); From 7dbfe9a29525657f31d8e7d59ff8954581ae2c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Mon, 2 Dec 2024 13:50:38 +0100 Subject: [PATCH 08/50] wip --- node-wasm/src/client.rs | 1 - types/src/blob/commitment.rs | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index baba5a2e..de6f68d7 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -456,7 +456,6 @@ mod tests { use libp2p::{multiaddr::Protocol, Multiaddr}; use rexie::Rexie; use serde_wasm_bindgen::from_value; - use tracing::info; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::wasm_bindgen_test; use web_sys::MessageChannel; diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index 4fe317fb..1e6baac8 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -52,20 +52,12 @@ use crate::{InfoByte, Share}; /// [`Nmt`]: crate::nmt::Nmt /// [`ExtendedDataSquare`]: crate::ExtendedDataSquare /// [`share commitment rules`]: https://github.com/celestiaorg/celestia-app/blob/main/specs/src/specs/data_square_layout.md#blob-share-commitment-rules -#[cfg(not(all(feature = "wasm-bindgen", target_arch = "wasm32")))] +#[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { /// hash of the commitment pub hash: merkle::Hash, } -#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] -#[wasm_bindgen] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Commitment { - /// hash of the commitment - #[wasm_bindgen(skip)] - pub hash: merkle::Hash, -} impl Commitment { /// Generate the share commitment from the given blob data. @@ -117,6 +109,18 @@ impl Commitment { Ok(Commitment { hash }) } + + /* + #[cfg(not(all(feature = "wasm-bindgen", target_arch = "wasm32")))] + pub fn hash(&self) -> &merkle::Hash { + &self.hash + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + pub fn hash(&self) -> &merkle::Hash { + &self.hash + } + */ } impl Serialize for Commitment { From 5d5ac5a0881e3bf0f3083f794cef30c4ec6cee88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 3 Dec 2024 09:30:28 +0100 Subject: [PATCH 09/50] fix commitment --- rpc/tests/blob.rs | 6 ++---- types/src/blob.rs | 2 +- types/src/blob/commitment.rs | 28 +++++++++++++++++++++------- types/src/blob/msg_pay_for_blobs.rs | 8 ++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/rpc/tests/blob.rs b/rpc/tests/blob.rs index 2ed0e3db..35b75015 100644 --- a/rpc/tests/blob.rs +++ b/rpc/tests/blob.rs @@ -6,7 +6,7 @@ use std::time::Duration; use celestia_rpc::blob::BlobsAtHeight; use celestia_rpc::prelude::*; use celestia_types::consts::appconsts::AppVersion; -use celestia_types::{Blob, Commitment}; +use celestia_types::Blob; use jsonrpsee::core::client::Subscription; pub mod utils; @@ -194,9 +194,7 @@ async fn blob_get_get_proof_wrong_commitment() { let namespace = random_ns(); let data = random_bytes(5); let blob = Blob::new(namespace, data, AppVersion::V2).unwrap(); - let commitment = Commitment { - hash: random_bytes_array(), - }; + let commitment = random_bytes_array().into(); let submitted_height = blob_submit(&client, &[blob.clone()]).await.unwrap(); diff --git a/types/src/blob.rs b/types/src/blob.rs index 58961c5b..7cef810d 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -401,7 +401,7 @@ mod tests { #[test] fn validate_blob_commitment_mismatch() { let mut blob = sample_blob(); - blob.commitment.hash.fill(7); + blob.commitment = [7; 32].into(); blob.validate(AppVersion::V2).unwrap_err(); } diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index 1e6baac8..d6dec75b 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -56,7 +56,7 @@ use crate::{InfoByte, Share}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { /// hash of the commitment - pub hash: merkle::Hash, + hash: merkle::Hash, } impl Commitment { @@ -110,17 +110,31 @@ impl Commitment { Ok(Commitment { hash }) } - /* - #[cfg(not(all(feature = "wasm-bindgen", target_arch = "wasm32")))] pub fn hash(&self) -> &merkle::Hash { &self.hash } - #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] - pub fn hash(&self) -> &merkle::Hash { - &self.hash +} + +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +#[wasm_bindgen] +impl Commitment { + #[wasm_bindgen(js_name = hash)] + pub fn js_hash(&self) -> Vec { + self.hash.to_vec() + } +} + +impl From for merkle::Hash { + fn from(commitment: Commitment) -> Self { + commitment.hash + } +} + +impl From for Commitment { + fn from(hash: merkle::Hash) -> Self { + Commitment { hash } } - */ } impl Serialize for Commitment { diff --git a/types/src/blob/msg_pay_for_blobs.rs b/types/src/blob/msg_pay_for_blobs.rs index 4b899a3d..45bbf8b9 100644 --- a/types/src/blob/msg_pay_for_blobs.rs +++ b/types/src/blob/msg_pay_for_blobs.rs @@ -2,6 +2,7 @@ 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::merkle::Hash; use tendermint_proto::google::protobuf::Any; use tendermint_proto::Protobuf; @@ -80,7 +81,7 @@ impl From for RawMsgPayForBlobs { let share_commitments = msg .share_commitments .into_iter() - .map(|c| c.hash.to_vec()) + .map(|c| Hash::from(c).to_vec()) .collect(); RawMsgPayForBlobs { @@ -106,9 +107,8 @@ impl TryFrom for MsgPayForBlobs { .share_commitments .into_iter() .map(|c| { - Ok(Commitment { - hash: c.try_into().map_err(|_| Error::InvalidComittmentLength)?, - }) + let hash = Hash::try_from(c).map_err(|_| Error::InvalidComittmentLength)?; + Ok(hash.into()) }) .collect::>()?; From e9e1ae0404575213e39fab13f95edd15dc0a69dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 3 Dec 2024 09:41:42 +0100 Subject: [PATCH 10/50] misc ci --- types/src/blob/commitment.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index d6dec75b..d5062c9d 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -110,15 +110,16 @@ impl Commitment { Ok(Commitment { hash }) } + /// hash of the commitment pub fn hash(&self) -> &merkle::Hash { &self.hash } - } #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[wasm_bindgen] impl Commitment { + /// hash of the commitment #[wasm_bindgen(js_name = hash)] pub fn js_hash(&self) -> Vec { self.hash.to_vec() From 82dc2cbe9fec3e0b7dcdd516b4b0b12aae371eb2 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 10 Dec 2024 12:10:01 +0100 Subject: [PATCH 11/50] 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 12/50] 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 13/50] 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 14/50] 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 9c6493c934cf20061b0421fada1e43932785a32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 11 Dec 2024 17:53:37 +0100 Subject: [PATCH 15/50] wip --- node-wasm/Cargo.toml | 2 +- node/Cargo.toml | 2 +- types/src/blob/commitment.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index d0d2788a..0349c980 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -46,7 +46,7 @@ tokio = { version = "1.38.0", features = ["sync"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.3" -wasm-bindgen = "0.2.93" +wasm-bindgen = "0.2.95" wasm-bindgen-futures = "0.4.43" web-sys = { version = "0.3.70", features = [ "BroadcastChannel", diff --git a/node/Cargo.toml b/node/Cargo.toml index 38ba4d8a..db7f548c 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -89,7 +89,7 @@ pin-project = "1.1.5" rexie = "0.6.2" send_wrapper = { version = "0.6.0", features = ["futures"] } serde-wasm-bindgen = "0.6.5" -wasm-bindgen = "0.2.93" +wasm-bindgen = "0.2.97" wasm-bindgen-futures = "0.4.43" libp2p-websocket-websys = "0.3.3" diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index d5062c9d..d739fb18 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -56,6 +56,7 @@ use crate::{InfoByte, Share}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { /// hash of the commitment + #[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen(skip))] hash: merkle::Hash, } From 6a7b71c8e78a5785eb777d352e72884abb680ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 12 Dec 2024 09:51:20 +0100 Subject: [PATCH 16/50] PR review, align to current wasm-bindgen --- Cargo.lock | 21 ++++++++++----------- grpc/tests/tonic.rs | 1 - grpc/tests/utils/mod.rs | 1 - node-wasm/Cargo.toml | 2 +- node/Cargo.toml | 2 +- rpc/tests/blob.rs | 4 ++-- types/src/blob.rs | 2 +- types/src/blob/commitment.rs | 14 +++++++------- types/src/blob/msg_pay_for_blobs.rs | 4 ++-- 9 files changed, 24 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 068f7a55..b0f83626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5994,9 +5994,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -6005,13 +6005,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.87", @@ -6032,9 +6031,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6042,9 +6041,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -6055,9 +6054,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-bindgen-test" diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index d17cea35..74207c4f 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,5 +1,4 @@ #![cfg(not(target_arch = "wasm32"))] -//! gRPC tests use celestia_grpc::types::auth::Account; use celestia_grpc::types::tx::sign_tx; diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 8289c5d7..91872634 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -1,5 +1,4 @@ #![cfg(not(target_arch = "wasm32"))] -//! Utilities for grpc tests use std::{env, fs}; diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 0349c980..d0d2788a 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -46,7 +46,7 @@ tokio = { version = "1.38.0", features = ["sync"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.3" -wasm-bindgen = "0.2.95" +wasm-bindgen = "0.2.93" wasm-bindgen-futures = "0.4.43" web-sys = { version = "0.3.70", features = [ "BroadcastChannel", diff --git a/node/Cargo.toml b/node/Cargo.toml index db7f548c..38ba4d8a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -89,7 +89,7 @@ pin-project = "1.1.5" rexie = "0.6.2" send_wrapper = { version = "0.6.0", features = ["futures"] } serde-wasm-bindgen = "0.6.5" -wasm-bindgen = "0.2.97" +wasm-bindgen = "0.2.93" wasm-bindgen-futures = "0.4.43" libp2p-websocket-websys = "0.3.3" diff --git a/rpc/tests/blob.rs b/rpc/tests/blob.rs index 35b75015..1789692f 100644 --- a/rpc/tests/blob.rs +++ b/rpc/tests/blob.rs @@ -6,7 +6,7 @@ use std::time::Duration; use celestia_rpc::blob::BlobsAtHeight; use celestia_rpc::prelude::*; use celestia_types::consts::appconsts::AppVersion; -use celestia_types::Blob; +use celestia_types::{Blob, Commitment}; use jsonrpsee::core::client::Subscription; pub mod utils; @@ -194,7 +194,7 @@ async fn blob_get_get_proof_wrong_commitment() { let namespace = random_ns(); let data = random_bytes(5); let blob = Blob::new(namespace, data, AppVersion::V2).unwrap(); - let commitment = random_bytes_array().into(); + let commitment = Commitment::new(random_bytes_array()); let submitted_height = blob_submit(&client, &[blob.clone()]).await.unwrap(); diff --git a/types/src/blob.rs b/types/src/blob.rs index 7cef810d..e12eff10 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -401,7 +401,7 @@ mod tests { #[test] fn validate_blob_commitment_mismatch() { let mut blob = sample_blob(); - blob.commitment = [7; 32].into(); + blob.commitment = Commitment::new([7; 32]); blob.validate(AppVersion::V2).unwrap_err(); } diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index d739fb18..69f2c48b 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -16,6 +16,8 @@ use crate::nmt::{Namespace, NamespacedHashExt, NamespacedSha2Hasher, Nmt, RawNam use crate::{Error, Result}; use crate::{InfoByte, Share}; +// TODO: once https://github.com/rustwasm/wasm-bindgen/pull/4351 is merged, +// this can be replaced with a single common type definition /// A merkle hash used to identify the [`Blob`]s data. /// /// In Celestia network, the transaction which pays for the blob's inclusion @@ -56,11 +58,15 @@ use crate::{InfoByte, Share}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Commitment { /// hash of the commitment - #[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen(skip))] hash: merkle::Hash, } impl Commitment { + /// Create a new commitment with hash + pub fn new(hash: merkle::Hash) -> Self { + Commitment { hash } + } + /// Generate the share commitment from the given blob data. pub fn from_blob( namespace: Namespace, @@ -133,12 +139,6 @@ impl From for merkle::Hash { } } -impl From for Commitment { - fn from(hash: merkle::Hash) -> Self { - Commitment { hash } - } -} - impl Serialize for Commitment { fn serialize(&self, serializer: S) -> Result where diff --git a/types/src/blob/msg_pay_for_blobs.rs b/types/src/blob/msg_pay_for_blobs.rs index 45bbf8b9..914486ef 100644 --- a/types/src/blob/msg_pay_for_blobs.rs +++ b/types/src/blob/msg_pay_for_blobs.rs @@ -81,7 +81,7 @@ impl From for RawMsgPayForBlobs { let share_commitments = msg .share_commitments .into_iter() - .map(|c| Hash::from(c).to_vec()) + .map(|c| c.hash().to_vec()) .collect(); RawMsgPayForBlobs { @@ -108,7 +108,7 @@ impl TryFrom for MsgPayForBlobs { .into_iter() .map(|c| { let hash = Hash::try_from(c).map_err(|_| Error::InvalidComittmentLength)?; - Ok(hash.into()) + Ok(Commitment::new(hash)) }) .collect::>()?; From ab3cff935c45cc4b9e20c293546acffc32fe39e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 13 Dec 2024 15:53:26 +0100 Subject: [PATCH 17/50] Add relevant getters to namespace --- Cargo.lock | 6 ++++-- grpc/tests/tonic.rs | 4 ++-- types/Cargo.toml | 5 +---- types/src/blob/commitment.rs | 2 -- types/src/nmt.rs | 22 ++++++++++++++++++++++ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 979c7983..2566b13d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -832,6 +832,7 @@ dependencies = [ "enum_dispatch", "getrandom", "indoc", + "js-sys", "leopard-codec", "libp2p-identity", "multiaddr", @@ -2478,10 +2479,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", ] diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 74207c4f..07d1387a 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -7,7 +7,7 @@ use celestia_types::blob::MsgPayForBlobs; use celestia_types::nmt::Namespace; use celestia_types::{AppVersion, Blob}; -mod utils; +pub mod utils; use crate::utils::{load_account, new_test_client}; @@ -101,7 +101,7 @@ async fn submit_blob() { .await .unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + tokio::time::sleep(std::time::Duration::from_secs(3)).await; let _submitted_tx = client .get_tx(response.txhash) diff --git a/types/Cargo.toml b/types/Cargo.toml index bbb0c4be..5329bd43 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -39,9 +39,6 @@ serde = { version = "1.0.203", features = ["derive"] } serde_repr = { version = "0.1.19", optional = true } sha2 = "0.10.6" thiserror = "1.0.61" - -# `time` is a dependency of a dependency but we need to specify it -# for fixing rust-lang/rust#125319. time = { version = "0.3.36", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -63,7 +60,7 @@ wasm-bindgen-test = "0.3.42" default = ["p2p"] p2p = ["dep:libp2p-identity", "dep:multiaddr", "dep:serde_repr"] test-utils = ["dep:ed25519-consensus", "dep:rand"] -wasm-bindgen = ["dep:wasm-bindgen", "time/wasm-bindgen"] +wasm-bindgen = ["dep:wasm-bindgen", "dep:js-sys", "time/wasm-bindgen"] tonic = ["celestia-proto/tonic"] [package.metadata.docs.rs] diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index 69f2c48b..0f03adb7 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -16,8 +16,6 @@ use crate::nmt::{Namespace, NamespacedHashExt, NamespacedSha2Hasher, Nmt, RawNam use crate::{Error, Result}; use crate::{InfoByte, Share}; -// TODO: once https://github.com/rustwasm/wasm-bindgen/pull/4351 is merged, -// this can be replaced with a single common type definition /// A merkle hash used to identify the [`Blob`]s data. /// /// In Celestia network, the transaction which pays for the blob's inclusion diff --git a/types/src/nmt.rs b/types/src/nmt.rs index 00a3a4fe..0af83d88 100644 --- a/types/src/nmt.rs +++ b/types/src/nmt.rs @@ -376,6 +376,28 @@ impl Namespace { } } +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +#[wasm_bindgen] +impl Namespace { + /// Converts the [`Namespace`] to a byte slice. + #[wasm_bindgen(js_name = "asBytes")] + pub fn js_as_bytes(&self) -> js_sys::Uint8Array { + (&self.0 .0[..]).into() + } + + /// Returns the first byte indicating the version of the [`Namespace`]. + #[wasm_bindgen(js_name = "version", getter)] + pub fn js_version(&self) -> u8 { + self.as_bytes()[0] + } + + /// Returns the trailing 28 bytes indicating the id of the [`Namespace`]. + #[wasm_bindgen(js_name = "id", getter)] + pub fn js_id(&self) -> js_sys::Uint8Array { + (&self.as_bytes()[1..]).into() + } +} + impl From for nmt_rs::NamespaceId { fn from(value: Namespace) -> Self { value.0 From 5ec36f89e228a3c146e5c67eec23f219076fed5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 13 Dec 2024 19:49:33 +0100 Subject: [PATCH 18/50] whoops didnt mean to commit that --- types/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/types/Cargo.toml b/types/Cargo.toml index 5329bd43..61069982 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -42,6 +42,7 @@ thiserror = "1.0.61" time = { version = "0.3.36", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { version = "0.3.76", optional = true } wasm-bindgen = { version = "0.2.95", optional = true } [dev-dependencies] From 7079948827b46e566afc7ac5994237135578bd76 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 13 Dec 2024 12:06:03 +0100 Subject: [PATCH 19/50] 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 20/50] 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 21/50] 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 22/50] 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 23/50] 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 24/50] 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 25/50] 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); } From 47c9f00bcbaa055d4ca2dc49810483b63d91089b Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 27 Dec 2024 11:05:13 +0100 Subject: [PATCH 26/50] wip --- Cargo.lock | 10 +++++++++ ci/docker-compose.yml | 2 +- cli/js/index.js | 3 +++ cli/js/package.json | 1 + grpc/Cargo.toml | 40 ++++++++++++++++++++++++++++++++++- grpc/src/error.rs | 12 +++++++++++ grpc/src/lib.rs | 2 ++ grpc/src/tx.rs | 47 +++++++++++++++++++++++++++++------------ grpc/tests/tonic.rs | 29 +++++++++++++++++++------ grpc/tests/utils/mod.rs | 11 ++++++++++ proto/build.rs | 2 ++ 11 files changed, 137 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74b41054..a185b2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,26 +754,36 @@ dependencies = [ "celestia-grpc-macros", "celestia-proto", "celestia-types", + "console_error_panic_hook", "dotenvy", "futures", "getrandom", "gloo-timers 0.3.0", "hex", "http-body 1.0.0", + "js-sys", "k256", "prost", "rand_core", "regex", "send_wrapper 0.6.0", "serde", + "serde-wasm-bindgen", + "serde_json", "tendermint", "tendermint-proto", "thiserror", + "time", "tokio", "tonic", "tonic-web-wasm-client", + "tracing", + "tracing-subscriber", + "tracing-web", + "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "web-sys", ] [[package]] diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index d8ae5ea4..96e4a41c 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -20,7 +20,7 @@ services: build: context: . dockerfile: Dockerfile.grpcwebproxy - command: --backend_addr=validator:9090 --run_tls_server=false --allow_all_origins + command: --backend_addr=public-celestia-mocha4-consensus.numia.xyz:9090 --run_tls_server=false --allow_all_origins ports: - 18080:8080 diff --git a/cli/js/index.js b/cli/js/index.js index f123c03b..8391e277 100644 --- a/cli/js/index.js +++ b/cli/js/index.js @@ -1,6 +1,9 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default import { NodeConfig, spawnNode } from "lumina-node"; +import { TxClient } from "celestia-grpc"; + +window.createTxClient = TxClient; async function showStats(node) { if (!node || !await node.isRunning()) { diff --git a/cli/js/package.json b/cli/js/package.json index f7cbe447..a0d8f871 100644 --- a/cli/js/package.json +++ b/cli/js/package.json @@ -10,6 +10,7 @@ "author": "", "license": "MIT", "dependencies": { + "celestia-grpc": "file:../../grpc/pkg", "lumina-node": "file:../../node-wasm/js", "lumina-node-wasm": "file:../../node-wasm/pkg" }, diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index bdde5c89..ac6b76c9 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -18,6 +18,9 @@ categories = [ "cryptography::cryptocurrencies", ] +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] celestia-grpc-macros = { version = "0.1.0", path = "grpc-macros" } celestia-proto = { workspace = true, features = ["tonic"] } @@ -37,6 +40,7 @@ tokio = { version = "1.38.0", features = ["sync"] } tonic = { version = "0.12.3", default-features = false, features = [ "codegen", "prost" ]} +tracing = "*" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.38.0", features = ["time"] } @@ -47,8 +51,42 @@ 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"] } +time = { version = "0.3.36", features = ["wasm-bindgen"] } tonic-web-wasm-client = "0.6" +console_error_panic_hook = "0.1.7" +js-sys = "0.3.70" +serde_json = "1.0.120" +serde-wasm-bindgen = "0.6.5" +tracing-subscriber = { version = "0.3.18", features = ["time"] } +tracing-web = "0.1.3" +wasm-bindgen = "0.2.93" +wasm-bindgen-futures = "0.4.43" +web-sys = { version = "0.3.70", features = [ + "BroadcastChannel", + "DedicatedWorkerGlobalScope", + "Headers", + "MessageChannel", + "MessageEvent", + "MessagePort", + "Navigator", + "Request", + "RequestInit", + "RequestMode", + "Response", + "ServiceWorker", + "ServiceWorkerGlobalScope", + "SharedWorker", + "SharedWorkerGlobalScope", + "StorageManager", + "Window", + "Worker", + "WorkerGlobalScope", + "WorkerNavigator", + "WorkerOptions", + "WorkerType", +] } + [dev-dependencies] rand_core = "0.6.4" @@ -57,5 +95,5 @@ dotenvy = "0.15.7" tokio = { version = "1.38.0", features = ["rt", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-futures = "0.4.43" +# wasm-bindgen-futures = "0.4.43" wasm-bindgen-test = "0.3.42" diff --git a/grpc/src/error.rs b/grpc/src/error.rs index 39b4cd82..68df664f 100644 --- a/grpc/src/error.rs +++ b/grpc/src/error.rs @@ -1,4 +1,5 @@ use celestia_types::{hash::Hash, state::ErrorCode}; +use k256::ecdsa::signature::Error as SignatureError; use tonic::Status; /// Alias for a `Result` with the error type [`celestia_tonic::Error`]. @@ -70,4 +71,15 @@ pub enum Error { /// Updating gas price failed #[error("Updating gas price failed: {0}")] UpdatingGasPriceFailed(String), + + /// Signing error + #[error(transparent)] + SigningError(#[from] SignatureError), +} + +#[cfg(target_arch = "wasm32")] +impl From for wasm_bindgen::JsValue { + fn from(error: Error) -> wasm_bindgen::JsValue { + error.to_string().into() + } } diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index a58084a6..3e03169b 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -2,6 +2,8 @@ mod error; pub mod grpc; +#[cfg(target_arch = "wasm32")] +mod js_client; mod tx; mod utils; diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 8a4294c5..706b6286 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::future::Future; use std::ops::Deref; use std::sync::{LazyLock, RwLock}; use std::time::Duration; @@ -15,7 +16,7 @@ use celestia_types::state::{ }; use celestia_types::{AppVersion, Height}; use http_body::Body; -use k256::ecdsa::signature::Signer; +use k256::ecdsa::signature::{Error as SignatureError, Signer}; use k256::ecdsa::{Signature, VerifyingKey}; use prost::{Message, Name}; use regex::Regex; @@ -95,6 +96,20 @@ impl TxConfig { } } +pub trait AsyncSigner { + fn try_sign(&self, doc: SignDoc) -> impl Future>; +} + +impl AsyncSigner for T +where + T: Signer, +{ + async fn try_sign(&self, doc: SignDoc) -> Result { + let bytes = doc.encode_to_vec(); + self.try_sign(&bytes) + } +} + /// A client for submitting messages and transactions to celestia. /// /// Client handles management of the accounts sequence (nonce), thus @@ -121,7 +136,7 @@ where T::Error: Into, T::ResponseBody: Body + Send + 'static, ::Error: Into + Send, - S: Signer, + S: AsyncSigner, { /// Create a new transaction client. /// @@ -317,14 +332,14 @@ where } 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 tx = sign_tx(tx.clone(), 0, 1).await?; 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); + let tx = sign_tx(tx, gas_limit, fee as u64).await?; self.broadcast_tx_with_account(tx.encode_to_vec(), account, gas_limit) .await @@ -358,6 +373,13 @@ where &self.signer, gas_limit, fee, + ) + .await?; + + println!( + "Signed tx; sequence: {}, signature: {:?}", + account.sequence, + tx.signatures.first().unwrap() ); let blobs = blobs.into_iter().map(Into::into).collect(); @@ -483,15 +505,15 @@ impl fmt::Debug for TxClient { } /// Sign `tx_body` and the transaction metadata as the `base_account` using `signer` -pub fn sign_tx( +pub async fn sign_tx( tx_body: RawTxBody, chain_id: Id, base_account: &BaseAccount, verifying_key: &VerifyingKey, - signer: &impl Signer, + signer: &impl AsyncSigner, gas_limit: u64, fee: u64, -) -> RawTx { +) -> Result { // 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 }, @@ -517,21 +539,20 @@ pub fn sign_tx( fee, }; - let bytes_to_sign = SignDoc { + let doc = 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); + let signature = signer.try_sign(doc).await?; - RawTx { + Ok(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 { diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index 483a5822..eee2aae0 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use celestia_grpc::{Error, TxConfig}; +use celestia_grpc::{Error, TxClient, TxConfig}; use celestia_proto::cosmos::bank::v1beta1::MsgSend; use celestia_types::nmt::Namespace; use celestia_types::state::{Coin, ErrorCode}; @@ -95,15 +95,30 @@ async fn get_blob_params() { #[async_test] async fn submit_and_get_tx() { - 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()]; + // let (_lock, tx_client) = new_tx_client().await; + let leap_account = TestAccount::from_pk( + &hex::decode("4f8fae3480edef0a19d4d5228b09fb37f3dddb9813d6641efee4b1ef95b925c9").unwrap(), + ); + let grpc_client = new_grpc_client(); + let tx_client = TxClient::new( + grpc_client, + leap_account.signing_key, + &leap_account.address, + Some(leap_account.verifying_key), + ) + .await + .unwrap(); + + let ns = Namespace::new_v0(b"abc").unwrap(); + let blob = Blob::new(ns, b"data".into(), AppVersion::V3).unwrap(); + // let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); + // let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; let tx = tx_client - .submit_blobs(&blobs, TxConfig::default()) + .submit_blobs(&[blob], TxConfig::default()) .await .unwrap(); + panic!(); let tx2 = tx_client.get_tx(tx.hash).await.unwrap(); assert_eq!(tx.hash, tx2.tx_response.txhash); @@ -200,7 +215,7 @@ async fn submit_message() { let msg = MsgSend { from_address: account.address.to_string(), - to_address: other_account.address.to_string(), + to_address: "celestia159edu39c3mmsudhg63dh4gph8ytpfdpff8q6ew".to_string(), // other_account.address.to_string(), amount: vec![amount.clone().into()], }; diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index d86406e0..1197460d 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -27,6 +27,17 @@ impl TestAccount { signing_key, } } + + pub fn from_pk(pk: &[u8]) -> Self { + let signing_key = SigningKey::from_slice(pk).unwrap(); + let verifying_key = *signing_key.verifying_key(); + + Self { + address: AccAddress::new(verifying_key.into()).into(), + verifying_key, + signing_key, + } + } } pub fn load_account() -> TestAccount { diff --git a/proto/build.rs b/proto/build.rs index 50014fe2..296fb767 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -45,6 +45,8 @@ static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[ (".cosmos.staking.v1beta1.QueryUnbondingDelegationResponse", SERIALIZED_DEFAULT), (".cosmos.staking.v1beta1.UnbondingDelegation", SERIALIZED_DEFAULT), (".cosmos.staking.v1beta1.UnbondingDelegationEntry", SERIALIZED_DEFAULT), + (".cosmos.tx.v1beta1.SignDoc", SERIALIZED_DEFAULT), + (".cosmos.tx.v1beta1.SignDoc", r#"#[serde(rename_all = "camelCase")]"#), (".header.pb.ExtendedHeader", SERIALIZED_DEFAULT), (".proof.pb.Proof", SERIALIZED_DEFAULT), (".proto.blob.v1.BlobProto", SERIALIZED), From a8b71dbd794a7cf1cd80a9fa17cb54b90e77ef6e Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 27 Dec 2024 18:31:25 +0100 Subject: [PATCH 27/50] allow constructing Namespace and Blob in js --- types/src/blob.rs | 16 ++++++- types/src/consts.rs | 44 +++++++++++++++++++ types/src/error.rs | 7 +++ types/src/nmt.rs | 105 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 162 insertions(+), 10 deletions(-) diff --git a/types/src/blob.rs b/types/src/blob.rs index e12eff10..283e2767 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -27,7 +27,7 @@ use wasm_bindgen::prelude::*; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr( all(feature = "wasm-bindgen", target_arch = "wasm32"), - wasm_bindgen(getter_with_clone) + wasm_bindgen(getter_with_clone, inspectable) )] pub struct Blob { /// A [`Namespace`] the [`Blob`] belongs to. @@ -327,6 +327,20 @@ impl From for RawBlob { } } +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +#[wasm_bindgen] +impl Blob { + /// Create a new blob with the given data within the [`Namespace`]. + #[wasm_bindgen(constructor)] + pub fn js_new( + namespace: Namespace, + data: Vec, + app_version: appconsts::JsAppVersion, + ) -> Result { + Self::new(namespace, data, app_version.into()) + } +} + fn shares_needed_for_blob(blob_len: usize) -> usize { let Some(without_first_share) = blob_len.checked_sub(appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE) diff --git a/types/src/consts.rs b/types/src/consts.rs index 9bef0edc..f139b9e1 100644 --- a/types/src/consts.rs +++ b/types/src/consts.rs @@ -23,6 +23,9 @@ pub mod version { /// /// [`celestia-app`]: https://github.com/celestiaorg/celestia-app pub mod appconsts { + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + use wasm_bindgen::prelude::*; + pub use global_consts::*; // celestia-app/pkg/appconsts/v1/app_consts @@ -103,6 +106,47 @@ pub mod appconsts { } } + /// Enum with all valid App versions. + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[wasm_bindgen(js_name = AppVersion)] + pub enum JsAppVersion { + V1 = 1, + V2 = 2, + V3 = 3, + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + #[wasm_bindgen(js_class = AppVersion)] + impl JsAppVersion { + /// Latest App version variant. + pub fn latest() -> JsAppVersion { + AppVersion::latest().into() + } + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + impl From for AppVersion { + fn from(js: JsAppVersion) -> AppVersion { + match js { + JsAppVersion::V1 => AppVersion::V1, + JsAppVersion::V2 => AppVersion::V2, + JsAppVersion::V3 => AppVersion::V3, + } + } + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + impl From for JsAppVersion { + fn from(js: AppVersion) -> JsAppVersion { + match js { + AppVersion::V1 => JsAppVersion::V1, + AppVersion::V2 => JsAppVersion::V2, + AppVersion::V3 => JsAppVersion::V3, + } + } + } + /// Maximum width of the original data square. pub const fn square_size_upper_bound(app_version: AppVersion) -> usize { match app_version { diff --git a/types/src/error.rs b/types/src/error.rs index a19b3152..c0caa1b1 100644 --- a/types/src/error.rs +++ b/types/src/error.rs @@ -259,6 +259,13 @@ impl From for Error { } } +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +impl From for wasm_bindgen::JsValue { + fn from(value: Error) -> Self { + js_sys::Error::new(&value.to_string()).into() + } +} + /// Representation of the errors that can occur when validating data. /// /// See [`ValidateBasic`] diff --git a/types/src/nmt.rs b/types/src/nmt.rs index 0af83d88..40c68f85 100644 --- a/types/src/nmt.rs +++ b/types/src/nmt.rs @@ -93,7 +93,10 @@ pub type Proof = nmt_rs::simple_merkle::proof::Proof; /// - secondary reserved namespaces - those use version `0xff` so they are always placed after /// user-submitted data. #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -#[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen)] +#[cfg_attr( + all(feature = "wasm-bindgen", target_arch = "wasm32"), + wasm_bindgen(inspectable) +)] pub struct Namespace(nmt_rs::NamespaceId); impl Namespace { @@ -379,22 +382,106 @@ impl Namespace { #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[wasm_bindgen] impl Namespace { + /// Namespace size in bytes. + #[wasm_bindgen(js_name = NS_SIZE, getter)] + pub fn js_ns_size() -> usize { + NS_SIZE + } + + /// Primary reserved [`Namespace`] for the compact `Share`s with `cosmos SDK` transactions. + #[wasm_bindgen(js_name = TRANSACTION, getter)] + pub fn js_transaction() -> Namespace { + Namespace::TRANSACTION + } + + /// Primary reserved [`Namespace`] for the compact Shares with MsgPayForBlobs transactions. + #[wasm_bindgen(js_name = PAY_FOR_BLOB, getter)] + pub fn js_pay_for_blob() -> Namespace { + Namespace::PAY_FOR_BLOB + } + + /// Primary reserved [`Namespace`] for the `Share`s used for padding. + /// + /// `Share`s with this namespace are inserted after other shares from primary reserved namespace + /// so that user-defined namespaces are correctly aligned in `ExtendedDataSquare` + #[wasm_bindgen(js_name = PRIMARY_RESERVED_PADDING, getter)] + pub fn js_primary_reserved_padding() -> Namespace { + Namespace::PRIMARY_RESERVED_PADDING + } + + /// Maximal primary reserved [`Namespace`]. + /// + /// Used to indicate the end of the primary reserved group. + #[wasm_bindgen(js_name = MAX_PRIMARY_RESERVED, getter)] + pub fn js_max_primary_reserved() -> Namespace { + Namespace::MAX_PRIMARY_RESERVED + } + + /// Minimal secondary reserved [`Namespace`]. + /// + /// Used to indicate the beginning of the secondary reserved group. + #[wasm_bindgen(js_name = MIN_SECONDARY_RESERVED, getter)] + pub fn js_min_secondary_reserved() -> Namespace { + Namespace::MIN_SECONDARY_RESERVED + } + + /// Secondary reserved [`Namespace`] used for padding after the blobs. + /// + /// It is used to fill up the `original data square` after all user-submitted + /// blobs before the parity data is generated for the `ExtendedDataSquare`. + #[wasm_bindgen(js_name = TAIL_PADDING, getter)] + pub fn js_tail_padding() -> Namespace { + Namespace::TAIL_PADDING + } + + /// The [`Namespace`] for `parity shares`. + /// + /// It is the namespace with which all the `parity shares` from + /// `ExtendedDataSquare` are inserted to the `Nmt` when computing + /// merkle roots. + #[wasm_bindgen(js_name = PARITY_SHARE, getter)] + pub fn js_parity_share() -> Namespace { + Namespace::PARITY_SHARE + } + + /// Create a new [`Namespace`] version `0` with given id. + /// + /// Check [`Namespace::new_v0`] for more details. + /// + /// [`Namespace::new_v0`]: https://docs.rs/celestia-types/latest/celestia_types/nmt/struct.Namespace.html#method.new_v0 + #[wasm_bindgen(js_name = newV0)] + pub fn js_new_v0(id: Vec) -> Result { + Self::new_v0(&id.to_vec()) + } + + /// Create a new [`Namespace`] from the raw bytes. + /// + /// # Errors + /// + /// This function will return an error if the slice length is different than + /// [`NS_SIZE`] or if the namespace is invalid. If you are constructing the + /// version `0` namespace, check [`newV0`]. + #[wasm_bindgen(js_name = fromRaw)] + pub fn js_from_raw(raw: Vec) -> Result { + Self::from_raw(&raw.to_vec()) + } + /// Converts the [`Namespace`] to a byte slice. - #[wasm_bindgen(js_name = "asBytes")] - pub fn js_as_bytes(&self) -> js_sys::Uint8Array { - (&self.0 .0[..]).into() + #[wasm_bindgen(js_name = asBytes)] + pub fn js_as_bytes(&self) -> Vec { + self.as_bytes().to_vec() } /// Returns the first byte indicating the version of the [`Namespace`]. - #[wasm_bindgen(js_name = "version", getter)] + #[wasm_bindgen(js_name = version, getter)] pub fn js_version(&self) -> u8 { - self.as_bytes()[0] + self.version() } /// Returns the trailing 28 bytes indicating the id of the [`Namespace`]. - #[wasm_bindgen(js_name = "id", getter)] - pub fn js_id(&self) -> js_sys::Uint8Array { - (&self.as_bytes()[1..]).into() + #[wasm_bindgen(js_name = id, getter)] + pub fn js_id(&self) -> Vec { + self.id().to_vec() } } From 6a0ead61c99172bb3f3ada56db8e508485011fe9 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 27 Dec 2024 18:36:50 +0100 Subject: [PATCH 28/50] add inspectable to Commitment and fix comments --- types/src/blob/commitment.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/types/src/blob/commitment.rs b/types/src/blob/commitment.rs index 0f03adb7..61ce5398 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -52,10 +52,13 @@ use crate::{InfoByte, Share}; /// [`Nmt`]: crate::nmt::Nmt /// [`ExtendedDataSquare`]: crate::ExtendedDataSquare /// [`share commitment rules`]: https://github.com/celestiaorg/celestia-app/blob/main/specs/src/specs/data_square_layout.md#blob-share-commitment-rules -#[cfg_attr(all(feature = "wasm-bindgen", target_arch = "wasm32"), wasm_bindgen)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + all(feature = "wasm-bindgen", target_arch = "wasm32"), + wasm_bindgen(inspectable) +)] pub struct Commitment { - /// hash of the commitment + /// Hash of the commitment hash: merkle::Hash, } @@ -115,7 +118,7 @@ impl Commitment { Ok(Commitment { hash }) } - /// hash of the commitment + /// Hash of the commitment pub fn hash(&self) -> &merkle::Hash { &self.hash } @@ -124,7 +127,7 @@ impl Commitment { #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[wasm_bindgen] impl Commitment { - /// hash of the commitment + /// Hash of the commitment #[wasm_bindgen(js_name = hash)] pub fn js_hash(&self) -> Vec { self.hash.to_vec() From c3b685a3be6f8b9fb964e5fbadf84e4465228b07 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 27 Dec 2024 18:38:31 +0100 Subject: [PATCH 29/50] revert grpc tests changes --- grpc/tests/utils/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 91872634..0ac2acdf 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -51,11 +51,11 @@ impl TestAuthInterceptor { } } -fn env_or(var_name: &str, or_value: &str) -> String { +pub fn env_or(var_name: &str, or_value: &str) -> String { env::var(var_name).unwrap_or_else(|_| or_value.to_owned()) } -pub(crate) async fn new_test_client() -> Result> { +pub async 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?; @@ -64,7 +64,7 @@ pub(crate) async fn new_test_client() -> Result> Ok(GrpcClient::new(grpc_channel, auth_interceptor)) } -pub(crate) fn load_account(path: &str) -> TestAccount { +pub fn load_account(path: &str) -> TestAccount { let account_file = format!("{path}.addr"); let key_file = format!("{path}.plaintext-key"); From 01c3033b4eef3fcb1dfb25302af9ad37d3dc1dc4 Mon Sep 17 00:00:00 2001 From: zvolin Date: Mon, 30 Dec 2024 13:16:50 +0100 Subject: [PATCH 30/50] make AppVersion binding a struct --- types/src/consts.rs | 48 +++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/types/src/consts.rs b/types/src/consts.rs index 0e69d0f2..6ee4d471 100644 --- a/types/src/consts.rs +++ b/types/src/consts.rs @@ -106,22 +106,36 @@ pub mod appconsts { } } - /// Enum with all valid App versions. + // wasm-bindgen duplicates classes for `impl` blocks for enums + // so we can't export enum with additional methods + // https://github.com/rustwasm/wasm-bindgen/issues/1715 + /// Version of the App #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[wasm_bindgen(js_name = AppVersion)] - pub enum JsAppVersion { - /// App v1 - V1 = 1, - /// App v2 - V2 = 2, - /// App v3 - V3 = 3, - } + pub struct JsAppVersion(AppVersion); #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] #[wasm_bindgen(js_class = AppVersion)] impl JsAppVersion { + /// App v1 + #[wasm_bindgen(js_name = V1, getter)] + pub fn v1() -> JsAppVersion { + JsAppVersion(AppVersion::V1) + } + + /// App v2 + #[wasm_bindgen(js_name = V2, getter)] + pub fn v2() -> JsAppVersion { + JsAppVersion(AppVersion::V2) + } + + /// App v3 + #[wasm_bindgen(js_name = V3, getter)] + pub fn v3() -> JsAppVersion { + JsAppVersion(AppVersion::V3) + } + /// Latest App version variant. pub fn latest() -> JsAppVersion { AppVersion::latest().into() @@ -130,23 +144,15 @@ pub mod appconsts { #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] impl From for AppVersion { - fn from(js: JsAppVersion) -> AppVersion { - match js { - JsAppVersion::V1 => AppVersion::V1, - JsAppVersion::V2 => AppVersion::V2, - JsAppVersion::V3 => AppVersion::V3, - } + fn from(value: JsAppVersion) -> AppVersion { + value.0 } } #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] impl From for JsAppVersion { - fn from(js: AppVersion) -> JsAppVersion { - match js { - AppVersion::V1 => JsAppVersion::V1, - AppVersion::V2 => JsAppVersion::V2, - AppVersion::V3 => JsAppVersion::V3, - } + fn from(value: AppVersion) -> JsAppVersion { + JsAppVersion(value) } } From 662eaa77f0763f6021043de94e5b6ac7f855ee5e Mon Sep 17 00:00:00 2001 From: zvolin Date: Mon, 30 Dec 2024 13:20:18 +0100 Subject: [PATCH 31/50] take references in blob constructor to not zero out pointers --- types/src/blob.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/src/blob.rs b/types/src/blob.rs index 283e2767..1bb423f9 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -333,11 +333,11 @@ impl Blob { /// Create a new blob with the given data within the [`Namespace`]. #[wasm_bindgen(constructor)] pub fn js_new( - namespace: Namespace, + namespace: &Namespace, data: Vec, - app_version: appconsts::JsAppVersion, + app_version: &appconsts::JsAppVersion, ) -> Result { - Self::new(namespace, data, app_version.into()) + Self::new(*namespace, data, (*app_version).into()) } } From 3787a9251b27122831f2ed5a1357c29b7f952090 Mon Sep 17 00:00:00 2001 From: zvolin Date: Mon, 30 Dec 2024 13:37:12 +0100 Subject: [PATCH 32/50] take namespace by ref also in request_all_blobs --- node-wasm/src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index 78329b9e..931c8640 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -286,12 +286,12 @@ impl NodeClient { pub async fn request_all_blobs( &self, header: JsValue, - namespace: Namespace, + namespace: &Namespace, timeout_secs: Option, ) -> Result> { let command = NodeCommand::RequestAllBlobs { header, - namespace, + namespace: *namespace, timeout_secs, }; let response = self.worker.exec(command).await?; @@ -574,7 +574,7 @@ mod tests { let client = spawn_connected_node(vec![bridge_ma.to_string()]).await; let mut blobs = client - .request_all_blobs(to_value(&header).unwrap(), namespace, None) + .request_all_blobs(to_value(&header).unwrap(), &namespace, None) .await .expect("to fetch blob"); From e25df056396103e6c27d86928edde884f9e79ef5 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 3 Jan 2025 14:19:42 +0100 Subject: [PATCH 33/50] submitBlobs working from wasm --- Cargo.lock | 7 +- cli/js/index.js | 22 ++++- cli/js/package.json | 4 +- grpc/Cargo.toml | 41 ++------ grpc/src/error.rs | 2 +- grpc/src/js_client.rs | 221 ++++++++++++++++++++++++++++++++++++++++++ grpc/src/lib.rs | 2 +- grpc/src/tx.rs | 90 ++++++++++++++--- grpc/src/utils.rs | 20 ++++ grpc/tests/tonic.rs | 39 +++++++- node-wasm/Cargo.toml | 1 + node-wasm/src/lib.rs | 2 + types/src/blob.rs | 6 ++ 13 files changed, 399 insertions(+), 58 deletions(-) create mode 100644 grpc/src/js_client.rs diff --git a/Cargo.lock b/Cargo.lock index 641438d6..31c5a7c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,6 @@ dependencies = [ "celestia-grpc-macros", "celestia-proto", "celestia-types", - "console_error_panic_hook", "dotenvy", "futures", "getrandom", @@ -777,8 +776,6 @@ dependencies = [ "regex", "send_wrapper 0.6.0", "serde", - "serde-wasm-bindgen", - "serde_json", "tendermint", "tendermint-proto", "thiserror", @@ -787,12 +784,9 @@ dependencies = [ "tonic", "tonic-web-wasm-client", "tracing", - "tracing-subscriber", - "tracing-web", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", - "web-sys", ] [[package]] @@ -3462,6 +3456,7 @@ version = "0.7.0" dependencies = [ "anyhow", "blockstore", + "celestia-grpc", "celestia-rpc", "celestia-types", "console_error_panic_hook", diff --git a/cli/js/index.js b/cli/js/index.js index 8391e277..ee831db3 100644 --- a/cli/js/index.js +++ b/cli/js/index.js @@ -1,9 +1,25 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -import { NodeConfig, spawnNode } from "lumina-node"; -import { TxClient } from "celestia-grpc"; +import { Namespace, Blob, AppVersion, TxClient, NodeConfig, protoEncodeSignDoc, spawnNode } from "lumina-node"; -window.createTxClient = TxClient; +//import { secp256k1 } from "@noble/curves/secp256k1"; + +// const address = "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm"; +// const privKey = "fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb"; +// const pubKey = secp256k1.getPublicKey(privKey); + +// const signer = (signDoc) => { +// const bytes = protoEncodeSignDoc(signDoc); +// const sig = secp256k1.sign(bytes, privKey, { prehash: true }); +// return sig.toCompactRawBytes(); +// }; + +// window.txClient = await new TxClient("http://127.0.0.1:18080", address, pubKey, signer); + +window.TxClient = TxClient; +window.AppVersion = AppVersion; +window.Blob = Blob; +window.Namespace = Namespace; async function showStats(node) { if (!node || !await node.isRunning()) { diff --git a/cli/js/package.json b/cli/js/package.json index a0d8f871..acc160af 100644 --- a/cli/js/package.json +++ b/cli/js/package.json @@ -10,11 +10,13 @@ "author": "", "license": "MIT", "dependencies": { - "celestia-grpc": "file:../../grpc/pkg", + "elliptic": "^6.6.1", + "@noble/curves": "^1.7.0", "lumina-node": "file:../../node-wasm/js", "lumina-node-wasm": "file:../../node-wasm/pkg" }, "devDependencies": { + "@types/elliptic": "^6.4.18", "webpack": "^5.38.1", "webpack-cli": "^4.7.2" } diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index ac6b76c9..15f7ccdb 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -54,38 +54,9 @@ send_wrapper = { version = "0.6.0", features = ["futures"] } time = { version = "0.3.36", features = ["wasm-bindgen"] } tonic-web-wasm-client = "0.6" -console_error_panic_hook = "0.1.7" -js-sys = "0.3.70" -serde_json = "1.0.120" -serde-wasm-bindgen = "0.6.5" -tracing-subscriber = { version = "0.3.18", features = ["time"] } -tracing-web = "0.1.3" -wasm-bindgen = "0.2.93" -wasm-bindgen-futures = "0.4.43" -web-sys = { version = "0.3.70", features = [ - "BroadcastChannel", - "DedicatedWorkerGlobalScope", - "Headers", - "MessageChannel", - "MessageEvent", - "MessagePort", - "Navigator", - "Request", - "RequestInit", - "RequestMode", - "Response", - "ServiceWorker", - "ServiceWorkerGlobalScope", - "SharedWorker", - "SharedWorkerGlobalScope", - "StorageManager", - "Window", - "Worker", - "WorkerGlobalScope", - "WorkerNavigator", - "WorkerOptions", - "WorkerType", -] } +js-sys = { version = "0.3.70", optional = true } +wasm-bindgen = { version = "0.2.93", optional = true } +wasm-bindgen-futures = { version = "0.4.43", optional = true } [dev-dependencies] rand_core = "0.6.4" @@ -95,5 +66,9 @@ dotenvy = "0.15.7" tokio = { version = "1.38.0", features = ["rt", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -# wasm-bindgen-futures = "0.4.43" wasm-bindgen-test = "0.3.42" +wasm-bindgen-futures = "0.4.43" + +[features] +default = [] +wasm-bindgen = ["dep:js-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] diff --git a/grpc/src/error.rs b/grpc/src/error.rs index 68df664f..01452b7c 100644 --- a/grpc/src/error.rs +++ b/grpc/src/error.rs @@ -77,7 +77,7 @@ pub enum Error { SigningError(#[from] SignatureError), } -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] impl From for wasm_bindgen::JsValue { fn from(error: Error) -> wasm_bindgen::JsValue { error.to_string().into() diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs new file mode 100644 index 00000000..91ee4aa9 --- /dev/null +++ b/grpc/src/js_client.rs @@ -0,0 +1,221 @@ +use celestia_proto::cosmos::tx::v1beta1::SignDoc; +use celestia_types::Blob; +use js_sys::{BigInt, Function, Promise, Uint8Array}; +use k256::ecdsa::signature::Error as SignatureError; +use k256::ecdsa::{Signature, VerifyingKey}; +use prost::Message; +use tonic_web_wasm_client::Client; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; + +use crate::tx::{DocSigner, JsTxConfig, JsTxInfo}; +use crate::utils::make_object; +use crate::{GrpcClient, Result, TxClient}; + +/// Celestia grpc transaction client. +#[wasm_bindgen(js_name = "TxClient")] +pub struct JsClient { + client: TxClient, +} + +#[wasm_bindgen(js_class = "TxClient")] +impl JsClient { + /// Create a new transaction client with the specified account. + /// + /// Url must point to a [grpc-web proxy](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md). + /// + /// # Example with noble/curves + /// ```js + /// import { secp256k1 } from "@noble/curves/secp256k1"; + /// + /// const address = "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm"; + /// const privKey = "fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb"; + /// const pubKey = secp256k1.getPublicKey(privKey); + /// + /// const signer = (signDoc) => { + /// const bytes = protoEncodeSignDoc(signDoc); + /// const sig = secp256k1.sign(bytes, privKey, { prehash: true }); + /// return sig.toCompactRawBytes(); + /// }; + /// + /// const txClient = await new TxClient("http://127.0.0.1:18080", address, pubKey, signer); + /// ``` + /// + /// # Example with leap wallet + /// ```js + /// await window.leap.enable("mocha-4") + /// const keys = await window.leap.getKey("mocha-4") + /// + /// const signer = (signDoc) => { + /// return window.leap.signDirect("mocha-4", keys.bech32Address, signDoc, { preferNoSetFee: true }) + /// .then(sig => Uint8Array.from(atob(sig.signature.signature), c => c.charCodeAt(0))) + /// } + /// + /// const tx_client = await new TxClient("http://127.0.0.1:18080", keys.bech32Address, keys.pubKey, signer) + /// ``` + #[wasm_bindgen(constructor)] + pub async fn new( + url: String, + bech32_address: String, + pubkey: Uint8Array, + signer_fn: JsSignerFn, + ) -> Result { + let grpc = GrpcClient::with_grpcweb_url(url); + let signer = JsSigner { signer_fn }; + let address = bech32_address.parse()?; + let pubkey = VerifyingKey::try_from(pubkey.to_vec().as_slice())?; + let client = TxClient::new(grpc, signer, &address, Some(pubkey)).await?; + Ok(Self { client }) + } + + /// Get current gas price of a client + #[wasm_bindgen(js_name = gasPrice, getter)] + pub fn get_gas_price(&self) -> f64 { + self.client.gas_price() + } + + /// Set current gas price of a client + #[wasm_bindgen(js_name = gasPrice, setter)] + pub fn set_gas_price(&self, price: f64) { + self.client.set_gas_price(price) + } + + /// Submit blobs to celestia network. + /// + /// Provided blobs will be consumed by this method, meaning + /// they will no longer be accessible. If this behavior is not desired, + /// consider using `Blob.clone()`. + /// + /// # Example + /// ```js + /// const ns = Namespace.newV0(new Uint8Array([97, 98, 99])); + /// const data = new Uint8Array([100, 97, 116, 97]); + /// const blob = new Blob(ns, data, AppVersion.latest()); + /// + /// const txInfo = await txClient.submitBlobs([blob]); + /// ``` + /// + /// When no `TxConfig` is provided, client will automatically calculate needed + /// gas and update the `gasPrice` if network agreed on a new minimal value. + /// To enforce specific values use a `TxConfig`. + /// + /// # Example + /// ```js + /// const txInfo = await txClient.submitBlobs([blob], { gasLimit: 100000n, gasPrice: 0.02 }); + /// ``` + #[wasm_bindgen(js_name = submitBlobs)] + pub async fn submit_blobs( + &self, + blobs: Vec, + tx_config: Option, + ) -> Result { + let tx_config = tx_config.map(Into::into).unwrap_or_default(); + let tx = self.client.submit_blobs(&blobs, tx_config).await?; + Ok(tx.into()) + } +} + +/// A helper to encode the SignDoc with protobuf to get bytes to sign directly. +#[wasm_bindgen(js_name = protoEncodeSignDoc)] +pub fn proto_encode_sign_doc(sign_doc: JsSignDoc) -> Vec { + SignDoc::from(sign_doc).encode_to_vec() +} + +/// Signer that uses a javascript function for signing. +pub struct JsSigner { + signer_fn: JsSignerFn, +} + +impl DocSigner for JsSigner { + async fn try_sign(&self, doc: SignDoc) -> Result { + let msg = JsSignDoc::from(doc); + + let mut res = self.signer_fn.call1(&JsValue::null(), &msg).map_err(|e| { + let err = format!("Error calling signer fn: {e:?}"); + SignatureError::from_source(err) + })?; + + // if signer_fn is async, await it + if res.has_type::() { + let promise = res.unchecked_into::(); + res = JsFuture::from(promise).await.map_err(|e| { + let err = format!("Error awaiting signer promise: {e:?}"); + SignatureError::from_source(err) + })? + } + + let sig = res.dyn_into::().map_err(|orig| { + let err = format!( + "Signature must be Uint8Array, found: {}", + orig.js_typeof().as_string().expect("typeof returns string") + ); + SignatureError::from_source(err) + })?; + + Signature::from_slice(&sig.to_vec()).map_err(SignatureError::from_source) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const _: &str = " +/** + * A payload to be signed + */ +export interface SignDoc { + bodyBytes: Uint8Array; + authInfoBytes: Uint8Array; + chainId: string; + accountNumber: BigInt; +} + +/** + * A function that produces a signature of a payload + */ +export type SignerFn = ((arg: SignDoc) => Uint8Array) | ((arg: SignDoc) => Promise); +"; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Function, typescript_type = "SignerFn")] + pub type JsSignerFn; + + #[wasm_bindgen(typescript_type = "SignDoc")] + pub type JsSignDoc; + + #[wasm_bindgen(method, getter, js_name = bodyBytes)] + pub fn body_bytes(this: &JsSignDoc) -> Vec; + + #[wasm_bindgen(method, getter, js_name = authInfoBytes)] + pub fn auth_info_bytes(this: &JsSignDoc) -> Vec; + + #[wasm_bindgen(method, getter, js_name = chainId)] + pub fn chain_id(this: &JsSignDoc) -> String; + + #[wasm_bindgen(method, getter, js_name = accountNumber)] + pub fn account_number(this: &JsSignDoc) -> u64; +} + +impl From for SignDoc { + fn from(value: JsSignDoc) -> SignDoc { + SignDoc { + body_bytes: value.body_bytes(), + auth_info_bytes: value.auth_info_bytes(), + chain_id: value.chain_id(), + account_number: value.account_number(), + } + } +} + +impl From for JsSignDoc { + fn from(value: SignDoc) -> JsSignDoc { + let obj = make_object!( + "bodyBytes" => Uint8Array::from(value.body_bytes.as_ref()), + "authInfoBytes" => Uint8Array::from(value.auth_info_bytes.as_ref()), + "chainId" => value.chain_id.into(), + "accountNumber" => BigInt::from(value.account_number) + ); + + obj.unchecked_into() + } +} diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index 3e03169b..0c7b0331 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -2,7 +2,7 @@ mod error; pub mod grpc; -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod js_client; mod tx; mod utils; diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 706b6286..e8331581 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -16,6 +16,8 @@ use celestia_types::state::{ }; use celestia_types::{AppVersion, Height}; use http_body::Body; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +use js_sys::{BigInt, Uint8Array}; use k256::ecdsa::signature::{Error as SignatureError, Signer}; use k256::ecdsa::{Signature, VerifyingKey}; use prost::{Message, Name}; @@ -27,10 +29,14 @@ use tendermint_proto::Protobuf; use tokio::sync::{Mutex, MutexGuard}; use tonic::body::BoxBody; use tonic::client::GrpcService; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +use wasm_bindgen::{prelude::*, JsCast}; use crate::grpc::Account; use crate::grpc::TxStatus; use crate::grpc::{GrpcClient, StdError}; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +use crate::utils::make_object; use crate::utils::Interval; use crate::{Error, Result}; @@ -73,10 +79,41 @@ pub struct TxInfo { pub height: Height, } +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +#[wasm_bindgen(typescript_custom_section)] +const _: &str = " +/** + * Transaction info + */ +export interface TxInfo { + hash: String; + height: BigInt; +} +"; + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TxInfo")] + pub type JsTxInfo; +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +impl From for JsTxInfo { + fn from(value: TxInfo) -> JsTxInfo { + let obj = make_object!( + "hash" => value.hash.to_string().into(), + "height" => BigInt::from(value.height.value()) + ); + + obj.unchecked_into() + } +} + /// Configuration for the transaction. #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct TxConfig { - /// Custom gas limit for the transaction. + /// Custom gas limit for the transaction (in `utia`). pub gas_limit: Option, /// Custom gas price for fee calculation. pub gas_price: Option, @@ -96,11 +133,47 @@ impl TxConfig { } } -pub trait AsyncSigner { +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +#[wasm_bindgen(typescript_custom_section)] +const _: &str = " +/** + * Transaction config. + */ +export interface TxConfig { + gasLimit?: BigInt; // utia + gasPrice?: number; +} +"; + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TxConfig")] + pub type JsTxConfig; + + #[wasm_bindgen(method, getter, js_name = gasLimit)] + pub fn gas_limit(this: &JsTxConfig) -> Option; + + #[wasm_bindgen(method, getter, js_name = gasPrice)] + pub fn gas_price(this: &JsTxConfig) -> Option; +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +impl From for TxConfig { + fn from(value: JsTxConfig) -> TxConfig { + TxConfig { + gas_limit: value.gas_limit(), + gas_price: value.gas_price(), + } + } +} + +/// Signer capable of producing ecdsa signature using secp256k1 curve. +pub trait DocSigner { fn try_sign(&self, doc: SignDoc) -> impl Future>; } -impl AsyncSigner for T +impl DocSigner for T where T: Signer, { @@ -136,7 +209,7 @@ where T::Error: Into, T::ResponseBody: Body + Send + 'static, ::Error: Into + Send, - S: AsyncSigner, + S: DocSigner, { /// Create a new transaction client. /// @@ -376,12 +449,6 @@ where ) .await?; - println!( - "Signed tx; sequence: {}, signature: {:?}", - account.sequence, - tx.signatures.first().unwrap() - ); - let blobs = blobs.into_iter().map(Into::into).collect(); let blob_tx = RawBlobTx { tx: tx.encode_to_vec(), @@ -510,7 +577,7 @@ pub async fn sign_tx( chain_id: Id, base_account: &BaseAccount, verifying_key: &VerifyingKey, - signer: &impl AsyncSigner, + signer: &impl DocSigner, gas_limit: u64, fee: u64, ) -> Result { @@ -545,7 +612,6 @@ pub async fn sign_tx( chain_id: chain_id.into(), account_number: base_account.account_number, }; - let signature = signer.try_sign(doc).await?; Ok(RawTx { diff --git a/grpc/src/utils.rs b/grpc/src/utils.rs index 6cbe9fd0..bfb7c1e7 100644 --- a/grpc/src/utils.rs +++ b/grpc/src/utils.rs @@ -48,3 +48,23 @@ mod imp { } } } + +/// Create a new javascript `Object` with given properties +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +macro_rules! make_object { + ($( $prop:expr => $val:expr ),+) => {{ + let object = ::js_sys::Object::new(); + $( + ::js_sys::Reflect::set( + &object, + &$prop.into(), + &$val, + ) + .expect("setting field on new object"); + )+ + object + }}; +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub(crate) use make_object; diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index eee2aae0..e47c651d 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -5,6 +5,7 @@ use celestia_proto::cosmos::bank::v1beta1::MsgSend; use celestia_types::nmt::Namespace; use celestia_types::state::{Coin, ErrorCode}; use celestia_types::{AppVersion, Blob}; +use k256::ecdsa::VerifyingKey; use utils::{load_account, TestAccount}; pub mod utils; @@ -97,7 +98,8 @@ async fn get_blob_params() { async fn submit_and_get_tx() { // let (_lock, tx_client) = new_tx_client().await; let leap_account = TestAccount::from_pk( - &hex::decode("4f8fae3480edef0a19d4d5228b09fb37f3dddb9813d6641efee4b1ef95b925c9").unwrap(), + // &hex::decode("4f8fae3480edef0a19d4d5228b09fb37f3dddb9813d6641efee4b1ef95b925c9").unwrap(), + &hex::decode("fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb").unwrap(), ); let grpc_client = new_grpc_client(); let tx_client = TxClient::new( @@ -109,6 +111,41 @@ async fn submit_and_get_tx() { .await .unwrap(); + let publickey = [ + 4, 8, 188, 120, 164, 120, 7, 175, 232, 84, 167, 48, 238, 98, 212, 81, 87, 204, 10, 27, 222, + 163, 234, 251, 14, 127, 77, 29, 70, 61, 7, 10, 229, 18, 1, 26, 153, 150, 19, 126, 23, 251, + 235, 180, 50, 39, 188, 212, 225, 126, 199, 54, 190, 234, 155, 217, 131, 23, 152, 212, 7, + 83, 18, 83, 133, + ]; + let pubkey = VerifyingKey::try_from(&publickey[..]).unwrap(); + assert_eq!(pubkey, leap_account.verifying_key); + // [170, 9, 246, 238, 124, 93, 176, 134, 9, 150, 251, 49, 57, 168, 239, 85, 1, 123, 213, 219, 203, 75, 241, 125, 240, 52, 69, 80, 69, 2, 102, 254, 21, 59, 193, 232, 205, 142, 226, 126, 105, 157, 197, 162, 161, 85, 77, 183, 232, 10, 185, 195, 230, 51, 46, 168, 10, 50, 48, 147, 223, 140, 174, 38] + // 0: 86 + let sig = [ + 114, 117, 141, 12, 73, 134, 3, 177, 51, 21, 177, 141, 197, 95, 203, 131, 50, 35, 240, 144, + 114, 86, 121, 120, 73, 1, 171, 246, 133, 238, 12, 51, 112, 3, 63, 229, 197, 144, 177, 109, + 44, 158, 234, 172, 158, 105, 9, 217, 141, 214, 166, 151, 151, 17, 109, 85, 174, 250, 233, + 152, 224, 217, 211, + ]; + let msg = [ + 10, 159, 1, 10, 156, 1, 10, 32, 47, 99, 101, 108, 101, 115, 116, 105, 97, 46, 98, 108, 111, + 98, 46, 118, 49, 46, 77, 115, 103, 80, 97, 121, 70, 111, 114, 66, 108, 111, 98, 115, 18, + 120, 10, 47, 99, 101, 108, 101, 115, 116, 105, 97, 49, 54, 57, 115, 53, 48, 112, 115, 121, + 106, 50, 102, 52, 108, 97, 57, 97, 50, 50, 51, 53, 51, 50, 57, 120, 122, 55, 114, 107, 54, + 99, 53, 51, 122, 104, 119, 57, 109, 109, 18, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 98, 99, 26, 1, 4, 34, 32, 49, 100, 4, 151, 53, 254, + 18, 25, 15, 69, 28, 38, 51, 19, 64, 49, 245, 182, 33, 160, 193, 70, 179, 244, 130, 127, + 151, 189, 210, 60, 16, 116, 66, 1, 0, 18, 150, 1, 10, 80, 10, 70, 10, 31, 47, 99, 111, 115, + 109, 111, 115, 46, 99, 114, 121, 112, 116, 111, 46, 115, 101, 99, 112, 50, 53, 54, 107, 49, + 46, 80, 117, 98, 75, 101, 121, 18, 35, 10, 33, 3, 8, 188, 120, 164, 120, 7, 175, 232, 84, + 167, 48, 238, 98, 212, 81, 87, 204, 10, 27, 222, 163, 234, 251, 14, 127, 77, 29, 70, 61, 7, + 10, 229, 18, 4, 10, 2, 8, 1, 24, 1, 18, 66, 10, 11, 10, 4, 117, 116, 105, 97, 18, 3, 49, + 55, 54, 16, 223, 173, 5, 26, 47, 99, 101, 108, 101, 115, 116, 105, 97, 49, 54, 57, 115, 53, + 48, 112, 115, 121, 106, 50, 102, 52, 108, 97, 57, 97, 50, 50, 51, 53, 51, 50, 57, 120, 122, + 55, 114, 107, 54, 99, 53, 51, 122, 104, 119, 57, 109, 109, 26, 7, 112, 114, 105, 118, 97, + 116, 101, 32, 8, + ]; + let ns = Namespace::new_v0(b"abc").unwrap(); let blob = Blob::new(ns, b"data".into(), AppVersion::V3).unwrap(); // let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 539f4333..05b47493 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -24,6 +24,7 @@ crate-type = ["cdylib", "rlib"] [target.'cfg(target_arch = "wasm32")'.dependencies] blockstore.workspace = true +celestia-grpc = { workspace = true, features = ["wasm-bindgen"] } celestia-types = { workspace = true, features = ["wasm-bindgen"] } libp2p = { workspace = true, features = ["serde"] } lumina-node.workspace = true diff --git a/node-wasm/src/lib.rs b/node-wasm/src/lib.rs index f6bcdcab..cc225b0f 100644 --- a/node-wasm/src/lib.rs +++ b/node-wasm/src/lib.rs @@ -9,5 +9,7 @@ pub mod utils; mod worker; mod wrapper; +pub use celestia_grpc::TxClient; + #[cfg(test)] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); diff --git a/types/src/blob.rs b/types/src/blob.rs index 99018e9a..ce318858 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -366,6 +366,12 @@ impl Blob { ) -> Result { Self::new(*namespace, data, (*app_version).into()) } + + /// Clone a blob creating a new deep copy of it. + #[wasm_bindgen(js_name = clone)] + pub fn js_clone(&self) -> Blob { + self.clone() + } } fn shares_needed_for_blob(blob_len: usize) -> usize { From b939926b6197abe627fe7469f1934242c9a07935 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 3 Jan 2025 16:42:28 +0100 Subject: [PATCH 34/50] simplify client creation --- Cargo.lock | 1 - grpc/Cargo.toml | 1 - grpc/src/error.rs | 12 ++---- grpc/src/js_client.rs | 3 +- grpc/src/tx.rs | 89 ++++++++++++++++++++++++++++------------- grpc/tests/tonic.rs | 66 ++++-------------------------- grpc/tests/utils/mod.rs | 22 +++++----- 7 files changed, 83 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31c5a7c1..f5366a74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,6 @@ dependencies = [ "celestia-grpc-macros", "celestia-proto", "celestia-types", - "dotenvy", "futures", "getrandom", "gloo-timers 0.3.0", diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 15f7ccdb..5f560745 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -62,7 +62,6 @@ wasm-bindgen-futures = { version = "0.4.43", optional = true } 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] diff --git a/grpc/src/error.rs b/grpc/src/error.rs index 01452b7c..8205662d 100644 --- a/grpc/src/error.rs +++ b/grpc/src/error.rs @@ -16,6 +16,10 @@ pub enum Error { #[error(transparent)] TonicError(#[from] Status), + /// Transport error + #[error("Transport: {0}")] + TransportError(String), + /// Tendermint Error #[error(transparent)] TendermintError(#[from] tendermint::Error), @@ -56,18 +60,10 @@ pub enum Error { #[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/js_client.rs b/grpc/src/js_client.rs index 91ee4aa9..e9580be2 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -61,11 +61,10 @@ impl JsClient { pubkey: Uint8Array, signer_fn: JsSignerFn, ) -> Result { - let grpc = GrpcClient::with_grpcweb_url(url); let signer = JsSigner { signer_fn }; let address = bech32_address.parse()?; let pubkey = VerifyingKey::try_from(pubkey.to_vec().as_slice())?; - let client = TxClient::new(grpc, signer, &address, Some(pubkey)).await?; + let client = TxClient::with_grpcweb_url(url, &address, pubkey, signer).await?; Ok(Self { client }) } diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index e8331581..4f289ed2 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -212,31 +212,18 @@ where S: DocSigner, { /// 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, + transport: T, account_address: &Address, - account_pubkey: Option, + account_pubkey: VerifyingKey, + signer: S, ) -> Result { + let client = GrpcClient::new(transport); 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 + if let Some(pubkey) = account.pub_key { + if pubkey != PublicKey::Secp256k1(account_pubkey) { + return Err(Error::PublicKeyMismatch); } - (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); @@ -250,7 +237,7 @@ where client, signer, account, - pubkey, + pubkey: account_pubkey, app_version, chain_id, gas_price: RwLock::new(DEFAULT_MIN_GAS_PRICE), @@ -266,7 +253,7 @@ where /// # Example /// ```no_run /// # async fn docs() { - /// use celestia_grpc::{GrpcClient, TxClient, TxConfig}; + /// use celestia_grpc::{TxClient, TxConfig}; /// use celestia_proto::cosmos::bank::v1beta1::MsgSend; /// use celestia_types::state::{AccAddress, Coin}; /// use tendermint::crypto::default::ecdsa_secp256k1::SigningKey; @@ -274,9 +261,9 @@ where /// 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 grpc_url = "public-celestia-mocha4-consensus.numia.xyz:9090"; /// - /// let tx_client = TxClient::new(grpc, signing_key, &address, Some(public_key)) + /// let tx_client = TxClient::with_url(grpc_url, &address, public_key, signing_key) /// .await /// .unwrap(); /// @@ -327,7 +314,7 @@ where /// # Example /// ```no_run /// # async fn docs() { - /// use celestia_grpc::{GrpcClient, TxClient, TxConfig}; + /// use celestia_grpc::{TxClient, TxConfig}; /// use celestia_types::state::{AccAddress, Coin}; /// use celestia_types::{AppVersion, Blob}; /// use celestia_types::nmt::Namespace; @@ -336,9 +323,9 @@ where /// 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 grpc_url = "public-celestia-mocha4-consensus.numia.xyz:9090"; /// - /// let tx_client = TxClient::new(grpc, signing_key, &address, Some(public_key)) + /// let tx_client = TxClient::with_url(grpc_url, &address, public_key, signing_key) /// .await /// .unwrap(); /// @@ -386,6 +373,16 @@ where *self.gas_price.write().expect("lock poisoned") = gas_price; } + /// Get client's chain id + pub fn chain_id(&self) -> &Id { + &self.chain_id + } + + /// Get client's app version + pub fn app_version(&self) -> AppVersion { + self.app_version + } + 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| { @@ -557,6 +554,44 @@ where } } +#[cfg(not(target_arch = "wasm32"))] +impl TxClient +where + S: DocSigner, +{ + /// Create a new client connected to the given `url` with default + /// settings of [`tonic::transport::Channel`]. + pub async fn with_url( + url: impl Into, + account_address: &Address, + account_pubkey: VerifyingKey, + signer: S, + ) -> Result { + let transport = tonic::transport::Endpoint::from_shared(url.into()) + .map_err(|e| Error::TransportError(e.to_string()))? + .connect_lazy(); + Self::new(transport, account_address, account_pubkey, signer).await + } +} + +#[cfg(target_arch = "wasm32")] +impl TxClient +where + S: DocSigner, +{ + /// Create a new client connected to the given `url` with default + /// settings of [`tonic_web_wasm_client::Client`]. + pub async fn with_grpcweb_url( + url: impl Into, + account_address: &Address, + account_pubkey: VerifyingKey, + signer: S, + ) -> Result { + let transport = tonic_web_wasm_client::Client::new(url.into()); + Self::new(transport, account_address, account_pubkey, signer).await + } +} + impl Deref for TxClient { type Target = GrpcClient; diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index e47c651d..483a5822 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use celestia_grpc::{Error, TxClient, TxConfig}; +use celestia_grpc::{Error, TxConfig}; use celestia_proto::cosmos::bank::v1beta1::MsgSend; use celestia_types::nmt::Namespace; use celestia_types::state::{Coin, ErrorCode}; use celestia_types::{AppVersion, Blob}; -use k256::ecdsa::VerifyingKey; use utils::{load_account, TestAccount}; pub mod utils; @@ -96,66 +95,15 @@ async fn get_blob_params() { #[async_test] async fn submit_and_get_tx() { - // let (_lock, tx_client) = new_tx_client().await; - let leap_account = TestAccount::from_pk( - // &hex::decode("4f8fae3480edef0a19d4d5228b09fb37f3dddb9813d6641efee4b1ef95b925c9").unwrap(), - &hex::decode("fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb").unwrap(), - ); - let grpc_client = new_grpc_client(); - let tx_client = TxClient::new( - grpc_client, - leap_account.signing_key, - &leap_account.address, - Some(leap_account.verifying_key), - ) - .await - .unwrap(); - - let publickey = [ - 4, 8, 188, 120, 164, 120, 7, 175, 232, 84, 167, 48, 238, 98, 212, 81, 87, 204, 10, 27, 222, - 163, 234, 251, 14, 127, 77, 29, 70, 61, 7, 10, 229, 18, 1, 26, 153, 150, 19, 126, 23, 251, - 235, 180, 50, 39, 188, 212, 225, 126, 199, 54, 190, 234, 155, 217, 131, 23, 152, 212, 7, - 83, 18, 83, 133, - ]; - let pubkey = VerifyingKey::try_from(&publickey[..]).unwrap(); - assert_eq!(pubkey, leap_account.verifying_key); - // [170, 9, 246, 238, 124, 93, 176, 134, 9, 150, 251, 49, 57, 168, 239, 85, 1, 123, 213, 219, 203, 75, 241, 125, 240, 52, 69, 80, 69, 2, 102, 254, 21, 59, 193, 232, 205, 142, 226, 126, 105, 157, 197, 162, 161, 85, 77, 183, 232, 10, 185, 195, 230, 51, 46, 168, 10, 50, 48, 147, 223, 140, 174, 38] - // 0: 86 - let sig = [ - 114, 117, 141, 12, 73, 134, 3, 177, 51, 21, 177, 141, 197, 95, 203, 131, 50, 35, 240, 144, - 114, 86, 121, 120, 73, 1, 171, 246, 133, 238, 12, 51, 112, 3, 63, 229, 197, 144, 177, 109, - 44, 158, 234, 172, 158, 105, 9, 217, 141, 214, 166, 151, 151, 17, 109, 85, 174, 250, 233, - 152, 224, 217, 211, - ]; - let msg = [ - 10, 159, 1, 10, 156, 1, 10, 32, 47, 99, 101, 108, 101, 115, 116, 105, 97, 46, 98, 108, 111, - 98, 46, 118, 49, 46, 77, 115, 103, 80, 97, 121, 70, 111, 114, 66, 108, 111, 98, 115, 18, - 120, 10, 47, 99, 101, 108, 101, 115, 116, 105, 97, 49, 54, 57, 115, 53, 48, 112, 115, 121, - 106, 50, 102, 52, 108, 97, 57, 97, 50, 50, 51, 53, 51, 50, 57, 120, 122, 55, 114, 107, 54, - 99, 53, 51, 122, 104, 119, 57, 109, 109, 18, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 98, 99, 26, 1, 4, 34, 32, 49, 100, 4, 151, 53, 254, - 18, 25, 15, 69, 28, 38, 51, 19, 64, 49, 245, 182, 33, 160, 193, 70, 179, 244, 130, 127, - 151, 189, 210, 60, 16, 116, 66, 1, 0, 18, 150, 1, 10, 80, 10, 70, 10, 31, 47, 99, 111, 115, - 109, 111, 115, 46, 99, 114, 121, 112, 116, 111, 46, 115, 101, 99, 112, 50, 53, 54, 107, 49, - 46, 80, 117, 98, 75, 101, 121, 18, 35, 10, 33, 3, 8, 188, 120, 164, 120, 7, 175, 232, 84, - 167, 48, 238, 98, 212, 81, 87, 204, 10, 27, 222, 163, 234, 251, 14, 127, 77, 29, 70, 61, 7, - 10, 229, 18, 4, 10, 2, 8, 1, 24, 1, 18, 66, 10, 11, 10, 4, 117, 116, 105, 97, 18, 3, 49, - 55, 54, 16, 223, 173, 5, 26, 47, 99, 101, 108, 101, 115, 116, 105, 97, 49, 54, 57, 115, 53, - 48, 112, 115, 121, 106, 50, 102, 52, 108, 97, 57, 97, 50, 50, 51, 53, 51, 50, 57, 120, 122, - 55, 114, 107, 54, 99, 53, 51, 122, 104, 119, 57, 109, 109, 26, 7, 112, 114, 105, 118, 97, - 116, 101, 32, 8, - ]; - - let ns = Namespace::new_v0(b"abc").unwrap(); - let blob = Blob::new(ns, b"data".into(), AppVersion::V3).unwrap(); - // let namespace = Namespace::new_v0(&[1, 2, 3]).unwrap(); - // let blobs = vec![Blob::new(namespace, "bleb".into(), AppVersion::V3).unwrap()]; + 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()]; let tx = tx_client - .submit_blobs(&[blob], TxConfig::default()) + .submit_blobs(&blobs, TxConfig::default()) .await .unwrap(); - panic!(); let tx2 = tx_client.get_tx(tx.hash).await.unwrap(); assert_eq!(tx.hash, tx2.tx_response.txhash); @@ -252,7 +200,7 @@ async fn submit_message() { let msg = MsgSend { from_address: account.address.to_string(), - to_address: "celestia159edu39c3mmsudhg63dh4gph8ytpfdpff8q6ew".to_string(), // other_account.address.to_string(), + to_address: other_account.address.to_string(), amount: vec![amount.clone().into()], }; diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 1197460d..ab39e4d1 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -68,10 +68,7 @@ mod imp { 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") + GrpcClient::with_url(CELESTIA_GRPC_URL).expect("creating client failed") } // we have to sequence the tests which submits transactions. @@ -83,11 +80,11 @@ mod imp { let creds = load_account(); let grpc_client = new_grpc_client(); - let client = TxClient::new( - grpc_client, - creds.signing_key, + let client = TxClient::with_url( + CELESTIA_GRPC_URL, &creds.address, - Some(creds.verifying_key), + creds.verifying_key, + creds.signing_key, ) .await .unwrap(); @@ -122,12 +119,11 @@ mod imp { pub async fn new_tx_client() -> ((), TxClient) { let creds = load_account(); - let grpc_client = new_grpc_client(); - let client = TxClient::new( - grpc_client, - creds.signing_key, + let client = TxClient::with_grpcweb_url( + CELESTIA_GRPCWEB_PROXY_URL, &creds.address, - Some(creds.verifying_key), + creds.verifying_key, + creds.signing_key, ) .await .unwrap(); From a9e86bfc8ffe775c2c9e67b0a98b18d5ff68b991 Mon Sep 17 00:00:00 2001 From: zvolin Date: Fri, 3 Jan 2025 21:48:05 +0100 Subject: [PATCH 35/50] submit message --- ci/docker-compose.yml | 2 +- cli/js/index.js | 17 +++- cli/js/package.json | 6 +- cli/js/webpack.config.js | 21 +++-- grpc/src/grpc.rs | 5 ++ grpc/src/grpc/auth.rs | 107 ++++++++++++++++++++++++- grpc/src/grpc/bank.rs | 40 ++++++++++ grpc/src/js_client.rs | 164 +++++++++++++++++++++++++++++++++++++-- grpc/src/tx.rs | 38 +++++---- 9 files changed, 362 insertions(+), 38 deletions(-) diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 96e4a41c..d8ae5ea4 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -20,7 +20,7 @@ services: build: context: . dockerfile: Dockerfile.grpcwebproxy - command: --backend_addr=public-celestia-mocha4-consensus.numia.xyz:9090 --run_tls_server=false --allow_all_origins + command: --backend_addr=validator:9090 --run_tls_server=false --allow_all_origins ports: - 18080:8080 diff --git a/cli/js/index.js b/cli/js/index.js index ee831db3..cee6242c 100644 --- a/cli/js/index.js +++ b/cli/js/index.js @@ -1,8 +1,7 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default import { Namespace, Blob, AppVersion, TxClient, NodeConfig, protoEncodeSignDoc, spawnNode } from "lumina-node"; - -//import { secp256k1 } from "@noble/curves/secp256k1"; +// import { secp256k1 } from "@noble/curves/secp256k1"; // const address = "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm"; // const privKey = "fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb"; @@ -16,6 +15,20 @@ import { Namespace, Blob, AppVersion, TxClient, NodeConfig, protoEncodeSignDoc, // window.txClient = await new TxClient("http://127.0.0.1:18080", address, pubKey, signer); +// import { Registry } from "@cosmjs/proto-signing"; + +// const registry = new Registry(); +// const sendMsg = { +// typeUrl: "/cosmos.bank.v1beta1.MsgSend", +// value: { +// fromAddress: address, +// toAddress: address, +// amount: [{ denom: "utia", amount: "10000" }], +// }, +// }; +// const sendMsgAny = registry.encodeAsAny(sendMsg); +// const txInfo = await window.txClient.submitMessage(sendMsgAny); + window.TxClient = TxClient; window.AppVersion = AppVersion; window.Blob = Blob; diff --git a/cli/js/package.json b/cli/js/package.json index acc160af..a9ac7866 100644 --- a/cli/js/package.json +++ b/cli/js/package.json @@ -10,13 +10,15 @@ "author": "", "license": "MIT", "dependencies": { - "elliptic": "^6.6.1", + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stargate": "^0.32.4", "@noble/curves": "^1.7.0", + "cosmjs-types": "^0.9.0", + "crypto-browserify": "^3.12.1", "lumina-node": "file:../../node-wasm/js", "lumina-node-wasm": "file:../../node-wasm/pkg" }, "devDependencies": { - "@types/elliptic": "^6.4.18", "webpack": "^5.38.1", "webpack-cli": "^4.7.2" } diff --git a/cli/js/webpack.config.js b/cli/js/webpack.config.js index 15bc95a5..ada6d7bf 100644 --- a/cli/js/webpack.config.js +++ b/cli/js/webpack.config.js @@ -10,13 +10,22 @@ module.exports = { experiments: { asyncWebAssembly: true, }, - // hack needed for our local setup as it uses 'file:../../node-wasm/...' dependencies in package.json - // which create symlinks in `node_modules`. Webpack will follow it, but then when it tries - // to resolve `lumina-node-wasm` from `lumina-node`, it won't jump back from symlink and - // instead it will try to find `node_modules` in parent directories of '../../node-wasm/js'. - // Instead we need to guide it to always use `node_modules` we have there locally. - // https://webpack.js.org/configuration/resolve/#resolvemodules resolve: { + // hack needed for our local setup as it uses 'file:../../node-wasm/...' dependencies in package.json + // which create symlinks in `node_modules`. Webpack will follow it, but then when it tries + // to resolve `lumina-node-wasm` from `lumina-node`, it won't jump back from symlink and + // instead it will try to find `node_modules` in parent directories of '../../node-wasm/js'. + // Instead we need to guide it to always use `node_modules` we have there locally. + // https://webpack.js.org/configuration/resolve/#resolvemodules modules: [path.resolve(__dirname, 'node_modules')], + // disable nodejs modules from cosmjs + fallback: { + buffer: false, + crypto: false, + events: false, + path: false, + stream: false, + string_decoder: false, + }, }, }; diff --git a/grpc/src/grpc.rs b/grpc/src/grpc.rs index c774dbfc..ad489293 100644 --- a/grpc/src/grpc.rs +++ b/grpc/src/grpc.rs @@ -42,6 +42,11 @@ pub use crate::grpc::auth::Account; pub use crate::grpc::celestia_tx::{TxStatus, TxStatusResponse}; pub use crate::grpc::cosmos_tx::{BroadcastMode, GetTxResponse}; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use crate::grpc::auth::{JsAuthParams, JsBaseAccount, JsPublicKey}; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use crate::grpc::bank::JsCoin; + /// Error convertible to std, used by grpc transports pub type StdError = Box; diff --git a/grpc/src/grpc/auth.rs b/grpc/src/grpc/auth.rs index b8afcf36..269f398f 100644 --- a/grpc/src/grpc/auth.rs +++ b/grpc/src/grpc/auth.rs @@ -1,5 +1,3 @@ -use prost::{Message, Name}; - use celestia_proto::cosmos::auth::v1beta1::{ QueryAccountRequest, QueryAccountResponse, QueryAccountsRequest, QueryAccountsResponse, QueryParamsRequest as QueryAuthParamsRequest, QueryParamsResponse as QueryAuthParamsResponse, @@ -8,11 +6,14 @@ use celestia_types::state::auth::{ AuthParams, BaseAccount, ModuleAccount, RawBaseAccount, RawModuleAccount, }; use celestia_types::state::Address; +use prost::{Message, Name}; use tendermint_proto::google::protobuf::Any; use crate::grpc::{make_empty_params, FromGrpcResponse, IntoGrpcParam}; use crate::{Error, Result}; +// TODO: move this stuff to types similarly to address +// + add vesting accounts /// Enum representing different types of account #[derive(Debug, PartialEq)] pub enum Account { @@ -98,3 +99,105 @@ fn account_from_any(any: Any) -> Result { Ok(account) } + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use wbg::*; + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +mod wbg { + use celestia_types::state::auth::AuthParams; + use js_sys::{BigInt, Uint8Array}; + use tendermint::PublicKey; + use wasm_bindgen::{prelude::*, JsCast}; + + use crate::utils::make_object; + + use super::Account; + + #[wasm_bindgen(typescript_custom_section)] + const _: &str = " + /** + * Public key + */ + export interface PublicKey { + type: string, + value: Uint8Array + } + + /** + * Common data of all account types + */ + export interface BaseAccount { + address: string, + pubkey?: Uint8Array, + accountNumber: bigint, + sequence: bigint + } + + /** + * Auth module parameters + */ + export interface AuthParams { + maxMemoCharacters: bigint, + txSigLimit: bigint, + txSizeCostPerByte: bigint, + sigVerifyCostEd25519: bigint, + sigVerifyCostSecp256k1: bigint + } + "; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "PublicKey")] + pub type JsPublicKey; + + #[wasm_bindgen(typescript_type = "BaseAccount")] + pub type JsBaseAccount; + + #[wasm_bindgen(typescript_type = "AuthParams")] + pub type JsAuthParams; + } + + impl From for JsPublicKey { + fn from(value: PublicKey) -> JsPublicKey { + let algo = match value { + PublicKey::Ed25519(..) => "ed25519", + PublicKey::Secp256k1(..) => "secp256k1", + _ => unreachable!("unsupported pubkey algo found"), + }; + let obj = make_object!( + "type" => algo.into(), + "value" => Uint8Array::from(value.to_bytes().as_ref()) + ); + + obj.unchecked_into() + } + } + + impl From for JsBaseAccount { + fn from(value: Account) -> JsBaseAccount { + let obj = make_object!( + "address" => value.address.to_string().into(), + "pubkey" => value.pub_key.map(JsPublicKey::from).into(), + "accountNumber" => BigInt::from(value.account_number), + "sequence" => BigInt::from(value.sequence) + ); + + obj.unchecked_into() + } + } + + impl From for JsAuthParams { + fn from(value: AuthParams) -> JsAuthParams { + let obj = make_object!( + "maxMemoCharacters" => BigInt::from(value.max_memo_characters), + "txSigLimit" => BigInt::from(value.tx_sig_limit), + "txSizeCostPerByte" => BigInt::from(value.tx_size_cost_per_byte), + "sigVerifyCostEd25519" => BigInt::from(value.sig_verify_cost_ed25519), + "sigVerifyCostSecp256k1" => BigInt::from(value.sig_verify_cost_secp256k1) + ); + + obj.unchecked_into() + } + } +} diff --git a/grpc/src/grpc/bank.rs b/grpc/src/grpc/bank.rs index 7a198943..7b78dbb9 100644 --- a/grpc/src/grpc/bank.rs +++ b/grpc/src/grpc/bank.rs @@ -82,3 +82,43 @@ impl FromGrpcResponse> for QueryTotalSupplyResponse { .collect::>()?) } } + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use wbg::*; + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +mod wbg { + use celestia_types::state::Coin; + use js_sys::BigInt; + use wasm_bindgen::{prelude::*, JsCast}; + + use crate::utils::make_object; + + #[wasm_bindgen(typescript_custom_section)] + const _: &str = " + /** + * Coin + */ + export interface Coin { + denom: string, + amount: bigint + } + "; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "Coin")] + pub type JsCoin; + } + + impl From for JsCoin { + fn from(value: Coin) -> JsCoin { + let obj = make_object!( + "denom" => value.denom.into(), + "amount" => BigInt::from(value.amount) + ); + + obj.unchecked_into() + } + } +} diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index e9580be2..36eefad8 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -1,17 +1,20 @@ use celestia_proto::cosmos::tx::v1beta1::SignDoc; +use celestia_types::consts::appconsts::JsAppVersion; use celestia_types::Blob; use js_sys::{BigInt, Function, Promise, Uint8Array}; use k256::ecdsa::signature::Error as SignatureError; use k256::ecdsa::{Signature, VerifyingKey}; use prost::Message; +use tendermint_proto::google::protobuf::Any; use tonic_web_wasm_client::Client; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; -use crate::tx::{DocSigner, JsTxConfig, JsTxInfo}; +use crate::grpc::{JsAuthParams, JsBaseAccount, JsCoin}; +use crate::tx::{DocSigner, IntoAny, JsTxConfig, JsTxInfo}; use crate::utils::make_object; -use crate::{GrpcClient, Result, TxClient}; +use crate::{Result, TxClient}; /// Celestia grpc transaction client. #[wasm_bindgen(js_name = "TxClient")] @@ -68,7 +71,7 @@ impl JsClient { Ok(Self { client }) } - /// Get current gas price of a client + /// Gas price of a client #[wasm_bindgen(js_name = gasPrice, getter)] pub fn get_gas_price(&self) -> f64 { self.client.gas_price() @@ -80,12 +83,28 @@ impl JsClient { self.client.set_gas_price(price) } + /// Chain id of the client + #[wasm_bindgen(js_name = chainId, getter)] + pub fn chain_id(&self) -> String { + self.client.chain_id().to_string() + } + + /// AppVersion of the client + #[wasm_bindgen(js_name = appVersion, getter)] + pub fn app_version(&self) -> JsAppVersion { + self.client.app_version().into() + } + /// Submit blobs to celestia network. /// /// Provided blobs will be consumed by this method, meaning /// they will no longer be accessible. If this behavior is not desired, /// consider using `Blob.clone()`. /// + /// When no `TxConfig` is provided, client will automatically calculate needed + /// gas and update the `gasPrice` if network agreed on a new minimal value. + /// To enforce specific values use a `TxConfig`. + /// /// # Example /// ```js /// const ns = Namespace.newV0(new Uint8Array([97, 98, 99])); @@ -93,7 +112,20 @@ impl JsClient { /// const blob = new Blob(ns, data, AppVersion.latest()); /// /// const txInfo = await txClient.submitBlobs([blob]); + /// await txClient.submitBlobs([blob], { gasLimit: 100000n, gasPrice: 0.02 }); /// ``` + #[wasm_bindgen(js_name = submitBlobs)] + pub async fn submit_blobs( + &self, + blobs: Vec, + tx_config: Option, + ) -> Result { + let tx_config = tx_config.map(Into::into).unwrap_or_default(); + let tx = self.client.submit_blobs(&blobs, tx_config).await?; + Ok(tx.into()) + } + + /// Submit message to celestia network. /// /// When no `TxConfig` is provided, client will automatically calculate needed /// gas and update the `gasPrice` if network agreed on a new minimal value. @@ -101,18 +133,102 @@ impl JsClient { /// /// # Example /// ```js - /// const txInfo = await txClient.submitBlobs([blob], { gasLimit: 100000n, gasPrice: 0.02 }); + /// import { Registry } from "@cosmjs/proto-signing"; + /// + /// const registry = new Registry(); + /// const sendMsg = { + /// typeUrl: "/cosmos.bank.v1beta1.MsgSend", + /// value: { + /// fromAddress: "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm", + /// toAddress: "celestia1t52q7uqgnjfzdh3wx5m5phvma3umrq8k6tq2p9", + /// amount: [{ denom: "utia", amount: "10000" }], + /// }, + /// }; + /// const sendMsgAny = registry.encodeAsAny(sendMsg); + /// + /// const txInfo = await txClient.submitMessage(sendMsgAny); /// ``` - #[wasm_bindgen(js_name = submitBlobs)] - pub async fn submit_blobs( + #[wasm_bindgen(js_name = submitMessage)] + pub async fn submit_message( &self, - blobs: Vec, + message: JsAny, tx_config: Option, ) -> Result { let tx_config = tx_config.map(Into::into).unwrap_or_default(); - let tx = self.client.submit_blobs(&blobs, tx_config).await?; + let tx = self.client.submit_message(message, tx_config).await?; Ok(tx.into()) } + + // cosmos.auth + + /// Get auth params + #[wasm_bindgen(js_name = getAuthParams)] + pub async fn get_auth_params(&self) -> Result { + self.client.get_auth_params().await.map(Into::into) + } + + /// Get account + #[wasm_bindgen(js_name = getAccount)] + pub async fn get_account(&self, account: String) -> Result { + self.client + .get_account(&account.parse()?) + .await + .map(Into::into) + } + + /// Get accounts + #[wasm_bindgen(js_name = getAccounts)] + pub async fn get_accounts(&self) -> Result> { + self.client + .get_accounts() + .await + .map(|accs| accs.into_iter().map(Into::into).collect()) + } + + // cosmos.bank + + /// Get balance of coins with given denom + #[wasm_bindgen(js_name = getBalance)] + pub async fn get_balance(&self, address: String, denom: String) -> Result { + self.client + .get_balance(&address.parse()?, denom) + .await + .map(Into::into) + } + + /// Get balance of all coins + #[wasm_bindgen(js_name = getAllBalances)] + pub async fn get_all_balances(&self, address: String) -> Result> { + self.client + .get_all_balances(&address.parse()?) + .await + .map(|coins| coins.into_iter().map(Into::into).collect()) + } + + /// Get balance of all spendable coins + #[wasm_bindgen(js_name = getSpendableBalances)] + pub async fn get_spendable_balances(&self, address: String) -> Result> { + self.client + .get_spendable_balances(&address.parse()?) + .await + .map(|coins| coins.into_iter().map(Into::into).collect()) + } + + /// Get total supply + #[wasm_bindgen(js_name = getTotalSupply)] + pub async fn get_total_supply(&self) -> Result> { + self.client + .get_total_supply() + .await + .map(|coins| coins.into_iter().map(Into::into).collect()) + } + + // TODO: + // - cosmos.base.node + // - cosmos.base.tendermint + // - cosmos.tx + // - celestia.blob + // - celestia.core.tx } /// A helper to encode the SignDoc with protobuf to get bytes to sign directly. @@ -218,3 +334,35 @@ impl From for JsSignDoc { obj.unchecked_into() } } + +#[wasm_bindgen(typescript_custom_section)] +const _: &str = " +/** + * Protobuf Any type + */ +export interface ProtoAny { + typeUrl: string; + value: Uint8Array; +} +"; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ProtoAny")] + pub type JsAny; + + #[wasm_bindgen(method, getter, js_name = typeUrl)] + pub fn type_url(this: &JsAny) -> String; + + #[wasm_bindgen(method, getter, js_name = value)] + pub fn value(this: &JsAny) -> Vec; +} + +impl IntoAny for JsAny { + fn into_any(self) -> Any { + Any { + type_url: self.type_url(), + value: self.value(), + } + } +} diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 4f289ed2..72e10260 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -16,8 +16,6 @@ use celestia_types::state::{ }; use celestia_types::{AppVersion, Height}; use http_body::Body; -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -use js_sys::{BigInt, Uint8Array}; use k256::ecdsa::signature::{Error as SignatureError, Signer}; use k256::ecdsa::{Signature, VerifyingKey}; use prost::{Message, Name}; @@ -103,7 +101,7 @@ impl From for JsTxInfo { fn from(value: TxInfo) -> JsTxInfo { let obj = make_object!( "hash" => value.hash.to_string().into(), - "height" => BigInt::from(value.height.value()) + "height" => js_sys::BigInt::from(value.height.value()) ); obj.unchecked_into() @@ -183,6 +181,23 @@ where } } +/// Value convertion into protobuf's Any +pub trait IntoAny { + fn into_any(self) -> Any; +} + +impl IntoAny for T +where + T: Name, +{ + fn into_any(self) -> Any { + Any { + type_url: T::type_url(), + value: self.encode_to_vec(), + } + } +} + /// A client for submitting messages and transactions to celestia. /// /// Client handles management of the accounts sequence (nonce), thus @@ -281,10 +296,10 @@ where /// ``` pub async fn submit_message(&self, message: M, cfg: TxConfig) -> Result where - M: Name, + M: IntoAny, { let tx_body = RawTxBody { - messages: vec![into_any(message)], + messages: vec![message.into_any()], ..RawTxBody::default() }; @@ -426,7 +441,7 @@ where let pfb = MsgPayForBlobs::new(&blobs, account.address.clone())?; let pfb = RawTxBody { - messages: vec![into_any(RawMsgPayForBlobs::from(pfb))], + messages: vec![RawMsgPayForBlobs::from(pfb).into_any()], ..RawTxBody::default() }; @@ -669,17 +684,6 @@ fn estimate_gas(blobs: &[Blob], app_version: AppVersion, gas_multiplier: f64) -> (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 From 1a9d14daf0e4e51c7fc8d3ee3a366d1a7d4f7984 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 7 Jan 2025 14:17:26 +0100 Subject: [PATCH 36/50] cleanup js and docs --- cli/js/index.js | 80 +++++++++++++++++++++++++++---------------- cli/js/package.json | 13 +++---- grpc/src/js_client.rs | 15 +++++--- 3 files changed, 68 insertions(+), 40 deletions(-) diff --git a/cli/js/index.js b/cli/js/index.js index cee6242c..b63d3539 100644 --- a/cli/js/index.js +++ b/cli/js/index.js @@ -1,39 +1,54 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -import { Namespace, Blob, AppVersion, TxClient, NodeConfig, protoEncodeSignDoc, spawnNode } from "lumina-node"; -// import { secp256k1 } from "@noble/curves/secp256k1"; - -// const address = "celestia169s50psyj2f4la9a2235329xz7rk6c53zhw9mm"; -// const privKey = "fdc8ac75dfa1c142dbcba77938a14dd03078052ce0b49a529dcf72a9885a3abb"; -// const pubKey = secp256k1.getPublicKey(privKey); - -// const signer = (signDoc) => { -// const bytes = protoEncodeSignDoc(signDoc); -// const sig = secp256k1.sign(bytes, privKey, { prehash: true }); -// return sig.toCompactRawBytes(); -// }; - -// window.txClient = await new TxClient("http://127.0.0.1:18080", address, pubKey, signer); - -// import { Registry } from "@cosmjs/proto-signing"; - -// const registry = new Registry(); -// const sendMsg = { -// typeUrl: "/cosmos.bank.v1beta1.MsgSend", -// value: { -// fromAddress: address, -// toAddress: address, -// amount: [{ denom: "utia", amount: "10000" }], -// }, -// }; -// const sendMsgAny = registry.encodeAsAny(sendMsg); -// const txInfo = await window.txClient.submitMessage(sendMsgAny); - -window.TxClient = TxClient; +import { AppVersion, Blob, Namespace, NodeConfig, TxClient, protoEncodeSignDoc, spawnNode } from "lumina-node"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { Registry } from "@cosmjs/proto-signing"; + + +// Expose classes on window so they can be used from the console window.AppVersion = AppVersion; window.Blob = Blob; window.Namespace = Namespace; +// cat ci/credentials/bridge-0.address +window.bridge0Address = "celestia1t52q7uqgnjfzdh3wx5m5phvma3umrq8k6tq2p9"; + +async function createTxClient() { + // cat ci/credentials/bridge-0.plaintext-key + const privKey = "393fdb5def075819de55756b45c9e2c8531a8c78dd6eede483d3440e9457d839"; + const pubKey = secp256k1.getPublicKey(privKey); + + const signer = (signDoc) => { + const bytes = protoEncodeSignDoc(signDoc); + const sig = secp256k1.sign(bytes, privKey, { prehash: true }); + return sig.toCompactRawBytes(); + }; + + const txClient = await new TxClient( + "http://127.0.0.1:18080", + window.bridge0Address, + pubKey, + signer + ); + return txClient; +} + +async function submitBankMsgSend(address, amount) { + const registry = new Registry(); + const sendMsg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: window.bridge0Address, + toAddress: address, + amount: [{ denom: "utia", amount: amount.toString() }], + }, + }; + const sendMsgAny = registry.encodeAsAny(sendMsg); + const txInfo = await window.txClient.submitMessage(sendMsgAny); + + return txInfo; +} + async function showStats(node) { if (!node || !await node.isRunning()) { return; @@ -113,6 +128,7 @@ function stopped(document) { async function main(document, window) { window.node = await spawnNode(); + window.txClient = await createTxClient(); window.events = await window.node.eventsChannel(); window.events.onmessage = (event) => { @@ -146,6 +162,10 @@ async function main(document, window) { await started(document, window); } }); + + // test submitting transfer + const txInfo = await submitBankMsgSend(window.bridge0Address, 10000); + console.log("Submitting bank MsgSend successful", txInfo); } await main(document, window); diff --git a/cli/js/package.json b/cli/js/package.json index a9ac7866..d516756f 100644 --- a/cli/js/package.json +++ b/cli/js/package.json @@ -10,13 +10,14 @@ "author": "", "license": "MIT", "dependencies": { - "@cosmjs/proto-signing": "^0.32.4", - "@cosmjs/stargate": "^0.32.4", - "@noble/curves": "^1.7.0", - "cosmjs-types": "^0.9.0", - "crypto-browserify": "^3.12.1", "lumina-node": "file:../../node-wasm/js", - "lumina-node-wasm": "file:../../node-wasm/pkg" + "lumina-node-wasm": "file:../../node-wasm/pkg", + "@cosmjs/proto-signing": "^0.32.4", + "@noble/curves": "^1.7.0" + }, + "dependenciesComments": { + "@cosmjs/proto-signing": "only needed for grpc submitMessage", + "@noble/curves": "only needed for creating grpc client from private key" }, "devDependencies": { "webpack": "^5.38.1", diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index 36eefad8..2628d1f7 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -97,10 +97,6 @@ impl JsClient { /// Submit blobs to celestia network. /// - /// Provided blobs will be consumed by this method, meaning - /// they will no longer be accessible. If this behavior is not desired, - /// consider using `Blob.clone()`. - /// /// When no `TxConfig` is provided, client will automatically calculate needed /// gas and update the `gasPrice` if network agreed on a new minimal value. /// To enforce specific values use a `TxConfig`. @@ -114,6 +110,17 @@ impl JsClient { /// const txInfo = await txClient.submitBlobs([blob]); /// await txClient.submitBlobs([blob], { gasLimit: 100000n, gasPrice: 0.02 }); /// ``` + /// + /// # Note + /// + /// Provided blobs will be consumed by this method, meaning + /// they will no longer be accessible. If this behavior is not desired, + /// consider using `Blob.clone()`. + /// + /// ```js + /// const blobs = [blob1, blob2, blob3]; + /// await txClient.submitBlobs(blobs.map(b => b.clone())); + /// ``` #[wasm_bindgen(js_name = submitBlobs)] pub async fn submit_blobs( &self, From db0fd708228e5bd80071402d0c8a3fb8da75de54 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 7 Jan 2025 14:23:50 +0100 Subject: [PATCH 37/50] fix ts types --- grpc/src/js_client.rs | 2 +- grpc/src/tx.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index 2628d1f7..d22a8142 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -288,7 +288,7 @@ export interface SignDoc { bodyBytes: Uint8Array; authInfoBytes: Uint8Array; chainId: string; - accountNumber: BigInt; + accountNumber: bigint; } /** diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 72e10260..6d69be2c 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -84,8 +84,8 @@ const _: &str = " * Transaction info */ export interface TxInfo { - hash: String; - height: BigInt; + hash: string; + height: bigint; } "; @@ -138,7 +138,7 @@ const _: &str = " * Transaction config. */ export interface TxConfig { - gasLimit?: BigInt; // utia + gasLimit?: bigint; // utia gasPrice?: number; } "; From e1114eb8384f02a4bd45f893c65968b37eb8df7d Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 7 Jan 2025 17:00:46 +0100 Subject: [PATCH 38/50] specify wbg test timeout also in 'tests' job --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fd337a4..97c3fe85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,8 @@ jobs: test: runs-on: ubuntu-latest + env: + WASM_BINDGEN_TEST_TIMEOUT: 120 steps: - uses: actions/checkout@v1 From a54016f02846b57ef42a16f66273f92bf42d4a68 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 7 Jan 2025 17:52:20 +0100 Subject: [PATCH 39/50] cleanups --- Cargo.lock | 2 - grpc/Cargo.toml | 2 - grpc/src/grpc/auth.rs | 6 +- grpc/src/tx.rs | 262 +++++++++++++++++++++--------------------- 4 files changed, 131 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5366a74..9cc0a7b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,11 +778,9 @@ dependencies = [ "tendermint", "tendermint-proto", "thiserror", - "time", "tokio", "tonic", "tonic-web-wasm-client", - "tracing", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 5f560745..90dfd884 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -40,7 +40,6 @@ tokio = { version = "1.38.0", features = ["sync"] } tonic = { version = "0.12.3", default-features = false, features = [ "codegen", "prost" ]} -tracing = "*" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.38.0", features = ["time"] } @@ -51,7 +50,6 @@ 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"] } -time = { version = "0.3.36", features = ["wasm-bindgen"] } tonic-web-wasm-client = "0.6" js-sys = { version = "0.3.70", optional = true } diff --git a/grpc/src/grpc/auth.rs b/grpc/src/grpc/auth.rs index 269f398f..6f08a7c0 100644 --- a/grpc/src/grpc/auth.rs +++ b/grpc/src/grpc/auth.rs @@ -115,12 +115,12 @@ mod wbg { use super::Account; #[wasm_bindgen(typescript_custom_section)] - const _: &str = " + const _: &str = r#" /** * Public key */ export interface PublicKey { - type: string, + type: "ed25519" | "secp256k1", value: Uint8Array } @@ -144,7 +144,7 @@ mod wbg { sigVerifyCostEd25519: bigint, sigVerifyCostSecp256k1: bigint } - "; + "#; #[wasm_bindgen] extern "C" { diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index 6d69be2c..6facfb74 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -27,14 +27,10 @@ use tendermint_proto::Protobuf; use tokio::sync::{Mutex, MutexGuard}; use tonic::body::BoxBody; use tonic::client::GrpcService; -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -use wasm_bindgen::{prelude::*, JsCast}; use crate::grpc::Account; use crate::grpc::TxStatus; use crate::grpc::{GrpcClient, StdError}; -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -use crate::utils::make_object; use crate::utils::Interval; use crate::{Error, Result}; @@ -68,136 +64,6 @@ 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, -} - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -#[wasm_bindgen(typescript_custom_section)] -const _: &str = " -/** - * Transaction info - */ -export interface TxInfo { - hash: string; - height: bigint; -} -"; - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "TxInfo")] - pub type JsTxInfo; -} - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -impl From for JsTxInfo { - fn from(value: TxInfo) -> JsTxInfo { - let obj = make_object!( - "hash" => value.hash.to_string().into(), - "height" => js_sys::BigInt::from(value.height.value()) - ); - - obj.unchecked_into() - } -} - -/// Configuration for the transaction. -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct TxConfig { - /// Custom gas limit for the transaction (in `utia`). - 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 - } -} - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -#[wasm_bindgen(typescript_custom_section)] -const _: &str = " -/** - * Transaction config. - */ -export interface TxConfig { - gasLimit?: bigint; // utia - gasPrice?: number; -} -"; - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "TxConfig")] - pub type JsTxConfig; - - #[wasm_bindgen(method, getter, js_name = gasLimit)] - pub fn gas_limit(this: &JsTxConfig) -> Option; - - #[wasm_bindgen(method, getter, js_name = gasPrice)] - pub fn gas_price(this: &JsTxConfig) -> Option; -} - -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -impl From for TxConfig { - fn from(value: JsTxConfig) -> TxConfig { - TxConfig { - gas_limit: value.gas_limit(), - gas_price: value.gas_price(), - } - } -} - -/// Signer capable of producing ecdsa signature using secp256k1 curve. -pub trait DocSigner { - fn try_sign(&self, doc: SignDoc) -> impl Future>; -} - -impl DocSigner for T -where - T: Signer, -{ - async fn try_sign(&self, doc: SignDoc) -> Result { - let bytes = doc.encode_to_vec(); - self.try_sign(&bytes) - } -} - -/// Value convertion into protobuf's Any -pub trait IntoAny { - fn into_any(self) -> Any; -} - -impl IntoAny for T -where - T: Name, -{ - fn into_any(self) -> Any { - Any { - type_url: T::type_url(), - value: self.encode_to_vec(), - } - } -} - /// A client for submitting messages and transactions to celestia. /// /// Client handles management of the accounts sequence (nonce), thus @@ -621,6 +487,134 @@ impl fmt::Debug for TxClient { } } +/// Signer capable of producing ecdsa signature using secp256k1 curve. +pub trait DocSigner { + fn try_sign(&self, doc: SignDoc) -> impl Future>; +} + +impl DocSigner for T +where + T: Signer, +{ + async fn try_sign(&self, doc: SignDoc) -> Result { + let bytes = doc.encode_to_vec(); + self.try_sign(&bytes) + } +} + +/// Value convertion into protobuf's Any +pub trait IntoAny { + fn into_any(self) -> Any; +} + +impl IntoAny for T +where + T: Name, +{ + fn into_any(self) -> Any { + Any { + type_url: T::type_url(), + value: self.encode_to_vec(), + } + } +} + +/// 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, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct TxConfig { + /// Custom gas limit for the transaction (in `utia`). + 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 + } +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use wbg::*; + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +mod wbg { + use wasm_bindgen::{prelude::*, JsCast}; + + use super::{TxConfig, TxInfo}; + use crate::utils::make_object; + + #[wasm_bindgen(typescript_custom_section)] + const _: &str = " + /** + * Transaction info + */ + export interface TxInfo { + hash: string; + height: bigint; + } + + /** + * Transaction config. + */ + export interface TxConfig { + gasLimit?: bigint; // utia + gasPrice?: number; + } + "; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "TxInfo")] + pub type JsTxInfo; + + #[wasm_bindgen(typescript_type = "TxConfig")] + pub type JsTxConfig; + + #[wasm_bindgen(method, getter, js_name = gasLimit)] + pub fn gas_limit(this: &JsTxConfig) -> Option; + + #[wasm_bindgen(method, getter, js_name = gasPrice)] + pub fn gas_price(this: &JsTxConfig) -> Option; + } + + impl From for JsTxInfo { + fn from(value: TxInfo) -> JsTxInfo { + let obj = make_object!( + "hash" => value.hash.to_string().into(), + "height" => js_sys::BigInt::from(value.height.value()) + ); + + obj.unchecked_into() + } + } + + impl From for TxConfig { + fn from(value: JsTxConfig) -> TxConfig { + TxConfig { + gas_limit: value.gas_limit(), + gas_price: value.gas_price(), + } + } + } +} + /// Sign `tx_body` and the transaction metadata as the `base_account` using `signer` pub async fn sign_tx( tx_body: RawTxBody, From 992446de0b7c3dd8978856703f9e4426c9cc1b7b Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 15:38:28 +0100 Subject: [PATCH 40/50] resolve missing conflicts --- .github/workflows/ci.yml | 143 --------------------------------------- grpc/tests/tonic.rs | 82 ---------------------- grpc/tests/utils/mod.rs | 7 -- 3 files changed, 232 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cd3c326..eec32e18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,149 +185,6 @@ jobs: run: cargo +nightly doc --no-deps -<<<<<<< HEAD - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run fmt - run: cargo fmt -- --check - - - test-wasm: - runs-on: ubuntu-latest - env: - WASM_BINDGEN_TEST_TIMEOUT: 120 - steps: - - uses: actions/checkout@v1 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: wasm32-unknown-unknown - - - name: Install wasm-pack - uses: taiki-e/cache-cargo-install-action@v1 - with: - tool: wasm-pack@0.12.1 - - - name: Install chromedriver # we don't specify chrome version to match whatever's installed - uses: nanasess/setup-chromedriver@v2 - - - name: Build (wasm32-unknown-unknown) - run: cargo build --all --target=wasm32-unknown-unknown --all-features - - - name: Test proto crate - run: wasm-pack test --headless --chrome proto - - - name: Test types crate - run: wasm-pack test --headless --chrome types --features=wasm-bindgen - - - name: Test node crate - run: wasm-pack test --headless --chrome node - - - name: Build and pack node-wasm - run: wasm-pack build --release --target web node-wasm && wasm-pack pack node-wasm - - - test: - runs-on: ubuntu-latest - env: - WASM_BINDGEN_TEST_TIMEOUT: 120 - steps: - - uses: actions/checkout@v1 - - - name: Set up cargo cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: - cargo-${{ hashFiles('**/Cargo.lock') }} - cargo- - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: wasm32-unknown-unknown - - - name: Install wasm-pack - uses: taiki-e/cache-cargo-install-action@v1 - with: - tool: wasm-pack@0.12.1 - - - name: Install chromedriver # we don't specify chrome version to match whatever's installed - uses: nanasess/setup-chromedriver@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - # needed for the buildx in order to access gha cache - # https://github.com/docker/bake-action/issues/36#issuecomment-1103961612 - - name: Expose github actions runtime - uses: crazy-max/ghaction-github-runtime@v1 - - - name: Build the docker-compose stack - run: | - cat > ci/cache.json <>>>>>> main unused-deps: runs-on: ubuntu-latest name: unused dependencies diff --git a/grpc/tests/tonic.rs b/grpc/tests/tonic.rs index dd3ac174..4272ce77 100644 --- a/grpc/tests/tonic.rs +++ b/grpc/tests/tonic.rs @@ -150,11 +150,7 @@ async fn submit_blobs_insufficient_gas_price_and_limit() { .unwrap_err(); assert!(matches!( err, -<<<<<<< HEAD - Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _, _) -======= Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _) ->>>>>>> main )); let err = tx_client @@ -163,46 +159,11 @@ async fn submit_blobs_insufficient_gas_price_and_limit() { .unwrap_err(); assert!(matches!( err, -<<<<<<< HEAD - Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) -======= Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _) ->>>>>>> main )); } #[async_test] -<<<<<<< HEAD -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 - 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 - tx_client - .submit_blobs(&blobs, TxConfig::default()) - .await - .unwrap(); - assert!(tx_client.gas_price() > 0.0005); -} - -#[async_test] -======= ->>>>>>> main async fn submit_message() { let account = load_account(); let other_account = TestAccount::random(); @@ -248,11 +209,7 @@ async fn submit_message_insufficient_gas_price_and_limit() { .unwrap_err(); assert!(matches!( err, -<<<<<<< HEAD - Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _, _) -======= Error::TxBroadcastFailed(_, ErrorCode::OutOfGas, _) ->>>>>>> main )); let err = tx_client @@ -261,45 +218,6 @@ async fn submit_message_insufficient_gas_price_and_limit() { .unwrap_err(); assert!(matches!( err, -<<<<<<< HEAD - Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _, _) - )); -} - -#[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 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 - 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 - tx_client - .submit_message(msg, TxConfig::default()) - .await - .unwrap(); - assert!(tx_client.gas_price() > 0.0005); -======= Error::TxBroadcastFailed(_, ErrorCode::InsufficientFee, _) )); ->>>>>>> main } diff --git a/grpc/tests/utils/mod.rs b/grpc/tests/utils/mod.rs index 9b1e063c..60ec408a 100644 --- a/grpc/tests/utils/mod.rs +++ b/grpc/tests/utils/mod.rs @@ -25,7 +25,6 @@ impl TestAccount { address: AccAddress::new(verifying_key.into()).into(), verifying_key, signing_key, -<<<<<<< HEAD } } @@ -37,8 +36,6 @@ impl TestAccount { address: AccAddress::new(verifying_key.into()).into(), verifying_key, signing_key, -======= ->>>>>>> main } } } @@ -82,10 +79,6 @@ mod imp { let lock = LOCK.get_or_init(|| Mutex::new(())).lock().await; let creds = load_account(); -<<<<<<< HEAD - let grpc_client = new_grpc_client(); -======= ->>>>>>> main let client = TxClient::with_url( CELESTIA_GRPC_URL, &creds.address, From 2daba925ccc13b5c5b3dd607d99f6aacf27afed6 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 15:43:34 +0100 Subject: [PATCH 41/50] remove regex --- Cargo.lock | 1 - grpc/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e9bf94e..fe15156f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,7 +857,6 @@ dependencies = [ "k256", "prost", "rand_core", - "regex", "send_wrapper 0.6.0", "serde", "tendermint", diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 4070e703..3e8af305 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -33,7 +33,6 @@ 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"] } From 5cf32100197b76ddde13de663b613a22c2f34849 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 15:45:58 +0100 Subject: [PATCH 42/50] fix public key in typescript account --- grpc/src/grpc/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/src/grpc/auth.rs b/grpc/src/grpc/auth.rs index 6f08a7c0..b16e8b95 100644 --- a/grpc/src/grpc/auth.rs +++ b/grpc/src/grpc/auth.rs @@ -129,7 +129,7 @@ mod wbg { */ export interface BaseAccount { address: string, - pubkey?: Uint8Array, + pubkey?: PublicKey, accountNumber: bigint, sequence: bigint } From c0fab0975039ea26938ca2ddea6b2aca3c953bf2 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 16:29:14 +0100 Subject: [PATCH 43/50] add missing wbg feature on types --- grpc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 3e8af305..3f7e4630 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -67,4 +67,4 @@ wasm-bindgen-test.workspace = true [features] default = [] -wasm-bindgen = ["dep:js-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] +wasm-bindgen = ["celestia-types/wasm-bindgen", "dep:js-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] From 55077a9aebba0269068139acefce6970f73751ec Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 16:30:11 +0100 Subject: [PATCH 44/50] update lockfile --- cli/js/package-lock.json | 221 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/cli/js/package-lock.json b/cli/js/package-lock.json index d94e07cc..db5b6216 100644 --- a/cli/js/package-lock.json +++ b/cli/js/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@cosmjs/proto-signing": "^0.32.4", + "@noble/curves": "^1.7.0", "lumina-node": "file:../../node-wasm/js", "lumina-node-wasm": "file:../../node-wasm/pkg" }, @@ -42,6 +44,73 @@ "version": "0.7.0", "license": "Apache-2.0" }, + "node_modules/@cosmjs/amino": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.32.4.tgz", + "integrity": "sha512-zKYOt6hPy8obIFtLie/xtygCkH9ZROiQ12UHfKsOkWaZfPQUvVbtgmu6R4Kn1tFLI/SRkw7eqhaogmW/3NYu/Q==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4" + } + }, + "node_modules/@cosmjs/crypto": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.32.4.tgz", + "integrity": "sha512-zicjGU051LF1V9v7bp8p7ovq+VyC91xlaHdsFOTo2oVry3KQikp8L/81RkXmUIT8FxMwdx1T7DmFwVQikcSDIw==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@cosmjs/encoding": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.4.tgz", + "integrity": "sha512-tjvaEy6ZGxJchiizzTn7HVRiyTg1i4CObRRaTRPknm5EalE13SV+TCHq38gIDfyUeden4fCuaBVEdBR5+ti7Hw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "node_modules/@cosmjs/math": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.32.4.tgz", + "integrity": "sha512-++dqq2TJkoB8zsPVYCvrt88oJWsy1vMOuSOKcdlnXuOA/ASheTJuYy4+oZlTQ3Fr8eALDLGGPhJI02W2HyAQaw==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "node_modules/@cosmjs/proto-signing": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz", + "integrity": "sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.32.4", + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "cosmjs-types": "^0.9.0" + } + }, + "node_modules/@cosmjs/utils": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.32.4.tgz", + "integrity": "sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w==", + "license": "Apache-2.0" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -109,6 +178,33 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -401,6 +497,44 @@ "ajv": "^6.9.1" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "license": "MIT" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", @@ -494,6 +628,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/cosmjs-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.9.0.tgz", + "integrity": "sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==", + "license": "Apache-2.0" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -514,6 +654,27 @@ "integrity": "sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A==", "dev": true }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -679,6 +840,16 @@ "node": ">=8" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -691,6 +862,17 @@ "node": ">= 0.4" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -710,6 +892,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -796,6 +984,21 @@ "node": ">=0.10.0" } }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -852,6 +1055,18 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -960,6 +1175,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/readonly-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", + "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==", + "license": "Apache-2.0" + }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", From 9e1783e3091edba672014761918b849c4360cef8 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 16:31:57 +0100 Subject: [PATCH 45/50] add dropped doc --- grpc/src/tx.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index bebb84e7..f94930c2 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -501,6 +501,7 @@ pub struct TxConfig { pub gas_price: Option, } +/// Configuration for the transaction. impl TxConfig { /// Attach gas limit to this config. pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { From d567ff7852589b7eb698e873f85e3795ee16e1bd Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 14 Jan 2025 16:36:55 +0100 Subject: [PATCH 46/50] move the doc to correct place --- grpc/src/tx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index f94930c2..a69f9200 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -493,6 +493,7 @@ pub struct TxInfo { pub height: Height, } +/// Configuration for the transaction. #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct TxConfig { /// Custom gas limit for the transaction (in `utia`). @@ -501,7 +502,6 @@ pub struct TxConfig { pub gas_price: Option, } -/// Configuration for the transaction. impl TxConfig { /// Attach gas limit to this config. pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { From f4aec41e07cb8d1576eadfe43977aafa21cfbf55 Mon Sep 17 00:00:00 2001 From: zvolin Date: Thu, 16 Jan 2025 16:45:14 +0100 Subject: [PATCH 47/50] add missing docs for wbg bindings --- grpc/src/grpc/auth.rs | 3 +++ grpc/src/grpc/bank.rs | 1 + grpc/src/js_client.rs | 1 + grpc/src/tx.rs | 2 ++ 4 files changed, 7 insertions(+) diff --git a/grpc/src/grpc/auth.rs b/grpc/src/grpc/auth.rs index b16e8b95..a86c62d7 100644 --- a/grpc/src/grpc/auth.rs +++ b/grpc/src/grpc/auth.rs @@ -148,12 +148,15 @@ mod wbg { #[wasm_bindgen] extern "C" { + /// Public key exposed to javascript. #[wasm_bindgen(typescript_type = "PublicKey")] pub type JsPublicKey; + /// BaseAccount exposed to javascript. #[wasm_bindgen(typescript_type = "BaseAccount")] pub type JsBaseAccount; + /// AuthParams exposed to javascript. #[wasm_bindgen(typescript_type = "AuthParams")] pub type JsAuthParams; } diff --git a/grpc/src/grpc/bank.rs b/grpc/src/grpc/bank.rs index 7b78dbb9..ea4d7189 100644 --- a/grpc/src/grpc/bank.rs +++ b/grpc/src/grpc/bank.rs @@ -107,6 +107,7 @@ mod wbg { #[wasm_bindgen] extern "C" { + /// Coin exposed to javascript #[wasm_bindgen(typescript_type = "Coin")] pub type JsCoin; } diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index dea9e3dd..44e48c14 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -349,6 +349,7 @@ export interface ProtoAny { #[wasm_bindgen] extern "C" { + /// Protobuf Any exposed to javascript #[wasm_bindgen(typescript_type = "ProtoAny")] pub type JsAny; diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index a69f9200..f2341e8c 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -547,9 +547,11 @@ mod wbg { #[wasm_bindgen] extern "C" { + /// TxInfo exposed to javascript #[wasm_bindgen(typescript_type = "TxInfo")] pub type JsTxInfo; + /// TxConfig exposed to javascript #[wasm_bindgen(typescript_type = "TxConfig")] pub type JsTxConfig; From 235868cac58718918e71ebfecbba831308e10046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zwoli=C5=84ski?= Date: Tue, 21 Jan 2025 13:57:25 +0100 Subject: [PATCH 48/50] florek's grammars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Florkiewicz Signed-off-by: Maciej Zwoliński --- cli/js/index.js | 1 - grpc/src/js_client.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cli/js/index.js b/cli/js/index.js index ffc6843f..67b8e409 100644 --- a/cli/js/index.js +++ b/cli/js/index.js @@ -4,7 +4,6 @@ import { AppVersion, Blob, Namespace, NodeConfig, TxClient, protoEncodeSignDoc, import { secp256k1 } from "@noble/curves/secp256k1"; import { Registry } from "@cosmjs/proto-signing"; - // Expose classes on window so they can be used from the console window.AppVersion = AppVersion; window.Blob = Blob; diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index 44e48c14..063b1e6c 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -89,10 +89,10 @@ impl JsClient { self.client.app_version().into() } - /// Submit blobs to celestia network. + /// Submit blobs to the celestia network. /// /// When no `TxConfig` is provided, client will automatically calculate needed - /// gas and update the `gasPrice` if network agreed on a new minimal value. + /// gas and update the `gasPrice`, if network agreed on a new minimal value. /// To enforce specific values use a `TxConfig`. /// /// # Example @@ -126,10 +126,10 @@ impl JsClient { Ok(tx.into()) } - /// Submit message to celestia network. + /// Submit message to the celestia network. /// /// When no `TxConfig` is provided, client will automatically calculate needed - /// gas and update the `gasPrice` if network agreed on a new minimal value. + /// gas and update the `gasPrice`, if network agreed on a new minimal value. /// To enforce specific values use a `TxConfig`. /// /// # Example From 432938c94308b23962bf20ae125e581b33b5be1c Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 21 Jan 2025 14:47:21 +0100 Subject: [PATCH 49/50] replace rlib with lib to allow cargo more freedom --- grpc/Cargo.toml | 2 +- node-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 3f7e4630..08c8d383 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -19,7 +19,7 @@ categories = [ ] [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "lib"] [dependencies] celestia-grpc-macros = { version = "0.1.0", path = "grpc-macros" } diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index fe534bb5..27c2e268 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -20,7 +20,7 @@ categories = [ ] [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "lib"] [target.'cfg(target_arch = "wasm32")'.dependencies] blockstore.workspace = true From 0d62ddfc7db94f6f68c6461b6a8c03ada7a42578 Mon Sep 17 00:00:00 2001 From: zvolin Date: Tue, 21 Jan 2025 14:48:12 +0100 Subject: [PATCH 50/50] rename last_gas_price --- grpc/src/js_client.rs | 6 +++--- grpc/src/tx.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grpc/src/js_client.rs b/grpc/src/js_client.rs index 063b1e6c..fac153ed 100644 --- a/grpc/src/js_client.rs +++ b/grpc/src/js_client.rs @@ -72,9 +72,9 @@ impl JsClient { } /// Last gas price fetched by the client - #[wasm_bindgen(js_name = lastGasPrice)] - pub fn last_gas_price(&self) -> f64 { - self.client.last_gas_price() + #[wasm_bindgen(js_name = lastSeenGasPrice)] + pub fn last_seen_gas_price(&self) -> f64 { + self.client.last_seen_gas_price() } /// Chain id of the client diff --git a/grpc/src/tx.rs b/grpc/src/tx.rs index f2341e8c..4f3ccdb2 100644 --- a/grpc/src/tx.rs +++ b/grpc/src/tx.rs @@ -234,7 +234,7 @@ where } /// Get most recent minimal gas price seen by the client - pub fn last_gas_price(&self) -> f64 { + pub fn last_seen_gas_price(&self) -> f64 { *self.gas_price.read().expect("lock poisoned") }