diff --git a/node-wasm/src/client.rs b/node-wasm/src/client.rs index fc1ee5920..583e29f03 100644 --- a/node-wasm/src/client.rs +++ b/node-wasm/src/client.rs @@ -3,7 +3,8 @@ use std::time::Duration; use blockstore::EitherBlockstore; -use celestia_types::ExtendedHeader; +use celestia_types::nmt::Namespace; +use celestia_types::{Blob, ExtendedHeader}; use js_sys::Array; use lumina_node::blockstore::{InMemoryBlockstore, IndexedDbBlockstore}; use lumina_node::network; @@ -250,13 +251,31 @@ impl NodeClient { amount: u64, ) -> Result> { let command = NodeCommand::GetVerifiedHeaders { - from: from.to_owned(), + from: from.clone(), amount, }; let response = self.worker.exec(command).await?; response.into_headers().check_variant()? } + /// 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: &ExtendedHeader, + namespace: &Namespace, + timeout_secs: Option, + ) -> Result> { + let command = NodeCommand::RequestAllBlobs { + header: header.clone(), + namespace: *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 { @@ -268,9 +287,6 @@ impl NodeClient { } /// Get the latest header announced in the network. - /// - /// Returns a javascript object with given structure: - /// https://docs.rs/celestia-types/latest/celestia_types/struct.ExtendedHeader.html #[wasm_bindgen(js_name = getNetworkHeadHeader)] pub async fn get_network_head_header(&self) -> Result> { let command = NodeCommand::LastSeenNetworkHead; @@ -279,9 +295,6 @@ impl NodeClient { } /// Get the latest locally synced header. - /// - /// Returns a javascript object with given structure: - /// https://docs.rs/celestia-types/latest/celestia_types/struct.ExtendedHeader.html #[wasm_bindgen(js_name = getLocalHeadHeader)] pub async fn get_local_head_header(&self) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::Head); @@ -290,9 +303,6 @@ impl NodeClient { } /// Get a synced header for the block with a given hash. - /// - /// Returns a javascript object with given structure: - /// https://docs.rs/celestia-types/latest/celestia_types/struct.ExtendedHeader.html #[wasm_bindgen(js_name = getHeaderByHash)] pub async fn get_header_by_hash(&self, hash: &str) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); @@ -301,9 +311,6 @@ impl NodeClient { } /// Get a synced header for the block with a given height. - /// - /// Returns a javascript object with given structure: - /// https://docs.rs/celestia-types/latest/celestia_types/struct.ExtendedHeader.html #[wasm_bindgen(js_name = getHeaderByHeight)] pub async fn get_header_by_height(&self, height: u64) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHeight(height)); @@ -320,9 +327,6 @@ impl NodeClient { /// # Errors /// /// If range contains a height of a header that is not found in the store. - /// - /// Returns an array of javascript objects with given structure: - /// https://docs.rs/celestia-types/latest/celestia_types/struct.ExtendedHeader.html #[wasm_bindgen(js_name = getHeaders)] pub async fn get_headers( &self, @@ -338,9 +342,6 @@ impl NodeClient { } /// Get data sampling metadata of an already sampled height. - /// - /// Returns a javascript object with given structure: - /// https://docs.rs/lumina-node/latest/lumina_node/store/struct.SamplingMetadata.html #[wasm_bindgen(js_name = getSamplingMetadata)] pub async fn get_sampling_metadata(&self, height: u64) -> Result> { let command = NodeCommand::GetSamplingMetadata { height }; @@ -435,9 +436,9 @@ mod tests { use std::time::Duration; - use celestia_rpc::{prelude::*, Client}; + use celestia_rpc::{prelude::*, Client, TxConfig}; use celestia_types::p2p::PeerId; - use celestia_types::ExtendedHeader; + use celestia_types::{AppVersion, ExtendedHeader}; use gloo_timers::future::sleep; use libp2p::{multiaddr::Protocol, Multiaddr}; use rexie::Rexie; @@ -497,6 +498,38 @@ mod tests { .unwrap(); } + #[wasm_bindgen_test] + async fn get_blob() { + 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()]; + + let submitted_height = rpc_client + .blob_submit(&blobs, TxConfig::default()) + .await + .expect("successful submission"); + + 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 mut blobs = client + .request_all_blobs(&header, &namespace, None) + .await + .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 { 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 163749bef..5a75ec484 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 libp2p::Multiaddr; use libp2p::PeerId; @@ -51,6 +53,11 @@ pub(crate) enum NodeCommand { GetSamplingMetadata { height: u64, }, + RequestAllBlobs { + header: ExtendedHeader, + namespace: Namespace, + timeout_secs: Option, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -80,6 +87,7 @@ pub(crate) enum WorkerResponse { Headers(Result, Error>), LastSeenNetworkHead(Result, Error>), SamplingMetadata(Result>), + Blobs(Result>), } pub(crate) trait CheckableResponseExt { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index c931c627a..3c12d0b58 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,6 +1,9 @@ use std::fmt::Debug; +use std::time::Duration; use blockstore::EitherBlockstore; +use celestia_types::nmt::Namespace; +use celestia_types::Blob; use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::to_value; @@ -224,6 +227,19 @@ impl NodeWorkerInstance { Ok(self.node.get_sampling_metadata(height).await?) } + async fn request_all_blobs( + &mut self, + header: ExtendedHeader, + namespace: Namespace, + timeout_secs: Option, + ) -> Result> { + 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), @@ -273,6 +289,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/src/store/utils.rs b/node/src/store/utils.rs index f285e64c1..ee40405bc 100644 --- a/node/src/store/utils.rs +++ b/node/src/store/utils.rs @@ -55,7 +55,7 @@ impl From for VerifiedExtendedHeaders { } } -impl From<&'_ ExtendedHeader> for VerifiedExtendedHeaders { +impl From<&ExtendedHeader> for VerifiedExtendedHeaders { fn from(value: &ExtendedHeader) -> Self { Self(vec![value.to_owned()]) } diff --git a/rpc/tests/blob.rs b/rpc/tests/blob.rs index 575772700..1789692f8 100644 --- a/rpc/tests/blob.rs +++ b/rpc/tests/blob.rs @@ -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 = Commitment(random_bytes_array()); + let commitment = Commitment::new(random_bytes_array()); let submitted_height = blob_submit(&client, &[blob.clone()]).await.unwrap(); diff --git a/types/Cargo.toml b/types/Cargo.toml index cb1748c89..417370dea 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -42,9 +42,9 @@ thiserror = "1.0.61" time = { version = "0.3.36", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { version = "0.2.95", optional = true } -serde-wasm-bindgen = { version = "0.6.5", optional = true } js-sys = { version = "0.3.76", optional = true } +serde-wasm-bindgen = { version = "0.6.5", optional = true } +wasm-bindgen = { version = "0.2.95", optional = true } [dev-dependencies] ed25519-consensus = "2.1.0" @@ -63,7 +63,8 @@ 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 = ["time/wasm-bindgen", "dep:wasm-bindgen", "dep:serde-wasm-bindgen", "dep:js-sys", "nmt-rs/serde"] +tonic = ["celestia-proto/tonic"] +wasm-bindgen = ["dep:js-sys", "dep:serde-wasm-bindgen", "dep:wasm-bindgen", "nmt-rs/serde", "time/wasm-bindgen"] [package.metadata.docs.rs] features = ["p2p", "test-utils"] diff --git a/types/src/blob.rs b/types/src/blob.rs index 697b3bed0..1bb423f93 100644 --- a/types/src/blob.rs +++ b/types/src/blob.rs @@ -17,12 +17,18 @@ 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(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +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( + all(feature = "wasm-bindgen", target_arch = "wasm32"), + wasm_bindgen(getter_with_clone, inspectable) +)] pub struct Blob { /// A [`Namespace`] the [`Blob`] belongs to. pub namespace: Namespace, @@ -321,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) @@ -395,7 +415,7 @@ mod tests { #[test] fn validate_blob_commitment_mismatch() { let mut blob = sample_blob(); - blob.commitment.0.fill(7); + 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 0f7effb39..61ce53984 100644 --- a/types/src/blob/commitment.rs +++ b/types/src/blob/commitment.rs @@ -8,6 +8,8 @@ use nmt_rs::NamespaceMerkleHasher; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tendermint::crypto::sha256::HASH_SIZE; use tendermint::{crypto, merkle}; +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +use wasm_bindgen::prelude::*; use crate::consts::appconsts; use crate::nmt::{Namespace, NamespacedHashExt, NamespacedSha2Hasher, Nmt, RawNamespacedHash}; @@ -51,9 +53,21 @@ use crate::{InfoByte, Share}; /// [`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 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Commitment(pub merkle::Hash); +#[cfg_attr( + all(feature = "wasm-bindgen", target_arch = "wasm32"), + wasm_bindgen(inspectable) +)] +pub struct Commitment { + /// Hash of the commitment + 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, @@ -101,7 +115,28 @@ impl Commitment { let hash = merkle::simple_hash_from_byte_vectors::(&subtree_roots); - Ok(Commitment(hash)) + 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() + } +} + +impl From for merkle::Hash { + fn from(commitment: Commitment) -> Self { + commitment.hash } } @@ -110,7 +145,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 +168,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 d333cf709..914486ef2 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.0.to_vec()) + .map(|c| c.hash().to_vec()) .collect(); RawMsgPayForBlobs { @@ -106,9 +107,8 @@ impl TryFrom for MsgPayForBlobs { .share_commitments .into_iter() .map(|c| { - Ok(Commitment( - c.try_into().map_err(|_| Error::InvalidComittmentLength)?, - )) + let hash = Hash::try_from(c).map_err(|_| Error::InvalidComittmentLength)?; + Ok(Commitment::new(hash)) }) .collect::>()?; diff --git a/types/src/consts.rs b/types/src/consts.rs index 9bef0edce..6ee4d4712 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,56 @@ pub mod appconsts { } } + // 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 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() + } + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + impl From for AppVersion { + fn from(value: JsAppVersion) -> AppVersion { + value.0 + } + } + + #[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] + impl From for JsAppVersion { + fn from(value: AppVersion) -> JsAppVersion { + JsAppVersion(value) + } + } + /// 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/nmt.rs b/types/src/nmt.rs index 920a9922a..40c68f855 100644 --- a/types/src/nmt.rs +++ b/types/src/nmt.rs @@ -24,6 +24,8 @@ use cid::CidGeneric; use multihash::Multihash; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tendermint::hash::SHA256_HASH_SIZE; +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +use wasm_bindgen::prelude::*; mod namespace_proof; mod namespaced_hash; @@ -91,6 +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(inspectable) +)] pub struct Namespace(nmt_rs::NamespaceId); impl Namespace { @@ -373,6 +379,112 @@ 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) -> Vec { + self.as_bytes().to_vec() + } + + /// Returns the first byte indicating the version of the [`Namespace`]. + #[wasm_bindgen(js_name = version, getter)] + pub fn js_version(&self) -> u8 { + self.version() + } + + /// Returns the trailing 28 bytes indicating the id of the [`Namespace`]. + #[wasm_bindgen(js_name = id, getter)] + pub fn js_id(&self) -> Vec { + self.id().to_vec() + } +} + impl From for nmt_rs::NamespaceId { fn from(value: Namespace) -> Self { value.0