From f9abacc6c28ed81157213521ef5373fc650ae430 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 14 Jan 2026 09:59:18 +0700 Subject: [PATCH 01/14] refactor(sdk): typed param for netowork --- packages/wasm-dpp2/TODO.md | 20 +--- packages/wasm-dpp2/src/enums/network.rs | 103 +++++++++++++++--- packages/wasm-dpp2/src/identity/public_key.rs | 2 +- packages/wasm-dpp2/src/lib.rs | 1 + .../wasm-dpp2/src/platform_address/address.rs | 23 +--- .../src/wallet/extended_derivation.rs | 8 +- .../wasm-sdk/src/wallet/key_derivation.rs | 15 +-- .../wasm-sdk/src/wallet/key_generation.rs | 75 +++++-------- 8 files changed, 138 insertions(+), 109 deletions(-) diff --git a/packages/wasm-dpp2/TODO.md b/packages/wasm-dpp2/TODO.md index 8f142e23ecc..8cd0b21a657 100644 --- a/packages/wasm-dpp2/TODO.md +++ b/packages/wasm-dpp2/TODO.md @@ -30,23 +30,15 @@ type ProTxHashLike = ProTxHash | Uint8Array | string; - `getEvonodesProposedEpochBlocksByIdsWithProofInfo` (proTxHashes parameter) - `EvonodeProposedBlocksRangeQuery.startAfter` field -### Network Wrapper +### ~~Network Wrapper~~ ✅ DONE -Create a wrapper around `Network` from dashcore with flexible input types: +~~Create a wrapper around `Network` from dashcore with flexible input types:~~ ```typescript -type NetworkLike = Network | string; +type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest"; ``` -**Requirements:** - -- Create `NetworkWasm` wrapper in wasm-dpp2 -- Accept string ("mainnet", "testnet", "devnet", "regtest") or Network object -- Centralize parsing logic -- Use everywhere we pass network parameter - -**Affected areas:** +**Implemented in:** -- SDK builder methods -- Wallet key derivation -- Address generation/validation +- `wasm-dpp2/src/enums/network.rs` - `NetworkWasm` with `TryFrom`, `try_from_options()`, `as_str()` +- `wasm-sdk/src/wallet/key_generation.rs` - Updated all functions to use `NetworkLike` diff --git a/packages/wasm-dpp2/src/enums/network.rs b/packages/wasm-dpp2/src/enums/network.rs index 91a51f238f2..97ede6a6b97 100644 --- a/packages/wasm-dpp2/src/enums/network.rs +++ b/packages/wasm-dpp2/src/enums/network.rs @@ -1,8 +1,21 @@ -use crate::error::WasmDppError; +use crate::error::{WasmDppError, WasmDppResult}; use dpp::dashcore::Network; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; + +/// TypeScript type alias for flexible network input +#[wasm_bindgen(typescript_custom_section)] +const NETWORK_LIKE_TS: &'static str = r#" +/** + * Flexible network type that accepts Network enum or string names. + * + * String values (case-insensitive): "mainnet", "testnet", "devnet", "regtest" + */ +export type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest"; +"#; + #[wasm_bindgen(js_name = "Network")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(non_camel_case_types)] pub enum NetworkWasm { Mainnet = 0, @@ -21,27 +34,14 @@ impl TryFrom for NetworkWasm { "devnet" => Ok(NetworkWasm::Devnet), "regtest" => Ok(NetworkWasm::Regtest), _ => Err(WasmDppError::invalid_argument(format!( - "unsupported network name ({})", - enum_val - ))), - }; - } - - if let Some(enum_val) = value.as_f64() { - return match enum_val as u8 { - 0 => Ok(NetworkWasm::Mainnet), - 1 => Ok(NetworkWasm::Testnet), - 2 => Ok(NetworkWasm::Devnet), - 3 => Ok(NetworkWasm::Regtest), - _ => Err(WasmDppError::invalid_argument(format!( - "unsupported network name ({})", + "unsupported network name '{}'. Expected: mainnet, testnet, devnet, or regtest", enum_val ))), }; } Err(WasmDppError::invalid_argument( - "cannot read value from network enum", + "network must be a string: 'mainnet', 'testnet', 'devnet', or 'regtest'", )) } } @@ -57,6 +57,13 @@ impl From for String { } } +impl TryFrom<&JsValue> for NetworkWasm { + type Error = WasmDppError; + fn try_from(value: &JsValue) -> Result { + NetworkWasm::try_from(value.clone()) + } +} + impl From for Network { fn from(network: NetworkWasm) -> Self { match network { @@ -67,3 +74,67 @@ impl From for Network { } } } + +impl From for NetworkWasm { + fn from(network: Network) -> Self { + match network { + Network::Dash => NetworkWasm::Mainnet, + Network::Testnet => NetworkWasm::Testnet, + Network::Devnet => NetworkWasm::Devnet, + Network::Regtest => NetworkWasm::Regtest, + // Handle any future network variants by defaulting to Testnet + _ => NetworkWasm::Testnet, + } + } +} + +impl NetworkWasm { + /// Try to extract a Network from an options object field. + /// + /// This helper reads the specified field from an options object and converts it + /// to a NetworkWasm. Accepts Network enum or string names. + pub fn try_from_options(options: &JsValue, field_name: &str) -> WasmDppResult { + let network_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)) + })?; + + if network_js.is_undefined() || network_js.is_null() { + return Err(WasmDppError::invalid_argument(format!( + "'{}' is required", + field_name + ))); + } + + NetworkWasm::try_from(&network_js) + } + + /// Try to extract an optional Network from an options object field. + /// + /// Returns None if the field is undefined or null, otherwise attempts conversion. + pub fn try_from_options_optional( + options: &JsValue, + field_name: &str, + ) -> WasmDppResult> { + let network_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Failed to get '{}'", field_name)) + })?; + + if network_js.is_undefined() || network_js.is_null() { + return Ok(None); + } + + NetworkWasm::try_from(&network_js).map(Some) + } + + /// Get the network name as a lowercase string (for compatibility with existing code) + pub fn as_str(&self) -> &'static str { + match self { + NetworkWasm::Mainnet => "mainnet", + NetworkWasm::Testnet => "testnet", + NetworkWasm::Devnet => "devnet", + NetworkWasm::Regtest => "regtest", + } + } +} diff --git a/packages/wasm-dpp2/src/identity/public_key.rs b/packages/wasm-dpp2/src/identity/public_key.rs index 84e013f010c..0c01d278935 100644 --- a/packages/wasm-dpp2/src/identity/public_key.rs +++ b/packages/wasm-dpp2/src/identity/public_key.rs @@ -104,7 +104,7 @@ impl IdentityPublicKeyWasm { pub fn validate_private_key( &self, js_private_key_bytes: Vec, - #[wasm_bindgen(unchecked_param_type = "Network | string")] network: JsValue, + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, ) -> WasmDppResult { let mut private_key_bytes = [0u8; 32]; let len = js_private_key_bytes.len().min(32); diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 5b73432ac4f..51430c77d4a 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -31,6 +31,7 @@ pub mod utils; pub mod voting; pub use core_script::CoreScriptWasm; +pub use enums::network::NetworkWasm; pub use identity::signer::IdentitySignerWasm; pub use identity::transitions::pooling::PoolingWasm; pub use private_key::PrivateKeyWasm; diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index 244a5e994c9..bbaeb368c21 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -1,3 +1,4 @@ +use crate::enums::network::NetworkWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::utils::IntoWasm; use dpp::address_funds::PlatformAddress; @@ -164,19 +165,6 @@ impl<'de> Deserialize<'de> for PlatformAddressWasm { } } -fn parse_network(network: &str) -> Result { - match network.to_lowercase().as_str() { - "mainnet" | "dash" => Ok(Network::Dash), - "testnet" => Ok(Network::Testnet), - "devnet" => Ok(Network::Devnet), - "regtest" => Ok(Network::Regtest), - _ => Err(WasmDppError::invalid_argument(format!( - "Invalid network: {}. Expected 'mainnet', 'testnet', 'devnet', or 'regtest'", - network - ))), - } -} - #[wasm_bindgen(js_class = PlatformAddress)] impl PlatformAddressWasm { #[wasm_bindgen(getter = __type)] @@ -204,11 +192,12 @@ impl PlatformAddressWasm { } /// Returns the bech32m-encoded address string for the specified network. - /// - /// @param network - "mainnet", "testnet", "devnet", or "regtest" #[wasm_bindgen(js_name = "toBech32m")] - pub fn to_bech32m(&self, network: &str) -> WasmDppResult { - let net = parse_network(network)?; + pub fn to_bech32m( + &self, + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, + ) -> WasmDppResult { + let net: Network = NetworkWasm::try_from(&network)?.into(); Ok(self.0.to_bech32m_string(net)) } diff --git a/packages/wasm-sdk/src/wallet/extended_derivation.rs b/packages/wasm-sdk/src/wallet/extended_derivation.rs index b2dea07112e..d68feddd188 100644 --- a/packages/wasm-sdk/src/wallet/extended_derivation.rs +++ b/packages/wasm-sdk/src/wallet/extended_derivation.rs @@ -14,6 +14,7 @@ use std::str::FromStr; use tracing::debug; use wasm_bindgen::prelude::*; use wasm_dpp2::identifier::IdentifierWasm; +use wasm_dpp2::NetworkWasm; // TypeScript option bags (module scope) for extended derivation helpers #[wasm_bindgen(typescript_custom_section)] @@ -97,11 +98,8 @@ fn derive_common_from_mnemonic( // Get seed from mnemonic let seed = WasmSdk::mnemonic_to_seed(mnemonic, passphrase)?; - let net = match network { - "mainnet" => dashcore::Network::Dash, - "testnet" => dashcore::Network::Testnet, - _ => return Err(WasmSdkError::invalid_argument("Invalid network")), - }; + let network_wasm = NetworkWasm::try_from(JsValue::from_str(network))?; + let net: dashcore::Network = network_wasm.into(); // Create master extended private key from seed let master_key = ExtendedPrivKey::new_master(net, &seed) diff --git a/packages/wasm-sdk/src/wallet/key_derivation.rs b/packages/wasm-sdk/src/wallet/key_derivation.rs index 5d69f488002..92ec3d0063f 100644 --- a/packages/wasm-sdk/src/wallet/key_derivation.rs +++ b/packages/wasm-sdk/src/wallet/key_derivation.rs @@ -17,6 +17,7 @@ use rand::{thread_rng, RngCore}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use wasm_bindgen::prelude::*; +use wasm_dpp2::NetworkWasm; // TypeScript option bags (module scope) for wallet derivation helpers #[wasm_bindgen(typescript_custom_section)] @@ -418,11 +419,8 @@ impl WasmSdk { return Err(WasmSdkError::generic("Seed too short")); }; - let net = match network.as_str() { - "mainnet" => dashcore::Network::Dash, - "testnet" => dashcore::Network::Testnet, - _ => return Err(WasmSdkError::invalid_argument("Invalid network")), - }; + let network_wasm = NetworkWasm::try_from(JsValue::from_str(&network))?; + let net: dashcore::Network = network_wasm.into(); // Create private key from seed bytes let key_array: [u8; 32] = key_bytes @@ -471,11 +469,8 @@ impl WasmSdk { // Get seed from mnemonic let seed = Self::mnemonic_to_seed(&mnemonic, passphrase)?; - let net = match network.as_str() { - "mainnet" => dashcore::Network::Dash, - "testnet" => dashcore::Network::Testnet, - _ => return Err(WasmSdkError::invalid_argument("Invalid network")), - }; + let network_wasm = NetworkWasm::try_from(JsValue::from_str(&network))?; + let net: dashcore::Network = network_wasm.into(); // Parse derivation path let derivation_path = DerivationPath::from_str(&path).map_err(|e| { diff --git a/packages/wasm-sdk/src/wallet/key_generation.rs b/packages/wasm-sdk/src/wallet/key_generation.rs index 77906d25039..804bbc3c417 100644 --- a/packages/wasm-sdk/src/wallet/key_generation.rs +++ b/packages/wasm-sdk/src/wallet/key_generation.rs @@ -11,6 +11,7 @@ use dash_sdk::dpp::dashcore::{Address, Network, PrivateKey, PublicKey}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use wasm_bindgen::prelude::*; +use wasm_dpp2::NetworkWasm; /// Key pair information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -81,16 +82,11 @@ impl WasmSdk { /// Generate a new random key pair #[wasm_bindgen(js_name = "generateKeyPair")] - pub fn generate_key_pair(network: &str) -> Result { - let net = match network { - "mainnet" => Network::Dash, - "testnet" => Network::Testnet, - _ => { - return Err(WasmSdkError::invalid_argument( - "Invalid network. Use 'mainnet' or 'testnet'", - )); - } - }; + pub fn generate_key_pair( + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, + ) -> Result { + let network_wasm = NetworkWasm::try_from(&network)?; + let net: Network = network_wasm.into(); // Generate random 32 bytes let mut key_bytes = [0u8; 32]; @@ -106,13 +102,16 @@ impl WasmSdk { SecretKey::from_slice(&key_bytes) .map_err(|e| WasmSdkError::invalid_argument(format!("Invalid secret key: {}", e)))?; - let key_pair = Self::build_key_pair(&private_key, net, network)?; + let key_pair = Self::build_key_pair(&private_key, net, network_wasm.as_str())?; Ok(KeyPairWasm::from(key_pair)) } /// Generate multiple key pairs #[wasm_bindgen(js_name = "generateKeyPairs")] - pub fn generate_key_pairs(network: &str, count: u32) -> Result, WasmSdkError> { + pub fn generate_key_pairs( + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, + count: u32, + ) -> Result, WasmSdkError> { if count == 0 || count > 100 { return Err(WasmSdkError::invalid_argument( "Count must be between 1 and 100", @@ -121,7 +120,7 @@ impl WasmSdk { let mut pairs = Vec::new(); for _ in 0..count { - pairs.push(Self::generate_key_pair(network)?); + pairs.push(Self::generate_key_pair(network.clone())?); } Ok(pairs) } @@ -134,13 +133,9 @@ impl WasmSdk { let private_key = PrivateKey::from_wif(private_key_wif) .map_err(|e| WasmSdkError::invalid_argument(format!("Invalid WIF: {}", e)))?; - let network = match private_key.network { - Network::Dash => "mainnet", - Network::Testnet => "testnet", - _ => return Err(WasmSdkError::invalid_argument("Unsupported network")), - }; - - let key_pair = Self::build_key_pair(&private_key, private_key.network, network)?; + let network_wasm: NetworkWasm = private_key.network.into(); + let key_pair = + Self::build_key_pair(&private_key, private_key.network, network_wasm.as_str())?; Ok(KeyPairWasm::from(key_pair)) } @@ -148,7 +143,7 @@ impl WasmSdk { #[wasm_bindgen(js_name = "keyPairFromHex")] pub fn key_pair_from_hex( #[wasm_bindgen(js_name = "privateKeyHex")] private_key_hex: &str, - network: &str, + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, ) -> Result { if private_key_hex.len() != 64 { return Err(WasmSdkError::invalid_argument( @@ -156,15 +151,8 @@ impl WasmSdk { )); } - let net = match network { - "mainnet" => Network::Dash, - "testnet" => Network::Testnet, - _ => { - return Err(WasmSdkError::invalid_argument( - "Invalid network. Use 'mainnet' or 'testnet'", - )); - } - }; + let network_wasm = NetworkWasm::try_from(&network)?; + let net: Network = network_wasm.into(); let key_bytes = hex::decode(private_key_hex) .map_err(|e| WasmSdkError::invalid_argument(format!("Invalid hex: {}", e)))?; @@ -175,7 +163,7 @@ impl WasmSdk { let private_key = PrivateKey::from_byte_array(&key_array, net) .map_err(|e| WasmSdkError::generic(format!("Failed to create private key: {}", e)))?; - let key_pair = Self::build_key_pair(&private_key, net, network)?; + let key_pair = Self::build_key_pair(&private_key, net, network_wasm.as_str())?; Ok(KeyPairWasm::from(key_pair)) } @@ -183,17 +171,10 @@ impl WasmSdk { #[wasm_bindgen(js_name = "pubkeyToAddress")] pub fn pubkey_to_address( #[wasm_bindgen(js_name = "pubkeyHex")] pubkey_hex: &str, - network: &str, + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, ) -> Result { - let net = match network { - "mainnet" => Network::Dash, - "testnet" => Network::Testnet, - _ => { - return Err(WasmSdkError::invalid_argument( - "Invalid network. Use 'mainnet' or 'testnet'", - )); - } - }; + let network_wasm = NetworkWasm::try_from(&network)?; + let net: Network = network_wasm.into(); let pubkey_bytes = hex::decode(pubkey_hex) .map_err(|e| WasmSdkError::invalid_argument(format!("Invalid hex: {}", e)))?; @@ -207,12 +188,14 @@ impl WasmSdk { /// Validate a Dash address #[wasm_bindgen(js_name = "validateAddress")] - pub fn validate_address(address: &str, network: &str) -> bool { - let net = match network { - "mainnet" => Network::Dash, - "testnet" => Network::Testnet, - _ => return false, + pub fn validate_address( + address: &str, + #[wasm_bindgen(unchecked_param_type = "NetworkLike")] network: JsValue, + ) -> bool { + let Ok(network_wasm) = NetworkWasm::try_from(&network) else { + return false; }; + let net: Network = network_wasm.into(); Address::from_str(address) .map(|addr| *addr.network() == net) From 705fcb9ee13d5bbadbc065bb0559f04f4e7209f5 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 14 Jan 2026 14:48:50 +0700 Subject: [PATCH 02/14] refactor: move core stuff to own module --- packages/wasm-dpp2/TODO.md | 26 +-- .../wasm-dpp2/src/{ => core}/core_script.rs | 2 +- packages/wasm-dpp2/src/core/mod.rs | 4 + .../wasm-dpp2/src/{enums => core}/network.rs | 0 .../wasm-dpp2/src/{ => core}/private_key.rs | 2 +- packages/wasm-dpp2/src/core/pro_tx_hash.rs | 157 ++++++++++++++++++ packages/wasm-dpp2/src/enums/mod.rs | 1 - packages/wasm-dpp2/src/identity/public_key.rs | 2 +- packages/wasm-dpp2/src/identity/signer.rs | 2 +- .../credit_withdrawal_transition.rs | 2 +- packages/wasm-dpp2/src/lib.rs | 10 +- .../wasm-dpp2/src/platform_address/address.rs | 2 +- .../wasm-dpp2/src/platform_address/signer.rs | 2 +- .../base/state_transition.rs | 2 +- packages/wasm-sdk/src/queries/epoch.rs | 38 ++--- packages/wasm-sdk/src/queries/protocol.rs | 61 ++----- .../src/state_transitions/addresses.rs | 2 +- .../src/state_transitions/identity.rs | 2 +- 18 files changed, 210 insertions(+), 107 deletions(-) rename packages/wasm-dpp2/src/{ => core}/core_script.rs (98%) create mode 100644 packages/wasm-dpp2/src/core/mod.rs rename packages/wasm-dpp2/src/{enums => core}/network.rs (100%) rename packages/wasm-dpp2/src/{ => core}/private_key.rs (99%) create mode 100644 packages/wasm-dpp2/src/core/pro_tx_hash.rs diff --git a/packages/wasm-dpp2/TODO.md b/packages/wasm-dpp2/TODO.md index 8cd0b21a657..7b96db6bbb7 100644 --- a/packages/wasm-dpp2/TODO.md +++ b/packages/wasm-dpp2/TODO.md @@ -2,33 +2,19 @@ ## Type Wrappers for Flexible Input -### ProTxHash Wrapper +### ~~ProTxHash Wrapper~~ ✅ DONE -Create a wrapper around `ProTxHash` from dashcore with flexible input types. - -`ProTxHash` is a newtype wrapper around `sha256d::Hash`: - -```rust -pub struct ProTxHash(sha256d::Hash); -``` +~~Create a wrapper around `ProTxHash` from dashcore with flexible input types.~~ ```typescript type ProTxHashLike = ProTxHash | Uint8Array | string; ``` -**Requirements:** - -- Create `ProTxHashWasm` wrapper in wasm-dpp2 -- Accept hex string, raw bytes (Uint8Array), or ProTxHash object -- Centralize parsing logic (currently duplicated in wasm-sdk methods) - -**Affected areas in wasm-sdk:** +**Implemented in:** -- `getProtocolVersionUpgradeVoteStatus` (startProTxHash parameter) -- `getProtocolVersionUpgradeVoteStatusWithProofInfo` (startProTxHash parameter) -- `getEvonodesProposedEpochBlocksByIds` (ids parameter) -- `getEvonodesProposedEpochBlocksByIdsWithProofInfo` (proTxHashes parameter) -- `EvonodeProposedBlocksRangeQuery.startAfter` field +- `wasm-dpp2/src/enums/pro_tx_hash.rs` - `ProTxHashWasm` with `TryFrom`, `from_hex()`, `from_bytes()`, `try_from_options()`, `try_from_options_optional()` +- `wasm-sdk/src/queries/protocol.rs` - Updated `getProtocolVersionUpgradeVoteStatus`, `getProtocolVersionUpgradeVoteStatusWithProofInfo` +- `wasm-sdk/src/queries/epoch.rs` - Updated `getEvonodesProposedEpochBlocksByIds`, `getEvonodesProposedEpochBlocksByIdsWithProofInfo`, `parse_evonode_range_query` ### ~~Network Wrapper~~ ✅ DONE diff --git a/packages/wasm-dpp2/src/core_script.rs b/packages/wasm-dpp2/src/core/core_script.rs similarity index 98% rename from packages/wasm-dpp2/src/core_script.rs rename to packages/wasm-dpp2/src/core/core_script.rs index c1f2e7d5fb4..bff72dc7aed 100644 --- a/packages/wasm-dpp2/src/core_script.rs +++ b/packages/wasm-dpp2/src/core/core_script.rs @@ -1,4 +1,4 @@ -use crate::enums::network::NetworkWasm; +use super::network::NetworkWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_options; use dpp::dashcore::address::Payload; diff --git a/packages/wasm-dpp2/src/core/mod.rs b/packages/wasm-dpp2/src/core/mod.rs new file mode 100644 index 00000000000..a7739dd86df --- /dev/null +++ b/packages/wasm-dpp2/src/core/mod.rs @@ -0,0 +1,4 @@ +pub mod core_script; +pub mod network; +pub mod private_key; +pub mod pro_tx_hash; diff --git a/packages/wasm-dpp2/src/enums/network.rs b/packages/wasm-dpp2/src/core/network.rs similarity index 100% rename from packages/wasm-dpp2/src/enums/network.rs rename to packages/wasm-dpp2/src/core/network.rs diff --git a/packages/wasm-dpp2/src/private_key.rs b/packages/wasm-dpp2/src/core/private_key.rs similarity index 99% rename from packages/wasm-dpp2/src/private_key.rs rename to packages/wasm-dpp2/src/core/private_key.rs index 3f694f014ec..a09b2af0bd4 100644 --- a/packages/wasm-dpp2/src/private_key.rs +++ b/packages/wasm-dpp2/src/core/private_key.rs @@ -1,6 +1,6 @@ use std::convert::TryInto; -use crate::enums::network::NetworkWasm; +use super::network::NetworkWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::public_key::PublicKeyWasm; use crate::utils::IntoWasm; diff --git a/packages/wasm-dpp2/src/core/pro_tx_hash.rs b/packages/wasm-dpp2/src/core/pro_tx_hash.rs new file mode 100644 index 00000000000..cb55ed5944c --- /dev/null +++ b/packages/wasm-dpp2/src/core/pro_tx_hash.rs @@ -0,0 +1,157 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use dpp::dashcore::ProTxHash; +use dpp::dashcore::hashes::{Hash, sha256d}; +use std::str::FromStr; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +/// TypeScript type alias for flexible ProTxHash input +#[wasm_bindgen(typescript_custom_section)] +const PRO_TX_HASH_LIKE_TS: &'static str = r#" +/** + * Flexible ProTxHash type that accepts ProTxHash object, hex string, or Uint8Array. + * + * - Hex string: 64-character hex-encoded hash (reversed byte order, as displayed) + * - Uint8Array: 32 bytes in internal byte order + */ +export type ProTxHashLike = ProTxHash | string | Uint8Array; +"#; + +#[wasm_bindgen(js_name = "ProTxHash")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProTxHashWasm(ProTxHash); + +impl ProTxHashWasm { + /// Create from inner ProTxHash + pub fn new(inner: ProTxHash) -> Self { + Self(inner) + } + + /// Get the inner ProTxHash + pub fn inner(&self) -> &ProTxHash { + &self.0 + } + + /// Consume and return the inner ProTxHash + pub fn into_inner(self) -> ProTxHash { + self.0 + } + + /// Get as hex string (reversed byte order, as typically displayed) + pub fn to_hex(&self) -> String { + self.0.to_string() + } + + /// Get as raw bytes (internal byte order) + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_byte_array() + } + + /// Create a ProTxHash from a hex string + pub fn from_hex(hex: &str) -> WasmDppResult { + let hash = ProTxHash::from_str(hex) + .map_err(|e| WasmDppError::invalid_argument(format!("Invalid ProTxHash hex: {}", e)))?; + Ok(ProTxHashWasm(hash)) + } + + /// Try to extract a ProTxHash from an options object field. + /// + /// This helper reads the specified field from an options object and converts it + /// to a ProTxHashWasm. Accepts hex string, Uint8Array, or ProTxHash object. + pub fn try_from_options(options: &JsValue, field_name: &str) -> WasmDppResult { + let hash_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)) + })?; + + if hash_js.is_undefined() || hash_js.is_null() { + return Err(WasmDppError::invalid_argument(format!( + "'{}' is required", + field_name + ))); + } + + ProTxHashWasm::try_from(&hash_js) + } + + /// Try to extract an optional ProTxHash from an options object field. + /// + /// Returns None if the field is undefined, null, or an empty string/array. + /// Otherwise attempts conversion. + pub fn try_from_options_optional( + options: &JsValue, + field_name: &str, + ) -> WasmDppResult> { + let hash_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Failed to get '{}'", field_name)) + })?; + + if hash_js.is_undefined() || hash_js.is_null() { + return Ok(None); + } + + // Check for empty string + if let Some(s) = hash_js.as_string() + && s.is_empty() + { + return Ok(None); + } + + ProTxHashWasm::try_from(&hash_js).map(Some) + } +} + +impl TryFrom for ProTxHashWasm { + type Error = WasmDppError; + + fn try_from(value: JsValue) -> Result { + // Try as string first (hex format) + if let Some(hex_str) = value.as_string() { + let hash = ProTxHash::from_str(&hex_str).map_err(|e| { + WasmDppError::invalid_argument(format!("Invalid ProTxHash hex string: {}", e)) + })?; + return Ok(ProTxHashWasm(hash)); + } + + // Try as Uint8Array + if value.is_object() { + let bytes = js_sys::Uint8Array::new(&value).to_vec(); + if bytes.len() != 32 { + return Err(WasmDppError::invalid_argument(format!( + "ProTxHash must be exactly 32 bytes, got {} bytes", + bytes.len() + ))); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + let raw = sha256d::Hash::from_byte_array(arr); + let hash = ProTxHash::from_raw_hash(raw); + return Ok(ProTxHashWasm(hash)); + } + + Err(WasmDppError::invalid_argument( + "ProTxHash must be a hex string or Uint8Array (32 bytes)", + )) + } +} + +impl TryFrom<&JsValue> for ProTxHashWasm { + type Error = WasmDppError; + + fn try_from(value: &JsValue) -> Result { + ProTxHashWasm::try_from(value.clone()) + } +} + +impl From for ProTxHash { + fn from(wrapper: ProTxHashWasm) -> Self { + wrapper.0 + } +} + +impl From for ProTxHashWasm { + fn from(hash: ProTxHash) -> Self { + ProTxHashWasm(hash) + } +} diff --git a/packages/wasm-dpp2/src/enums/mod.rs b/packages/wasm-dpp2/src/enums/mod.rs index 0f7c0624464..7f9e3e9ad16 100644 --- a/packages/wasm-dpp2/src/enums/mod.rs +++ b/packages/wasm-dpp2/src/enums/mod.rs @@ -2,6 +2,5 @@ pub mod batch; pub mod contested; pub mod keys; pub mod lock_types; -pub mod network; pub mod platform; pub mod token; diff --git a/packages/wasm-dpp2/src/identity/public_key.rs b/packages/wasm-dpp2/src/identity/public_key.rs index 0c01d278935..a414ba64b8c 100644 --- a/packages/wasm-dpp2/src/identity/public_key.rs +++ b/packages/wasm-dpp2/src/identity/public_key.rs @@ -1,8 +1,8 @@ +use crate::core::network::NetworkWasm; use crate::data_contract::contract_bounds::ContractBoundsWasm; use crate::enums::keys::key_type::KeyTypeWasm; use crate::enums::keys::purpose::PurposeWasm; use crate::enums::keys::security_level::SecurityLevelWasm; -use crate::enums::network::NetworkWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_options; use crate::serialization; diff --git a/packages/wasm-dpp2/src/identity/signer.rs b/packages/wasm-dpp2/src/identity/signer.rs index e167d13a88e..5019e95dc04 100644 --- a/packages/wasm-dpp2/src/identity/signer.rs +++ b/packages/wasm-dpp2/src/identity/signer.rs @@ -3,9 +3,9 @@ //! This module provides a signer for identity-based state transitions that implements //! `Signer`. +use crate::core::private_key::PrivateKeyWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_options; -use crate::private_key::PrivateKeyWasm; use crate::utils::IntoWasm; use dpp::ProtocolError; use dpp::address_funds::{AddressWitness, PlatformAddress}; diff --git a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs index 3cac596b523..ecc3963654f 100644 --- a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs @@ -1,6 +1,6 @@ use super::pooling::PoolingWasm; use crate::asset_lock_proof::AssetLockProofWasm; -use crate::core_script::CoreScriptWasm; +use crate::core::core_script::CoreScriptWasm; use crate::enums::keys::purpose::PurposeWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::IdentifierWasm; diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 51430c77d4a..2bdb6751c60 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -12,7 +12,7 @@ pub mod asset_lock_proof; pub mod block; pub mod consensus_error; -pub mod core_script; +pub mod core; pub mod data_contract; pub mod enums; pub mod epoch; @@ -22,7 +22,6 @@ pub mod identifier; pub mod identity; pub mod mock_bls; pub mod platform_address; -pub mod private_key; pub mod public_key; pub mod serialization; pub mod state_transitions; @@ -30,11 +29,12 @@ pub mod tokens; pub mod utils; pub mod voting; -pub use core_script::CoreScriptWasm; -pub use enums::network::NetworkWasm; +pub use core::core_script::CoreScriptWasm; +pub use core::network::NetworkWasm; +pub use core::private_key::PrivateKeyWasm; +pub use core::pro_tx_hash::ProTxHashWasm; pub use identity::signer::IdentitySignerWasm; pub use identity::transitions::pooling::PoolingWasm; -pub use private_key::PrivateKeyWasm; pub use data_contract::{ ContractBoundsWasm, DataContractCreateTransitionWasm, DataContractUpdateTransitionWasm, diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index bbaeb368c21..419eabccdce 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -1,4 +1,4 @@ -use crate::enums::network::NetworkWasm; +use crate::core::network::NetworkWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::utils::IntoWasm; use dpp::address_funds::PlatformAddress; diff --git a/packages/wasm-dpp2/src/platform_address/signer.rs b/packages/wasm-dpp2/src/platform_address/signer.rs index 776ef352000..3d11b9a0e85 100644 --- a/packages/wasm-dpp2/src/platform_address/signer.rs +++ b/packages/wasm-dpp2/src/platform_address/signer.rs @@ -1,7 +1,7 @@ use super::PlatformAddressWasm; +use crate::core::private_key::PrivateKeyWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_options; -use crate::private_key::PrivateKeyWasm; use crate::utils::IntoWasm; use dpp::ProtocolError; use dpp::address_funds::{AddressWitness, PlatformAddress}; diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index d3eec313fa0..fbd2a28ce35 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -1,3 +1,4 @@ +use crate::core::private_key::PrivateKeyWasm; use crate::enums::keys::key_type::KeyTypeWasm; use crate::enums::keys::purpose::PurposeWasm; use crate::enums::keys::security_level::SecurityLevelWasm; @@ -5,7 +6,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::IdentifierWasm; use crate::identity::public_key::IdentityPublicKeyWasm; use crate::mock_bls::MockBLS; -use crate::private_key::PrivateKeyWasm; use dpp::dashcore::secp256k1::hashes::hex::Case::Lower; use dpp::dashcore::secp256k1::hashes::hex::DisplayHex; use dpp::data_contract::serialized_version::DataContractInSerializationFormat; diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs index e808423fec6..0235b49d05d 100644 --- a/packages/wasm-sdk/src/queries/epoch.rs +++ b/packages/wasm-sdk/src/queries/epoch.rs @@ -10,11 +10,11 @@ use dash_sdk::platform::types::proposed_blocks::ProposedBlockCountEx; use dash_sdk::platform::{FetchMany, LimitQuery, QueryStartInfo}; use js_sys::{BigInt, Map, Number}; use serde::Deserialize; -use std::str::FromStr; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; use wasm_dpp2::epoch::{ExtendedEpochInfoWasm, FinalizedEpochInfoWasm}; use wasm_dpp2::identifier::IdentifierWasm; +use wasm_dpp2::ProTxHashWasm; #[wasm_bindgen(typescript_custom_section)] const EPOCHS_QUERY_TS: &'static str = r#" @@ -199,9 +199,7 @@ fn parse_evonode_range_query( )?; let start_info = if let Some(start) = input.start_after { - let pro_tx_hash = ProTxHash::from_str(&start).map_err(|e| { - WasmSdkError::invalid_argument(format!("Invalid startAfter ProTxHash: {}", e)) - })?; + let pro_tx_hash: ProTxHash = ProTxHashWasm::from_hex(&start)?.into(); Some(QueryStartInfo { start_key: pro_tx_hash.to_byte_array().to_vec(), start_included: false, @@ -319,22 +317,15 @@ impl WasmSdk { pub async fn get_evonodes_proposed_epoch_blocks_by_ids( &self, epoch: u16, - ids: Vec, + #[wasm_bindgen(unchecked_param_type = "ProTxHashLike[]")] ids: Vec, ) -> Result { use drive_proof_verifier::types::ProposerBlockCountById; - // Parse the ProTxHash strings + // Parse the ProTxHash values using centralized wrapper let pro_tx_hashes: Vec = ids .into_iter() - .map(|hash_str| { - ProTxHash::from_str(&hash_str).map_err(|e| { - WasmSdkError::invalid_argument(format!( - "Invalid ProTxHash '{}': {}", - hash_str, e - )) - }) - }) - .collect::, WasmSdkError>>()?; + .map(|hash_js| ProTxHashWasm::try_from(&hash_js).map(|w| w.into())) + .collect::, _>>()?; // Use FetchMany to get block counts for specific IDs let counts = @@ -511,22 +502,17 @@ impl WasmSdk { pub async fn get_evonodes_proposed_epoch_blocks_by_ids_with_proof_info( &self, epoch: u16, - #[wasm_bindgen(js_name = "proTxHashes")] pro_tx_hashes: Vec, + #[wasm_bindgen(js_name = "proTxHashes")] + #[wasm_bindgen(unchecked_param_type = "ProTxHashLike[]")] + pro_tx_hashes: Vec, ) -> Result { use drive_proof_verifier::types::ProposerBlockCountById; - // Parse the ProTxHash strings + // Parse the ProTxHash values using centralized wrapper let parsed_hashes: Vec = pro_tx_hashes .into_iter() - .map(|hash_str| { - ProTxHash::from_str(&hash_str).map_err(|e| { - WasmSdkError::invalid_argument(format!( - "Invalid ProTxHash '{}': {}", - hash_str, e - )) - }) - }) - .collect::, WasmSdkError>>()?; + .map(|hash_js| ProTxHashWasm::try_from(&hash_js).map(|w| w.into())) + .collect::, _>>()?; // Use FetchMany with proof to get block counts for specific IDs let (counts, metadata, proof) = ProposerBlockCountById::fetch_many_with_metadata_and_proof( diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs index 57b149e6dc2..055d077966e 100644 --- a/packages/wasm-sdk/src/queries/protocol.rs +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -2,11 +2,11 @@ use crate::error::WasmSdkError; use crate::impl_wasm_serde_conversions; use crate::queries::ProofMetadataResponseWasm; use crate::sdk::WasmSdk; -use dash_sdk::dpp::dashcore::hashes::{sha256d, Hash as _}; use js_sys::Map; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; +use wasm_dpp2::ProTxHashWasm; #[wasm_bindgen(js_name = "ProtocolVersionUpgradeState")] #[derive(Clone, Serialize, Deserialize)] @@ -147,40 +147,26 @@ impl WasmSdk { pub async fn get_protocol_version_upgrade_vote_status( &self, #[wasm_bindgen(js_name = "startProTxHash")] - #[wasm_bindgen(unchecked_param_type = "string | Uint8Array")] + #[wasm_bindgen(unchecked_param_type = "ProTxHashLike | null")] start_pro_tx_hash: JsValue, count: u32, ) -> Result { - use dash_sdk::dpp::dashcore::ProTxHash; use dash_sdk::platform::types::version_votes::MasternodeProtocolVoteEx; use drive_proof_verifier::types::MasternodeProtocolVote; - use std::str::FromStr; - // Parse the ProTxHash - let start_hash = if let Some(s) = start_pro_tx_hash.as_string() { + // Parse the ProTxHash using centralized helper + let start_hash = if start_pro_tx_hash.is_null() || start_pro_tx_hash.is_undefined() { + None + } else if let Some(s) = start_pro_tx_hash.as_string() { if s.is_empty() { None } else { - Some(ProTxHash::from_str(&s).map_err(|e| { - WasmSdkError::invalid_argument(format!("Invalid ProTxHash: {}", e)) - })?) + Some(ProTxHashWasm::try_from(&start_pro_tx_hash)?.into()) } } else { - let bytes = js_sys::Uint8Array::new(&start_pro_tx_hash).to_vec(); - if bytes.is_empty() { - None - } else { - if bytes.len() != 32 { - return Err(WasmSdkError::invalid_argument( - "ProTxHash must be 32 bytes or an empty value", - )); - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - let raw = sha256d::Hash::from_byte_array(arr); - Some(ProTxHash::from_raw_hash(raw)) - } + Some(ProTxHashWasm::try_from(&start_pro_tx_hash)?.into()) }; + let votes_result = MasternodeProtocolVote::fetch_votes(self.as_ref(), start_hash, Some(count)).await?; @@ -258,39 +244,24 @@ impl WasmSdk { pub async fn get_protocol_version_upgrade_vote_status_with_proof_info( &self, #[wasm_bindgen(js_name = "startProTxHash")] - #[wasm_bindgen(unchecked_param_type = "string | Uint8Array")] + #[wasm_bindgen(unchecked_param_type = "ProTxHashLike | null")] start_pro_tx_hash: JsValue, count: u32, ) -> Result { - use dash_sdk::dpp::dashcore::ProTxHash; use dash_sdk::platform::{FetchMany, LimitQuery}; use drive_proof_verifier::types::MasternodeProtocolVote; - use std::str::FromStr; - // Parse the ProTxHash - let start_hash: Option = if let Some(s) = start_pro_tx_hash.as_string() { + // Parse the ProTxHash using centralized helper + let start_hash = if start_pro_tx_hash.is_null() || start_pro_tx_hash.is_undefined() { + None + } else if let Some(s) = start_pro_tx_hash.as_string() { if s.is_empty() { None } else { - Some(ProTxHash::from_str(&s).map_err(|e| { - WasmSdkError::invalid_argument(format!("Invalid ProTxHash: {}", e)) - })?) + Some(ProTxHashWasm::try_from(&start_pro_tx_hash)?.into()) } } else { - let bytes = js_sys::Uint8Array::new(&start_pro_tx_hash).to_vec(); - if bytes.is_empty() { - None - } else { - if bytes.len() != 32 { - return Err(WasmSdkError::invalid_argument( - "ProTxHash must be 32 bytes or an empty value", - )); - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - let raw = sha256d::Hash::from_byte_array(arr); - Some(ProTxHash::from_raw_hash(raw)) - } + Some(ProTxHashWasm::try_from(&start_pro_tx_hash)?.into()) }; // Create a LimitQuery with the start hash and count diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index 8e497ed00c5..a003ead0a67 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -754,7 +754,7 @@ impl WasmSdk { ) -> Result { use dash_sdk::platform::transition::top_up_address::TopUpAddress; use wasm_dpp2::asset_lock_proof::AssetLockProofWasm; - use wasm_dpp2::private_key::PrivateKeyWasm; + use wasm_dpp2::PrivateKeyWasm; let options_value: JsValue = options.into(); diff --git a/packages/wasm-sdk/src/state_transitions/identity.rs b/packages/wasm-sdk/src/state_transitions/identity.rs index 873f5b2304a..792ede2548d 100644 --- a/packages/wasm-sdk/src/state_transitions/identity.rs +++ b/packages/wasm-sdk/src/state_transitions/identity.rs @@ -21,8 +21,8 @@ use wasm_bindgen::prelude::*; use wasm_dpp2::asset_lock_proof::AssetLockProofWasm; use wasm_dpp2::identifier::IdentifierWasm; use wasm_dpp2::identity::IdentityPublicKeyWasm; -use wasm_dpp2::private_key::PrivateKeyWasm; use wasm_dpp2::utils::IntoWasm; +use wasm_dpp2::PrivateKeyWasm; use wasm_dpp2::{IdentityPublicKeyInCreationWasm, IdentitySignerWasm, IdentityWasm}; // ============================================================================ From 2f9ad42cd2fe987c13bc2e96fac09aa94d71ee5d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 14 Jan 2026 16:27:07 +0700 Subject: [PATCH 03/14] docs: remove unnecessary todo --- packages/wasm-dpp2/TODO.md | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 packages/wasm-dpp2/TODO.md diff --git a/packages/wasm-dpp2/TODO.md b/packages/wasm-dpp2/TODO.md deleted file mode 100644 index 7b96db6bbb7..00000000000 --- a/packages/wasm-dpp2/TODO.md +++ /dev/null @@ -1,30 +0,0 @@ -# wasm-dpp2 TODO - -## Type Wrappers for Flexible Input - -### ~~ProTxHash Wrapper~~ ✅ DONE - -~~Create a wrapper around `ProTxHash` from dashcore with flexible input types.~~ - -```typescript -type ProTxHashLike = ProTxHash | Uint8Array | string; -``` - -**Implemented in:** - -- `wasm-dpp2/src/enums/pro_tx_hash.rs` - `ProTxHashWasm` with `TryFrom`, `from_hex()`, `from_bytes()`, `try_from_options()`, `try_from_options_optional()` -- `wasm-sdk/src/queries/protocol.rs` - Updated `getProtocolVersionUpgradeVoteStatus`, `getProtocolVersionUpgradeVoteStatusWithProofInfo` -- `wasm-sdk/src/queries/epoch.rs` - Updated `getEvonodesProposedEpochBlocksByIds`, `getEvonodesProposedEpochBlocksByIdsWithProofInfo`, `parse_evonode_range_query` - -### ~~Network Wrapper~~ ✅ DONE - -~~Create a wrapper around `Network` from dashcore with flexible input types:~~ - -```typescript -type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest"; -``` - -**Implemented in:** - -- `wasm-dpp2/src/enums/network.rs` - `NetworkWasm` with `TryFrom`, `try_from_options()`, `as_str()` -- `wasm-sdk/src/wallet/key_generation.rs` - Updated all functions to use `NetworkLike` From fd6c410e4308408cf9f5d8d5da5aa577c40a9471 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 14 Jan 2026 16:41:26 +0700 Subject: [PATCH 04/14] fix: a couple of minor issues --- packages/wasm-dpp2/src/core/network.rs | 3 +-- packages/wasm-dpp2/src/core/pro_tx_hash.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/wasm-dpp2/src/core/network.rs b/packages/wasm-dpp2/src/core/network.rs index 97ede6a6b97..1cca50018a5 100644 --- a/packages/wasm-dpp2/src/core/network.rs +++ b/packages/wasm-dpp2/src/core/network.rs @@ -82,8 +82,7 @@ impl From for NetworkWasm { Network::Testnet => NetworkWasm::Testnet, Network::Devnet => NetworkWasm::Devnet, Network::Regtest => NetworkWasm::Regtest, - // Handle any future network variants by defaulting to Testnet - _ => NetworkWasm::Testnet, + _ => unreachable!("Unknown network variant"), } } } diff --git a/packages/wasm-dpp2/src/core/pro_tx_hash.rs b/packages/wasm-dpp2/src/core/pro_tx_hash.rs index cb55ed5944c..beebf323018 100644 --- a/packages/wasm-dpp2/src/core/pro_tx_hash.rs +++ b/packages/wasm-dpp2/src/core/pro_tx_hash.rs @@ -2,8 +2,8 @@ use crate::error::{WasmDppError, WasmDppResult}; use dpp::dashcore::ProTxHash; use dpp::dashcore::hashes::{Hash, sha256d}; use std::str::FromStr; -use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; /// TypeScript type alias for flexible ProTxHash input #[wasm_bindgen(typescript_custom_section)] @@ -115,7 +115,7 @@ impl TryFrom for ProTxHashWasm { } // Try as Uint8Array - if value.is_object() { + if value.is_instance_of::() { let bytes = js_sys::Uint8Array::new(&value).to_vec(); if bytes.len() != 32 { return Err(WasmDppError::invalid_argument(format!( From a1dcdc03d17722aaaa08c17099e65e55975b2f78 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 14 Jan 2026 19:32:46 +0700 Subject: [PATCH 05/14] fix: invalid types in evo sdk --- packages/js-evo-sdk/src/wallet/functions.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/js-evo-sdk/src/wallet/functions.ts b/packages/js-evo-sdk/src/wallet/functions.ts index b4f3c5716da..e38e89c53fb 100644 --- a/packages/js-evo-sdk/src/wallet/functions.ts +++ b/packages/js-evo-sdk/src/wallet/functions.ts @@ -1,4 +1,5 @@ import * as wasm from '../wasm.js'; +import type { NetworkLike } from '../wasm.js'; export namespace wallet { export async function generateMnemonic(params?: wasm.GenerateMnemonicParams): Promise { @@ -76,12 +77,12 @@ export namespace wallet { return wasm.WasmSdk.xprvToXpub(xprv); } - export async function generateKeyPair(network: string): Promise { + export async function generateKeyPair(network: NetworkLike): Promise { await wasm.ensureInitialized(); return wasm.WasmSdk.generateKeyPair(network); } - export async function generateKeyPairs(network: string, count: number): Promise { + export async function generateKeyPairs(network: NetworkLike, count: number): Promise { await wasm.ensureInitialized(); return wasm.WasmSdk.generateKeyPairs(network, count); } @@ -91,17 +92,17 @@ export namespace wallet { return wasm.WasmSdk.keyPairFromWif(privateKeyWif); } - export async function keyPairFromHex(privateKeyHex: string, network: string): Promise { + export async function keyPairFromHex(privateKeyHex: string, network: NetworkLike): Promise { await wasm.ensureInitialized(); return wasm.WasmSdk.keyPairFromHex(privateKeyHex, network); } - export async function pubkeyToAddress(pubkeyHex: string, network: string): Promise { + export async function pubkeyToAddress(pubkeyHex: string, network: NetworkLike): Promise { await wasm.ensureInitialized(); return wasm.WasmSdk.pubkeyToAddress(pubkeyHex, network); } - export async function validateAddress(address: string, network: string): Promise { + export async function validateAddress(address: string, network: NetworkLike): Promise { await wasm.ensureInitialized(); return wasm.WasmSdk.validateAddress(address, network); } From 029620f2acdb28bca05814c8123be9bd3286d403 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 11:15:41 +0700 Subject: [PATCH 06/14] fix: error mapping --- packages/wasm-sdk/src/error.rs | 17 ++++++++++------- packages/wasm-sdk/tests/unit/errors.spec.mjs | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/wasm-sdk/src/error.rs b/packages/wasm-sdk/src/error.rs index 65e5154b715..55e3a45ef19 100644 --- a/packages/wasm-sdk/src/error.rs +++ b/packages/wasm-sdk/src/error.rs @@ -205,13 +205,16 @@ impl From for WasmSdkError { impl From for WasmSdkError { fn from(err: WasmDppError) -> Self { - // Map WasmDppError to appropriate WasmSdkError kind - Self::new( - WasmSdkErrorKind::SerializationError, - err.to_string(), - None, - false, - ) + use wasm_dpp2::error::WasmDppErrorKind; + // Map WasmDppError kind to appropriate WasmSdkError kind + let kind = match err.kind() { + WasmDppErrorKind::Protocol => WasmSdkErrorKind::Protocol, + WasmDppErrorKind::InvalidArgument => WasmSdkErrorKind::InvalidArgument, + WasmDppErrorKind::Serialization => WasmSdkErrorKind::SerializationError, + WasmDppErrorKind::Conversion => WasmSdkErrorKind::SerializationError, + WasmDppErrorKind::Generic => WasmSdkErrorKind::Generic, + }; + Self::new(kind, err.to_string(), None, false) } } diff --git a/packages/wasm-sdk/tests/unit/errors.spec.mjs b/packages/wasm-sdk/tests/unit/errors.spec.mjs index c35e5f3983e..fe506f2121d 100644 --- a/packages/wasm-sdk/tests/unit/errors.spec.mjs +++ b/packages/wasm-sdk/tests/unit/errors.spec.mjs @@ -7,13 +7,13 @@ describe('WasmSdkError shape (unit)', () => { it('invalid network on generateKeyPair exposes InvalidArgument', () => { try { - sdk.WasmSdk.generateKeyPair('devnet'); + sdk.WasmSdk.generateKeyPair('invalid_network'); expect.fail('expected to throw'); } catch (e) { // wasm-bindgen returns our WasmSdkError as an object, not necessarily instanceof Error expect(e).to.be.instanceOf(sdk.WasmSdkError); expect(e.name).to.equal('InvalidArgument'); - expect(e.message).to.match(/Invalid network/i); + expect(e.message).to.match(/unsupported network name/i); expect(e.retriable).to.equal(false); expect(e.code).to.equal(-1); } @@ -43,7 +43,7 @@ describe('WasmSdkError shape (unit)', () => { } catch (e) { expect(e).to.be.instanceOf(sdk.WasmSdkError); expect(e.name).to.equal('InvalidArgument'); - expect(e.message).to.match(/Invalid network/i); + expect(e.message).to.match(/unsupported network name/i); } }); }); From 431a611a4220254071c714659fb20905a3cc5ba9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 21:10:01 +0700 Subject: [PATCH 07/14] fix(sdk): `toJSON` returns empty object --- .../wasm-dpp2/src/platform_address/address.rs | 13 +- .../src/serialization/conversions.rs | 153 +++++- .../tests/unit/js-value-to-json.spec.mjs | 467 ++++++++++++++++++ packages/wasm-sdk/src/queries/address.rs | 41 +- packages/wasm-sdk/src/queries/identity.rs | 37 +- packages/wasm-sdk/src/queries/protocol.rs | 4 +- packages/wasm-sdk/src/queries/system.rs | 32 +- packages/wasm-sdk/src/queries/token.rs | 4 +- .../src/wallet/extended_derivation.rs | 4 +- .../wasm-sdk/src/wallet/key_derivation.rs | 8 +- .../wasm-sdk/src/wallet/key_generation.rs | 2 +- 11 files changed, 696 insertions(+), 69 deletions(-) create mode 100644 packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index 419eabccdce..9033b533ba8 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -5,7 +5,8 @@ use dpp::address_funds::PlatformAddress; use dpp::dashcore::Network; use js_sys::Uint8Array; use serde::de::{self, Error, Visitor}; -use serde::{Deserialize, Deserializer}; +use serde::ser::Serializer; +use serde::{Deserialize, Deserializer, Serialize}; use std::fmt; use wasm_bindgen::prelude::*; @@ -165,6 +166,16 @@ impl<'de> Deserialize<'de> for PlatformAddressWasm { } } +impl Serialize for PlatformAddressWasm { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize as hex string (consistent with to_hex method) + serializer.serialize_str(&hex::encode(self.0.to_bytes())) + } +} + #[wasm_bindgen(js_class = PlatformAddress)] impl PlatformAddressWasm { #[wasm_bindgen(getter = __type)] diff --git a/packages/wasm-dpp2/src/serialization/conversions.rs b/packages/wasm-dpp2/src/serialization/conversions.rs index 180af2d7fcf..90efff10b54 100644 --- a/packages/wasm-dpp2/src/serialization/conversions.rs +++ b/packages/wasm-dpp2/src/serialization/conversions.rs @@ -29,6 +29,25 @@ use serde_json::Value as JsonValue; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; +/// Try to call toJSON() on a WASM object if it has one. +/// +/// Returns Some(result) if the object has a toJSON method and it succeeds, +/// None otherwise (for plain objects, arrays, primitives, etc.) +fn try_call_to_json(value: &JsValue) -> Option { + if !value.is_object() || value.is_null() || js_sys::Array::is_array(value) { + return None; + } + + // Check for toJSON method + let to_json_fn = js_sys::Reflect::get(value, &JsValue::from_str("toJSON")).ok()?; + if !to_json_fn.is_function() { + return None; + } + + let func: js_sys::Function = to_json_fn.unchecked_into(); + func.call0(value).ok() +} + /// Convert JsValue to serde_json::Value, handling BigInt values and WASM objects. /// /// This function: @@ -37,18 +56,9 @@ use wasm_bindgen::prelude::*; /// - Falls back to serde_wasm_bindgen conversion for plain objects pub fn js_value_to_json(value: &JsValue) -> WasmDppResult { // Check if the value has a toJSON method (WASM objects like DataContractWasm, IdentityWasm) - if value.is_object() - && !value.is_null() - && !js_sys::Array::is_array(value) - && let Ok(to_json_fn) = js_sys::Reflect::get(value, &JsValue::from_str("toJSON")) - && to_json_fn.is_function() - { - let func: js_sys::Function = to_json_fn.unchecked_into(); - // Call toJSON() on the object - if let Ok(json_result) = func.call0(value) { - // Recursively convert the result (it might contain BigInt or nested WASM objects) - return js_value_to_json(&json_result); - } + if let Some(json_result) = try_call_to_json(value) { + // Recursively convert the result (it might contain BigInt or nested WASM objects) + return js_value_to_json(&json_result); } let normalized = normalize_js_value_for_json(value)?; @@ -70,12 +80,15 @@ pub fn json_to_js_value(value: &JsonValue) -> WasmDppResult { /// Recursively normalizes a JsValue for JSON conversion. /// /// This converts: +/// - WASM objects with toJSON() method - calls toJSON() and normalizes the result /// - BigInt values to strings (JSON doesn't support BigInt natively) /// - Uint8Array to plain arrays (so they serialize as JSON number arrays) +/// - JavaScript Map to plain objects +/// - Recursively processes nested objects and arrays /// /// Performance: Uses fast path for primitives, only recursively processes objects/arrays. fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { - // Fast path: primitives that can't contain BigInt + // Fast path: primitives that can't contain BigInt or need conversion if value.is_string() || value.as_f64().is_some() || value.is_null() @@ -85,6 +98,7 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { return Ok(value.clone()); } + // Convert BigInt to string (JSON doesn't support BigInt) if value.is_bigint() { let bigint: js_sys::BigInt = value.clone().unchecked_into(); let bigint_str = bigint @@ -101,6 +115,13 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { return Ok(plain_array.into()); } + // Convert JavaScript Map to an object for JSON compatibility + // Maps don't have enumerable properties, so Object.keys() returns empty array + if value.is_instance_of::() { + return normalize_map_for_json(value); + } + + // Handle arrays - recursively normalize each element if js_sys::Array::is_array(value) { let arr = js_sys::Array::from(value); let new_arr = js_sys::Array::new(); @@ -112,7 +133,15 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { return Ok(new_arr.into()); } + // Handle objects - check for toJSON method first (WASM objects), then normalize properties if value.is_object() && !value.is_null() { + // Try to call toJSON() on WASM objects (Identity, Identifier, DataContract, etc.) + if let Some(json_result) = try_call_to_json(value) { + // Recursively normalize the result (might contain BigInt, nested objects, etc.) + return normalize_js_value_for_json(&json_result); + } + + // Plain object - normalize each property let obj = Object::from(value.clone()); let new_obj = Object::new(); let keys = Object::keys(&obj); @@ -135,6 +164,86 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { Ok(value.clone()) } +/// Convert a JavaScript Map key to a string for JSON object keys. +fn map_key_to_string(key: &JsValue) -> String { + // String keys - use as-is + if let Some(s) = key.as_string() { + return s; + } + + // Number keys - convert to string + if key.as_f64().is_some() { + return js_sys::Number::from(key.clone()) + .to_string(10) + .map(|s| s.into()) + .unwrap_or_else(|_| "0".to_string()); + } + + // BigInt keys - convert to string + if key.is_bigint() { + let bigint: js_sys::BigInt = key.clone().unchecked_into(); + return bigint + .to_string(10) + .map(|s| s.into()) + .unwrap_or_else(|_| "0".to_string()); + } + + // Objects with toString (like Identifier) - call toString() + if let Ok(to_string_fn) = js_sys::Reflect::get(key, &JsValue::from_str("toString")) + && to_string_fn.is_function() + { + let func: js_sys::Function = to_string_fn.unchecked_into(); + if let Ok(str_result) = func.call0(key) { + if let Some(s) = str_result.as_string() { + return s; + } + } + } + + // Fallback - use debug representation + format!("{:?}", key) +} + +/// Convert a JavaScript Map to a plain object for JSON serialization. +fn normalize_map_for_json(value: &JsValue) -> WasmDppResult { + let map: js_sys::Map = value.clone().unchecked_into(); + let new_obj = Object::new(); + + // We need to collect errors from the closure since for_each doesn't support Result + let error: std::cell::RefCell> = std::cell::RefCell::new(None); + + map.for_each(&mut |val, key| { + // Skip if we already have an error + if error.borrow().is_some() { + return; + } + + let key_str = map_key_to_string(&key); + + // Normalize the value - handle WASM objects, BigInt, nested Maps, etc. + match normalize_js_value_for_json(&val) { + Ok(normalized_val) => { + if let Err(e) = js_sys::Reflect::set(&new_obj, &JsValue::from_str(&key_str), &normalized_val) { + *error.borrow_mut() = Some(WasmDppError::serialization(format!( + "Failed to set Map entry '{}': {:?}", + key_str, e + ))); + } + } + Err(e) => { + *error.borrow_mut() = Some(e); + } + } + }); + + // Check if any error occurred during iteration + if let Some(e) = error.into_inner() { + return Err(e); + } + + Ok(new_obj.into()) +} + /// Serialize to JsValue as a JS object (non-human-readable). /// /// Uses the serde-wasm-bindgen serializer with `is_human_readable() -> false`, @@ -354,6 +463,24 @@ pub fn js_value_to_platform_value(value: &JsValue) -> WasmDppResult Result { + let json_value = js_value_to_json(value)?; + json_to_js_value(&json_value) +} + /// Macro to implement `toObject`, `fromObject`, `toJSON`, and `fromJSON` methods /// for a wasm_bindgen newtype wrapper using the serialization::conversions module. /// diff --git a/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs b/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs new file mode 100644 index 00000000000..206dc766c45 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs @@ -0,0 +1,467 @@ +import { getWasm } from './helpers/wasm.js'; + +/** + * Tests for JavaScript value to JSON conversion via testJsValueToJson. + * + * These tests verify that normalize_js_value_for_json correctly handles + * various JavaScript types for JSON serialization: + * + * - Maps: converted to plain objects (keys become strings) + * - BigInt: converted to strings (JSON doesn't support BigInt) + * - Uint8Array: converted to regular arrays + * - WASM objects: their toJSON() method is called + * - Nested structures: recursively normalized + * - Primitives: passed through unchanged + */ + +describe('JS value to JSON conversion (testJsValueToJson)', () => { + let wasm; + + before(async () => { + wasm = await getWasm(); + }); + + describe('Map with string keys', () => { + it('should serialize empty Map to empty object', () => { + const emptyMap = new Map(); + + const json = wasm.testJsValueToJson(emptyMap); + + expect(json).to.deep.equal({}); + }); + + it('should serialize Map with string keys and primitive values', () => { + const map = new Map([ + ['key1', 'value1'], + ['key2', 42], + ['key3', true], + ['key4', null], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.deep.equal({ + key1: 'value1', + key2: 42, + key3: true, + key4: null, + }); + }); + + it('should serialize Map with string keys and object values', () => { + const map = new Map([ + ['epoch1', { index: 1, startTime: 1000 }], + ['epoch2', { index: 2, startTime: 2000 }], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.deep.equal({ + epoch1: { index: 1, startTime: 1000 }, + epoch2: { index: 2, startTime: 2000 }, + }); + }); + + it('should serialize Map with string keys and array values', () => { + const map = new Map([ + ['items1', [1, 2, 3]], + ['items2', ['a', 'b', 'c']], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.deep.equal({ + items1: [1, 2, 3], + items2: ['a', 'b', 'c'], + }); + }); + }); + + describe('Map with numeric keys', () => { + it('should serialize Map with number keys (converted to strings)', () => { + // This simulates getEpochsInfo which returns Map + const map = new Map([ + [0, { index: 0, name: 'epoch0' }], + [1, { index: 1, name: 'epoch1' }], + [100, { index: 100, name: 'epoch100' }], + ]); + + const json = wasm.testJsValueToJson(map); + + // Keys should be strings in JSON + expect(json).to.have.property('0'); + expect(json).to.have.property('1'); + expect(json).to.have.property('100'); + expect(json['0']).to.deep.equal({ index: 0, name: 'epoch0' }); + expect(json['1']).to.deep.equal({ index: 1, name: 'epoch1' }); + expect(json['100']).to.deep.equal({ index: 100, name: 'epoch100' }); + }); + + it('should serialize Map with negative number keys', () => { + const map = new Map([ + [-1, 'negative one'], + [0, 'zero'], + [1, 'one'], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.have.property('-1'); + expect(json).to.have.property('0'); + expect(json).to.have.property('1'); + expect(json['-1']).to.equal('negative one'); + }); + + it('should serialize Map with floating point keys', () => { + const map = new Map([ + [1.5, 'one point five'], + [2.7, 'two point seven'], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.have.property('1.5'); + expect(json).to.have.property('2.7'); + }); + }); + + describe('Map with BigInt keys', () => { + it('should serialize Map with BigInt keys (converted to strings)', () => { + const map = new Map([ + [1n, 'one'], + [9007199254740993n, 'large number'], // > MAX_SAFE_INTEGER + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.have.property('1'); + expect(json).to.have.property('9007199254740993'); + expect(json['1']).to.equal('one'); + expect(json['9007199254740993']).to.equal('large number'); + }); + }); + + describe('Map with BigInt values', () => { + it('should serialize Map with BigInt values (converted to strings)', () => { + // This simulates token balances or credit amounts + const map = new Map([ + ['balance1', 1000000000n], + ['balance2', 9007199254740993n], // > MAX_SAFE_INTEGER + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json.balance1).to.equal('1000000000'); + expect(json.balance2).to.equal('9007199254740993'); + }); + }); + + describe('Map with Identifier keys', () => { + it('should serialize Map with Identifier keys using toString()', () => { + const id1 = wasm.Identifier.fromBase58('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + const id2 = wasm.Identifier.fromBase58('ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U'); + + // This simulates getEvonodesProposedEpochBlocksByIds which returns Map + const map = new Map([ + [id1, 100n], + [id2, 200n], + ]); + + const json = wasm.testJsValueToJson(map); + + // Keys should be the string representation of Identifier (Base58) + expect(json).to.have.property('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + expect(json).to.have.property('ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U'); + // Values are BigInt, should be strings + expect(json['H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1']).to.equal('100'); + expect(json['ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U']).to.equal('200'); + }); + }); + + describe('Map with WASM object values', () => { + it('should serialize Map with Identity values by calling their toJSON()', () => { + const identity = new wasm.Identity('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + identity.balance = 1000000000n; + identity.revision = 5n; + + const map = new Map([ + ['identity1', identity], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json).to.have.property('identity1'); + expect(json.identity1).to.have.property('id'); + expect(json.identity1.id).to.equal('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + expect(json.identity1.balance).to.equal(1000000000); + expect(json.identity1.revision).to.equal(5); + }); + + it('should serialize Map with Identifier values', () => { + const id1 = wasm.Identifier.fromBase58('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + const id2 = wasm.Identifier.fromBase58('ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U'); + + const map = new Map([ + ['id1', id1], + ['id2', id2], + ]); + + const json = wasm.testJsValueToJson(map); + + // Identifier.toJSON() returns base58 string + expect(json.id1).to.equal('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); + expect(json.id2).to.equal('ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U'); + }); + + it('should serialize Map with undefined/null values', () => { + // This simulates queries that return Map + const map = new Map([ + [0, { epoch: 0 }], + [1, undefined], + [2, null], + [3, { epoch: 3 }], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json['0']).to.deep.equal({ epoch: 0 }); + // Note: In JSON, undefined becomes null (JSON doesn't have undefined) + expect(json['1']).to.satisfy(v => v === null || v === undefined); + expect(json['2']).to.equal(null); + expect(json['3']).to.deep.equal({ epoch: 3 }); + }); + }); + + describe('Nested Maps', () => { + it('should serialize nested Map as value', () => { + const innerMap = new Map([ + ['a', 1], + ['b', 2], + ]); + const outerMap = new Map([ + ['inner', innerMap], + ]); + + const json = wasm.testJsValueToJson(outerMap); + + expect(json).to.have.property('inner'); + expect(json.inner).to.deep.equal({ a: 1, b: 2 }); + }); + + it('should serialize deeply nested Maps', () => { + const level3 = new Map([['deep', 'value']]); + const level2 = new Map([['level3', level3]]); + const level1 = new Map([['level2', level2]]); + + const json = wasm.testJsValueToJson(level1); + + expect(json.level2.level3.deep).to.equal('value'); + }); + + it('should serialize Map inside array inside Map', () => { + const innerMap = new Map([['key', 'value']]); + const outerMap = new Map([ + ['items', [innerMap, { plain: 'object' }]], + ]); + + const json = wasm.testJsValueToJson(outerMap); + + expect(json.items[0]).to.deep.equal({ key: 'value' }); + expect(json.items[1]).to.deep.equal({ plain: 'object' }); + }); + }); + + describe('Map inside array', () => { + it('should serialize array containing Maps', () => { + const map1 = new Map([['a', 1]]); + const map2 = new Map([['b', 2]]); + const arrayWithMaps = [map1, map2]; + + const json = wasm.testJsValueToJson(arrayWithMaps); + + expect(json).to.be.an('array'); + expect(json[0]).to.deep.equal({ a: 1 }); + expect(json[1]).to.deep.equal({ b: 2 }); + }); + }); + + describe('Map inside object', () => { + it('should serialize object containing Map property', () => { + const map = new Map([['key', 'value']]); + const objectWithMap = { + normalProp: 'normal', + mapProp: map, + }; + + const json = wasm.testJsValueToJson(objectWithMap); + + expect(json.normalProp).to.equal('normal'); + expect(json.mapProp).to.deep.equal({ key: 'value' }); + }); + }); + + describe('Map with Uint8Array values', () => { + it('should serialize Map with Uint8Array values to arrays', () => { + const map = new Map([ + ['bytes1', new Uint8Array([1, 2, 3])], + ['bytes2', new Uint8Array([4, 5, 6])], + ]); + + const json = wasm.testJsValueToJson(map); + + // Uint8Array should be converted to regular arrays for JSON + expect(json.bytes1).to.deep.equal([1, 2, 3]); + expect(json.bytes2).to.deep.equal([4, 5, 6]); + }); + }); + + describe('Real-world epoch query simulation', () => { + it('should serialize epoch info Map like getEpochsInfoWithProofInfo returns', () => { + // Simulating the structure returned by getEpochsInfoWithProofInfo + const epochsMap = new Map([ + [0, { + index: 0, + firstBlockHeight: 1n, + firstCoreBlockHeight: 100, + startTime: 1609459200000n, + feeMultiplier: 1.0, + }], + [1, { + index: 1, + firstBlockHeight: 1000n, + firstCoreBlockHeight: 200, + startTime: 1609545600000n, + feeMultiplier: 1.1, + }], + [2, undefined], // Some epochs might not exist + ]); + + const json = wasm.testJsValueToJson(epochsMap); + + expect(json).to.have.property('0'); + expect(json).to.have.property('1'); + expect(json).to.have.property('2'); + + expect(json['0'].index).to.equal(0); + expect(json['0'].firstBlockHeight).to.equal('1'); // BigInt -> string + expect(json['0'].startTime).to.equal('1609459200000'); // BigInt -> string + + expect(json['1'].index).to.equal(1); + // Note: In JSON, undefined becomes null (JSON doesn't have undefined) + expect(json['2']).to.satisfy(v => v === null || v === undefined); + }); + }); + + describe('Edge cases', () => { + it('should handle Map with special string keys', () => { + const map = new Map([ + ['', 'empty key'], + ['with spaces', 'spaced'], + ['with.dots', 'dotted'], + ['with/slash', 'slashed'], + ['with\\backslash', 'backslashed'], + ['unicode: \u00e9', 'accented'], + ]); + + const json = wasm.testJsValueToJson(map); + + expect(json['']).to.equal('empty key'); + expect(json['with spaces']).to.equal('spaced'); + expect(json['with.dots']).to.equal('dotted'); + expect(json['with/slash']).to.equal('slashed'); + expect(json['with\\backslash']).to.equal('backslashed'); + expect(json['unicode: \u00e9']).to.equal('accented'); + }); + + it('should preserve key order in Map', () => { + const map = new Map([ + ['z', 1], + ['a', 2], + ['m', 3], + ]); + + const json = wasm.testJsValueToJson(map); + const keys = Object.keys(json); + + // Maps preserve insertion order + expect(keys).to.deep.equal(['z', 'a', 'm']); + }); + + it('should handle large Maps', () => { + const map = new Map(); + for (let i = 0; i < 1000; i++) { + map.set(i, { index: i, value: `item${i}` }); + } + + const json = wasm.testJsValueToJson(map); + + expect(Object.keys(json).length).to.equal(1000); + expect(json['0']).to.deep.equal({ index: 0, value: 'item0' }); + expect(json['999']).to.deep.equal({ index: 999, value: 'item999' }); + }); + }); + + describe('Primitive values', () => { + it('should pass through strings', () => { + const result = wasm.testJsValueToJson('hello'); + expect(result).to.equal('hello'); + }); + + it('should pass through numbers', () => { + const result = wasm.testJsValueToJson(42); + expect(result).to.equal(42); + }); + + it('should pass through booleans', () => { + expect(wasm.testJsValueToJson(true)).to.equal(true); + expect(wasm.testJsValueToJson(false)).to.equal(false); + }); + + it('should pass through null', () => { + const result = wasm.testJsValueToJson(null); + expect(result).to.equal(null); + }); + + it('should convert BigInt to string', () => { + const result = wasm.testJsValueToJson(9007199254740993n); + expect(result).to.equal('9007199254740993'); + }); + + it('should convert Uint8Array to array', () => { + const result = wasm.testJsValueToJson(new Uint8Array([1, 2, 3])); + expect(result).to.deep.equal([1, 2, 3]); + }); + }); + + describe('Plain objects and arrays', () => { + it('should pass through plain objects', () => { + const obj = { a: 1, b: 'two', c: true }; + const result = wasm.testJsValueToJson(obj); + expect(result).to.deep.equal(obj); + }); + + it('should pass through arrays', () => { + const arr = [1, 'two', true, null]; + const result = wasm.testJsValueToJson(arr); + expect(result).to.deep.equal(arr); + }); + + it('should recursively handle nested objects with BigInt', () => { + const obj = { + outer: { + inner: { + value: 9007199254740993n, + }, + }, + }; + const result = wasm.testJsValueToJson(obj); + expect(result.outer.inner.value).to.equal('9007199254740993'); + }); + + it('should recursively handle arrays with BigInt', () => { + const arr = [1n, 2n, 9007199254740993n]; + const result = wasm.testJsValueToJson(arr); + expect(result).to.deep.equal(['1', '2', '9007199254740993']); + }); + }); +}); diff --git a/packages/wasm-sdk/src/queries/address.rs b/packages/wasm-sdk/src/queries/address.rs index 526a0a0dc19..6bc31b23e7f 100644 --- a/packages/wasm-sdk/src/queries/address.rs +++ b/packages/wasm-sdk/src/queries/address.rs @@ -1,10 +1,12 @@ use crate::error::WasmSdkError; +use crate::impl_wasm_serde_conversions; use crate::queries::ProofMetadataResponseWasm; use crate::sdk::WasmSdk; use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::platform::{Fetch, FetchMany}; use drive_proof_verifier::types::{AddressInfo, AddressInfos}; use js_sys::{BigInt, Map}; +use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; @@ -12,7 +14,8 @@ use wasm_dpp2::PlatformAddressWasm; /// Information about a Platform address including its nonce and balance. #[wasm_bindgen(js_name = "PlatformAddressInfo")] -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PlatformAddressInfoWasm { address: PlatformAddressWasm, nonce: u32, @@ -50,6 +53,8 @@ impl PlatformAddressInfoWasm { } } +impl_wasm_serde_conversions!(PlatformAddressInfoWasm, PlatformAddressInfo); + #[wasm_bindgen] impl WasmSdk { /// Fetches information about a Platform address including its nonce and balance. @@ -94,9 +99,13 @@ impl WasmSdk { AddressInfo::fetch_with_metadata_and_proof(self.as_ref(), platform_address, None) .await?; - let data = address_info - .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info))) - .unwrap_or(JsValue::UNDEFINED); + let data = match address_info { + Some(info) => { + let wrapper = PlatformAddressInfoWasm::from(info); + wrapper.to_object()? + } + None => JsValue::UNDEFINED, + }; Ok(ProofMetadataResponseWasm::from_sdk_parts( data, metadata, proof, @@ -135,11 +144,13 @@ impl WasmSdk { for address in platform_addresses { let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = address_infos - .get(&address) - .and_then(|opt| opt.as_ref()) - .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info.clone()))) - .unwrap_or(JsValue::UNDEFINED); + let value = match address_infos.get(&address).and_then(|opt| opt.as_ref()) { + Some(info) => { + let wrapper = PlatformAddressInfoWasm::from(info.clone()); + wrapper.to_object()? + } + None => JsValue::UNDEFINED, + }; results_map.set(&key, &value); } @@ -183,11 +194,13 @@ impl WasmSdk { for address in platform_addresses { let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = address_infos - .get(&address) - .and_then(|opt| opt.as_ref()) - .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info.clone()))) - .unwrap_or(JsValue::UNDEFINED); + let value = match address_infos.get(&address).and_then(|opt| opt.as_ref()) { + Some(info) => { + let wrapper = PlatformAddressInfoWasm::from(info.clone()); + wrapper.to_object()? + } + None => JsValue::UNDEFINED, + }; results_map.set(&key, &value); } diff --git a/packages/wasm-sdk/src/queries/identity.rs b/packages/wasm-sdk/src/queries/identity.rs index e911b210e38..fea7b732a90 100644 --- a/packages/wasm-sdk/src/queries/identity.rs +++ b/packages/wasm-sdk/src/queries/identity.rs @@ -20,7 +20,8 @@ use wasm_dpp2::identifier::IdentifierWasm; use wasm_dpp2::identity::IdentityWasm; #[wasm_bindgen(js_name = "IdentityKeyInfo")] -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct IdentityKeyInfoWasm { key_id: u32, key_type: String, @@ -83,8 +84,11 @@ impl IdentityKeyInfoWasm { } } +impl_wasm_serde_conversions!(IdentityKeyInfoWasm, IdentityKeyInfo); + #[wasm_bindgen(js_name = "IdentityContractKeys")] -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct IdentityContractKeysWasm { identity_id: IdentifierWasm, keys: Vec, @@ -104,15 +108,18 @@ impl IdentityContractKeysWasm { } #[wasm_bindgen(getter = "keys")] - pub fn keys(&self) -> Array { + pub fn keys(&self) -> Result { let array = Array::new(); for key in &self.keys { - array.push(&JsValue::from(key.clone())); + // Use to_object() to properly serialize each key + array.push(&key.to_object()?); } - array + Ok(array) } } +impl_wasm_serde_conversions!(IdentityContractKeysWasm, IdentityContractKeys); + #[wasm_bindgen(js_name = "IdentityBalanceAndRevision")] #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -140,7 +147,7 @@ impl IdentityBalanceAndRevisionWasm { } } -impl_wasm_serde_conversions!(IdentityBalanceAndRevisionWasm); +impl_wasm_serde_conversions!(IdentityBalanceAndRevisionWasm, IdentityBalanceAndRevision); #[wasm_bindgen(typescript_custom_section)] const IDENTITIES_CONTRACT_KEYS_QUERY_TS: &'static str = r#" /** @@ -608,9 +615,9 @@ impl WasmSdk { let array = Array::new(); for (key_id, key_opt) in keys_result { if let Some(key) = key_opt { - array.push(&JsValue::from(IdentityKeyInfoWasm::from_entry( - key_id, &key, - ))); + let key_info = IdentityKeyInfoWasm::from_entry(key_id, &key); + // Use to_object() to properly serialize the key info + array.push(&key_info.to_object()?); } } @@ -910,7 +917,8 @@ impl WasmSdk { let array = Array::new(); for response in responses { - array.push(&JsValue::from(response)); + // Use to_object() to properly serialize the response with nested keys + array.push(&response.to_object()?); } Ok(array) @@ -1083,9 +1091,9 @@ impl WasmSdk { let keys_array = Array::new(); for (key_id, key_opt) in keys_result { if let Some(key) = key_opt { - keys_array.push(&JsValue::from(IdentityKeyInfoWasm::from_entry( - key_id, &key, - ))); + let key_info = IdentityKeyInfoWasm::from_entry(key_id, &key); + // Use to_object() to properly serialize the key info + keys_array.push(&key_info.to_object()?); } } @@ -1359,7 +1367,8 @@ impl WasmSdk { let responses_array = Array::new(); for response in all_responses { - responses_array.push(&JsValue::from(response)); + // Use to_object() to properly serialize the response with nested keys + responses_array.push(&response.to_object()?); } Ok(ProofMetadataResponseWasm::from_sdk_parts( diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs index 055d077966e..9367dad795b 100644 --- a/packages/wasm-sdk/src/queries/protocol.rs +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -95,8 +95,8 @@ impl ProtocolVersionUpgradeVoteStatusWasm { } } -impl_wasm_serde_conversions!(ProtocolVersionUpgradeStateWasm); -impl_wasm_serde_conversions!(ProtocolVersionUpgradeVoteStatusWasm); +impl_wasm_serde_conversions!(ProtocolVersionUpgradeStateWasm, ProtocolVersionUpgradeState); +impl_wasm_serde_conversions!(ProtocolVersionUpgradeVoteStatusWasm, ProtocolVersionUpgradeVoteStatus); #[wasm_bindgen] impl WasmSdk { diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 95dd9f94e5b..81010fce868 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -275,22 +275,22 @@ impl StatusResponseWasm { } } -impl_wasm_serde_conversions!(StatusSoftwareWasm); -impl_wasm_serde_conversions!(StatusTenderdashProtocolWasm); -impl_wasm_serde_conversions!(StatusDriveProtocolWasm); -impl_wasm_serde_conversions!(StatusProtocolWasm); -impl_wasm_serde_conversions!(StatusVersionWasm); -impl_wasm_serde_conversions!(StatusNodeWasm); -impl_wasm_serde_conversions!(StatusChainWasm); -impl_wasm_serde_conversions!(StatusNetworkWasm); -impl_wasm_serde_conversions!(StatusStateSyncWasm); -impl_wasm_serde_conversions!(StatusTimeWasm); -impl_wasm_serde_conversions!(StatusResponseWasm); -impl_wasm_serde_conversions!(QuorumInfoWasm); -impl_wasm_serde_conversions!(CurrentQuorumsInfoWasm); -impl_wasm_serde_conversions!(PrefundedSpecializedBalanceWasm); -impl_wasm_serde_conversions!(PathElementWasm); -impl_wasm_serde_conversions!(StateTransitionResultWasm); +impl_wasm_serde_conversions!(StatusSoftwareWasm, StatusSoftware); +impl_wasm_serde_conversions!(StatusTenderdashProtocolWasm, StatusTenderdashProtocol); +impl_wasm_serde_conversions!(StatusDriveProtocolWasm, StatusDriveProtocol); +impl_wasm_serde_conversions!(StatusProtocolWasm, StatusProtocol); +impl_wasm_serde_conversions!(StatusVersionWasm, StatusVersion); +impl_wasm_serde_conversions!(StatusNodeWasm, StatusNode); +impl_wasm_serde_conversions!(StatusChainWasm, StatusChain); +impl_wasm_serde_conversions!(StatusNetworkWasm, StatusNetwork); +impl_wasm_serde_conversions!(StatusStateSyncWasm, StatusStateSync); +impl_wasm_serde_conversions!(StatusTimeWasm, StatusTime); +impl_wasm_serde_conversions!(StatusResponseWasm, StatusResponse); +impl_wasm_serde_conversions!(QuorumInfoWasm, QuorumInfo); +impl_wasm_serde_conversions!(CurrentQuorumsInfoWasm, CurrentQuorumsInfo); +impl_wasm_serde_conversions!(PrefundedSpecializedBalanceWasm, PrefundedSpecializedBalance); +impl_wasm_serde_conversions!(PathElementWasm, PathElement); +impl_wasm_serde_conversions!(StateTransitionResultWasm, StateTransitionResult); #[wasm_bindgen(js_name = "QuorumInfo")] #[derive(Clone, Serialize, Deserialize)] diff --git a/packages/wasm-sdk/src/queries/token.rs b/packages/wasm-sdk/src/queries/token.rs index cc8e56b3675..69de5fdbeb3 100644 --- a/packages/wasm-sdk/src/queries/token.rs +++ b/packages/wasm-sdk/src/queries/token.rs @@ -125,8 +125,8 @@ impl TokenTotalSupplyWasm { } } -impl_wasm_serde_conversions!(TokenTotalSupplyWasm); -impl_wasm_serde_conversions!(TokenPriceInfoWasm); +impl_wasm_serde_conversions!(TokenTotalSupplyWasm, TokenTotalSupply); +impl_wasm_serde_conversions!(TokenPriceInfoWasm, TokenPriceInfo); #[wasm_bindgen] impl WasmSdk { diff --git a/packages/wasm-sdk/src/wallet/extended_derivation.rs b/packages/wasm-sdk/src/wallet/extended_derivation.rs index d68feddd188..b8a929605df 100644 --- a/packages/wasm-sdk/src/wallet/extended_derivation.rs +++ b/packages/wasm-sdk/src/wallet/extended_derivation.rs @@ -169,7 +169,7 @@ impl From for DerivedKeyInfoWasm { } // Field getters are generated via getter_with_clone annotations above -impl_wasm_serde_conversions!(DerivedKeyInfoWasm); +impl_wasm_serde_conversions!(DerivedKeyInfoWasm, DerivedKeyInfo); #[wasm_bindgen(js_name = "DashpayContactKeyInfo")] #[derive(Clone, Serialize, Deserialize)] @@ -235,7 +235,7 @@ impl DashpayContactKeyInfoWasm { } // Field getters are generated via getter_with_clone annotations above -impl_wasm_serde_conversions!(DashpayContactKeyInfoWasm); +impl_wasm_serde_conversions!(DashpayContactKeyInfoWasm, DashpayContactKeyInfo); #[wasm_bindgen] impl WasmSdk { /// Derive a key from seed phrase with extended path supporting 256-bit indices diff --git a/packages/wasm-sdk/src/wallet/key_derivation.rs b/packages/wasm-sdk/src/wallet/key_derivation.rs index 92ec3d0063f..ad5db95a1b5 100644 --- a/packages/wasm-sdk/src/wallet/key_derivation.rs +++ b/packages/wasm-sdk/src/wallet/key_derivation.rs @@ -266,10 +266,10 @@ pub struct PathDerivedKeyInfoWasm { pub network: String, } -impl_wasm_serde_conversions!(DerivationPathWasm); -impl_wasm_serde_conversions!(Dip13DerivationPathWasm); -impl_wasm_serde_conversions!(SeedPhraseKeyInfoWasm); -impl_wasm_serde_conversions!(PathDerivedKeyInfoWasm); +impl_wasm_serde_conversions!(DerivationPathWasm, DerivationPathInfo); +impl_wasm_serde_conversions!(Dip13DerivationPathWasm, Dip13DerivationPathInfo); +impl_wasm_serde_conversions!(SeedPhraseKeyInfoWasm, SeedPhraseKeyInfo); +impl_wasm_serde_conversions!(PathDerivedKeyInfoWasm, PathDerivedKeyInfo); /// HD Key information #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/packages/wasm-sdk/src/wallet/key_generation.rs b/packages/wasm-sdk/src/wallet/key_generation.rs index 804bbc3c417..c7c44173511 100644 --- a/packages/wasm-sdk/src/wallet/key_generation.rs +++ b/packages/wasm-sdk/src/wallet/key_generation.rs @@ -57,7 +57,7 @@ impl From for KeyPairWasm { } } -impl_wasm_serde_conversions!(KeyPairWasm); +impl_wasm_serde_conversions!(KeyPairWasm, KeyPair); #[wasm_bindgen] impl WasmSdk { From 92790cad21fb7ad4c85c776a96b84f79768f6ef6 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 21:42:05 +0700 Subject: [PATCH 08/14] feat: handle non human readable serialization --- packages/wasm-dpp2/src/platform_address/address.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index 9033b533ba8..c6ad43fa92a 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -171,8 +171,13 @@ impl Serialize for PlatformAddressWasm { where S: Serializer, { - // Serialize as hex string (consistent with to_hex method) - serializer.serialize_str(&hex::encode(self.0.to_bytes())) + if serializer.is_human_readable() { + // JSON, TOML, etc. - use hex string + serializer.serialize_str(&hex::encode(self.0.to_bytes())) + } else { + // Binary formats (bincode, MessagePack, etc.) - use raw bytes + serializer.serialize_bytes(&self.0.to_bytes()) + } } } From 83d03ccb6a60fa594b1f65af512603d3d0aa271e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 21:42:19 +0700 Subject: [PATCH 09/14] fix: missing number to network --- packages/wasm-dpp2/src/core/network.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/wasm-dpp2/src/core/network.rs b/packages/wasm-dpp2/src/core/network.rs index 1cca50018a5..08b5eda2fcc 100644 --- a/packages/wasm-dpp2/src/core/network.rs +++ b/packages/wasm-dpp2/src/core/network.rs @@ -7,11 +7,12 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] const NETWORK_LIKE_TS: &'static str = r#" /** - * Flexible network type that accepts Network enum or string names. + * Flexible network type that accepts Network enum, string names, or numeric values. * * String values (case-insensitive): "mainnet", "testnet", "devnet", "regtest" + * Numeric values: 0 (mainnet), 1 (testnet), 2 (devnet), 3 (regtest) */ -export type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest"; +export type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest" | 0 | 1 | 2 | 3; "#; #[wasm_bindgen(js_name = "Network")] @@ -27,6 +28,7 @@ pub enum NetworkWasm { impl TryFrom for NetworkWasm { type Error = WasmDppError; fn try_from(value: JsValue) -> Result { + // Handle string input if let Some(enum_val) = value.as_string() { return match enum_val.to_lowercase().as_str() { "mainnet" => Ok(NetworkWasm::Mainnet), @@ -40,8 +42,22 @@ impl TryFrom for NetworkWasm { }; } + // Handle numeric enum value (Network.Mainnet = 0, Testnet = 1, etc.) + if let Some(num) = value.as_f64() { + return match num as u32 { + 0 => Ok(NetworkWasm::Mainnet), + 1 => Ok(NetworkWasm::Testnet), + 2 => Ok(NetworkWasm::Devnet), + 3 => Ok(NetworkWasm::Regtest), + _ => Err(WasmDppError::invalid_argument(format!( + "unsupported network value '{}'. Expected: 0 (mainnet), 1 (testnet), 2 (devnet), or 3 (regtest)", + num + ))), + }; + } + Err(WasmDppError::invalid_argument( - "network must be a string: 'mainnet', 'testnet', 'devnet', or 'regtest'", + "network must be a string ('mainnet', 'testnet', 'devnet', 'regtest') or Network enum value", )) } } From d66d97e2539bb9ef03ef8930c0e0ba67719551eb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 22:26:43 +0700 Subject: [PATCH 10/14] refactor: use IdentityPublicKeyWasm --- packages/js-evo-sdk/src/identities/facade.ts | 4 +- packages/wasm-sdk/src/queries/identity.rs | 150 ++++--------------- 2 files changed, 30 insertions(+), 124 deletions(-) diff --git a/packages/js-evo-sdk/src/identities/facade.ts b/packages/js-evo-sdk/src/identities/facade.ts index f98521b4019..19c5cc39fed 100644 --- a/packages/js-evo-sdk/src/identities/facade.ts +++ b/packages/js-evo-sdk/src/identities/facade.ts @@ -23,12 +23,12 @@ export class IdentitiesFacade { return w.getIdentityUnproved(identityId); } - async getKeys(query: wasm.IdentityKeysQuery): Promise { + async getKeys(query: wasm.IdentityKeysQuery): Promise { const w = await this.sdk.getWasmSdkConnected(); return w.getIdentityKeys(query); } - async getKeysWithProof(query: wasm.IdentityKeysQuery): Promise> { + async getKeysWithProof(query: wasm.IdentityKeysQuery): Promise> { const w = await this.sdk.getWasmSdkConnected(); return w.getIdentityKeysWithProofInfo(query); } diff --git a/packages/wasm-sdk/src/queries/identity.rs b/packages/wasm-sdk/src/queries/identity.rs index fea7b732a90..ad13284e68b 100644 --- a/packages/wasm-sdk/src/queries/identity.rs +++ b/packages/wasm-sdk/src/queries/identity.rs @@ -17,85 +17,17 @@ use std::collections::{BTreeMap, HashMap}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; use wasm_dpp2::identifier::IdentifierWasm; +use wasm_dpp2::identity::public_key::IdentityPublicKeyWasm; use wasm_dpp2::identity::IdentityWasm; -#[wasm_bindgen(js_name = "IdentityKeyInfo")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct IdentityKeyInfoWasm { - key_id: u32, - key_type: String, - public_key_data: String, - purpose: String, - security_level: String, - read_only: bool, - disabled: bool, -} - -impl IdentityKeyInfoWasm { - fn from_entry(key_id: u32, key: &IdentityPublicKey) -> Self { - IdentityKeyInfoWasm { - key_id, - key_type: format!("{:?}", key.key_type()), - public_key_data: hex::encode(key.data().as_slice()), - purpose: format!("{:?}", key.purpose()), - security_level: format!("{:?}", key.security_level()), - read_only: key.read_only(), - disabled: key.disabled_at().is_some(), - } - } -} - -#[wasm_bindgen(js_class = IdentityKeyInfo)] -impl IdentityKeyInfoWasm { - #[wasm_bindgen(getter = "keyId")] - pub fn key_id(&self) -> u32 { - self.key_id - } - - #[wasm_bindgen(getter = "keyType")] - pub fn key_type(&self) -> String { - self.key_type.clone() - } - - #[wasm_bindgen(getter = "publicKeyData")] - pub fn public_key_data(&self) -> String { - self.public_key_data.clone() - } - - #[wasm_bindgen(getter = "purpose")] - pub fn purpose(&self) -> String { - self.purpose.clone() - } - - #[wasm_bindgen(getter = "securityLevel")] - pub fn security_level(&self) -> String { - self.security_level.clone() - } - - #[wasm_bindgen(getter = "readOnly")] - pub fn read_only(&self) -> bool { - self.read_only - } - - #[wasm_bindgen(getter = "disabled")] - pub fn disabled(&self) -> bool { - self.disabled - } -} - -impl_wasm_serde_conversions!(IdentityKeyInfoWasm, IdentityKeyInfo); - #[wasm_bindgen(js_name = "IdentityContractKeys")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct IdentityContractKeysWasm { identity_id: IdentifierWasm, - keys: Vec, + keys: Vec, } impl IdentityContractKeysWasm { - fn new(identity_id: IdentifierWasm, keys: Vec) -> Self { + fn new(identity_id: IdentifierWasm, keys: Vec) -> Self { IdentityContractKeysWasm { identity_id, keys } } } @@ -108,18 +40,11 @@ impl IdentityContractKeysWasm { } #[wasm_bindgen(getter = "keys")] - pub fn keys(&self) -> Result { - let array = Array::new(); - for key in &self.keys { - // Use to_object() to properly serialize each key - array.push(&key.to_object()?); - } - Ok(array) + pub fn keys(&self) -> Vec { + self.keys.clone() } } -impl_wasm_serde_conversions!(IdentityContractKeysWasm, IdentityContractKeys); - #[wasm_bindgen(js_name = "IdentityBalanceAndRevision")] #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -488,7 +413,7 @@ impl WasmSdk { #[wasm_bindgen( js_name = "getIdentityKeys", - unchecked_return_type = "Array" + unchecked_return_type = "Array" )] pub async fn get_identity_keys( &self, @@ -613,11 +538,9 @@ impl WasmSdk { }; let array = Array::new(); - for (key_id, key_opt) in keys_result { + for (_key_id, key_opt) in keys_result { if let Some(key) = key_opt { - let key_info = IdentityKeyInfoWasm::from_entry(key_id, &key); - // Use to_object() to properly serialize the key info - array.push(&key_info.to_object()?); + array.push(&IdentityPublicKeyWasm::from(key).into()); } } @@ -895,32 +818,24 @@ impl WasmSdk { let keys_result: Option = IdentitiesContractKeys::fetch(self.as_ref(), query).await?; - let mut responses: Vec = Vec::new(); + let array = Array::new(); if let Some(keys_map) = keys_result { for (identity_id, purposes_map) in keys_map { - let mut identity_keys = Vec::new(); - for (_, key_opt) in purposes_map { - if let Some(key) = key_opt { - identity_keys.push(IdentityKeyInfoWasm::from_entry(key.id(), &key)); - } - } + let identity_keys: Vec = purposes_map + .into_iter() + .filter_map(|(_, key_opt)| key_opt.map(IdentityPublicKeyWasm::from)) + .collect(); if !identity_keys.is_empty() { - let identity_id_str = IdentifierWasm::from(identity_id); - responses.push(IdentityContractKeysWasm::new( - identity_id_str, + let response = IdentityContractKeysWasm::new( + IdentifierWasm::from(identity_id), identity_keys, - )); + ); + array.push(&response.into()); } } } - let array = Array::new(); - for response in responses { - // Use to_object() to properly serialize the response with nested keys - array.push(&response.to_object()?); - } - Ok(array) } @@ -1043,7 +958,7 @@ impl WasmSdk { #[wasm_bindgen( js_name = "getIdentityKeysWithProofInfo", - unchecked_return_type = "ProofMetadataResponseTyped>" + unchecked_return_type = "ProofMetadataResponseTyped>" )] pub async fn get_identity_keys_with_proof_info( &self, @@ -1089,11 +1004,9 @@ impl WasmSdk { }; let keys_array = Array::new(); - for (key_id, key_opt) in keys_result { + for (_key_id, key_opt) in keys_result { if let Some(key) = key_opt { - let key_info = IdentityKeyInfoWasm::from_entry(key_id, &key); - // Use to_object() to properly serialize the key info - keys_array.push(&key_info.to_object()?); + keys_array.push(&IdentityPublicKeyWasm::from(key).into()); } } @@ -1346,31 +1259,24 @@ impl WasmSdk { IdentitiesContractKeys::fetch_with_metadata_and_proof(self.as_ref(), query, None) .await?; - let mut all_responses: Vec = Vec::new(); + let responses_array = Array::new(); if let Some(keys_map) = keys_result { for (identity_id, purposes_map) in keys_map { - let mut identity_keys = Vec::new(); - for (_, key_opt) in purposes_map { - if let Some(key) = key_opt { - identity_keys.push(IdentityKeyInfoWasm::from_entry(key.id(), &key)); - } - } + let identity_keys: Vec = purposes_map + .into_iter() + .filter_map(|(_, key_opt)| key_opt.map(IdentityPublicKeyWasm::from)) + .collect(); if !identity_keys.is_empty() { - all_responses.push(IdentityContractKeysWasm::new( + let response = IdentityContractKeysWasm::new( IdentifierWasm::from(identity_id), identity_keys, - )); + ); + responses_array.push(&response.into()); } } } - let responses_array = Array::new(); - for response in all_responses { - // Use to_object() to properly serialize the response with nested keys - responses_array.push(&response.to_object()?); - } - Ok(ProofMetadataResponseWasm::from_sdk_parts( responses_array, metadata, From 164bc0c13982b38ebd8b76ed0e633bda40dfaa77 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 22:37:39 +0700 Subject: [PATCH 11/14] chore: fix formatting and linter warnings --- .../src/serialization/conversions.rs | 4 +++- .../tests/unit/BatchTransition.spec.mjs | 24 +++++++++---------- .../tests/unit/DataContract.spec.mjs | 4 +++- .../wasm-dpp2/tests/unit/Identity.spec.mjs | 1 - .../tests/unit/PlatformAddress.spec.mjs | 2 +- .../tests/unit/PlatformAddressOutput.spec.mjs | 8 +++---- .../tests/unit/js-value-to-json.spec.mjs | 10 ++++---- packages/wasm-sdk/src/queries/protocol.rs | 5 +++- 8 files changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/wasm-dpp2/src/serialization/conversions.rs b/packages/wasm-dpp2/src/serialization/conversions.rs index 90efff10b54..25149a4e333 100644 --- a/packages/wasm-dpp2/src/serialization/conversions.rs +++ b/packages/wasm-dpp2/src/serialization/conversions.rs @@ -223,7 +223,9 @@ fn normalize_map_for_json(value: &JsValue) -> WasmDppResult { // Normalize the value - handle WASM objects, BigInt, nested Maps, etc. match normalize_js_value_for_json(&val) { Ok(normalized_val) => { - if let Err(e) = js_sys::Reflect::set(&new_obj, &JsValue::from_str(&key_str), &normalized_val) { + if let Err(e) = + js_sys::Reflect::set(&new_obj, &JsValue::from_str(&key_str), &normalized_val) + { *error.borrow_mut() = Some(WasmDppError::serialization(format!( "Failed to set Map entry '{}': {:?}", key_str, e diff --git a/packages/wasm-dpp2/tests/unit/BatchTransition.spec.mjs b/packages/wasm-dpp2/tests/unit/BatchTransition.spec.mjs index 90abab7d8b9..66b2d066a56 100644 --- a/packages/wasm-dpp2/tests/unit/BatchTransition.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/BatchTransition.spec.mjs @@ -46,23 +46,23 @@ describe('BatchTransition', () => { }); it('should allow to convert batch transition to base64 and back', () => { - const documentInstance = new wasm.Document(document, documentTypeName, revision, dataContractId, ownerId, id); - const createTransition = new wasm.DocumentCreateTransition(documentInstance, BigInt(1)); + const documentInstance = new wasm.Document(document, documentTypeName, revision, dataContractId, ownerId, id); + const createTransition = new wasm.DocumentCreateTransition(documentInstance, BigInt(1)); - const documentTransition = createTransition.toDocumentTransition(); + const documentTransition = createTransition.toDocumentTransition(); - const batchedTransition = new wasm.BatchedTransition(documentTransition); + const batchedTransition = new wasm.BatchedTransition(documentTransition); - const batch = wasm.BatchTransition.fromBatchedTransitions([batchedTransition], documentInstance.ownerId, 1); + const batch = wasm.BatchTransition.fromBatchedTransitions([batchedTransition], documentInstance.ownerId, 1); - const base64 = batch.toBase64(); - const bytes = batch.toBytes(); + const base64 = batch.toBase64(); + const bytes = batch.toBytes(); - expect(Buffer.from(base64, 'base64')).to.deep.equal(Buffer.from(bytes)); + expect(Buffer.from(base64, 'base64')).to.deep.equal(Buffer.from(bytes)); - const restoredBatch = wasm.BatchTransition.fromBase64(base64); + const restoredBatch = wasm.BatchTransition.fromBase64(base64); - expect(Buffer.from(restoredBatch.toBytes())).to.deep.equal(Buffer.from(bytes)); + expect(Buffer.from(restoredBatch.toBytes())).to.deep.equal(Buffer.from(bytes)); }); it('should round-trip via object and JSON', () => { @@ -87,8 +87,8 @@ describe('BatchTransition', () => { const fromJson = wasm.BatchTransition.fromJSON(json); expect(Buffer.from(fromJson.toBytes())).to.deep.equal(Buffer.from(batch.toBytes())); }); - }); - describe('tokens', () => { + }); + describe('tokens', () => { it('should allow to create from v1 transition', () => { const baseTransition = new wasm.TokenBaseTransition(BigInt(1), 1, dataContractId, ownerId); diff --git a/packages/wasm-dpp2/tests/unit/DataContract.spec.mjs b/packages/wasm-dpp2/tests/unit/DataContract.spec.mjs index ca7ebcd4423..2f438db7dfc 100644 --- a/packages/wasm-dpp2/tests/unit/DataContract.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/DataContract.spec.mjs @@ -1,5 +1,7 @@ import getWasm from './helpers/wasm.js'; -import { json, object, id, ownerId } from './mocks/DataContract/index.js'; +import { + json, object, id, ownerId +} from './mocks/DataContract/index.js'; import { fromHexString } from './utils/hex.js'; let wasm; diff --git a/packages/wasm-dpp2/tests/unit/Identity.spec.mjs b/packages/wasm-dpp2/tests/unit/Identity.spec.mjs index 247f2849b98..69950494137 100644 --- a/packages/wasm-dpp2/tests/unit/Identity.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/Identity.spec.mjs @@ -57,7 +57,6 @@ describe('Identity', () => { expect(restoredIdentity.id.toBytes()).to.deep.equal(identity.id.toBytes()); expect(restoredIdentity.getPublicKeys().length).to.equal(identity.getPublicKeys().length); }); - }); describe('getters', () => { diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs index 1836364411c..1fe445e05ce 100644 --- a/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs @@ -86,7 +86,7 @@ describe('PlatformAddress', () => { describe('fromHex', () => { it('should create address from hex string', () => { - const hexString = '00' + '01020304050607080910111213141516171819'.padEnd(40, '0').substring(0, 40); + const hexString = `00${'01020304050607080910111213141516171819'.padEnd(40, '0').substring(0, 40)}`; const addr = wasm.PlatformAddress.fromHex(hexString); expect(addr).to.exist; expect(addr.addressType).to.equal('P2PKH'); diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs index e187813aac0..ca14b2605d5 100644 --- a/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs @@ -56,12 +56,10 @@ describe('PlatformAddressOutput', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - try { + expect(() => { + // eslint-disable-next-line no-new new wasm.PlatformAddressOutput(platformAddr, BigInt(-1)); - expect.fail('Should have thrown error for negative amount'); - } catch (error) { - expect(error).to.exist; - } + }).to.throw(); }); }); diff --git a/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs b/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs index 206dc766c45..c4709f5d072 100644 --- a/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/js-value-to-json.spec.mjs @@ -173,8 +173,8 @@ describe('JS value to JSON conversion (testJsValueToJson)', () => { expect(json).to.have.property('H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'); expect(json).to.have.property('ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U'); // Values are BigInt, should be strings - expect(json['H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1']).to.equal('100'); - expect(json['ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U']).to.equal('200'); + expect(json.H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1).to.equal('100'); + expect(json.ckBqfQe7LU7vwrwXopyCB4n5phZShjA16BGhNGpsD5U).to.equal('200'); }); }); @@ -226,7 +226,7 @@ describe('JS value to JSON conversion (testJsValueToJson)', () => { expect(json['0']).to.deep.equal({ epoch: 0 }); // Note: In JSON, undefined becomes null (JSON doesn't have undefined) - expect(json['1']).to.satisfy(v => v === null || v === undefined); + expect(json['1']).to.satisfy((v) => v === null || v === undefined); expect(json['2']).to.equal(null); expect(json['3']).to.deep.equal({ epoch: 3 }); }); @@ -348,7 +348,7 @@ describe('JS value to JSON conversion (testJsValueToJson)', () => { expect(json['1'].index).to.equal(1); // Note: In JSON, undefined becomes null (JSON doesn't have undefined) - expect(json['2']).to.satisfy(v => v === null || v === undefined); + expect(json['2']).to.satisfy((v) => v === null || v === undefined); }); }); @@ -389,7 +389,7 @@ describe('JS value to JSON conversion (testJsValueToJson)', () => { it('should handle large Maps', () => { const map = new Map(); - for (let i = 0; i < 1000; i++) { + for (let i = 0; i < 1000; i += 1) { map.set(i, { index: i, value: `item${i}` }); } diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs index 9367dad795b..37ea2a6e7c8 100644 --- a/packages/wasm-sdk/src/queries/protocol.rs +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -96,7 +96,10 @@ impl ProtocolVersionUpgradeVoteStatusWasm { } impl_wasm_serde_conversions!(ProtocolVersionUpgradeStateWasm, ProtocolVersionUpgradeState); -impl_wasm_serde_conversions!(ProtocolVersionUpgradeVoteStatusWasm, ProtocolVersionUpgradeVoteStatus); +impl_wasm_serde_conversions!( + ProtocolVersionUpgradeVoteStatusWasm, + ProtocolVersionUpgradeVoteStatus +); #[wasm_bindgen] impl WasmSdk { From bbdf09cdfcb394737880b9530424517643d55262 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 22:53:51 +0700 Subject: [PATCH 12/14] fix: silent error --- packages/wasm-dpp2/src/core/network.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/wasm-dpp2/src/core/network.rs b/packages/wasm-dpp2/src/core/network.rs index 08b5eda2fcc..34d0d9894e9 100644 --- a/packages/wasm-dpp2/src/core/network.rs +++ b/packages/wasm-dpp2/src/core/network.rs @@ -44,6 +44,14 @@ impl TryFrom for NetworkWasm { // Handle numeric enum value (Network.Mainnet = 0, Testnet = 1, etc.) if let Some(num) = value.as_f64() { + // Validate that the number is a non-negative integer within u32 range + if num.fract() != 0.0 || num < 0.0 || num > u32::MAX as f64 { + return Err(WasmDppError::invalid_argument(format!( + "network value must be a non-negative integer, got '{}'", + num + ))); + } + return match num as u32 { 0 => Ok(NetworkWasm::Mainnet), 1 => Ok(NetworkWasm::Testnet), From 42052fb8f44c9c9fbf06137664b17f82b17909cf Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 22:54:10 +0700 Subject: [PATCH 13/14] fallback to hex for PlatformAddress try from --- .../wasm-dpp2/src/platform_address/address.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index c6ad43fa92a..1195cc330b5 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -104,9 +104,20 @@ impl TryFrom<&str> for PlatformAddressWasm { type Error = WasmDppError; fn try_from(value: &str) -> Result { - // Try parsing as bech32m string - PlatformAddress::from_bech32m_string(value) - .map(|(addr, _network)| PlatformAddressWasm(addr)) + // Try parsing as bech32m string first (e.g., "dashevo1..." or "tdashevo1...") + if let Ok((addr, _network)) = PlatformAddress::from_bech32m_string(value) { + return Ok(PlatformAddressWasm(addr)); + } + + // Fall back to hex decoding for compatibility with serialized format + let bytes = hex::decode(value).map_err(|e| { + WasmDppError::invalid_argument(format!( + "Invalid PlatformAddress: not valid bech32m or hex: {}", + e + )) + })?; + PlatformAddress::from_bytes(&bytes) + .map(PlatformAddressWasm) .map_err(|e| WasmDppError::invalid_argument(e.to_string())) } } From 8d5e399d09efeb802acf99f1653b46b5708290cc Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 15 Jan 2026 23:01:01 +0700 Subject: [PATCH 14/14] refactor: remove nested if --- packages/wasm-dpp2/src/serialization/conversions.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wasm-dpp2/src/serialization/conversions.rs b/packages/wasm-dpp2/src/serialization/conversions.rs index 25149a4e333..a106b2f6cb7 100644 --- a/packages/wasm-dpp2/src/serialization/conversions.rs +++ b/packages/wasm-dpp2/src/serialization/conversions.rs @@ -193,10 +193,10 @@ fn map_key_to_string(key: &JsValue) -> String { && to_string_fn.is_function() { let func: js_sys::Function = to_string_fn.unchecked_into(); - if let Ok(str_result) = func.call0(key) { - if let Some(s) = str_result.as_string() { - return s; - } + if let Ok(str_result) = func.call0(key) + && let Some(s) = str_result.as_string() + { + return s; } }