From fd546befedf422f566520c7370e4d26854b5b516 Mon Sep 17 00:00:00 2001 From: quantum Date: Wed, 9 Jul 2025 19:58:58 -0500 Subject: [PATCH 01/30] dpns usernames in rust sdk --- packages/rs-sdk/src/platform.rs | 1 + .../rs-sdk/src/platform/dpns_usernames.rs | 351 ++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 packages/rs-sdk/src/platform/dpns_usernames.rs diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index 782c6b7d602..8482ea7b830 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -17,6 +17,7 @@ pub mod transition; pub mod types; pub mod documents; +pub mod dpns_usernames; pub mod group_actions; pub mod tokens; diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs new file mode 100644 index 00000000000..b516c1dc7b7 --- /dev/null +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -0,0 +1,351 @@ +use crate::platform::transition::put_document::PutDocument; +use crate::platform::{Document, FetchMany, Fetch}; +use crate::{Error, Sdk}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::document::{DocumentV0, DocumentV0Getters}; +use dpp::identity::signer::Signer; +use dpp::identity::{Identity, IdentityPublicKey}; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::platform_value::{Bytes32, Value}; +use dpp::prelude::Identifier; +use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; +use dpp::dashcore::secp256k1::rand::rngs::StdRng; +use std::collections::BTreeMap; + +/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' +/// with '0', '1', and '1' respectively to prevent homograph attacks +pub fn convert_to_homograph_safe_chars(input: &str) -> String { + input + .chars() + .map(|c| match c { + 'o' | 'O' => '0', + 'i' | 'I' => '1', + 'l' | 'L' => '1', + _ => c.to_ascii_lowercase(), + }) + .collect() +} + +/// Hash a buffer twice using SHA256 (double SHA256) +fn hash_double(data: Vec) -> [u8; 32] { + use dpp::dashcore::hashes::{sha256d, Hash}; + // sha256d already does double SHA256 + let hash = sha256d::Hash::hash(&data); + let mut result = [0u8; 32]; + result.copy_from_slice(hash.as_byte_array()); + result +} + +/// Input for registering a DPNS name +pub struct RegisterDpnsNameInput { + /// The label for the domain (e.g., "alice" for "alice.dash") + pub label: String, + /// The identity that will own the domain + pub identity: Identity, + /// The identity public key to use for signing + pub identity_public_key: IdentityPublicKey, + /// The signer for the identity + pub signer: S, +} + +/// Result of a DPNS name registration +#[derive(Debug)] +pub struct RegisterDpnsNameResult { + /// The preorder document that was created + pub preorder_document: Document, + /// The domain document that was created + pub domain_document: Document, + /// The full domain name (e.g., "alice.dash") + pub full_domain_name: String, +} + +impl Sdk { + /// Register a DPNS username in a single operation + /// + /// This method handles both the preorder and domain registration steps automatically. + /// It generates the necessary entropy, creates both documents, and submits them in order. + /// + /// # Arguments + /// + /// * `input` - The registration input containing label, identity, public key, and signer + /// + /// # Returns + /// + /// Returns a `RegisterDpnsNameResult` containing both created documents and the full domain name + /// + /// # Errors + /// + /// Returns an error if: + /// - The DPNS contract cannot be fetched + /// - Document types are not found in the contract + /// - Document creation or submission fails + pub async fn register_dpns_name( + &self, + input: RegisterDpnsNameInput, + ) -> Result { + // Fetch the DPNS contract + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + + // Get document types + let preorder_document_type = dpns_contract + .document_type_for_name("preorder") + .map_err(|_| Error::DapiClientError("DPNS preorder document type not found".to_string()))?; + + let domain_document_type = dpns_contract + .document_type_for_name("domain") + .map_err(|_| Error::DapiClientError("DPNS domain document type not found".to_string()))?; + + // Generate entropy and salt + let mut rng = StdRng::from_entropy(); + let entropy = Bytes32::random_with_rng(&mut rng); + let salt: [u8; 32] = rng.gen(); + + // Generate document IDs + let identity_id = input.identity.id().to_owned(); + let preorder_id = Document::generate_document_id_v0( + &dpns_contract.id(), + &identity_id, + preorder_document_type.name(), + entropy.as_slice(), + ); + let domain_id = Document::generate_document_id_v0( + &dpns_contract.id(), + &identity_id, + domain_document_type.name(), + entropy.as_slice(), + ); + + // Create salted domain hash for preorder + let normalized_label = convert_to_homograph_safe_chars(&input.label); + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(salt); + salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); + let salted_domain_hash = hash_double(salted_domain_buffer); + + // Create preorder document + let preorder_document = Document::V0(DocumentV0 { + id: preorder_id, + owner_id: identity_id, + properties: BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(salted_domain_hash), + )]), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + }); + + // Create domain document + let domain_document = Document::V0(DocumentV0 { + id: domain_id, + owner_id: identity_id, + properties: BTreeMap::from([ + ("parentDomainName".to_string(), Value::Text("dash".to_string())), + ("normalizedParentDomainName".to_string(), Value::Text("dash".to_string())), + ("label".to_string(), Value::Text(input.label.clone())), + ("normalizedLabel".to_string(), Value::Text(normalized_label.clone())), + ("preorderSalt".to_string(), Value::Bytes32(salt)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(identity_id.to_buffer()), + )]), + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ), + ]), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + }); + + // Submit preorder document first + preorder_document + .put_to_platform_and_wait_for_response( + self, + preorder_document_type.to_owned_document_type(), + Some(entropy.0), + input.identity_public_key.clone(), + None, // token payment info + &input.signer, + None, // settings + ) + .await?; + + // Submit domain document after preorder + domain_document + .put_to_platform_and_wait_for_response( + self, + domain_document_type.to_owned_document_type(), + Some(entropy.0), + input.identity_public_key, + None, // token payment info + &input.signer, + None, // settings + ) + .await?; + + Ok(RegisterDpnsNameResult { + preorder_document, + domain_document, + full_domain_name: format!("{}.dash", normalized_label), + }) + } + + /// Check if a DPNS name is available + /// + /// # Arguments + /// + /// * `label` - The username label to check (e.g., "alice") + /// + /// # Returns + /// + /// Returns `true` if the name is available, `false` if it's taken + pub async fn is_dpns_name_available(&self, label: &str) -> Result { + use crate::platform::documents::document_query::DocumentQuery; + use drive::query::WhereClause; + use drive::query::WhereOperator; + + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + + let normalized_label = convert_to_homograph_safe_chars(label); + + // Query for existing domain with this label + let query = DocumentQuery { + data_contract: dpns_contract.into(), + document_type_name: "domain".to_string(), + where_clauses: vec![ + WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("dash".to_string()), + }, + WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(normalized_label), + }, + ], + order_by_clauses: vec![], + limit: 1, + start: None, + }; + + let documents = Document::fetch_many(self, query).await?; + + // If no documents found, the name is available + Ok(documents.is_empty()) + } + + /// Resolve a DPNS name to an identity ID + /// + /// # Arguments + /// + /// * `name` - The full domain name (e.g., "alice.dash") or just the label (e.g., "alice") + /// + /// # Returns + /// + /// Returns the identity ID associated with the domain, or None if not found + pub async fn resolve_dpns_name(&self, name: &str) -> Result, Error> { + use crate::platform::documents::document_query::DocumentQuery; + use drive::query::WhereClause; + use drive::query::WhereOperator; + + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + + // Extract label from full name if needed + let label = name.trim_end_matches(".dash"); + let normalized_label = convert_to_homograph_safe_chars(label); + + // Query for domain with this label + let query = DocumentQuery { + data_contract: dpns_contract.into(), + document_type_name: "domain".to_string(), + where_clauses: vec![ + WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("dash".to_string()), + }, + WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(normalized_label), + }, + ], + order_by_clauses: vec![], + limit: 1, + start: None, + }; + + let documents = Document::fetch_many(self, query).await?; + + if let Some((_, Some(doc))) = documents.into_iter().next() { + // Extract the identity from records.identity + if let Some(Value::Map(records)) = doc.properties().get("records") { + for (key, value) in records { + if let (Value::Text(k), Value::Identifier(id_bytes)) = (key, value) { + if k == "identity" { + return Ok(Some(Identifier::from_bytes(id_bytes) + .map_err(|e| Error::DapiClientError(format!("Invalid identifier: {}", e)))?)); + } + } + } + } + } + + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_to_homograph_safe_chars() { + assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); + assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b"); + assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); + assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); + } +} \ No newline at end of file From ddca52a648f94c81c716e717bd670057de09412f Mon Sep 17 00:00:00 2001 From: quantum Date: Wed, 9 Jul 2025 21:28:40 -0500 Subject: [PATCH 02/30] dpns improvements --- packages/data-contracts/Cargo.toml | 31 ++- packages/data-contracts/src/error.rs | 10 + packages/data-contracts/src/lib.rs | 137 +++++++-- packages/rs-dpp/Cargo.toml | 15 +- packages/rs-dpp/src/system_data_contracts.rs | 47 +++- packages/rs-sdk/Cargo.toml | 13 +- .../rs-sdk/src/platform/dpns_usernames.rs | 259 +++++++++++++++--- packages/scripts/build-wasm.sh | 16 +- packages/wasm-sdk/Cargo.toml | 13 + packages/wasm-sdk/build.sh | 3 +- packages/wasm-sdk/index.html | 225 ++++++++++++++- packages/wasm-sdk/src/dpns.rs | 132 +++++++++ packages/wasm-sdk/src/lib.rs | 2 + 13 files changed, 821 insertions(+), 82 deletions(-) create mode 100644 packages/wasm-sdk/src/dpns.rs diff --git a/packages/data-contracts/Cargo.toml b/packages/data-contracts/Cargo.toml index a9c6350c267..42e71d68de5 100644 --- a/packages/data-contracts/Cargo.toml +++ b/packages/data-contracts/Cargo.toml @@ -6,16 +6,31 @@ edition = "2021" rust-version.workspace = true license = "MIT" +[features] +default = ["all-contracts"] +# Include all contracts +all-contracts = ["withdrawals", "masternode-rewards", "dpns", "dashpay", "feature-flags", "wallet-utils", "token-history", "keyword-search"] + +# Individual contract features +withdrawals = ["dep:withdrawals-contract"] +masternode-rewards = ["dep:masternode-reward-shares-contract"] +dpns = ["dep:dpns-contract"] +dashpay = ["dep:dashpay-contract"] +feature-flags = ["dep:feature-flags-contract"] +wallet-utils = ["dep:wallet-utils-contract"] +token-history = ["dep:token-history-contract"] +keyword-search = ["dep:keyword-search-contract"] + [dependencies] thiserror = "2.0.12" platform-version = { path = "../rs-platform-version" } serde_json = { version = "1.0" } -withdrawals-contract = { path = "../withdrawals-contract" } -masternode-reward-shares-contract = { path = "../masternode-reward-shares-contract" } -dpns-contract = { path = "../dpns-contract" } -dashpay-contract = { path = "../dashpay-contract" } -feature-flags-contract = { path = "../feature-flags-contract" } +withdrawals-contract = { path = "../withdrawals-contract", optional = true } +masternode-reward-shares-contract = { path = "../masternode-reward-shares-contract", optional = true } +dpns-contract = { path = "../dpns-contract", optional = true } +dashpay-contract = { path = "../dashpay-contract", optional = true } +feature-flags-contract = { path = "../feature-flags-contract", optional = true } platform-value = { path = "../rs-platform-value" } -wallet-utils-contract = { path = "../wallet-utils-contract" } -token-history-contract = { path = "../token-history-contract" } -keyword-search-contract = { path = "../keyword-search-contract" } +wallet-utils-contract = { path = "../wallet-utils-contract", optional = true } +token-history-contract = { path = "../token-history-contract", optional = true } +keyword-search-contract = { path = "../keyword-search-contract", optional = true } diff --git a/packages/data-contracts/src/error.rs b/packages/data-contracts/src/error.rs index 9f82617bc18..08b13705275 100644 --- a/packages/data-contracts/src/error.rs +++ b/packages/data-contracts/src/error.rs @@ -14,8 +14,11 @@ pub enum Error { }, #[error("schema deserialize error: {0}")] InvalidSchemaJson(#[from] serde_json::Error), + #[error("contract '{0}' not included in build (enable feature '{0}')")] + ContractNotIncluded(&'static str), } +#[cfg(feature = "withdrawals")] impl From for Error { fn from(e: withdrawals_contract::Error) -> Self { match e { @@ -33,6 +36,7 @@ impl From for Error { } } +#[cfg(feature = "dashpay")] impl From for Error { fn from(e: dashpay_contract::Error) -> Self { match e { @@ -50,6 +54,7 @@ impl From for Error { } } +#[cfg(feature = "dpns")] impl From for Error { fn from(e: dpns_contract::Error) -> Self { match e { @@ -67,6 +72,7 @@ impl From for Error { } } +#[cfg(feature = "masternode-rewards")] impl From for Error { fn from(e: masternode_reward_shares_contract::Error) -> Self { match e { @@ -86,6 +92,7 @@ impl From for Error { } } +#[cfg(feature = "feature-flags")] impl From for Error { fn from(e: feature_flags_contract::Error) -> Self { match e { @@ -103,6 +110,7 @@ impl From for Error { } } +#[cfg(feature = "wallet-utils")] impl From for Error { fn from(e: wallet_utils_contract::Error) -> Self { match e { @@ -120,6 +128,7 @@ impl From for Error { } } +#[cfg(feature = "token-history")] impl From for Error { fn from(e: token_history_contract::Error) -> Self { match e { @@ -137,6 +146,7 @@ impl From for Error { } } +#[cfg(feature = "keyword-search")] impl From for Error { fn from(e: keyword_search_contract::Error) -> Self { match e { diff --git a/packages/data-contracts/src/lib.rs b/packages/data-contracts/src/lib.rs index bd7754cfef3..69ed1bb1644 100644 --- a/packages/data-contracts/src/lib.rs +++ b/packages/data-contracts/src/lib.rs @@ -3,15 +3,32 @@ mod error; use serde_json::Value; use crate::error::Error; + +#[cfg(feature = "dashpay")] pub use dashpay_contract; + +#[cfg(feature = "dpns")] pub use dpns_contract; + +#[cfg(feature = "feature-flags")] pub use feature_flags_contract; + +#[cfg(feature = "keyword-search")] pub use keyword_search_contract; + +#[cfg(feature = "masternode-rewards")] pub use masternode_reward_shares_contract; + use platform_value::Identifier; use platform_version::version::PlatformVersion; + +#[cfg(feature = "token-history")] pub use token_history_contract; + +#[cfg(feature = "wallet-utils")] pub use wallet_utils_contract; + +#[cfg(feature = "withdrawals")] pub use withdrawals_contract; #[repr(u8)] @@ -36,76 +53,150 @@ pub struct DataContractSource { } impl SystemDataContract { - pub fn id(&self) -> Identifier { + pub fn id(&self) -> Result { let bytes = match self { + #[cfg(feature = "withdrawals")] SystemDataContract::Withdrawals => withdrawals_contract::ID_BYTES, + #[cfg(not(feature = "withdrawals"))] + SystemDataContract::Withdrawals => { + return Err(Error::ContractNotIncluded("withdrawals")) + } + + #[cfg(feature = "masternode-rewards")] SystemDataContract::MasternodeRewards => masternode_reward_shares_contract::ID_BYTES, + #[cfg(not(feature = "masternode-rewards"))] + SystemDataContract::MasternodeRewards => { + return Err(Error::ContractNotIncluded("masternode-rewards")) + } + + #[cfg(feature = "feature-flags")] SystemDataContract::FeatureFlags => feature_flags_contract::ID_BYTES, + #[cfg(not(feature = "feature-flags"))] + SystemDataContract::FeatureFlags => { + return Err(Error::ContractNotIncluded("feature-flags")) + } + + #[cfg(feature = "dpns")] SystemDataContract::DPNS => dpns_contract::ID_BYTES, + #[cfg(not(feature = "dpns"))] + SystemDataContract::DPNS => return Err(Error::ContractNotIncluded("dpns")), + + #[cfg(feature = "dashpay")] SystemDataContract::Dashpay => dashpay_contract::ID_BYTES, + #[cfg(not(feature = "dashpay"))] + SystemDataContract::Dashpay => return Err(Error::ContractNotIncluded("dashpay")), + + #[cfg(feature = "wallet-utils")] SystemDataContract::WalletUtils => wallet_utils_contract::ID_BYTES, + #[cfg(not(feature = "wallet-utils"))] + SystemDataContract::WalletUtils => { + return Err(Error::ContractNotIncluded("wallet-utils")) + } + + #[cfg(feature = "token-history")] SystemDataContract::TokenHistory => token_history_contract::ID_BYTES, + #[cfg(not(feature = "token-history"))] + SystemDataContract::TokenHistory => { + return Err(Error::ContractNotIncluded("token-history")) + } + + #[cfg(feature = "keyword-search")] SystemDataContract::KeywordSearch => keyword_search_contract::ID_BYTES, + #[cfg(not(feature = "keyword-search"))] + SystemDataContract::KeywordSearch => { + return Err(Error::ContractNotIncluded("keyword-search")) + } }; - Identifier::new(bytes) + Ok(Identifier::new(bytes)) } /// Returns [DataContractSource] pub fn source(self, platform_version: &PlatformVersion) -> Result { - let data = match self { - SystemDataContract::Withdrawals => DataContractSource { + match self { + #[cfg(feature = "withdrawals")] + SystemDataContract::Withdrawals => Ok(DataContractSource { id_bytes: withdrawals_contract::ID_BYTES, owner_id_bytes: withdrawals_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.withdrawals as u32, definitions: withdrawals_contract::load_definitions(platform_version)?, document_schemas: withdrawals_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::MasternodeRewards => DataContractSource { + }), + #[cfg(not(feature = "withdrawals"))] + SystemDataContract::Withdrawals => Err(Error::ContractNotIncluded("withdrawals")), + + #[cfg(feature = "masternode-rewards")] + SystemDataContract::MasternodeRewards => Ok(DataContractSource { id_bytes: masternode_reward_shares_contract::ID_BYTES, owner_id_bytes: masternode_reward_shares_contract::OWNER_ID_BYTES, version: platform_version .system_data_contracts .masternode_reward_shares as u32, - definitions: withdrawals_contract::load_definitions(platform_version)?, + definitions: masternode_reward_shares_contract::load_definitions(platform_version)?, document_schemas: masternode_reward_shares_contract::load_documents_schemas( platform_version, )?, - }, - SystemDataContract::FeatureFlags => DataContractSource { + }), + #[cfg(not(feature = "masternode-rewards"))] + SystemDataContract::MasternodeRewards => { + Err(Error::ContractNotIncluded("masternode-rewards")) + } + + #[cfg(feature = "feature-flags")] + SystemDataContract::FeatureFlags => Ok(DataContractSource { id_bytes: feature_flags_contract::ID_BYTES, owner_id_bytes: feature_flags_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.feature_flags as u32, definitions: feature_flags_contract::load_definitions(platform_version)?, document_schemas: feature_flags_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::DPNS => DataContractSource { + }), + #[cfg(not(feature = "feature-flags"))] + SystemDataContract::FeatureFlags => Err(Error::ContractNotIncluded("feature-flags")), + + #[cfg(feature = "dpns")] + SystemDataContract::DPNS => Ok(DataContractSource { id_bytes: dpns_contract::ID_BYTES, owner_id_bytes: dpns_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.dpns as u32, definitions: dpns_contract::load_definitions(platform_version)?, document_schemas: dpns_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::Dashpay => DataContractSource { + }), + #[cfg(not(feature = "dpns"))] + SystemDataContract::DPNS => Err(Error::ContractNotIncluded("dpns")), + + #[cfg(feature = "dashpay")] + SystemDataContract::Dashpay => Ok(DataContractSource { id_bytes: dashpay_contract::ID_BYTES, owner_id_bytes: dashpay_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.dashpay as u32, definitions: dashpay_contract::load_definitions(platform_version)?, document_schemas: dashpay_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::WalletUtils => DataContractSource { + }), + #[cfg(not(feature = "dashpay"))] + SystemDataContract::Dashpay => Err(Error::ContractNotIncluded("dashpay")), + + #[cfg(feature = "wallet-utils")] + SystemDataContract::WalletUtils => Ok(DataContractSource { id_bytes: wallet_utils_contract::ID_BYTES, owner_id_bytes: wallet_utils_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.wallet as u32, definitions: wallet_utils_contract::load_definitions(platform_version)?, document_schemas: wallet_utils_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::TokenHistory => DataContractSource { + }), + #[cfg(not(feature = "wallet-utils"))] + SystemDataContract::WalletUtils => Err(Error::ContractNotIncluded("wallet-utils")), + + #[cfg(feature = "token-history")] + SystemDataContract::TokenHistory => Ok(DataContractSource { id_bytes: token_history_contract::ID_BYTES, owner_id_bytes: token_history_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.token_history as u32, definitions: token_history_contract::load_definitions(platform_version)?, document_schemas: token_history_contract::load_documents_schemas(platform_version)?, - }, - SystemDataContract::KeywordSearch => DataContractSource { + }), + #[cfg(not(feature = "token-history"))] + SystemDataContract::TokenHistory => Err(Error::ContractNotIncluded("token-history")), + + #[cfg(feature = "keyword-search")] + SystemDataContract::KeywordSearch => Ok(DataContractSource { id_bytes: keyword_search_contract::ID_BYTES, owner_id_bytes: keyword_search_contract::OWNER_ID_BYTES, version: platform_version.system_data_contracts.keyword_search as u32, @@ -113,9 +204,9 @@ impl SystemDataContract { document_schemas: keyword_search_contract::load_documents_schemas( platform_version, )?, - }, - }; - - Ok(data) + }), + #[cfg(not(feature = "keyword-search"))] + SystemDataContract::KeywordSearch => Err(Error::ContractNotIncluded("keyword-search")), + } } } diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 321194a4640..c3025796ffc 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -49,7 +49,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] } serde_repr = { version = "0.1.7" } sha2 = { version = "0.10" } thiserror = { version = "2.0.12" } -data-contracts = { path = "../data-contracts", optional = true } +data-contracts = { path = "../data-contracts", optional = true, default-features = false } platform-value = { path = "../rs-platform-value" } platform-version = { path = "../rs-platform-version" } platform-versioning = { path = "../rs-platform-versioning" } @@ -286,7 +286,18 @@ core-types = ["bls-signatures"] core-types-serialization = ["core-types"] core-types-serde-conversion = ["core-types"] state-transitions = [] -system_contracts = ["factories", "data-contracts", "platform-value-json"] +# All system data contracts +system_contracts = ["factories", "data-contracts", "data-contracts/all-contracts", "platform-value-json"] + +# Individual data contract features +dpns-contract = ["data-contracts", "data-contracts/dpns", "platform-value-json"] +dashpay-contract = ["data-contracts", "data-contracts/dashpay", "platform-value-json"] +withdrawals-contract = ["data-contracts", "data-contracts/withdrawals", "platform-value-json"] +masternode-rewards-contract = ["data-contracts", "data-contracts/masternode-rewards", "platform-value-json"] +feature-flags-contract = ["data-contracts", "data-contracts/feature-flags", "platform-value-json"] +wallet-utils-contract = ["data-contracts", "data-contracts/wallet-utils", "platform-value-json"] +token-history-contract = ["data-contracts", "data-contracts/token-history", "platform-value-json"] +keywords-contract = ["data-contracts", "data-contracts/keyword-search", "platform-value-json"] fixtures-and-mocks = ["system_contracts", "platform-value/json"] random-public-keys = ["bls-signatures", "ed25519-dalek"] random-identities = ["random-public-keys"] diff --git a/packages/rs-dpp/src/system_data_contracts.rs b/packages/rs-dpp/src/system_data_contracts.rs index 92bb23364e4..64b4b8e0034 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -22,17 +22,50 @@ impl ConfigurationForSystemContract for SystemDataContract { platform_version: &PlatformVersion, ) -> Result { match self { - SystemDataContract::Withdrawals - | SystemDataContract::MasternodeRewards - | SystemDataContract::FeatureFlags - | SystemDataContract::DPNS - | SystemDataContract::Dashpay - | SystemDataContract::WalletUtils => { + #[cfg(any(feature = "withdrawals-contract", feature = "system_contracts"))] + SystemDataContract::Withdrawals => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - SystemDataContract::TokenHistory | SystemDataContract::KeywordSearch => { + #[cfg(any(feature = "masternode-rewards-contract", feature = "system_contracts"))] + SystemDataContract::MasternodeRewards => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + #[cfg(any(feature = "feature-flags-contract", feature = "system_contracts"))] + SystemDataContract::FeatureFlags => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + #[cfg(any(feature = "dpns-contract", feature = "system_contracts"))] + SystemDataContract::DPNS => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + #[cfg(any(feature = "dashpay-contract", feature = "system_contracts"))] + SystemDataContract::Dashpay => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + #[cfg(any(feature = "wallet-utils-contract", feature = "system_contracts"))] + SystemDataContract::WalletUtils => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + #[cfg(any(feature = "token-history-contract", feature = "system_contracts"))] + SystemDataContract::TokenHistory => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(true); + Ok(config) + } + #[cfg(any(feature = "keywords-contract", feature = "system_contracts"))] + SystemDataContract::KeywordSearch => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(true); Ok(config) diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 1e2a8a18d47..9b8343410b3 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -116,7 +116,18 @@ generate-test-vectors = ["network-testing"] # Have the system data contracts inside the dpp crate -system-data-contracts = ["dpp/data-contracts"] +# All system contracts (default behavior) +system-data-contracts = ["dpp/system_contracts"] + +# Individual contract features - these enable specific contracts in DPP +withdrawals-contract = ["dpp/withdrawals-contract"] +masternode-rewards-contract = ["dpp/masternode-rewards-contract"] +feature-flags-contract = ["dpp/feature-flags-contract"] +dpns-contract = ["dpp/dpns-contract"] +dashpay-contract = ["dpp/dashpay-contract"] +wallet-utils-contract = ["dpp/wallet-utils-contract"] +token-history-contract = ["dpp/token-history-contract"] +keywords-contract = ["dpp/keywords-contract"] token_reward_explanations = ["dpp/token-reward-explanations"] diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs index b516c1dc7b7..a1a8a32ad7c 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -1,19 +1,19 @@ use crate::platform::transition::put_document::PutDocument; -use crate::platform::{Document, FetchMany, Fetch}; +use crate::platform::{Document, Fetch, FetchMany}; use crate::{Error, Sdk}; +use dpp::dashcore::secp256k1::rand::rngs::StdRng; +use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::{DocumentV0, DocumentV0Getters}; +use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::signer::Signer; use dpp::identity::{Identity, IdentityPublicKey}; -use dpp::identity::accessors::IdentityGettersV0; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::Identifier; -use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; -use dpp::dashcore::secp256k1::rand::rngs::StdRng; use std::collections::BTreeMap; -/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' +/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' /// with '0', '1', and '1' respectively to prevent homograph attacks pub fn convert_to_homograph_safe_chars(input: &str) -> String { input @@ -27,6 +27,85 @@ pub fn convert_to_homograph_safe_chars(input: &str) -> String { .collect() } +/// Check if a username is valid according to DPNS rules +/// +/// A username is valid if: +/// - It's between 3 and 63 characters long +/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) +/// - It contains only alphanumeric characters and hyphens +/// - It doesn't have consecutive hyphens (enforced by the pattern) +/// +/// Pattern: ^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$ +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username is valid, `false` otherwise +pub fn is_valid_username(label: &str) -> bool { + // Check length + if label.len() < 3 || label.len() > 63 { + return false; + } + + let chars: Vec = label.chars().collect(); + + // Check first character (must be alphanumeric) + if !chars[0].is_ascii_alphanumeric() { + return false; + } + + // Check last character (must be alphanumeric) + if !chars[chars.len() - 1].is_ascii_alphanumeric() { + return false; + } + + // Check middle characters (can be alphanumeric or hyphen) + for i in 1..chars.len() - 1 { + if !chars[i].is_ascii_alphanumeric() && chars[i] != '-' { + return false; + } + } + + // Additional check: no consecutive hyphens (good practice) + for i in 0..chars.len() - 1 { + if chars[i] == '-' && chars[i + 1] == '-' { + return false; + } + } + + true +} + +/// Check if a username is contested (requires masternode voting) +/// +/// A username is contested if its normalized label: +/// - Is between 3 and 19 characters long (inclusive) +/// - Contains only lowercase letters a-z, digits 0-1, and hyphens +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username would be contested, `false` otherwise +pub fn is_contested_username(label: &str) -> bool { + let normalized = convert_to_homograph_safe_chars(label); + + // Check length + if normalized.len() < 3 || normalized.len() > 19 { + return false; + } + + // Check if all characters match the pattern [a-z01-] + normalized + .chars() + .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-')) +} + /// Hash a buffer twice using SHA256 (double SHA256) fn hash_double(data: Vec) -> [u8; 32] { use dpp::dashcore::hashes::{sha256d, Hash}; @@ -62,7 +141,7 @@ pub struct RegisterDpnsNameResult { impl Sdk { /// Register a DPNS username in a single operation - /// + /// /// This method handles both the preorder and domain registration steps automatically. /// It generates the necessary entropy, creates both documents, and submits them in order. /// @@ -86,21 +165,30 @@ impl Sdk { ) -> Result { // Fetch the DPNS contract const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; - + let dpns_contract_id = Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) .await? .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; // Get document types - let preorder_document_type = dpns_contract - .document_type_for_name("preorder") - .map_err(|_| Error::DapiClientError("DPNS preorder document type not found".to_string()))?; - - let domain_document_type = dpns_contract - .document_type_for_name("domain") - .map_err(|_| Error::DapiClientError("DPNS domain document type not found".to_string()))?; + let preorder_document_type = + dpns_contract + .document_type_for_name("preorder") + .map_err(|_| { + Error::DapiClientError("DPNS preorder document type not found".to_string()) + })?; + + let domain_document_type = + dpns_contract + .document_type_for_name("domain") + .map_err(|_| { + Error::DapiClientError("DPNS domain document type not found".to_string()) + })?; // Generate entropy and salt let mut rng = StdRng::from_entropy(); @@ -154,10 +242,19 @@ impl Sdk { id: domain_id, owner_id: identity_id, properties: BTreeMap::from([ - ("parentDomainName".to_string(), Value::Text("dash".to_string())), - ("normalizedParentDomainName".to_string(), Value::Text("dash".to_string())), + ( + "parentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()), + ), ("label".to_string(), Value::Text(input.label.clone())), - ("normalizedLabel".to_string(), Value::Text(normalized_label.clone())), + ( + "normalizedLabel".to_string(), + Value::Text(normalized_label.clone()), + ), ("preorderSalt".to_string(), Value::Bytes32(salt)), ( "records".to_string(), @@ -232,17 +329,20 @@ impl Sdk { use crate::platform::documents::document_query::DocumentQuery; use drive::query::WhereClause; use drive::query::WhereOperator; - + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; - + let dpns_contract_id = Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) .await? .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; let normalized_label = convert_to_homograph_safe_chars(label); - + // Query for existing domain with this label let query = DocumentQuery { data_contract: dpns_contract.into(), @@ -265,7 +365,7 @@ impl Sdk { }; let documents = Document::fetch_many(self, query).await?; - + // If no documents found, the name is available Ok(documents.is_empty()) } @@ -283,11 +383,14 @@ impl Sdk { use crate::platform::documents::document_query::DocumentQuery; use drive::query::WhereClause; use drive::query::WhereOperator; - + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string(DPNS_CONTRACT_ID, dpp::platform_value::string_encoding::Encoding::Base58) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; - + let dpns_contract_id = Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) .await? .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; @@ -295,7 +398,7 @@ impl Sdk { // Extract label from full name if needed let label = name.trim_end_matches(".dash"); let normalized_label = convert_to_homograph_safe_chars(label); - + // Query for domain with this label let query = DocumentQuery { data_contract: dpns_contract.into(), @@ -318,21 +421,22 @@ impl Sdk { }; let documents = Document::fetch_many(self, query).await?; - + if let Some((_, Some(doc))) = documents.into_iter().next() { // Extract the identity from records.identity if let Some(Value::Map(records)) = doc.properties().get("records") { for (key, value) in records { if let (Value::Text(k), Value::Identifier(id_bytes)) = (key, value) { if k == "identity" { - return Ok(Some(Identifier::from_bytes(id_bytes) - .map_err(|e| Error::DapiClientError(format!("Invalid identifier: {}", e)))?)); + return Ok(Some(Identifier::from_bytes(id_bytes).map_err(|e| { + Error::DapiClientError(format!("Invalid identifier: {}", e)) + })?)); } } } } } - + Ok(None) } } @@ -348,4 +452,89 @@ mod tests { assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); } -} \ No newline at end of file + + #[test] + fn test_is_valid_username() { + // Valid usernames + assert!(is_valid_username("abc")); + assert!(is_valid_username("alice")); + assert!(is_valid_username("Alice123")); + assert!(is_valid_username("dash-p2p")); + assert!(is_valid_username("test-name-123")); + assert!(is_valid_username("a-b-c")); + assert!(is_valid_username("user2024")); + assert!(is_valid_username("CryptoKing")); + assert!(is_valid_username("web3-developer")); + assert!(is_valid_username("a".repeat(63).as_str())); // Max length + + // Invalid - too short + assert!(!is_valid_username("ab")); + assert!(!is_valid_username("a")); + assert!(!is_valid_username("")); + + // Invalid - too long + assert!(!is_valid_username("a".repeat(64).as_str())); + + // Invalid - starts with hyphen + assert!(!is_valid_username("-alice")); + assert!(!is_valid_username("-test")); + + // Invalid - ends with hyphen + assert!(!is_valid_username("alice-")); + assert!(!is_valid_username("test-")); + + // Invalid - starts and ends with hyphen + assert!(!is_valid_username("-alice-")); + + // Invalid - contains invalid characters + assert!(!is_valid_username("alice_bob")); // underscore + assert!(!is_valid_username("alice.bob")); // dot + assert!(!is_valid_username("alice@dash")); // at sign + assert!(!is_valid_username("alice!")); // exclamation + assert!(!is_valid_username("alice bob")); // space + assert!(!is_valid_username("alice#1")); // hash + assert!(!is_valid_username("alice$")); // dollar + assert!(!is_valid_username("alice%20")); // percent + + // Invalid - consecutive hyphens + assert!(!is_valid_username("alice--bob")); + assert!(!is_valid_username("test---name")); + } + + #[test] + fn test_is_contested_username() { + // Contested usernames (3-19 chars, only [a-z01-]) + assert!(is_contested_username("abc")); + assert!(is_contested_username("alice")); // becomes "a11ce" + assert!(is_contested_username("b0b")); + assert!(is_contested_username("cool")); // becomes "c001" + assert!(is_contested_username("a-b-c")); + assert!(is_contested_username("hello")); // becomes "he110" + assert!(is_contested_username("world")); // becomes "w0r1d" + assert!(is_contested_username("dash")); + assert!(is_contested_username("a11ce")); // already normalized + assert!(is_contested_username("dash-dao")); // becomes "dash-da0" + + // Not contested - too short + assert!(!is_contested_username("ab")); + assert!(!is_contested_username("io")); // becomes "10" which is 2 chars + assert!(!is_contested_username("a")); + + // Not contested - too long (20+ chars) + assert!(!is_contested_username("twenty-characters-ab")); // 20 chars + assert!(!is_contested_username( + "this-is-a-very-long-username-that-exceeds-limit" + )); + + // Not contested - contains invalid characters after normalization + assert!(!is_contested_username("alice2")); // contains '2' + assert!(!is_contested_username("alice_bob")); // contains '_' + assert!(!is_contested_username("alice.bob")); // contains '.' + assert!(!is_contested_username("alice@dash")); // contains '@' + assert!(!is_contested_username("alice!")); // contains '!' + assert!(!is_contested_username("test123")); // contains '2' and '3' + assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2' + assert!(!is_contested_username("user5")); // contains '5' + assert!(!is_contested_username("name_with_underscore")); // contains '_' + } +} diff --git a/packages/scripts/build-wasm.sh b/packages/scripts/build-wasm.sh index 46934de5e98..8172df61ed9 100755 --- a/packages/scripts/build-wasm.sh +++ b/packages/scripts/build-wasm.sh @@ -107,12 +107,24 @@ if [ "$USE_WASM_PACK" = true ]; then export CARGO_PROFILE_RELEASE_LTO=false export RUSTFLAGS="-C lto=off" - wasm-pack build --target "$TARGET_TYPE" --release --no-opt + # Add features if specified + FEATURES_ARG="" + if [ -n "${CARGO_BUILD_FEATURES:-}" ]; then + FEATURES_ARG="--features $CARGO_BUILD_FEATURES" + fi + + wasm-pack build --target "$TARGET_TYPE" --release --no-opt $FEATURES_ARG else # Build using cargo directly echo "Building with cargo..." - cargo build --target wasm32-unknown-unknown --release \ + # Add features if specified + FEATURES_ARG="" + if [ -n "${CARGO_BUILD_FEATURES:-}" ]; then + FEATURES_ARG="--features $CARGO_BUILD_FEATURES" + fi + + cargo build --target wasm32-unknown-unknown --release $FEATURES_ARG \ --config 'profile.release.panic="abort"' \ --config 'profile.release.strip=true' \ --config 'profile.release.debug=false' \ diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index 7a7f5cd7b53..2c5db87eed2 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -9,7 +9,20 @@ crate-type = ["cdylib"] default = [] mocks = ["dash-sdk/mocks"] + +# All system contracts system-data-contracts = ["dash-sdk/system-data-contracts"] + +# Individual contract features +withdrawals-contract = ["dash-sdk/withdrawals-contract"] +masternode-rewards-contract = ["dash-sdk/masternode-rewards-contract"] +feature-flags-contract = ["dash-sdk/feature-flags-contract"] +dpns-contract = ["dash-sdk/dpns-contract"] +dashpay-contract = ["dash-sdk/dashpay-contract"] +wallet-utils-contract = ["dash-sdk/wallet-utils-contract"] +token-history-contract = ["dash-sdk/token-history-contract"] +keywords-contract = ["dash-sdk/keywords-contract"] + token_reward_explanations = ["dash-sdk/token_reward_explanations"] [dependencies] diff --git a/packages/wasm-sdk/build.sh b/packages/wasm-sdk/build.sh index 81af0bd508c..b4e1e47461a 100755 --- a/packages/wasm-sdk/build.sh +++ b/packages/wasm-sdk/build.sh @@ -16,5 +16,6 @@ if [ "${CARGO_BUILD_PROFILE:-}" = "dev" ] || [ "${CI:-}" != "true" ]; then OPT_LEVEL="minimal" fi -# Call unified build script +# Call unified build script with only the contracts we need +export CARGO_BUILD_FEATURES="dpns-contract,dashpay-contract,wallet-utils-contract,keywords-contract" exec "$SCRIPT_DIR/../scripts/build-wasm.sh" --package wasm-sdk --opt-level "$OPT_LEVEL" diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 2bacaa7cef4..7e5add93698 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -658,7 +658,7 @@

Authentication

- +
@@ -863,7 +863,14 @@

Results

get_group_info, get_group_infos, get_group_actions, - get_group_action_signers + get_group_action_signers, + // DPNS functions + dpns_convert_to_homograph_safe, + dpns_is_valid_username, + dpns_is_contested_username, + dpns_register_name, + dpns_is_name_available, + dpns_resolve_name } from './pkg/wasm_sdk.js'; // Import all placeholder query functions @@ -1018,6 +1025,13 @@

Results

{ name: "documentId", type: "text", label: "Document ID", required: true }, { name: "price", type: "number", label: "Price (credits, 0 to remove)", required: true } ] + }, + dpnsRegister: { + label: "DPNS Register Name", + description: "Register a new DPNS username", + inputs: [ + { name: "label", type: "text", label: "Username", required: true, placeholder: "Enter username (e.g., alice)", validateOnType: true } + ] } } }, @@ -1337,6 +1351,20 @@

Results

inputs: [ { name: "identityId", type: "text", label: "Identity ID", required: true } ] + }, + dpnsCheckAvailability: { + label: "DPNS Check Availability", + description: "Check if a DPNS username is available", + inputs: [ + { name: "label", type: "text", label: "Username to Check", required: true, placeholder: "Enter username (e.g., alice)" } + ] + }, + dpnsResolve: { + label: "DPNS Resolve Name", + description: "Resolve a DPNS name to an identity ID", + inputs: [ + { name: "name", type: "text", label: "DPNS Name", required: true, placeholder: "Enter name (e.g., alice or alice.dash)" } + ] } } }, @@ -2487,8 +2515,57 @@

Results

const input = document.createElement('input'); input.type = inputDef.type; input.name = inputDef.name; - input.placeholder = inputDef.label; + input.placeholder = inputDef.placeholder || inputDef.label; + if (inputDef.defaultValue !== undefined) { + input.value = inputDef.defaultValue; + } inputGroup.appendChild(input); + + // Add validation message container for DPNS username + if (inputDef.validateOnType) { + const validationMessage = document.createElement('div'); + validationMessage.className = 'validation-message'; + validationMessage.style.fontSize = '0.9em'; + validationMessage.style.marginTop = '5px'; + inputGroup.appendChild(validationMessage); + + // Add real-time validation + input.addEventListener('input', (e) => { + const value = e.target.value; + + if (!value) { + validationMessage.textContent = ''; + validationMessage.className = 'validation-message'; + return; + } + + // Check if it's a valid username + const isValid = dpns_is_valid_username(value); + + if (!isValid) { + validationMessage.textContent = '❌ Invalid username format. Must be 3-63 chars, start/end with letter/number, only letters/numbers/hyphens.'; + validationMessage.className = 'validation-message error'; + validationMessage.style.color = '#dc3545'; + } else { + // Check if it's contested + const isContested = dpns_is_contested_username(value); + const homographSafe = dpns_convert_to_homograph_safe(value); + const isHomographDifferent = value !== homographSafe; + + let message = '✅ Valid username'; + if (isContested) { + message += ' (contested - requires masternode voting)'; + } + if (isHomographDifferent) { + message += `. Will be stored as "${homographSafe}"`; + } + + validationMessage.textContent = message; + validationMessage.className = 'validation-message success'; + validationMessage.style.color = isContested ? '#ff9800' : '#28a745'; + } + }); + } } else if (inputDef.type === 'checkbox') { const checkboxContainer = document.createElement('div'); const checkbox = document.createElement('input'); @@ -2890,6 +2967,56 @@

Results

values.identityId ); // Result is the username string or null + } else if (queryType === 'dpnsCheckAvailability') { + // Handle DPNS availability check + const isValid = dpns_is_valid_username(values.label); + + if (!isValid) { + result = { + label: values.label, + valid: false, + message: "❌ Invalid username format", + requirements: [ + "Must be 3-63 characters long", + "Must start and end with a letter or number", + "Can only contain letters, numbers, and hyphens", + "Cannot have consecutive hyphens" + ] + }; + } else { + const isAvailable = await dpns_is_name_available( + sdk, + values.label + ); + + const homographSafe = dpns_convert_to_homograph_safe(values.label); + const isHomographDifferent = values.label !== homographSafe; + const isContested = dpns_is_contested_username(values.label); + + result = { + label: values.label, + valid: true, + homographSafeLabel: homographSafe, + available: isAvailable, + contested: isContested, + message: isAvailable ? `"${values.label}" is available!` : `"${values.label}" is already taken.`, + note: isHomographDifferent ? `Note: Your username will be stored as "${homographSafe}" to prevent homograph attacks` : undefined, + contestedNote: isContested && isAvailable ? + "⚠️ This is a contested username (3-19 chars, only a-z/0/1/-). It requires masternode voting to register." : undefined + }; + } + } else if (queryType === 'dpnsResolve') { + // Handle DPNS name resolution + const resolvedIdentityId = await dpns_resolve_name( + sdk, + values.name + ); + + result = { + name: values.name, + identityId: resolvedIdentityId, + message: resolvedIdentityId ? `Resolved to identity: ${resolvedIdentityId}` : `Name "${values.name}" not found` + }; } // Protocol/Version queries else if (queryType === 'getProtocolVersionUpgradeState') { @@ -3308,6 +3435,98 @@

Results

// Pass the result object directly to displayResult displayResult(result); updateStatus('Document price set successfully', 'success'); + } else if (transitionType === 'dpnsRegister') { + // Handle DPNS registration + // First validate the username + const isValid = dpns_is_valid_username(values.label); + + if (!isValid) { + displayResult(JSON.stringify({ + label: values.label, + valid: false, + message: "❌ Invalid username format", + requirements: [ + "Must be 3-63 characters long", + "Must start and end with a letter or number", + "Can only contain letters, numbers, and hyphens", + "Cannot have consecutive hyphens" + ] + }, null, 2)); + updateStatus('Invalid username format', 'error'); + return; + } + + // Check if name is available + const isAvailable = await dpns_is_name_available(sdk, values.label); + if (!isAvailable) { + displayResult(JSON.stringify({ + label: values.label, + available: false, + message: `❌ Username "${values.label}" is already taken` + }, null, 2)); + updateStatus('Username already taken', 'error'); + return; + } + + // Fetch identity to get the key ID + const identity = await identity_fetch(sdk, identityId); + const identityJSON = identity.toJSON(); + + console.log('Identity public keys:', identityJSON.publicKeys); + + // We need to derive the public key from the private key to find the matching key ID + // For now, we'll prompt the user to specify the key ID since we can't easily derive + // the public key from WIF in JavaScript without additional crypto libraries + + // Check if the private key has a key ID suffix (format: privateKey:keyId) + let keyId = null; + let actualPrivateKey = privateKey; + + if (privateKey.includes(':')) { + const parts = privateKey.split(':'); + actualPrivateKey = parts[0]; + keyId = parseInt(parts[1]); + + // Verify the key exists and has the right security level + const publicKey = identityJSON.publicKeys.find(key => key.id === keyId); + if (!publicKey) { + throw new Error(`Key ID ${keyId} not found in identity`); + } + if (publicKey.disabledAt) { + throw new Error(`Key ID ${keyId} is disabled`); + } + // Check security level - MASTER=0, CRITICAL=1, HIGH=2 + console.log(`Key ${keyId} has security level: ${publicKey.securityLevel}`); + if (publicKey.securityLevel === 0) { + throw new Error(`Key ID ${keyId} has MASTER security level. DPNS requires CRITICAL (1) or HIGH (2) security level.`); + } + } else { + // If no key ID provided, try to find the first suitable key + const suitableKey = identityJSON.publicKeys.find(key => + !key.disabledAt && key.securityLevel !== 0 // Not MASTER + ); + + if (suitableKey) { + keyId = suitableKey.id; + const levelName = suitableKey.securityLevel === 1 ? 'CRITICAL' : 'HIGH'; + console.log(`No key ID specified. Using key ${keyId} with ${levelName} security level. For explicit control, use format: privateKey:keyId`); + } else { + throw new Error('No suitable keys found. DPNS requires CRITICAL or HIGH security level. Specify key ID using format: privateKey:keyId'); + } + } + + console.log(`Using key ID ${keyId} for DPNS registration`); + + result = await dpns_register_name( + sdk, + values.label, + identityId, // Use the identity ID from authentication + keyId, // Use the determined key ID + actualPrivateKey // Use the actual private key (without :keyId suffix) + ); + + displayResult(JSON.stringify(result, null, 2)); + updateStatus('DPNS name registered successfully', 'success'); } else if (transitionType === 'documentPurchase') { // Handle document purchase result = await sdk.documentPurchase( diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs new file mode 100644 index 00000000000..6491616887f --- /dev/null +++ b/packages/wasm-sdk/src/dpns.rs @@ -0,0 +1,132 @@ +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use crate::sdk::WasmSdk; +use serde::{Serialize, Deserialize}; +use dash_sdk::platform::dpns_usernames::{convert_to_homograph_safe_chars, is_contested_username, is_valid_username, RegisterDpnsNameInput}; +use dash_sdk::platform::{Fetch, Identity}; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::prelude::Identifier; +use simple_signer::SingleKeySigner; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterDpnsNameResult { + pub preorder_document_id: String, + pub domain_document_id: String, + pub full_domain_name: String, +} + +/// Convert a string to homograph-safe characters +#[wasm_bindgen] +pub fn dpns_convert_to_homograph_safe(input: &str) -> String { + convert_to_homograph_safe_chars(input) +} + +/// Check if a username is valid according to DPNS rules +#[wasm_bindgen] +pub fn dpns_is_valid_username(label: &str) -> bool { + is_valid_username(label) +} + +/// Check if a username is contested (requires masternode voting) +#[wasm_bindgen] +pub fn dpns_is_contested_username(label: &str) -> bool { + is_contested_username(label) +} + +/// Register a DPNS username +#[wasm_bindgen] +pub async fn dpns_register_name( + sdk: &WasmSdk, + label: &str, + identity_id: &str, + public_key_id: u32, + private_key_wif: &str, +) -> Result { + // Parse identity ID + let identity_id_parsed = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).map_err(|e| JsError::new(&format!("Invalid identity ID: {}", e)))?; + + // Fetch the identity + let identity = Identity::fetch(sdk.as_ref(), identity_id_parsed) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsError::new("Identity not found"))?; + + // Create signer + let signer = SingleKeySigner::new(private_key_wif) + .map_err(|e| JsError::new(&format!("Invalid private key WIF: {}", e)))?; + + // Get the specific identity public key + let identity_public_key = identity + .get_public_key_by_id(public_key_id.into()) + .ok_or_else(|| JsError::new(&format!("Public key with ID {} not found", public_key_id)))? + .clone(); + + // Create registration input + let input = RegisterDpnsNameInput { + label: label.to_string(), + identity, + identity_public_key, + signer, + }; + + // Register the name + let result = sdk.as_ref() + .register_dpns_name(input) + .await + .map_err(|e| JsError::new(&format!("Failed to register DPNS name: {}", e)))?; + + // Convert result to JS-friendly format + let js_result = RegisterDpnsNameResult { + preorder_document_id: result.preorder_document.id().to_string( + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58 + ), + domain_document_id: result.domain_document.id().to_string( + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58 + ), + full_domain_name: result.full_domain_name, + }; + + // Serialize to JsValue + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + js_result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize result: {}", e))) +} + +/// Check if a DPNS name is available +#[wasm_bindgen] +pub async fn dpns_is_name_available( + sdk: &WasmSdk, + label: &str, +) -> Result { + sdk.as_ref() + .is_dpns_name_available(label) + .await + .map_err(|e| JsError::new(&format!("Failed to check name availability: {}", e))) +} + +/// Resolve a DPNS name to an identity ID +#[wasm_bindgen] +pub async fn dpns_resolve_name( + sdk: &WasmSdk, + name: &str, +) -> Result { + let result = sdk.as_ref() + .resolve_dpns_name(name) + .await + .map_err(|e| JsError::new(&format!("Failed to resolve DPNS name: {}", e)))?; + + match result { + Some(identity_id) => { + let id_string = identity_id.to_string( + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58 + ); + Ok(JsValue::from_str(&id_string)) + }, + None => Ok(JsValue::NULL), + } +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index ab24b755628..d1b3a186c37 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -2,6 +2,7 @@ use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; pub mod context_provider; pub mod dpp; +pub mod dpns; pub mod error; pub mod sdk; pub mod state_transitions; @@ -12,6 +13,7 @@ pub mod queries; pub use sdk::{WasmSdk, WasmSdkBuilder}; pub use queries::*; pub use state_transitions::*; +pub use dpns::*; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; From 72d819709bb924b7f54b97ab9350cdcc5c8e9719 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 10 Jul 2025 05:46:19 +0300 Subject: [PATCH 03/30] more dpns improvements --- Cargo.lock | 1 - packages/data-contracts/src/lib.rs | 56 +++++++++++-------- .../src/provider.rs | 2 +- packages/rs-sdk/Cargo.toml | 1 - packages/rs-sdk/tests/fetch/config.rs | 6 +- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a343a587501..80b50c387b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,7 +1308,6 @@ dependencies = [ "dapi-grpc-macros", "dash-context-provider", "dashcore-rpc", - "data-contracts", "derive_more 1.0.0", "dotenvy", "dpp", diff --git a/packages/data-contracts/src/lib.rs b/packages/data-contracts/src/lib.rs index 69ed1bb1644..d2e1f8b9cc2 100644 --- a/packages/data-contracts/src/lib.rs +++ b/packages/data-contracts/src/lib.rs @@ -53,61 +53,73 @@ pub struct DataContractSource { } impl SystemDataContract { - pub fn id(&self) -> Result { + pub fn id(&self) -> Identifier { let bytes = match self { #[cfg(feature = "withdrawals")] SystemDataContract::Withdrawals => withdrawals_contract::ID_BYTES, #[cfg(not(feature = "withdrawals"))] - SystemDataContract::Withdrawals => { - return Err(Error::ContractNotIncluded("withdrawals")) - } + SystemDataContract::Withdrawals => [ + 54, 98, 187, 97, 225, 127, 174, 62, 162, 148, 207, 96, 49, 151, 251, 10, 171, 109, + 81, 24, 11, 216, 182, 16, 76, 73, 68, 166, 47, 226, 217, 127, + ], #[cfg(feature = "masternode-rewards")] SystemDataContract::MasternodeRewards => masternode_reward_shares_contract::ID_BYTES, #[cfg(not(feature = "masternode-rewards"))] - SystemDataContract::MasternodeRewards => { - return Err(Error::ContractNotIncluded("masternode-rewards")) - } + SystemDataContract::MasternodeRewards => [ + 12, 172, 226, 5, 36, 102, 147, 167, 200, 21, 101, 35, 98, 13, 170, 147, 125, 47, + 34, 71, 147, 68, 99, 238, 176, 31, 247, 33, 149, 144, 149, 140, + ], #[cfg(feature = "feature-flags")] SystemDataContract::FeatureFlags => feature_flags_contract::ID_BYTES, #[cfg(not(feature = "feature-flags"))] - SystemDataContract::FeatureFlags => { - return Err(Error::ContractNotIncluded("feature-flags")) - } + SystemDataContract::FeatureFlags => [ + 245, 172, 216, 200, 193, 110, 185, 172, 40, 110, 7, 132, 190, 86, 127, 80, 9, 244, + 86, 26, 243, 212, 255, 2, 91, 7, 90, 243, 68, 55, 152, 34, + ], #[cfg(feature = "dpns")] SystemDataContract::DPNS => dpns_contract::ID_BYTES, #[cfg(not(feature = "dpns"))] - SystemDataContract::DPNS => return Err(Error::ContractNotIncluded("dpns")), + SystemDataContract::DPNS => [ + 230, 104, 198, 89, 175, 102, 174, 225, 231, 44, 24, 109, 222, 123, 91, 126, 10, 29, + 113, 42, 9, 196, 13, 87, 33, 246, 34, 191, 83, 197, 49, 85, + ], #[cfg(feature = "dashpay")] SystemDataContract::Dashpay => dashpay_contract::ID_BYTES, #[cfg(not(feature = "dashpay"))] - SystemDataContract::Dashpay => return Err(Error::ContractNotIncluded("dashpay")), + SystemDataContract::Dashpay => [ + 162, 161, 180, 172, 111, 239, 34, 234, 42, 26, 104, 232, 18, 54, 68, 179, 87, 135, + 95, 107, 65, 44, 24, 16, 146, 129, 193, 70, 231, 178, 113, 188, + ], #[cfg(feature = "wallet-utils")] SystemDataContract::WalletUtils => wallet_utils_contract::ID_BYTES, #[cfg(not(feature = "wallet-utils"))] - SystemDataContract::WalletUtils => { - return Err(Error::ContractNotIncluded("wallet-utils")) - } + SystemDataContract::WalletUtils => [ + 92, 20, 14, 101, 92, 2, 101, 187, 194, 168, 8, 113, 109, 225, 132, 121, 133, 19, + 89, 24, 173, 81, 205, 253, 11, 118, 102, 75, 169, 91, 163, 124, + ], #[cfg(feature = "token-history")] SystemDataContract::TokenHistory => token_history_contract::ID_BYTES, #[cfg(not(feature = "token-history"))] - SystemDataContract::TokenHistory => { - return Err(Error::ContractNotIncluded("token-history")) - } + SystemDataContract::TokenHistory => [ + 45, 67, 89, 21, 34, 216, 145, 78, 156, 243, 17, 58, 202, 190, 13, 92, 61, 40, 122, + 201, 84, 99, 187, 110, 233, 128, 63, 48, 172, 29, 210, 108, + ], #[cfg(feature = "keyword-search")] SystemDataContract::KeywordSearch => keyword_search_contract::ID_BYTES, #[cfg(not(feature = "keyword-search"))] - SystemDataContract::KeywordSearch => { - return Err(Error::ContractNotIncluded("keyword-search")) - } + SystemDataContract::KeywordSearch => [ + 92, 20, 14, 101, 92, 2, 101, 187, 194, 168, 8, 113, 109, 225, 132, 121, 133, 19, + 89, 24, 173, 81, 205, 253, 11, 118, 102, 75, 169, 91, 163, 124, + ], }; - Ok(Identifier::new(bytes)) + Identifier::new(bytes) } /// Returns [DataContractSource] pub fn source(self, platform_version: &PlatformVersion) -> Result { diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index b5caddb2516..4bf3d8d3e7e 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -139,7 +139,7 @@ impl TrustedHttpContextProvider { } /// Set known contracts that will be served immediately without fallback - pub fn with_known_contracts(mut self, contracts: Vec) -> Self { + pub fn with_known_contracts(self, contracts: Vec) -> Self { let mut known = self.known_contracts.lock().unwrap(); for contract in contracts { let id = contract.id(); diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 9b8343410b3..b50f51e146a 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -61,7 +61,6 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ "validation", "random-documents", ] } -data-contracts = { path = "../data-contracts" } tokio-test = { version = "0.4.4" } clap = { version = "4.5.4", features = ["derive"] } sanitize-filename = { version = "0.6.0" } diff --git a/packages/rs-sdk/tests/fetch/config.rs b/packages/rs-sdk/tests/fetch/config.rs index 5d8d7f73f01..a7d07cd4962 100644 --- a/packages/rs-sdk/tests/fetch/config.rs +++ b/packages/rs-sdk/tests/fetch/config.rs @@ -232,7 +232,11 @@ impl Config { } fn default_data_contract_id() -> Identifier { - data_contracts::dpns_contract::ID_BYTES.into() + [ + 230, 104, 198, 89, 175, 102, 174, 225, 231, 44, 24, 109, 222, 123, 91, 126, 10, 29, + 113, 42, 9, 196, 13, 87, 33, 246, 34, 191, 83, 197, 49, 85, + ] + .into() } fn default_document_type_name() -> String { From 84fb154fc5163334b34c8a1fdc81bdbe63663f6d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 10 Jul 2025 05:52:46 +0300 Subject: [PATCH 04/30] more dpns improvements --- packages/rs-dpp/Cargo.toml | 16 ---------------- packages/rs-drive/Cargo.toml | 1 - packages/rs-sdk/Cargo.toml | 2 -- packages/wasm-sdk/Cargo.toml | 2 -- 4 files changed, 21 deletions(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index c3025796ffc..701d94fad0a 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -126,27 +126,11 @@ all_features = [ ] dash-sdk-features = [ - # "json-object", - # "platform-value", - # "system_contracts", - # "validation", # TODO: This one is big "identity-hashing", "data-contract-json-conversion", - # "identity-serialization", - # "vote-serialization", - # "document-value-conversion", - # "data-contract-value-conversion", "identity-value-conversion", - # "core-types", - # "core-types-serialization", - # "core-types-serde-conversion", - # "state-transition-serde-conversion", "state-transition-value-conversion", - # "state-transition-json-conversion", - # "state-transition-validation", "state-transition-signing", - # "state-transitions", - # "fee-distribution", "client", "platform-value-cbor", ] diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index d03d583efff..017efcc47b9 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -122,5 +122,4 @@ verify = [ "grovedb/verify", "grovedb-costs", "dpp/state-transitions", - "dpp/system_contracts", ] diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index b50f51e146a..0b57b9757d5 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -120,8 +120,6 @@ system-data-contracts = ["dpp/system_contracts"] # Individual contract features - these enable specific contracts in DPP withdrawals-contract = ["dpp/withdrawals-contract"] -masternode-rewards-contract = ["dpp/masternode-rewards-contract"] -feature-flags-contract = ["dpp/feature-flags-contract"] dpns-contract = ["dpp/dpns-contract"] dashpay-contract = ["dpp/dashpay-contract"] wallet-utils-contract = ["dpp/wallet-utils-contract"] diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index 2c5db87eed2..fc430dc14a8 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -15,8 +15,6 @@ system-data-contracts = ["dash-sdk/system-data-contracts"] # Individual contract features withdrawals-contract = ["dash-sdk/withdrawals-contract"] -masternode-rewards-contract = ["dash-sdk/masternode-rewards-contract"] -feature-flags-contract = ["dash-sdk/feature-flags-contract"] dpns-contract = ["dash-sdk/dpns-contract"] dashpay-contract = ["dash-sdk/dashpay-contract"] wallet-utils-contract = ["dash-sdk/wallet-utils-contract"] From 0964da36f3e0acae1d5c2227944cece024185762 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 10 Jul 2025 06:19:29 +0300 Subject: [PATCH 05/30] more dpns improvements --- packages/rs-dpp/Cargo.toml | 23 ++++++++++---------- packages/rs-dpp/src/lib.rs | 4 ++-- packages/rs-dpp/src/system_data_contracts.rs | 8 ------- packages/rs-dpp/src/withdrawal/mod.rs | 2 +- packages/rs-drive/Cargo.toml | 3 ++- packages/rs-sdk/Cargo.toml | 2 +- packages/wasm-dpp/Cargo.toml | 2 +- packages/wasm-drive-verify/Cargo.toml | 4 ++-- packages/wasm-sdk/Cargo.toml | 2 +- 9 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 701d94fad0a..ec7cbf0da2a 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -82,7 +82,7 @@ bls-signatures = ["dashcore/bls"] ed25519-dalek = ["dashcore/eddsa"] all_features = [ "json-object", - "system_contracts", + "all-system_contracts", "state-transitions", "extended-document", "bls-signatures", @@ -137,7 +137,7 @@ dash-sdk-features = [ all_features_without_client = [ "json-object", "platform-value", - "system_contracts", + "all-system_contracts", "state-transitions", "extended-document", "cbor", @@ -271,18 +271,17 @@ core-types-serialization = ["core-types"] core-types-serde-conversion = ["core-types"] state-transitions = [] # All system data contracts -system_contracts = ["factories", "data-contracts", "data-contracts/all-contracts", "platform-value-json"] +all-system_contracts = ["data-contracts", "data-contracts/all-contracts", "dpns-contract", "dashpay-contract", "withdrawals-contract", "masternode-rewards-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] # Individual data contract features -dpns-contract = ["data-contracts", "data-contracts/dpns", "platform-value-json"] -dashpay-contract = ["data-contracts", "data-contracts/dashpay", "platform-value-json"] -withdrawals-contract = ["data-contracts", "data-contracts/withdrawals", "platform-value-json"] -masternode-rewards-contract = ["data-contracts", "data-contracts/masternode-rewards", "platform-value-json"] -feature-flags-contract = ["data-contracts", "data-contracts/feature-flags", "platform-value-json"] -wallet-utils-contract = ["data-contracts", "data-contracts/wallet-utils", "platform-value-json"] -token-history-contract = ["data-contracts", "data-contracts/token-history", "platform-value-json"] -keywords-contract = ["data-contracts", "data-contracts/keyword-search", "platform-value-json"] -fixtures-and-mocks = ["system_contracts", "platform-value/json"] +dpns-contract = ["data-contracts", "data-contracts/dpns"] +dashpay-contract = ["data-contracts", "data-contracts/dashpay"] +withdrawals-contract = ["data-contracts", "data-contracts/withdrawals"] +masternode-rewards-contract = ["data-contracts", "data-contracts/masternode-rewards"] +wallet-utils-contract = ["data-contracts", "data-contracts/wallet-utils"] +token-history-contract = ["data-contracts", "data-contracts/token-history"] +keywords-contract = ["data-contracts", "data-contracts/keyword-search"] +fixtures-and-mocks = ["all-system_contracts", "platform-value/json"] random-public-keys = ["bls-signatures", "ed25519-dalek"] random-identities = ["random-public-keys"] random-documents = [] diff --git a/packages/rs-dpp/src/lib.rs b/packages/rs-dpp/src/lib.rs index 66599277405..6e4ae0467b0 100644 --- a/packages/rs-dpp/src/lib.rs +++ b/packages/rs-dpp/src/lib.rs @@ -48,7 +48,7 @@ pub mod serialization; feature = "message-signature-verification" ))] pub mod signing; -#[cfg(feature = "system_contracts")] +#[cfg(feature = "data-contracts")] pub mod system_data_contracts; pub mod tokens; @@ -116,7 +116,7 @@ pub use bincode; pub use dashcore::blsful as bls_signatures; #[cfg(feature = "ed25519-dalek")] pub use dashcore::ed25519_dalek; -#[cfg(feature = "system_contracts")] +#[cfg(feature = "data-contracts")] pub use data_contracts; #[cfg(feature = "jsonschema")] pub use jsonschema; diff --git a/packages/rs-dpp/src/system_data_contracts.rs b/packages/rs-dpp/src/system_data_contracts.rs index 64b4b8e0034..13ed6cd214a 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -22,49 +22,41 @@ impl ConfigurationForSystemContract for SystemDataContract { platform_version: &PlatformVersion, ) -> Result { match self { - #[cfg(any(feature = "withdrawals-contract", feature = "system_contracts"))] SystemDataContract::Withdrawals => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "masternode-rewards-contract", feature = "system_contracts"))] SystemDataContract::MasternodeRewards => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "feature-flags-contract", feature = "system_contracts"))] SystemDataContract::FeatureFlags => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "dpns-contract", feature = "system_contracts"))] SystemDataContract::DPNS => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "dashpay-contract", feature = "system_contracts"))] SystemDataContract::Dashpay => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "wallet-utils-contract", feature = "system_contracts"))] SystemDataContract::WalletUtils => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - #[cfg(any(feature = "token-history-contract", feature = "system_contracts"))] SystemDataContract::TokenHistory => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(true); Ok(config) } - #[cfg(any(feature = "keywords-contract", feature = "system_contracts"))] SystemDataContract::KeywordSearch => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(true); diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index f2031e68e0e..570d13b40fb 100644 --- a/packages/rs-dpp/src/withdrawal/mod.rs +++ b/packages/rs-dpp/src/withdrawal/mod.rs @@ -1,5 +1,5 @@ pub mod daily_withdrawal_limit; -#[cfg(feature = "system_contracts")] +#[cfg(feature = "withdrawals-contract")] mod document_try_into_asset_unlock_base_transaction_info; use bincode::{Decode, Encode}; diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 017efcc47b9..01380e46f18 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -72,7 +72,7 @@ dpp = { path = "../rs-dpp", features = [ "random-identities", "random-public-keys", "fixtures-and-mocks", - "system_contracts", + "all-system_contracts", "factories", "data-contract-json-conversion", ], default-features = false } @@ -122,4 +122,5 @@ verify = [ "grovedb/verify", "grovedb-costs", "dpp/state-transitions", + "dpp/data-contracts" ] diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 0b57b9757d5..8f6c7b2e008 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -116,7 +116,7 @@ generate-test-vectors = ["network-testing"] # Have the system data contracts inside the dpp crate # All system contracts (default behavior) -system-data-contracts = ["dpp/system_contracts"] +all-system-contracts = ["dpp/all-system_contracts"] # Individual contract features - these enable specific contracts in DPP withdrawals-contract = ["dpp/withdrawals-contract"] diff --git a/packages/wasm-dpp/Cargo.toml b/packages/wasm-dpp/Cargo.toml index b281e6fb477..e7510eb10a8 100644 --- a/packages/wasm-dpp/Cargo.toml +++ b/packages/wasm-dpp/Cargo.toml @@ -43,7 +43,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ "extended-document", "document-value-conversion", "document-json-conversion", - "system_contracts", + "data-contracts", ] } itertools = { version = "0.13" } log = { version = "0.4.6" } diff --git a/packages/wasm-drive-verify/Cargo.toml b/packages/wasm-drive-verify/Cargo.toml index d43ea145f43..e4943b35b48 100644 --- a/packages/wasm-drive-verify/Cargo.toml +++ b/packages/wasm-drive-verify/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib", "rlib"] drive = { path = "../rs-drive", default-features = false, features = ["verify"] } dpp = { path = "../rs-dpp", default-features = false, features = [ "state-transitions", - "system_contracts", + "data-contracts", "data-contract-serde-conversion", "data-contract-json-conversion", "identity-serde-conversion", @@ -46,7 +46,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ "random-public-keys", "random-identities", "random-documents", - "system_contracts", + "all-system_contracts", "data-contract-serde-conversion", "data-contract-json-conversion", "identity-serde-conversion", diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index fc430dc14a8..3e1ac04193a 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -11,7 +11,7 @@ default = [] mocks = ["dash-sdk/mocks"] # All system contracts -system-data-contracts = ["dash-sdk/system-data-contracts"] +all-system-contracts = ["dash-sdk/all-system-contracts"] # Individual contract features withdrawals-contract = ["dash-sdk/withdrawals-contract"] From bcacb007546de9b4a71deddb4cb371d5584a4eb4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 10 Jul 2025 06:25:03 +0300 Subject: [PATCH 06/30] more dpns improvements --- packages/rs-drive/Cargo.toml | 2 +- packages/wasm-sdk/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 01380e46f18..b6db06a002b 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -103,7 +103,7 @@ server = [ "moka", "dpp/validation", "dpp/platform-value-json", - "dpp/system_contracts", + "dpp/all-system_contracts", "dpp/state-transitions", "fee-distribution", "grovedb/minimal", diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index 3e1ac04193a..2a2b36810e6 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -6,7 +6,7 @@ publish = false crate-type = ["cdylib"] [features] -default = [] +default = ["dpns-contract", "dashpay-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] mocks = ["dash-sdk/mocks"] From 9b0adf5552035c410c4341763c46cd266a47101e Mon Sep 17 00:00:00 2001 From: quantum Date: Wed, 9 Jul 2025 23:32:16 -0500 Subject: [PATCH 07/30] dpns improvements --- .../Cargo.toml | 12 ++++ .../src/provider.rs | 67 ++++++++++++++++++- packages/wasm-sdk/Cargo.toml | 14 ++-- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/rs-sdk-trusted-context-provider/Cargo.toml b/packages/rs-sdk-trusted-context-provider/Cargo.toml index 18e83d229a1..3d1a1281abc 100644 --- a/packages/rs-sdk-trusted-context-provider/Cargo.toml +++ b/packages/rs-sdk-trusted-context-provider/Cargo.toml @@ -22,6 +22,18 @@ dashcore = { git = "https://github.com/dashpay/rust-dashcore", features = ["bls- futures = "0.3" url = "2.5" +[features] +# All system contracts (default behavior) +all-system-contracts = ["dpp/all-system_contracts"] + +# Individual contract features - these enable specific contracts in DPP +withdrawals-contract = ["dpp/withdrawals-contract"] +dpns-contract = ["dpp/dpns-contract"] +dashpay-contract = ["dpp/dashpay-contract"] +wallet-utils-contract = ["dpp/wallet-utils-contract"] +token-history-contract = ["dpp/token-history-contract"] +keywords-contract = ["dpp/keywords-contract"] + [dev-dependencies] tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] } tokio-test = "0.4.4" \ No newline at end of file diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index 4bf3d8d3e7e..b029666c008 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -11,6 +11,16 @@ type QuorumHash = [u8; 32]; use dpp::dashcore::Network; use dpp::data_contract::TokenConfiguration; use dpp::version::PlatformVersion; +#[cfg(any( + feature = "dpns-contract", + feature = "dashpay-contract", + feature = "withdrawals-contract", + feature = "wallet-utils-contract", + feature = "token-history-contract", + feature = "keywords-contract", + feature = "all-system-contracts" +))] +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use lru::LruCache; use reqwest::Client; @@ -486,7 +496,62 @@ impl ContextProvider for TrustedHttpContextProvider { } drop(known); - // If not found in known contracts, delegate to fallback provider if available + // Check if this is a system data contract and the corresponding feature is enabled + #[cfg(any( + feature = "dpns-contract", + feature = "dashpay-contract", + feature = "withdrawals-contract", + feature = "wallet-utils-contract", + feature = "token-history-contract", + feature = "keywords-contract", + feature = "all-system-contracts" + ))] + { + // Check each system contract if its feature is enabled + #[cfg(any(feature = "dpns-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::DPNS.id() { + return load_system_data_contract(SystemDataContract::DPNS, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load DPNS contract: {}", e))); + } + + #[cfg(any(feature = "dashpay-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::Dashpay.id() { + return load_system_data_contract(SystemDataContract::Dashpay, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load Dashpay contract: {}", e))); + } + + #[cfg(any(feature = "withdrawals-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::Withdrawals.id() { + return load_system_data_contract(SystemDataContract::Withdrawals, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load Withdrawals contract: {}", e))); + } + + #[cfg(any(feature = "wallet-utils-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::WalletUtils.id() { + return load_system_data_contract(SystemDataContract::WalletUtils, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load WalletUtils contract: {}", e))); + } + + #[cfg(any(feature = "token-history-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::TokenHistory.id() { + return load_system_data_contract(SystemDataContract::TokenHistory, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load TokenHistory contract: {}", e))); + } + + #[cfg(any(feature = "keywords-contract", feature = "all-system-contracts"))] + if *id == SystemDataContract::KeywordSearch.id() { + return load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| ContextProviderError::Generic(format!("Failed to load KeywordSearch contract: {}", e))); + } + } + + // If not found in known contracts or system contracts, delegate to fallback provider if available if let Some(ref provider) = self.fallback_provider { provider.get_data_contract(id, platform_version) } else { diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index 2a2b36810e6..eb829cddbb1 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -11,15 +11,15 @@ default = ["dpns-contract", "dashpay-contract", "wallet-utils-contract", "token- mocks = ["dash-sdk/mocks"] # All system contracts -all-system-contracts = ["dash-sdk/all-system-contracts"] +all-system-contracts = ["dash-sdk/all-system-contracts", "rs-sdk-trusted-context-provider/all-system-contracts"] # Individual contract features -withdrawals-contract = ["dash-sdk/withdrawals-contract"] -dpns-contract = ["dash-sdk/dpns-contract"] -dashpay-contract = ["dash-sdk/dashpay-contract"] -wallet-utils-contract = ["dash-sdk/wallet-utils-contract"] -token-history-contract = ["dash-sdk/token-history-contract"] -keywords-contract = ["dash-sdk/keywords-contract"] +withdrawals-contract = ["dash-sdk/withdrawals-contract", "rs-sdk-trusted-context-provider/withdrawals-contract"] +dpns-contract = ["dash-sdk/dpns-contract", "rs-sdk-trusted-context-provider/dpns-contract"] +dashpay-contract = ["dash-sdk/dashpay-contract", "rs-sdk-trusted-context-provider/dashpay-contract"] +wallet-utils-contract = ["dash-sdk/wallet-utils-contract", "rs-sdk-trusted-context-provider/wallet-utils-contract"] +token-history-contract = ["dash-sdk/token-history-contract", "rs-sdk-trusted-context-provider/token-history-contract"] +keywords-contract = ["dash-sdk/keywords-contract", "rs-sdk-trusted-context-provider/keywords-contract"] token_reward_explanations = ["dash-sdk/token_reward_explanations"] From ef119691a600142a52b31d249e2f8886d77dda4d Mon Sep 17 00:00:00 2001 From: quantum Date: Thu, 10 Jul 2025 03:09:55 -0500 Subject: [PATCH 08/30] more work --- .../Cargo.toml | 14 +++- .../src/provider.rs | 72 +++++++++++++++---- packages/wasm-sdk/index.html | 35 +++++---- packages/wasm-sdk/src/queries/document.rs | 62 ++++++++++------ 4 files changed, 131 insertions(+), 52 deletions(-) diff --git a/packages/rs-sdk-trusted-context-provider/Cargo.toml b/packages/rs-sdk-trusted-context-provider/Cargo.toml index 3d1a1281abc..082f11e7418 100644 --- a/packages/rs-sdk-trusted-context-provider/Cargo.toml +++ b/packages/rs-sdk-trusted-context-provider/Cargo.toml @@ -23,8 +23,18 @@ futures = "0.3" url = "2.5" [features] -# All system contracts (default behavior) -all-system-contracts = ["dpp/all-system_contracts"] +default = [] + +# All system contracts (includes all individual contracts) +all-system-contracts = [ + "dpp/all-system_contracts", + "withdrawals-contract", + "dpns-contract", + "dashpay-contract", + "wallet-utils-contract", + "token-history-contract", + "keywords-contract" +] # Individual contract features - these enable specific contracts in DPP withdrawals-contract = ["dpp/withdrawals-contract"] diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index b029666c008..6d75000516c 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -10,7 +10,6 @@ use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; type QuorumHash = [u8; 32]; use dpp::dashcore::Network; use dpp::data_contract::TokenConfiguration; -use dpp::version::PlatformVersion; #[cfg(any( feature = "dpns-contract", feature = "dashpay-contract", @@ -21,6 +20,7 @@ use dpp::version::PlatformVersion; feature = "all-system-contracts" ))] use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use dpp::version::PlatformVersion; use lru::LruCache; use reqwest::Client; @@ -512,42 +512,84 @@ impl ContextProvider for TrustedHttpContextProvider { if *id == SystemDataContract::DPNS.id() { return load_system_data_contract(SystemDataContract::DPNS, platform_version) .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load DPNS contract: {}", e))); + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load DPNS contract: {}", + e + )) + }); } #[cfg(any(feature = "dashpay-contract", feature = "all-system-contracts"))] if *id == SystemDataContract::Dashpay.id() { return load_system_data_contract(SystemDataContract::Dashpay, platform_version) .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load Dashpay contract: {}", e))); + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load Dashpay contract: {}", + e + )) + }); } #[cfg(any(feature = "withdrawals-contract", feature = "all-system-contracts"))] if *id == SystemDataContract::Withdrawals.id() { - return load_system_data_contract(SystemDataContract::Withdrawals, platform_version) - .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load Withdrawals contract: {}", e))); + return load_system_data_contract( + SystemDataContract::Withdrawals, + platform_version, + ) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load Withdrawals contract: {}", + e + )) + }); } #[cfg(any(feature = "wallet-utils-contract", feature = "all-system-contracts"))] if *id == SystemDataContract::WalletUtils.id() { - return load_system_data_contract(SystemDataContract::WalletUtils, platform_version) - .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load WalletUtils contract: {}", e))); + return load_system_data_contract( + SystemDataContract::WalletUtils, + platform_version, + ) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load WalletUtils contract: {}", + e + )) + }); } #[cfg(any(feature = "token-history-contract", feature = "all-system-contracts"))] if *id == SystemDataContract::TokenHistory.id() { - return load_system_data_contract(SystemDataContract::TokenHistory, platform_version) - .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load TokenHistory contract: {}", e))); + return load_system_data_contract( + SystemDataContract::TokenHistory, + platform_version, + ) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load TokenHistory contract: {}", + e + )) + }); } #[cfg(any(feature = "keywords-contract", feature = "all-system-contracts"))] if *id == SystemDataContract::KeywordSearch.id() { - return load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) - .map(|contract| Some(Arc::new(contract))) - .map_err(|e| ContextProviderError::Generic(format!("Failed to load KeywordSearch contract: {}", e))); + return load_system_data_contract( + SystemDataContract::KeywordSearch, + platform_version, + ) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load KeywordSearch contract: {}", + e + )) + }); } } diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 7e5add93698..a42b645604f 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -60,7 +60,7 @@ .app-container { display: flex; - height: 100vh; + height: calc(100vh - 50px); /* Account for status banner height */ overflow: hidden; } @@ -280,12 +280,17 @@ bottom: 0; left: 0; right: 0; - padding: 10px 20px; + height: 50px; + padding: 0 20px; + display: flex; + align-items: center; + justify-content: center; text-align: center; font-weight: 500; font-size: 14px; z-index: 999; transition: all 0.3s ease; + box-sizing: border-box; } .status-banner.success { @@ -638,6 +643,7 @@ + @@ -828,6 +834,7 @@

Results

get_documents, get_document, get_dpns_username, + get_dpns_usernames, // Protocol/Version queries get_protocol_version_upgrade_state, get_protocol_version_upgrade_vote_status, @@ -1344,12 +1351,18 @@

Results

{ name: "documentType", type: "text", label: "Document Type", required: true }, { name: "documentId", type: "text", label: "Document ID", required: true } ] - }, + } + } + }, + dpns: { + label: "DPNS Queries", + queries: { getDpnsUsername: { - label: "Get DPNS Username", - description: "Get DPNS username for an identity", + label: "Get DPNS Usernames", + description: "Get DPNS usernames for an identity", inputs: [ - { name: "identityId", type: "text", label: "Identity ID", required: true } + { name: "identityId", type: "text", label: "Identity ID", required: true }, + { name: "limit", type: "number", label: "Limit", required: false, placeholder: "Default: 10" } ] }, dpnsCheckAvailability: { @@ -2962,11 +2975,12 @@

Results

); // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getDpnsUsername') { - result = await get_dpns_username( + result = await get_dpns_usernames( sdk, - values.identityId + values.identityId, + values.limit || 10 // Default to 10 if not specified ); - // Result is the username string or null + // Result is an array of usernames } else if (queryType === 'dpnsCheckAvailability') { // Handle DPNS availability check const isValid = dpns_is_valid_username(values.label); @@ -3291,8 +3305,6 @@

Results

button.disabled = true; button.textContent = 'Processing...'; - const preloader = document.getElementById('preloader'); - preloader.style.display = 'block'; updateStatus(`Executing ${transitionType} state transition...`, 'loading'); try { @@ -3863,7 +3875,6 @@

Results

displayResult(`Error executing state transition: ${error.message || error}`, true); updateStatus(`Error: ${error.message || error}`, 'error'); } finally { - preloader.style.display = 'none'; button.disabled = false; button.textContent = originalButtonText; } diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index aa6260c917f..174efea1790 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -371,9 +371,10 @@ pub async fn get_document( } #[wasm_bindgen] -pub async fn get_dpns_username( +pub async fn get_dpns_usernames( sdk: &WasmSdk, identity_id: &str, + limit: Option, ) -> Result { use dash_sdk::platform::documents::document_query::DocumentQuery; use dash_sdk::platform::FetchMany; @@ -412,41 +413,56 @@ pub async fn get_dpns_username( }; query = query.with_where(where_clause); - query.limit = 1; // We only need the first result + + // Set limit from parameter or default to 10 + query.limit = limit.unwrap_or(10); // Execute query let documents_result: Documents = Document::fetch_many(sdk.as_ref(), query) .await .map_err(|e| JsError::new(&format!("Failed to fetch DPNS documents: {}", e)))?; - // Process the result + // Collect all usernames + let mut usernames: Vec = Vec::new(); + + // Process all results for (_, doc_opt) in documents_result { if let Some(doc) = doc_opt { // Extract the username from the document let properties = doc.properties(); - let label = properties.get("label") - .and_then(|v| match v { - Value::Text(s) => Some(s.clone()), - _ => None, - }) - .ok_or_else(|| JsError::new("DPNS document missing label field"))?; - - let parent_domain = properties.get("normalizedParentDomainName") - .and_then(|v| match v { - Value::Text(s) => Some(s.clone()), - _ => None, - }) - .ok_or_else(|| JsError::new("DPNS document missing normalizedParentDomainName field"))?; - - // Construct the full username - let username = format!("{}.{}", label, parent_domain); - - // Return the username as a JSON string - return Ok(JsValue::from_str(&username)); + if let (Some(Value::Text(label)), Some(Value::Text(parent_domain))) = ( + properties.get("label"), + properties.get("normalizedParentDomainName") + ) { + // Construct the full username + let username = format!("{}.{}", label, parent_domain); + usernames.push(username); + } + } + } + + // Return usernames as a JSON array + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + usernames.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize usernames: {}", e))) +} + +// Keep the old function for backward compatibility but have it call the new one +#[wasm_bindgen] +pub async fn get_dpns_username( + sdk: &WasmSdk, + identity_id: &str, +) -> Result { + // Call the new function with limit 1 + let result = get_dpns_usernames(sdk, identity_id, Some(1)).await?; + + // Extract the first username from the array + if let Some(array) = result.dyn_ref::() { + if array.length() > 0 { + return Ok(array.get(0)); } } - // No DPNS name found for this identity Ok(JsValue::NULL) } \ No newline at end of file From 647da0cee5863c73e130d4ca82db9f846d5fb718 Mon Sep 17 00:00:00 2001 From: quantum Date: Thu, 10 Jul 2025 03:46:52 -0500 Subject: [PATCH 09/30] more work --- .../rs-sdk/src/platform/dpns_usernames.rs | 175 ++++++++++++++---- packages/wasm-sdk/src/queries/document.rs | 2 +- 2 files changed, 137 insertions(+), 40 deletions(-) diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs index a1a8a32ad7c..ccc1952f953 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -1,6 +1,7 @@ use crate::platform::transition::put_document::PutDocument; use crate::platform::{Document, Fetch, FetchMany}; use crate::{Error, Sdk}; +use dash_context_provider::ContextProvider; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -12,6 +13,7 @@ use dpp::identity::{Identity, IdentityPublicKey}; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::Identifier; use std::collections::BTreeMap; +use std::sync::Arc; /// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' /// with '0', '1', and '1' respectively to prevent homograph attacks @@ -63,8 +65,8 @@ pub fn is_valid_username(label: &str) -> bool { } // Check middle characters (can be alphanumeric or hyphen) - for i in 1..chars.len() - 1 { - if !chars[i].is_ascii_alphanumeric() && chars[i] != '-' { + for &ch in &chars[1..chars.len() - 1] { + if !ch.is_ascii_alphanumeric() && ch != '-' { return false; } } @@ -111,9 +113,7 @@ fn hash_double(data: Vec) -> [u8; 32] { use dpp::dashcore::hashes::{sha256d, Hash}; // sha256d already does double SHA256 let hash = sha256d::Hash::hash(&data); - let mut result = [0u8; 32]; - result.copy_from_slice(hash.as_byte_array()); - result + hash.to_byte_array() } /// Input for registering a DPNS name @@ -126,6 +126,8 @@ pub struct RegisterDpnsNameInput { pub identity_public_key: IdentityPublicKey, /// The signer for the identity pub signer: S, + /// Optional callback to be called with the preorder document result + pub preorder_callback: Option>, } /// Result of a DPNS name registration @@ -163,17 +165,40 @@ impl Sdk { &self, input: RegisterDpnsNameInput, ) -> Result { - // Fetch the DPNS contract - const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string( - DPNS_CONTRACT_ID, - dpp::platform_value::string_encoding::Encoding::Base58, - ) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; - - let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) - .await? - .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + // Get DPNS contract ID from system contract if available + #[cfg(feature = "dpns-contract")] + let dpns_contract_id = { + use dpp::system_data_contracts::SystemDataContract; + SystemDataContract::DPNS.id() + }; + + #[cfg(not(feature = "dpns-contract"))] + let dpns_contract_id = { + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? + }; + + // First check if the contract is available in the context provider + let context_provider = self.context_provider() + .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; + + let dpns_contract = match context_provider.get_data_contract( + &dpns_contract_id, + self.version(), + )? { + Some(contract) => contract, + None => { + // If not in context, fetch from platform + let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + Arc::new(contract) + } + }; // Get document types let preorder_document_type = @@ -284,7 +309,7 @@ impl Sdk { }); // Submit preorder document first - preorder_document + let platform_preorder_document = preorder_document .put_to_platform_and_wait_for_response( self, preorder_document_type.to_owned_document_type(), @@ -296,8 +321,13 @@ impl Sdk { ) .await?; + // Call the preorder callback if provided + if let Some(callback) = input.preorder_callback { + callback(&platform_preorder_document); + } + // Submit domain document after preorder - domain_document + let platform_domain_document = domain_document .put_to_platform_and_wait_for_response( self, domain_document_type.to_owned_document_type(), @@ -310,8 +340,8 @@ impl Sdk { .await?; Ok(RegisterDpnsNameResult { - preorder_document, - domain_document, + preorder_document: platform_preorder_document, + domain_document: platform_domain_document, full_domain_name: format!("{}.dash", normalized_label), }) } @@ -330,16 +360,40 @@ impl Sdk { use drive::query::WhereClause; use drive::query::WhereOperator; - const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string( - DPNS_CONTRACT_ID, - dpp::platform_value::string_encoding::Encoding::Base58, - ) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + // Get DPNS contract ID from system contract if available + #[cfg(feature = "dpns-contract")] + let dpns_contract_id = { + use dpp::system_data_contracts::SystemDataContract; + SystemDataContract::DPNS.id() + }; + + #[cfg(not(feature = "dpns-contract"))] + let dpns_contract_id = { + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? + }; - let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) - .await? - .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + // First check if the contract is available in the context provider + let context_provider = self.context_provider() + .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; + + let dpns_contract = match context_provider.get_data_contract( + &dpns_contract_id, + self.version(), + )? { + Some(contract) => contract, + None => { + // If not in context, fetch from platform + let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + Arc::new(contract) + } + }; let normalized_label = convert_to_homograph_safe_chars(label); @@ -384,19 +438,62 @@ impl Sdk { use drive::query::WhereClause; use drive::query::WhereOperator; - const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - let dpns_contract_id = Identifier::from_string( - DPNS_CONTRACT_ID, - dpp::platform_value::string_encoding::Encoding::Base58, - ) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))?; + // Get DPNS contract ID from system contract if available + #[cfg(feature = "dpns-contract")] + let dpns_contract_id = { + use dpp::system_data_contracts::SystemDataContract; + SystemDataContract::DPNS.id() + }; + + #[cfg(not(feature = "dpns-contract"))] + let dpns_contract_id = { + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + Identifier::from_string( + DPNS_CONTRACT_ID, + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? + }; - let dpns_contract = crate::platform::DataContract::fetch(self, dpns_contract_id) - .await? - .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + // First check if the contract is available in the context provider + let context_provider = self.context_provider() + .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; + + let dpns_contract = match context_provider.get_data_contract( + &dpns_contract_id, + self.version(), + )? { + Some(contract) => contract, + None => { + // If not in context, fetch from platform + let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) + .await? + .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; + Arc::new(contract) + } + }; // Extract label from full name if needed - let label = name.trim_end_matches(".dash"); + // Handle both "alice" and "alice.dash" formats + let label = if let Some(dot_pos) = name.rfind('.') { + let (label_part, suffix) = name.split_at(dot_pos); + // Only strip the suffix if it's exactly ".dash" + if suffix == ".dash" { + label_part + } else { + // If it's not ".dash", treat the whole thing as the label + name + } + } else { + // No dot found, use the whole name as the label + name + }; + + // Validate the label before proceeding + if label.is_empty() { + return Ok(None); + } + let normalized_label = convert_to_homograph_safe_chars(label); // Query for domain with this label diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 174efea1790..7f4fa2dbf32 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -1,6 +1,6 @@ use crate::sdk::WasmSdk; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{JsError, JsValue}; +use wasm_bindgen::{JsError, JsValue, JsCast}; use serde::{Serialize, Deserialize}; use dash_sdk::platform::Fetch; use dash_sdk::dpp::prelude::Identifier; From 69ac84af0f1fd296664d52e2ca39e44b6f11c9d1 Mon Sep 17 00:00:00 2001 From: quantum Date: Thu, 10 Jul 2025 04:21:38 -0500 Subject: [PATCH 10/30] more work --- .../rs-sdk/src/platform/dpns_usernames.rs | 46 ++++++++--------- packages/wasm-sdk/index.html | 31 +++++++++++- packages/wasm-sdk/src/dpns.rs | 49 ++++++++++++++++++- 3 files changed, 99 insertions(+), 27 deletions(-) diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs index ccc1952f953..5ee52ce511a 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -171,7 +171,7 @@ impl Sdk { use dpp::system_data_contracts::SystemDataContract; SystemDataContract::DPNS.id() }; - + #[cfg(not(feature = "dpns-contract"))] let dpns_contract_id = { const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; @@ -183,13 +183,13 @@ impl Sdk { }; // First check if the contract is available in the context provider - let context_provider = self.context_provider() + let context_provider = self + .context_provider() .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - - let dpns_contract = match context_provider.get_data_contract( - &dpns_contract_id, - self.version(), - )? { + + let dpns_contract = match context_provider + .get_data_contract(&dpns_contract_id, self.version())? + { Some(contract) => contract, None => { // If not in context, fetch from platform @@ -366,7 +366,7 @@ impl Sdk { use dpp::system_data_contracts::SystemDataContract; SystemDataContract::DPNS.id() }; - + #[cfg(not(feature = "dpns-contract"))] let dpns_contract_id = { const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; @@ -378,13 +378,13 @@ impl Sdk { }; // First check if the contract is available in the context provider - let context_provider = self.context_provider() + let context_provider = self + .context_provider() .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - - let dpns_contract = match context_provider.get_data_contract( - &dpns_contract_id, - self.version(), - )? { + + let dpns_contract = match context_provider + .get_data_contract(&dpns_contract_id, self.version())? + { Some(contract) => contract, None => { // If not in context, fetch from platform @@ -444,7 +444,7 @@ impl Sdk { use dpp::system_data_contracts::SystemDataContract; SystemDataContract::DPNS.id() }; - + #[cfg(not(feature = "dpns-contract"))] let dpns_contract_id = { const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; @@ -456,13 +456,13 @@ impl Sdk { }; // First check if the contract is available in the context provider - let context_provider = self.context_provider() + let context_provider = self + .context_provider() .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - - let dpns_contract = match context_provider.get_data_contract( - &dpns_contract_id, - self.version(), - )? { + + let dpns_contract = match context_provider + .get_data_contract(&dpns_contract_id, self.version())? + { Some(contract) => contract, None => { // If not in context, fetch from platform @@ -488,12 +488,12 @@ impl Sdk { // No dot found, use the whole name as the label name }; - + // Validate the label before proceeding if label.is_empty() { return Ok(None); } - + let normalized_label = convert_to_homograph_safe_chars(label); // Query for domain with this label diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index a42b645604f..1c7f4cf225e 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -3529,16 +3529,43 @@

Results

console.log(`Using key ID ${keyId} for DPNS registration`); + // Show initial status + updateStatus('Submitting DPNS preorder document...', 'info'); + + // Add a small delay to let the status message show + await new Promise(resolve => setTimeout(resolve, 100)); + result = await dpns_register_name( sdk, values.label, identityId, // Use the identity ID from authentication keyId, // Use the determined key ID - actualPrivateKey // Use the actual private key (without :keyId suffix) + actualPrivateKey, // Use the actual private key (without :keyId suffix) + // Callback for preorder success + (preorderInfo) => { + console.log('Preorder successful:', preorderInfo); + updateStatus('Preorder successful! Now submitting domain document...', 'info'); + + // Show preorder info in a temporary notification + const preorderMsg = `Preorder Document ID: ${preorderInfo.documentId}`; + const notification = document.createElement('div'); + notification.className = 'preorder-notification'; + notification.style.cssText = 'background: #4CAF50; color: white; padding: 10px; margin: 10px 0; border-radius: 4px;'; + notification.textContent = preorderMsg; + + const resultsSection = document.getElementById('results'); + if (resultsSection) { + resultsSection.insertBefore(notification, resultsSection.firstChild); + // Remove notification after 5 seconds + setTimeout(() => notification.remove(), 5000); + } + } ); + // Since our callback is called at the end, let's update the message + // to reflect that both documents were submitted displayResult(JSON.stringify(result, null, 2)); - updateStatus('DPNS name registered successfully', 'success'); + updateStatus('DPNS name registered successfully! Both preorder and domain documents submitted.', 'success'); } else if (transitionType === 'documentPurchase') { // Handle document purchase result = await sdk.documentPurchase( diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index 6491616887f..2e96eeb35da 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -4,10 +4,11 @@ use crate::sdk::WasmSdk; use serde::{Serialize, Deserialize}; use dash_sdk::platform::dpns_usernames::{convert_to_homograph_safe_chars, is_contested_username, is_valid_username, RegisterDpnsNameInput}; use dash_sdk::platform::{Fetch, Identity}; -use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::document::{Document, DocumentV0Getters}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::prelude::Identifier; use simple_signer::SingleKeySigner; +use std::sync::Mutex; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -43,6 +44,7 @@ pub async fn dpns_register_name( identity_id: &str, public_key_id: u32, private_key_wif: &str, + preorder_callback: Option, ) -> Result { // Parse identity ID let identity_id_parsed = Identifier::from_string( @@ -66,12 +68,50 @@ pub async fn dpns_register_name( .ok_or_else(|| JsError::new(&format!("Public key with ID {} not found", public_key_id)))? .clone(); - // Create registration input + // Store the JS callback in a thread-local variable that we can access from the closure + thread_local! { + static PREORDER_CALLBACK: std::cell::RefCell> = std::cell::RefCell::new(None); + } + + // Set the callback if provided + if let Some(ref js_callback) = preorder_callback { + PREORDER_CALLBACK.with(|cb| { + *cb.borrow_mut() = Some(js_callback.clone()); + }); + } + + // Create a Rust callback that will call the JavaScript callback + let callback_box = if preorder_callback.is_some() { + Some(Box::new(move |doc: &Document| { + PREORDER_CALLBACK.with(|cb| { + if let Some(js_callback) = cb.borrow().as_ref() { + let preorder_info = serde_json::json!({ + "documentId": doc.id().to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + "ownerId": doc.owner_id().to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + "revision": doc.revision().unwrap_or(0), + "createdAt": doc.created_at(), + "createdAtBlockHeight": doc.created_at_block_height(), + "createdAtCoreBlockHeight": doc.created_at_core_block_height(), + "message": "Preorder document submitted successfully", + }); + + if let Ok(js_value) = serde_wasm_bindgen::to_value(&preorder_info) { + let _ = js_callback.call1(&JsValue::NULL, &js_value); + } + } + }); + }) as Box) + } else { + None + }; + + // Create registration input with the callback let input = RegisterDpnsNameInput { label: label.to_string(), identity, identity_public_key, signer, + preorder_callback: callback_box, }; // Register the name @@ -80,6 +120,11 @@ pub async fn dpns_register_name( .await .map_err(|e| JsError::new(&format!("Failed to register DPNS name: {}", e)))?; + // Clear the thread-local callback + PREORDER_CALLBACK.with(|cb| { + *cb.borrow_mut() = None; + }); + // Convert result to JS-friendly format let js_result = RegisterDpnsNameResult { preorder_document_id: result.preorder_document.id().to_string( From 057b61814f21d6bb0a8a0d24969ec4568ae8dc8d Mon Sep 17 00:00:00 2001 From: quantum Date: Fri, 11 Jul 2025 08:02:18 -0500 Subject: [PATCH 11/30] more work --- .../rs-sdk/src/platform/dpns_usernames.rs | 138 +--- packages/wasm-sdk/index.html | 508 ++++++++++-- .../wasm-sdk/src/queries/data_contract.rs | 126 +++ packages/wasm-sdk/src/queries/document.rs | 310 ++++++++ packages/wasm-sdk/src/queries/epoch.rs | 206 ++++- packages/wasm-sdk/src/queries/group.rs | 556 +++++++++++++- packages/wasm-sdk/src/queries/identity.rs | 723 +++++++++++++++++- packages/wasm-sdk/src/queries/mod.rs | 65 +- packages/wasm-sdk/src/queries/protocol.rs | 64 ++ packages/wasm-sdk/src/queries/system.rs | 139 +++- packages/wasm-sdk/src/queries/token.rs | 518 ++++++++++++- packages/wasm-sdk/src/queries/voting.rs | 380 +++++++-- packages/wasm-sdk/src/sdk.rs | 5 + 13 files changed, 3497 insertions(+), 241 deletions(-) diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs index 5ee52ce511a..c00aedec21a 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -142,29 +142,8 @@ pub struct RegisterDpnsNameResult { } impl Sdk { - /// Register a DPNS username in a single operation - /// - /// This method handles both the preorder and domain registration steps automatically. - /// It generates the necessary entropy, creates both documents, and submits them in order. - /// - /// # Arguments - /// - /// * `input` - The registration input containing label, identity, public key, and signer - /// - /// # Returns - /// - /// Returns a `RegisterDpnsNameResult` containing both created documents and the full domain name - /// - /// # Errors - /// - /// Returns an error if: - /// - The DPNS contract cannot be fetched - /// - Document types are not found in the contract - /// - Document creation or submission fails - pub async fn register_dpns_name( - &self, - input: RegisterDpnsNameInput, - ) -> Result { + /// Helper method to get the DPNS contract ID + fn get_dpns_contract_id(&self) -> Result { // Get DPNS contract ID from system contract if available #[cfg(feature = "dpns-contract")] let dpns_contract_id = { @@ -182,23 +161,54 @@ impl Sdk { .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? }; + Ok(dpns_contract_id) + } + + /// Helper method to fetch the DPNS contract, checking context provider first + async fn fetch_dpns_contract(&self) -> Result, Error> { + let dpns_contract_id = self.get_dpns_contract_id()?; + // First check if the contract is available in the context provider let context_provider = self .context_provider() .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - let dpns_contract = match context_provider - .get_data_contract(&dpns_contract_id, self.version())? - { - Some(contract) => contract, + match context_provider.get_data_contract(&dpns_contract_id, self.version())? { + Some(contract) => Ok(contract), None => { // If not in context, fetch from platform let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) .await? .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; - Arc::new(contract) + Ok(Arc::new(contract)) } - }; + } + } + + /// Register a DPNS username in a single operation + /// + /// This method handles both the preorder and domain registration steps automatically. + /// It generates the necessary entropy, creates both documents, and submits them in order. + /// + /// # Arguments + /// + /// * `input` - The registration input containing label, identity, public key, and signer + /// + /// # Returns + /// + /// Returns a `RegisterDpnsNameResult` containing both created documents and the full domain name + /// + /// # Errors + /// + /// Returns an error if: + /// - The DPNS contract cannot be fetched + /// - Document types are not found in the contract + /// - Document creation or submission fails + pub async fn register_dpns_name( + &self, + input: RegisterDpnsNameInput, + ) -> Result { + let dpns_contract = self.fetch_dpns_contract().await?; // Get document types let preorder_document_type = @@ -360,40 +370,7 @@ impl Sdk { use drive::query::WhereClause; use drive::query::WhereOperator; - // Get DPNS contract ID from system contract if available - #[cfg(feature = "dpns-contract")] - let dpns_contract_id = { - use dpp::system_data_contracts::SystemDataContract; - SystemDataContract::DPNS.id() - }; - - #[cfg(not(feature = "dpns-contract"))] - let dpns_contract_id = { - const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - Identifier::from_string( - DPNS_CONTRACT_ID, - dpp::platform_value::string_encoding::Encoding::Base58, - ) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? - }; - - // First check if the contract is available in the context provider - let context_provider = self - .context_provider() - .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - - let dpns_contract = match context_provider - .get_data_contract(&dpns_contract_id, self.version())? - { - Some(contract) => contract, - None => { - // If not in context, fetch from platform - let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) - .await? - .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; - Arc::new(contract) - } - }; + let dpns_contract = self.fetch_dpns_contract().await?; let normalized_label = convert_to_homograph_safe_chars(label); @@ -438,40 +415,7 @@ impl Sdk { use drive::query::WhereClause; use drive::query::WhereOperator; - // Get DPNS contract ID from system contract if available - #[cfg(feature = "dpns-contract")] - let dpns_contract_id = { - use dpp::system_data_contracts::SystemDataContract; - SystemDataContract::DPNS.id() - }; - - #[cfg(not(feature = "dpns-contract"))] - let dpns_contract_id = { - const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; - Identifier::from_string( - DPNS_CONTRACT_ID, - dpp::platform_value::string_encoding::Encoding::Base58, - ) - .map_err(|e| Error::DapiClientError(format!("Invalid DPNS contract ID: {}", e)))? - }; - - // First check if the contract is available in the context provider - let context_provider = self - .context_provider() - .ok_or_else(|| Error::DapiClientError("Context provider not set".to_string()))?; - - let dpns_contract = match context_provider - .get_data_contract(&dpns_contract_id, self.version())? - { - Some(contract) => contract, - None => { - // If not in context, fetch from platform - let contract = crate::platform::DataContract::fetch(self, dpns_contract_id) - .await? - .ok_or_else(|| Error::DapiClientError("DPNS contract not found".to_string()))?; - Arc::new(contract) - } - }; + let dpns_contract = self.fetch_dpns_contract().await?; // Extract label from full name if needed // Handle both "alice" and "alice.dash" formats diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 1c7f4cf225e..71785ef278d 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -104,6 +104,67 @@ color: white; } + .proof-toggle { + padding: 15px 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #e0e0e0; + display: flex; + align-items: center; + justify-content: space-between; + } + + .proof-toggle label { + font-weight: 500; + color: #333; + margin: 0; + } + + .toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + } + + .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; + } + + .toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + } + + input:checked + .toggle-slider { + background-color: #4CAF50; + } + + input:checked + .toggle-slider:before { + transform: translateX(26px); + } + + .query-container { padding: 20px; flex: 1; @@ -228,6 +289,41 @@ flex-direction: column; overflow: hidden; } + + .result-split-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .result-data-section, + .result-proof-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + border-bottom: 1px solid #e0e0e0; + } + + .result-proof-section { + border-bottom: none; + background-color: #f8f9fa; + } + + .section-header { + padding: 10px 20px; + background-color: #e3f2fd; + border-bottom: 1px solid #ccc; + font-weight: 600; + color: #1976d2; + font-size: 14px; + } + + .result-proof-section .section-header { + background-color: #f3e5f5; + color: #7b1fa2; + } .result-header { padding: 20px; @@ -354,6 +450,45 @@ opacity: 1; } + .nonce-value { + color: #9c27b0; + cursor: help; + position: relative; + } + + .nonce-value:hover { + background-color: #f3e5f5; + border-radius: 3px; + padding: 0 2px; + } + + /* Enhanced tooltip for nonce with better formatting */ + .nonce-value::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 10px; + border-radius: 4px; + font-size: 12px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + white-space: pre; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + margin-bottom: 5px; + z-index: 1000; + max-width: 500px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + } + + .nonce-value:hover::after { + opacity: 1; + } + .json-key { color: #d73a49; font-weight: 500; @@ -676,6 +811,16 @@

Query Parameters

+ + @@ -689,7 +834,9 @@

Results

-
No data fetched yet. Select a query category and type to begin.
+
+
No data fetched yet. Select a query category and type to begin.
+
@@ -812,14 +959,19 @@

Results

import init, { WasmSdkBuilder, - identity_fetch, - data_contract_fetch, + identity_fetch, + identity_fetch_unproved, + identity_fetch_with_proof_info, + data_contract_fetch, + data_contract_fetch_with_proof_info, prefetch_trusted_quorums_mainnet, prefetch_trusted_quorums_testnet, // Identity queries get_identity_keys, get_identity_nonce, + get_identity_nonce_with_proof_info, get_identity_contract_nonce, + get_identity_contract_nonce_with_proof_info, get_identity_balance, get_identities_balances, get_identity_balance_and_revision, @@ -832,34 +984,47 @@

Results

get_data_contracts, // Document queries get_documents, + get_documents_with_proof_info, get_document, + get_document_with_proof_info, get_dpns_username, get_dpns_usernames, + get_dpns_username_by_name, + get_dpns_username_by_name_with_proof_info, // Protocol/Version queries get_protocol_version_upgrade_state, + get_protocol_version_upgrade_state_with_proof_info, get_protocol_version_upgrade_vote_status, // Epoch/Block queries get_epochs_info, + get_epochs_info_with_proof_info, get_finalized_epoch_infos, get_current_epoch, + get_current_epoch_with_proof_info, get_evonodes_proposed_epoch_blocks_by_ids, get_evonodes_proposed_epoch_blocks_by_range, // System/Utility queries get_status, get_total_credits_in_platform, + get_total_credits_in_platform_with_proof_info, get_current_quorums_info, get_prefunded_specialized_balance, + get_prefunded_specialized_balance_with_proof_info, get_path_elements, + get_path_elements_with_proof_info, wait_for_state_transition_result, // Token queries get_identities_token_balances, + get_identities_token_balances_with_proof_info, get_identity_token_infos, get_identities_token_infos, get_token_statuses, + get_token_statuses_with_proof_info, get_token_direct_purchase_prices, get_token_contract_info, get_token_perpetual_distribution_last_claim, get_token_total_supply, + get_token_total_supply_with_proof_info, // Voting/Contested Resource queries get_contested_resources, get_contested_resource_vote_state, @@ -868,7 +1033,9 @@

Results

get_vote_polls_by_end_date, // Group queries get_group_info, + get_group_info_with_proof_info, get_group_infos, + get_group_infos_with_proof_info, get_group_actions, get_group_action_signers, // DPNS functions @@ -1237,7 +1404,7 @@

Results

] }, getIdentityByPublicKeyHash: { - label: "Get Identity by Public Key Hash", + label: "Get Identity by Unique Public Key Hash", description: "Find an identity by its unique public key hash", inputs: [ { name: "publicKeyHash", type: "text", label: "Public Key Hash", required: true } @@ -1245,7 +1412,7 @@

Results

}, getIdentityByNonUniquePublicKeyHash: { label: "Get Identity by Non-Unique Public Key Hash", - description: "Find identities by non-unique public key hash", + description: "Find identities by non-unique public key hash (for ECDSA_HASH160, BIP13_SCRIPT_HASH, or EDDSA_25519_HASH160 key types - supports pagination)", inputs: [ { name: "publicKeyHash", type: "text", label: "Public Key Hash", required: true }, { name: "startAfter", type: "text", label: "Start After (Identity ID)", required: false } @@ -1269,22 +1436,18 @@

Results

}, getIdentityTokenInfos: { label: "Get Identity Token Info", - description: "Get token information for an identity", + description: "Get token information (frozen status) for an identity's tokens", inputs: [ { name: "identityId", type: "text", label: "Identity ID", required: true }, - { name: "tokenIds", type: "array", label: "Token IDs (optional)", required: false }, - { name: "withPurchaseInfo", type: "checkbox", label: "Include Purchase Info", required: false }, - { name: "limit", type: "number", label: "Limit", required: false }, - { name: "offset", type: "number", label: "Offset", required: false } + { name: "tokenIds", type: "array", label: "Token IDs", required: true } ] }, getIdentitiesTokenInfos: { label: "Get Identities Token Info", - description: "Get token information for multiple identities", + description: "Get token information (frozen status) for multiple identities with a specific token", inputs: [ { name: "identityIds", type: "array", label: "Identity IDs", required: true }, - { name: "tokenId", type: "text", label: "Token Contract Position", required: true }, - { name: "withPurchaseInfo", type: "checkbox", label: "Include Purchase Info", required: false } + { name: "tokenId", type: "text", label: "Token ID", required: true } ] } } @@ -2357,13 +2520,15 @@

Results

} function displayResult(data, isError = false) { - const resultContent = document.getElementById('identityInfo'); + const splitContainer = document.getElementById('resultSplitContainer'); + const proofToggle = document.getElementById('proofToggle'); + const isProofMode = proofToggle && proofToggle.checked; + if (isError) { - resultContent.className = 'result-content error-result'; - resultContent.textContent = data; + // For errors, show in single view + splitContainer.innerHTML = `
${data}
`; currentResult = null; } else { - resultContent.className = 'result-content'; // Parse JSON string if necessary let dataToFormat = data; if (typeof data === 'string') { @@ -2374,8 +2539,52 @@

Results

dataToFormat = data; } } - // Use custom formatter for better display with credit tooltips - resultContent.innerHTML = formatResultWithCredits(dataToFormat); + + // Check if this is a proof response (has data, metadata, and proof fields) + const isProofResponse = dataToFormat && + typeof dataToFormat === 'object' && + 'data' in dataToFormat && + 'metadata' in dataToFormat && + 'proof' in dataToFormat; + + if (isProofMode && isProofResponse) { + // Split view for proof mode + splitContainer.innerHTML = ` +
+
Data
+
+
+
+
Proof & Metadata
+
+
+ `; + + // Display data in top section + const dataContent = document.getElementById('identityInfo'); + dataContent.innerHTML = formatResultWithCredits(dataToFormat.data); + + // Display proof and metadata in bottom section + const proofContent = document.getElementById('proofInfo'); + const proofDisplay = { + metadata: dataToFormat.metadata, + proof: { + grovedbProof: dataToFormat.proof.grovedbProof, + quorumHash: dataToFormat.proof.quorumHash, + signature: dataToFormat.proof.signature, + round: dataToFormat.proof.round, + blockIdHash: dataToFormat.proof.blockIdHash, + quorumType: dataToFormat.proof.quorumType + } + }; + proofContent.innerHTML = formatResultWithCredits(proofDisplay); + } else { + // Single view for non-proof mode or non-proof responses + splitContainer.innerHTML = `
`; + const resultContent = document.getElementById('identityInfo'); + resultContent.innerHTML = formatResultWithCredits(dataToFormat); + } + currentResult = data; } } @@ -2390,13 +2599,110 @@

Results

return `${credits}`; } + function formatNonceValue(nonce, key) { + // Parse the nonce as a BigInt to handle large numbers + const nonceBigInt = BigInt(nonce); + + // Determine if this is an identity contract nonce based on the field name + const isContractNonce = key && key.toLowerCase().includes('contract'); + + let nonceTooltip; + + if (isContractNonce) { + // Identity Contract Nonce: First 24 bits are revision bitset, lower 40 bits are the nonce value + const IDENTITY_NONCE_VALUE_FILTER = 0xFFFFFFFFFFn; // 40 bits + const MISSING_IDENTITY_REVISIONS_FILTER = 0xFFFFFF0000000000n; // Upper 24 bits + + // Extract components + const nonceValue = nonceBigInt & IDENTITY_NONCE_VALUE_FILTER; // Lower 40 bits + const missingRevisionsBitset = (nonceBigInt & MISSING_IDENTITY_REVISIONS_FILTER) >> 40n; // Upper 24 bits + + // Convert to binary representation + const binaryStr = nonceBigInt.toString(2).padStart(64, '0'); + const revisionBitsetBits = binaryStr.slice(0, 24); + const nonceValueBits = binaryStr.slice(24, 64); + + // Find missing nonces based on the bitset + const missingNonces = []; + for (let i = 0; i < 24; i++) { + if ((missingRevisionsBitset >> BigInt(i)) & 1n) { + const missingNonce = nonceValue - BigInt(i + 1); + missingNonces.push(missingNonce); + } + } + + nonceTooltip = `Identity Contract Nonce: ${nonce} (0x${nonceBigInt.toString(16).padStart(16, '0')}) + +Binary: ${binaryStr} + +Bit Structure (Identity Contract Nonce): +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Bits 63-40 (Missing Revisions): ${revisionBitsetBits} = ${missingRevisionsBitset} +Bits 39-0 (Nonce Value): ${nonceValueBits} = ${nonceValue} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Decoded Values: +• Nonce Value: ${nonceValue} (current sequence number) +• Missing Revisions Bitset: ${missingRevisionsBitset} (tracks up to 24 missing nonces) +${missingNonces.length > 0 ? `• Missing Nonces: ${missingNonces.map(n => n.toString()).join(', ')}` : '• No missing nonces detected'} + +Used for: Document operations and data contract updates`; + } else { + // Identity Nonce: Same structure as contract nonce but used for different operations + const IDENTITY_NONCE_VALUE_FILTER = 0xFFFFFFFFFFn; // 40 bits + const MISSING_IDENTITY_REVISIONS_FILTER = 0xFFFFFF0000000000n; // Upper 24 bits + + // Extract components + const nonceValue = nonceBigInt & IDENTITY_NONCE_VALUE_FILTER; // Lower 40 bits + const missingRevisionsBitset = (nonceBigInt & MISSING_IDENTITY_REVISIONS_FILTER) >> 40n; // Upper 24 bits + + // Convert to binary representation + const binaryStr = nonceBigInt.toString(2).padStart(64, '0'); + const revisionBitsetBits = binaryStr.slice(0, 24); + const nonceValueBits = binaryStr.slice(24, 64); + + // Find missing nonces based on the bitset + const missingNonces = []; + for (let i = 0; i < 24; i++) { + if ((missingRevisionsBitset >> BigInt(i)) & 1n) { + const missingNonce = nonceValue - BigInt(i + 1); + missingNonces.push(missingNonce); + } + } + + nonceTooltip = `Identity Nonce: ${nonce} (0x${nonceBigInt.toString(16).padStart(16, '0')}) + +Binary: ${binaryStr} + +Bit Structure (Identity Nonce): +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Bits 63-40 (Missing Revisions): ${revisionBitsetBits} = ${missingRevisionsBitset} +Bits 39-0 (Nonce Value): ${nonceValueBits} = ${nonceValue} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Decoded Values: +• Nonce Value: ${nonceValue} (current sequence number) +• Missing Revisions Bitset: ${missingRevisionsBitset} (tracks up to 24 missing nonces) +${missingNonces.length > 0 ? `• Missing Nonces: ${missingNonces.map(n => n.toString()).join(', ')}` : '• No missing nonces detected'} + +Used for: Identity state transitions that use balance or create data contracts`; + } + + return `${nonce}`; + } + function processValue(value, key, indent = 0) { // Check if this is a credits field const creditsFields = ['balance', 'totalCreditsInPlatform', 'credits', 'amount', 'totalCredits']; const isCreditsField = creditsFields.some(field => key.toLowerCase().includes(field.toLowerCase())); + // Check if this is a nonce field + const isNonceField = key.toLowerCase() === 'nonce'; + if (typeof value === 'string' && isCreditsField && /^\d+$/.test(value)) { return formatCreditsValue(value); + } else if (typeof value === 'string' && isNonceField) { + return formatNonceValue(value, key); } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { return formatObject(value, indent); } else if (Array.isArray(value)) { @@ -2493,6 +2799,11 @@

Results

sdk = newSdk; console.log(`Initialized ${network} SDK (${modeStr} mode):`, sdk); updateStatus(`WASM SDK successfully loaded on ${network.toUpperCase()} (${modeStr} mode)`, 'success'); + + // Proof support is now available for identity queries + // When the proof toggle is ON (checked), queries return raw JSON without proof verification + // When the proof toggle is OFF (unchecked), queries return verified data with proofs + // Note: Most SDK queries don't yet support unproved mode, only identity_fetch_unproved is implemented } } catch (error) { if (currentRequestToken === initRequestCounter) { @@ -2872,10 +3183,22 @@

Results

const values = getInputValues(); let result; + // Check if proof toggle is enabled + const proofToggle = document.getElementById('proofToggle'); + const useProofs = proofToggle && proofToggle.checked; + // Identity queries - if (queryType === 'getIdentity' && values.id) { - result = await identity_fetch(sdk, values.id); - result = result.toJSON(); + if (queryType === 'getIdentity') { + if (!values.id) { + throw new Error('Identity ID is required for getIdentity query'); + } + if (useProofs) { + result = await identity_fetch_with_proof_info(sdk, values.id); + // Result is already a JS object with proof data + } else { + result = await identity_fetch(sdk, values.id); + result = result.toJSON(); + } } else if (queryType === 'getIdentityKeys') { const keyIds = values.keyRequestType === 'specific' ? values.specificKeyIds : undefined; result = await get_identity_keys( @@ -2898,9 +3221,17 @@

Results

); // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getIdentityNonce') { - result = await get_identity_nonce(sdk, values.identityId); + if (useProofs) { + result = await get_identity_nonce_with_proof_info(sdk, values.identityId); + } else { + result = await get_identity_nonce(sdk, values.identityId); + } } else if (queryType === 'getIdentityContractNonce') { - result = await get_identity_contract_nonce(sdk, values.identityId, values.contractId); + if (useProofs) { + result = await get_identity_contract_nonce_with_proof_info(sdk, values.identityId, values.contractId); + } else { + result = await get_identity_contract_nonce(sdk, values.identityId, values.contractId); + } } else if (queryType === 'getIdentityBalance') { result = await get_identity_balance(sdk, values.id); // Result is already an object with balance field @@ -2926,8 +3257,13 @@

Results

} // Data contract queries else if (queryType === 'getDataContract' && values.id) { - result = await data_contract_fetch(sdk, values.id); - result = result.toJSON(); + if (useProofs) { + result = await data_contract_fetch_with_proof_info(sdk, values.id); + // Result is already a JS object with proof data + } else { + result = await data_contract_fetch(sdk, values.id); + result = result.toJSON(); + } } else if (queryType === 'getDataContractHistory') { result = await get_data_contract_history( sdk, @@ -2936,10 +3272,10 @@

Results

values.offset, values.startAtMs ); - result = JSON.parse(result); + // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getDataContracts') { result = await get_data_contracts(sdk, values.ids); - result = JSON.parse(result); + // Result is already a JS object from serde_wasm_bindgen } // Document queries else if (queryType === 'getDocuments') { @@ -2955,24 +3291,46 @@

Results

limit: values.limit }); - result = await get_documents( - sdk, - values.dataContractId, - values.documentType, - values.where ? JSON.stringify(values.where) : undefined, - values.orderBy ? JSON.stringify(values.orderBy) : undefined, - values.limit, - startAfter, - startAt - ); + if (useProofs) { + result = await get_documents_with_proof_info( + sdk, + values.dataContractId, + values.documentType, + values.where ? JSON.stringify(values.where) : undefined, + values.orderBy ? JSON.stringify(values.orderBy) : undefined, + values.limit, + startAfter, + startAt + ); + } else { + result = await get_documents( + sdk, + values.dataContractId, + values.documentType, + values.where ? JSON.stringify(values.where) : undefined, + values.orderBy ? JSON.stringify(values.orderBy) : undefined, + values.limit, + startAfter, + startAt + ); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getDocument') { - result = await get_document( - sdk, - values.dataContractId, - values.documentType, - values.documentId - ); + if (useProofs) { + result = await get_document_with_proof_info( + sdk, + values.dataContractId, + values.documentType, + values.documentId + ); + } else { + result = await get_document( + sdk, + values.dataContractId, + values.documentType, + values.documentId + ); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getDpnsUsername') { result = await get_dpns_usernames( @@ -3046,15 +3404,28 @@

Results

} // Epoch/Block queries else if (queryType === 'getEpochsInfo') { - result = await get_epochs_info( - sdk, - values.startEpoch, - values.count, - values.ascending - ); + if (useProofs) { + result = await get_epochs_info_with_proof_info( + sdk, + values.startEpoch, + values.count, + values.ascending + ); + } else { + result = await get_epochs_info( + sdk, + values.startEpoch, + values.count, + values.ascending + ); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getCurrentEpoch') { - result = await get_current_epoch(sdk); + if (useProofs) { + result = await get_current_epoch_with_proof_info(sdk); + } else { + result = await get_current_epoch(sdk); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getFinalizedEpochInfos') { result = await get_finalized_epoch_infos( @@ -3086,7 +3457,11 @@

Results

result = await get_status(sdk); // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getTotalCreditsInPlatform') { - result = await get_total_credits_in_platform(sdk); + if (useProofs) { + result = await get_total_credits_in_platform_with_proof_info(sdk); + } else { + result = await get_total_credits_in_platform(sdk); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'waitForStateTransitionResult') { if (!values.stateTransitionHash) { @@ -3112,7 +3487,11 @@

Results

} // Token queries else if (queryType === 'getIdentitiesTokenBalances') { - result = await get_identities_token_balances(sdk, values.identityIds, values.tokenId); + if (useProofs) { + result = await get_identities_token_balances_with_proof_info(sdk, values.identityIds, values.tokenId); + } else { + result = await get_identities_token_balances(sdk, values.identityIds, values.tokenId); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getIdentityTokenInfos') { result = await get_identity_token_infos( @@ -3131,7 +3510,11 @@

Results

); // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getTokenStatuses') { - result = await get_token_statuses(sdk, values.tokenIds); + if (useProofs) { + result = await get_token_statuses_with_proof_info(sdk, values.tokenIds); + } else { + result = await get_token_statuses(sdk, values.tokenIds); + } // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getTokenDirectPurchasePrices') { result = await get_token_direct_purchase_prices(sdk, values.tokenIds); @@ -3147,7 +3530,11 @@

Results

); // Result is already a JS object from serde_wasm_bindgen } else if (queryType === 'getTokenTotalSupply') { - result = await get_token_total_supply(sdk, values.tokenId); + if (useProofs) { + result = await get_token_total_supply_with_proof_info(sdk, values.tokenId); + } else { + result = await get_token_total_supply(sdk, values.tokenId); + } // Result is already a JS object from serde_wasm_bindgen } // Voting/Contested Resource queries @@ -3941,6 +4328,7 @@

Results

document.getElementById('queryType').innerHTML = ''; document.getElementById('queryType').style.display = 'none'; document.getElementById('queryInputs').style.display = 'none'; + document.getElementById('proofToggleContainer').style.display = 'none'; document.getElementById('executeQuery').style.display = 'none'; document.getElementById('queryDescription').style.display = 'none'; }); @@ -3959,6 +4347,7 @@

Results

// Hide inputs and button queryInputs.style.display = 'none'; + document.getElementById('proofToggleContainer').style.display = 'none'; executeButton.style.display = 'none'; queryDescription.style.display = 'none'; @@ -4045,6 +4434,14 @@

Results

queryInputs.style.display = 'block'; executeButton.style.display = 'block'; + // Show proof toggle for queries only + const proofToggleContainer = document.getElementById('proofToggleContainer'); + if (operationType === 'queries') { + proofToggleContainer.style.display = 'block'; + } else { + proofToggleContainer.style.display = 'none'; + } + // Update button text based on operation type executeButton.textContent = operationType === 'transitions' ? 'Execute' : 'Execute Query'; @@ -4089,6 +4486,7 @@

Results

} } else { queryInputs.style.display = 'none'; + document.getElementById('proofToggleContainer').style.display = 'none'; executeButton.style.display = 'none'; queryDescription.style.display = 'none'; } diff --git a/packages/wasm-sdk/src/queries/data_contract.rs b/packages/wasm-sdk/src/queries/data_contract.rs index 8e79467866e..5904269d06c 100644 --- a/packages/wasm-sdk/src/queries/data_contract.rs +++ b/packages/wasm-sdk/src/queries/data_contract.rs @@ -1,5 +1,6 @@ use crate::dpp::DataContractWasm; use crate::sdk::WasmSdk; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; use dash_sdk::platform::{DataContract, Fetch, FetchMany, Identifier}; use dash_sdk::platform::query::LimitQuery; use drive_proof_verifier::types::{DataContractHistory, DataContracts}; @@ -25,6 +26,36 @@ pub async fn data_contract_fetch( .map(Into::into) } +#[wasm_bindgen] +pub async fn data_contract_fetch_with_proof_info( + sdk: &WasmSdk, + base58_id: &str, +) -> Result { + let id = Identifier::from_string( + base58_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (contract, metadata, proof) = DataContract::fetch_with_metadata_and_proof(sdk, id, None) + .await?; + + match contract { + Some(contract) => { + let response = ProofMetadataResponse { + data: contract.to_json(&dash_sdk::dpp::version::PlatformVersion::get(sdk.version()).unwrap())?, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } + None => Err(JsError::new("Data contract not found")), + } +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct DataContractHistoryResponse { @@ -112,6 +143,101 @@ pub async fn get_data_contracts(sdk: &WasmSdk, ids: Vec) -> Result, + _offset: Option, + start_at_ms: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create query with start timestamp + let query = LimitQuery { + query: (contract_id, start_at_ms.unwrap_or(0)), + start_info: None, + limit, + }; + + // Fetch contract history with proof + let (history_result, metadata, proof) = DataContractHistory::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract history with proof: {}", e)))?; + + // Convert to response format + let mut versions = BTreeMap::new(); + let platform_version = sdk.as_ref().version(); + + if let Some(history) = history_result { + for (revision, contract) in history { + versions.insert(revision, contract.to_json(platform_version)?); + } + } + + let data = DataContractHistoryResponse { versions }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_data_contracts_with_proof_info(sdk: &WasmSdk, ids: Vec) -> Result { + // Parse all contract IDs + let identifiers: Result, _> = ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identifiers = identifiers?; + + // Fetch all contracts with proof + let (contracts_result, metadata, proof) = DataContract::fetch_many_with_metadata_and_proof(sdk.as_ref(), identifiers, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contracts with proof: {}", e)))?; + + // Convert to response format + let mut data_contracts = BTreeMap::new(); + let platform_version = sdk.as_ref().version(); + for (id, contract_opt) in contracts_result { + let id_str = id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58); + let contract_json = match contract_opt { + Some(contract) => Some(contract.to_json(platform_version)?), + None => None, + }; + data_contracts.insert(id_str, contract_json); + } + + let data = DataContractsResponse { data_contracts }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + // Use json_compatible serializer let serializer = serde_wasm_bindgen::Serializer::json_compatible(); response.serialize(&serializer) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 7f4fa2dbf32..be4e1d16f6d 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -1,8 +1,10 @@ use crate::sdk::WasmSdk; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue, JsCast}; use serde::{Serialize, Deserialize}; use dash_sdk::platform::Fetch; +use drive_proof_verifier::types::Documents; use dash_sdk::dpp::prelude::Identifier; use dash_sdk::dpp::document::Document; use dash_sdk::dpp::document::DocumentV0Getters; @@ -311,6 +313,120 @@ pub async fn get_documents( .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } +#[wasm_bindgen] +pub async fn get_documents_with_proof_info( + sdk: &WasmSdk, + data_contract_id: &str, + document_type: &str, + where_clause: Option, + order_by: Option, + limit: Option, + start_after: Option, + start_at: Option, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::Documents; + + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create base document query + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + document_type, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Set limit if provided + if let Some(limit_val) = limit { + query.limit = limit_val; + } else { + query.limit = 100; // Default limit + } + + // Handle start parameters + if let Some(start_after_id) = start_after { + let doc_id = Identifier::from_string( + &start_after_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + query.start = Some(dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::Start::StartAfter( + doc_id.to_vec() + )); + } else if let Some(start_at_id) = start_at { + let doc_id = Identifier::from_string( + &start_at_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + query.start = Some(dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::Start::StartAt( + doc_id.to_vec() + )); + } + + // Parse and set where clauses if provided + if let Some(where_json) = where_clause { + let clauses: Vec = serde_json::from_str(&where_json) + .map_err(|e| JsError::new(&format!("Invalid where clause JSON: {}", e)))?; + + for clause_json in clauses { + let where_clause = parse_where_clause(&clause_json)?; + query.where_clauses.push(where_clause); + } + } + + // Parse and set order by clauses if provided + if let Some(order_json) = order_by { + let clauses: Vec = serde_json::from_str(&order_json) + .map_err(|e| JsError::new(&format!("Invalid order by JSON: {}", e)))?; + + for clause_json in clauses { + let order_clause = parse_order_clause(&clause_json)?; + query = query.with_order_by(order_clause); + } + } + + // Execute query with proof + let (documents_result, metadata, proof) = Document::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch documents: {}", e)))?; + + // Fetch the data contract to get the document type + let data_contract = dash_sdk::platform::DataContract::fetch(sdk.as_ref(), contract_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsError::new("Data contract not found"))?; + + // Get the document type + let document_type_ref = data_contract + .document_type_for_name(document_type) + .map_err(|e| JsError::new(&format!("Document type not found: {}", e)))?; + + // Convert documents to response format + let mut responses: Vec = Vec::new(); + for (_, doc_opt) in documents_result { + if let Some(doc) = doc_opt { + responses.push(DocumentResponse::from_document(&doc, &data_contract, document_type_ref)?); + } + } + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + #[wasm_bindgen] pub async fn get_document( sdk: &WasmSdk, @@ -370,6 +486,83 @@ pub async fn get_document( } } +#[wasm_bindgen] +pub async fn get_document_with_proof_info( + sdk: &WasmSdk, + data_contract_id: &str, + document_type: &str, + document_id: &str, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + + // Parse IDs + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let doc_id = Identifier::from_string( + document_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create document query + let query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + document_type, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + // Fetch the data contract to get the document type + let data_contract = dash_sdk::platform::DataContract::fetch(sdk.as_ref(), contract_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsError::new("Data contract not found"))?; + + // Get the document type + let document_type_ref = data_contract + .document_type_for_name(document_type) + .map_err(|e| JsError::new(&format!("Document type not found: {}", e)))?; + + // Execute query with proof + let (document_result, metadata, proof) = Document::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch document: {}", e)))?; + + match document_result { + Some(doc) => { + let doc_response = DocumentResponse::from_document(&doc, &data_contract, document_type_ref)?; + + let response = ProofMetadataResponse { + data: doc_response, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + }, + None => { + // Return null data with proof + let response = ProofMetadataResponse { + data: Option::::None, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } + } +} + #[wasm_bindgen] pub async fn get_dpns_usernames( sdk: &WasmSdk, @@ -465,4 +658,121 @@ pub async fn get_dpns_username( } Ok(JsValue::NULL) +} + +// Proof info versions for DPNS queries + +#[wasm_bindgen] +pub async fn get_dpns_usernames_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + limit: Option, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::Documents; + + // DPNS contract ID on testnet + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + const DPNS_DOCUMENT_TYPE: &str = "domain"; + + // Parse identity ID + let identity_id_parsed = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse DPNS contract ID + let contract_id = Identifier::from_string( + DPNS_CONTRACT_ID, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create document query for DPNS domains owned by this identity + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + DPNS_DOCUMENT_TYPE, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Query by records.identity using the identityId index + let where_clause = WhereClause { + field: "records.identity".to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(identity_id_parsed.to_buffer()), + }; + + query = query.with_where(where_clause); + + // Set limit from parameter or default to 10 + query.limit = limit.unwrap_or(10); + + // Execute query with proof + let (documents_result, metadata, proof) = Document::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch DPNS documents with proof: {}", e)))?; + + // Collect all usernames + let mut usernames: Vec = Vec::new(); + + // Process all results + for (_, doc_opt) in documents_result { + if let Some(doc) = doc_opt { + // Extract the username from the document + let properties = doc.properties(); + + if let (Some(Value::Text(label)), Some(Value::Text(parent_domain))) = ( + properties.get("label"), + properties.get("normalizedParentDomainName") + ) { + // Construct the full username + let username = format!("{}.{}", label, parent_domain); + usernames.push(username); + } + } + } + + let response = ProofMetadataResponse { + data: usernames, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_dpns_username_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, +) -> Result { + // Call the new function with limit 1 + let result = get_dpns_usernames_with_proof_info(sdk, identity_id, Some(1)).await?; + + // The result already contains proof info, just modify the data field + // Parse the result to extract first username + let result_obj: serde_json::Value = serde_wasm_bindgen::from_value(result.clone())?; + + if let Some(data_array) = result_obj.get("data").and_then(|d| d.as_array()) { + if let Some(first_username) = data_array.first() { + // Create a new response with just the first username + let mut modified_result = result_obj.clone(); + modified_result["data"] = first_username.clone(); + + return serde_wasm_bindgen::to_value(&modified_result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))); + } + } + + // If no username found, return null data with proof info + let mut modified_result = result_obj.clone(); + modified_result["data"] = serde_json::Value::Null; + + serde_wasm_bindgen::to_value(&modified_result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs index 0ddf1d455a0..f244375d1f0 100644 --- a/packages/wasm-sdk/src/queries/epoch.rs +++ b/packages/wasm-sdk/src/queries/epoch.rs @@ -1,12 +1,17 @@ use crate::sdk::WasmSdk; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; use dash_sdk::platform::{FetchMany, LimitQuery}; use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; use dash_sdk::dpp::block::extended_epoch_info::v0::ExtendedEpochInfoV0Getters; use dash_sdk::dpp::dashcore::hashes::Hash; +use dash_sdk::dpp::dashcore::ProTxHash; +use std::collections::BTreeMap; +use std::str::FromStr; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -19,6 +24,12 @@ struct EpochInfo { protocol_version: u32, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct EvonodesProposedBlocksResponse { + evonodes_proposed_block_counts: BTreeMap, +} + impl From for EpochInfo { fn from(epoch: ExtendedEpochInfo) -> Self { Self { @@ -155,18 +166,11 @@ pub async fn get_evonodes_proposed_epoch_blocks_by_ids( epoch: u32, ids: Vec, ) -> Result { - use dash_sdk::dpp::dashcore::ProTxHash; - use std::str::FromStr; - use dash_sdk::platform::FetchMany; + use dash_sdk::platform::FetchMany; use drive_proof_verifier::types::ProposerBlockCountById; - // Parse all ProTxHashes - let pro_tx_hashes: Result, _> = ids - .iter() - .map(|id| ProTxHash::from_str(id)) - .collect(); - let pro_tx_hashes = pro_tx_hashes - .map_err(|e| JsError::new(&format!("Invalid ProTxHash: {}", e)))?; + // Silence unused variables since this function is not yet implemented + let _ = (sdk, ids); // Check if epoch fits in u16 before casting if epoch > u16::MAX as u32 { @@ -221,9 +225,7 @@ pub async fn get_evonodes_proposed_epoch_blocks_by_range( ) -> Result { use dash_sdk::platform::types::proposed_blocks::ProposedBlockCountEx; use drive_proof_verifier::types::ProposerBlockCounts; - use dash_sdk::dpp::dashcore::ProTxHash; - use std::str::FromStr; - use dash_sdk::platform::QueryStartInfo; + use dash_sdk::platform::QueryStartInfo; // Parse start_after if provided let start_info = if let Some(start) = start_after { @@ -294,4 +296,182 @@ pub async fn get_current_epoch(sdk: &WasmSdk) -> Result { serde_wasm_bindgen::to_value(&epoch_info) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_epochs_info_with_proof_info( + sdk: &WasmSdk, + start_epoch: Option, + count: Option, + ascending: Option, +) -> Result { + use dash_sdk::platform::types::epoch::EpochQuery; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + let query = LimitQuery { + query: EpochQuery { + start: start_epoch, + ascending: ascending.unwrap_or(true), + }, + limit: count, + start_info: None, + }; + + let (epochs_result, metadata, proof) = ExtendedEpochInfo::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch epochs info with proof: {}", e)))?; + + // Convert to our response format + let epochs: Vec = epochs_result + .into_iter() + .filter_map(|(_, epoch_opt)| epoch_opt.map(Into::into)) + .collect(); + + let response = ProofMetadataResponse { + data: epochs, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_current_epoch_with_proof_info(sdk: &WasmSdk) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + let (epoch, metadata, proof) = ExtendedEpochInfo::fetch_current_with_metadata_and_proof(sdk.as_ref()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch current epoch with proof: {}", e)))?; + + let epoch_info = EpochInfo::from(epoch); + + let response = ProofMetadataResponse { + data: epoch_info, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Additional proof info versions for epoch queries + +#[wasm_bindgen] +pub async fn get_finalized_epoch_infos_with_proof_info( + sdk: &WasmSdk, + start_epoch: Option, + count: Option, + ascending: Option, +) -> Result { + use dash_sdk::platform::types::finalized_epoch::FinalizedEpochQuery; + use drive_proof_verifier::types::FinalizedEpochInfos; + + if start_epoch.is_none() { + return Err(JsError::new("start_epoch is required for finalized epoch queries")); + } + + let start = start_epoch.unwrap(); + let is_ascending = ascending.unwrap_or(true); + let limit = count.unwrap_or(100); + + // Ensure limit is at least 1 to avoid underflow + let limit = limit.max(1); + + // Calculate end epoch based on direction and limit + let end_epoch = if is_ascending { + start.saturating_add((limit - 1) as u16) + } else { + start.saturating_sub((limit - 1) as u16) + }; + + let query = if is_ascending { + FinalizedEpochQuery { + start_epoch_index: start, + start_epoch_index_included: true, + end_epoch_index: end_epoch, + end_epoch_index_included: true, + } + } else { + FinalizedEpochQuery { + start_epoch_index: end_epoch, + start_epoch_index_included: true, + end_epoch_index: start, + end_epoch_index_included: true, + } + }; + + let (epochs_result, metadata, proof) = dash_sdk::dpp::block::finalized_epoch_info::FinalizedEpochInfo::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch finalized epochs info with proof: {}", e)))?; + + // Convert to our response format and sort by epoch index + let mut epochs: Vec = epochs_result + .into_iter() + .filter_map(|(epoch_index, epoch_opt)| { + epoch_opt.map(|epoch| { + use dash_sdk::dpp::block::finalized_epoch_info::v0::getters::FinalizedEpochInfoGettersV0; + EpochInfo { + index: epoch_index as u16, + first_core_block_height: epoch.first_core_block_height(), + first_block_height: epoch.first_block_height(), + start_time: epoch.first_block_time(), + fee_multiplier: epoch.fee_multiplier_permille() as f64 / 1000.0, + protocol_version: epoch.protocol_version(), + } + }) + }) + .collect(); + + // Sort based on ascending flag + epochs.sort_by(|a, b| { + if is_ascending { + a.index.cmp(&b.index) + } else { + b.index.cmp(&a.index) + } + }); + + let response = ProofMetadataResponse { + data: epochs, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_evonodes_proposed_epoch_blocks_by_ids_with_proof_info( + sdk: &WasmSdk, + epoch: u16, + pro_tx_hashes: Vec, +) -> Result { + // TODO: Implement once SDK Query trait is implemented for ProposerBlockCountById + // Currently not supported due to query format issues + let _ = (sdk, epoch, pro_tx_hashes); // Parameters will be used when implemented + Err(JsError::new("get_evonodes_proposed_epoch_blocks_by_ids_with_proof_info is not yet implemented")) +} + +#[wasm_bindgen] +pub async fn get_evonodes_proposed_epoch_blocks_by_range_with_proof_info( + sdk: &WasmSdk, + epoch: u16, + limit: Option, + start_after: Option, + order_ascending: Option, +) -> Result { + // TODO: Implement once SDK Query trait is implemented for ProposerBlockCountByRange + // Currently not supported due to query format issues + let _ = (sdk, epoch, limit, start_after, order_ascending); // Parameters will be used when implemented + Err(JsError::new("get_evonodes_proposed_epoch_blocks_by_range_with_proof_info is not yet implemented")) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/group.rs b/packages/wasm-sdk/src/queries/group.rs index f0be03a47a4..23243152e3b 100644 --- a/packages/wasm-sdk/src/queries/group.rs +++ b/packages/wasm-sdk/src/queries/group.rs @@ -12,9 +12,11 @@ use dash_sdk::dpp::group::group_action_status::GroupActionStatus; use dash_sdk::dpp::data_contract::group::GroupMemberPower; use std::collections::BTreeMap; +// Proof info functions are now included below + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct GroupInfoResponse { +pub struct GroupInfoResponse { members: BTreeMap, required_power: u32, } @@ -244,14 +246,14 @@ pub async fn get_identity_groups( #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct GroupsDataContractInfo { +pub struct GroupsDataContractInfo { data_contract_id: String, groups: Vec, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct GroupContractPositionInfo { +pub struct GroupContractPositionInfo { position: u32, group: GroupInfoResponse, } @@ -514,4 +516,552 @@ pub async fn get_groups_data_contracts( let serializer = serde_wasm_bindgen::Serializer::json_compatible(); results.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Proof versions for group queries + +#[wasm_bindgen] +pub async fn get_group_info_with_proof_info( + sdk: &WasmSdk, + data_contract_id: &str, + group_contract_position: u32, +) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create group query + let query = GroupQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + }; + + // Fetch group with proof + let (group_result, metadata, proof) = Group::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group with proof: {}", e)))?; + + let data = group_result.map(|group| GroupInfoResponse::from_group(&group)); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_group_infos_with_proof_info( + sdk: &WasmSdk, + contract_id: &str, + start_at_info: JsValue, + count: Option, +) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse start at info if provided + let start_group_contract_position = if !start_at_info.is_null() && !start_at_info.is_undefined() { + let info = serde_wasm_bindgen::from_value::(start_at_info); + match info { + Ok(json) => { + let position = json["position"].as_u64().ok_or_else(|| JsError::new("Invalid start position"))? as u32; + let included = json["included"].as_bool().unwrap_or(false); + Some((position as GroupContractPosition, included)) + } + Err(_) => None + } + } else { + None + }; + + // Create query + let query = GroupInfosQuery { + contract_id, + start_group_contract_position, + limit: count.map(|c| c as u16), + }; + + // Fetch groups with proof + let (groups_result, metadata, proof) = Group::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups with proof: {}", e)))?; + + // Convert result to response format + let mut group_infos = Vec::new(); + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + group_infos.push(GroupContractPositionInfo { + position: position as u32, + group: GroupInfoResponse::from_group(&group), + }); + } + } + + let data = serde_json::json!({ + "groupInfos": group_infos + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Additional proof info versions for remaining group queries +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + +#[wasm_bindgen] +pub async fn get_group_members_with_proof_info( + sdk: &WasmSdk, + data_contract_id: &str, + group_contract_position: u32, + member_ids: Option>, + start_at: Option, + limit: Option, +) -> Result { + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create group query + let query = GroupQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + }; + + // Fetch the group with proof + let (group_result, metadata, proof) = Group::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group with proof: {}", e)))?; + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct GroupMember { + member_id: String, + power: u32, + } + + let data = match group_result { + Some(group) => { + let mut members: Vec = Vec::new(); + + // If specific member IDs are requested, filter by them + if let Some(requested_ids) = member_ids { + let requested_identifiers: Result, _> = requested_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let requested_identifiers = requested_identifiers?; + + for id in requested_identifiers { + if let Ok(power) = group.member_power(id) { + members.push(GroupMember { + member_id: id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + power, + }); + } + } + } else { + // Return all members with pagination + let all_members = group.members(); + let mut sorted_members: Vec<_> = all_members.iter().collect(); + sorted_members.sort_by_key(|(id, _)| *id); + + // Apply start_at if provided + let start_index = if let Some(start_id) = start_at { + let start_identifier = Identifier::from_string( + &start_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + sorted_members.iter().position(|(id, _)| **id > start_identifier).unwrap_or(sorted_members.len()) + } else { + 0 + }; + + // Apply limit + let end_index = if let Some(lim) = limit { + (start_index + lim as usize).min(sorted_members.len()) + } else { + sorted_members.len() + }; + + for (id, power) in &sorted_members[start_index..end_index] { + members.push(GroupMember { + member_id: (*id).to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + power: **power, + }); + } + } + + Some(members) + }, + None => None, + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_groups_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + member_data_contracts: Option>, + owner_data_contracts: Option>, + moderator_data_contracts: Option>, +) -> Result { + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct IdentityGroupInfo { + data_contract_id: String, + group_contract_position: u32, + role: String, // "member", "owner", or "moderator" + power: Option, // Only for members + } + + // Parse identity ID + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let mut groups: Vec = Vec::new(); + let mut combined_metadata: Option = None; + let mut combined_proof: Option = None; + + // Check member data contracts + if let Some(contracts) = member_data_contracts { + for contract_id_str in contracts { + let contract_id = Identifier::from_string( + &contract_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all groups for this contract with proof + let query = GroupInfosQuery { + contract_id, + start_group_contract_position: None, + limit: None, + }; + + let (groups_result, metadata, proof) = Group::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups with proof: {}", e)))?; + + // Store first metadata and proof + if combined_metadata.is_none() { + combined_metadata = Some(metadata.into()); + combined_proof = Some(proof.into()); + } + + // Check each group for the identity + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + if let Ok(power) = group.member_power(id) { + groups.push(IdentityGroupInfo { + data_contract_id: contract_id_str.clone(), + group_contract_position: position as u32, + role: "member".to_string(), + power: Some(power), + }); + } + } + } + } + } + + // Note: Owner and moderator roles would require additional contract queries + // which are not yet implemented in the SDK. For now, return a warning. + if owner_data_contracts.is_some() || moderator_data_contracts.is_some() { + web_sys::console::warn_1(&JsValue::from_str( + "Warning: Owner and moderator role queries are not yet implemented" + )); + } + + let response = ProofMetadataResponse { + data: groups, + metadata: combined_metadata.unwrap_or_else(|| ResponseMetadata { + height: 0, + core_chain_locked_height: 0, + epoch: 0, + time_ms: 0, + protocol_version: 0, + chain_id: String::new(), + }), + proof: combined_proof.unwrap_or_else(|| ProofInfo { + grovedb_proof: String::new(), + quorum_hash: String::new(), + signature: String::new(), + round: 0, + block_id_hash: String::new(), + quorum_type: 0, + }), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_group_actions_with_proof_info( + sdk: &WasmSdk, + contract_id: &str, + group_contract_position: u32, + status: &str, + start_at_info: JsValue, + count: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse status + let status = match status { + "ACTIVE" => GroupActionStatus::ActionActive, + "CLOSED" => GroupActionStatus::ActionClosed, + _ => return Err(JsError::new(&format!("Invalid status: {}. Must be ACTIVE or CLOSED", status))), + }; + + // Parse start action ID if provided + let start_at_action_id = if !start_at_info.is_null() && !start_at_info.is_undefined() { + let info = serde_wasm_bindgen::from_value::(start_at_info); + match info { + Ok(json) => { + let action_id = json["actionId"].as_str().ok_or_else(|| JsError::new("Invalid action ID"))?; + let included = json["included"].as_bool().unwrap_or(false); + Some(( + Identifier::from_string( + action_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?, + included + )) + } + Err(_) => None + } + } else { + None + }; + + // Create query + let query = GroupActionsQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + status, + start_at_action_id, + limit: count.map(|c| c as u16), + }; + + // Fetch actions with proof + let (actions_result, metadata, proof) = GroupAction::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group actions with proof: {}", e)))?; + + // Convert result to response format + let mut group_actions = Vec::new(); + for (action_id, action_opt) in actions_result { + if let Some(_action) = action_opt { + // For now, just return the action ID + // The full action structure requires custom serialization + group_actions.push(serde_json::json!({ + "actionId": action_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + // TODO: Serialize the full action event structure + })); + } + } + + let data = serde_json::json!({ + "groupActions": group_actions + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_group_action_signers_with_proof_info( + sdk: &WasmSdk, + contract_id: &str, + group_contract_position: u32, + status: &str, + action_id: &str, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse action ID + let action_id = Identifier::from_string( + action_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse status + let status = match status { + "ACTIVE" => GroupActionStatus::ActionActive, + "CLOSED" => GroupActionStatus::ActionClosed, + _ => return Err(JsError::new(&format!("Invalid status: {}. Must be ACTIVE or CLOSED", status))), + }; + + // Create query + let query = GroupActionSignersQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + status, + action_id, + }; + + // Fetch signers with proof + let (signers_result, metadata, proof) = GroupMemberPower::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group action signers with proof: {}", e)))?; + + // Convert result to response format + let mut signers = Vec::new(); + for (signer_id, power_opt) in signers_result { + if let Some(power) = power_opt { + signers.push(serde_json::json!({ + "signerId": signer_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + "power": power + })); + } + } + + let data = serde_json::json!({ + "signers": signers + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_groups_data_contracts_with_proof_info( + sdk: &WasmSdk, + data_contract_ids: Vec, +) -> Result { + let mut results: Vec = Vec::new(); + let mut combined_metadata: Option = None; + let mut combined_proof: Option = None; + + for contract_id_str in data_contract_ids { + let contract_id = Identifier::from_string( + &contract_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all groups for this contract with proof + let query = GroupInfosQuery { + contract_id, + start_group_contract_position: None, + limit: None, + }; + + let (groups_result, metadata, proof) = Group::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups for contract {} with proof: {}", contract_id_str, e)))?; + + // Store first metadata and proof + if combined_metadata.is_none() { + combined_metadata = Some(metadata.into()); + combined_proof = Some(proof.into()); + } + + let mut groups: Vec = Vec::new(); + + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + groups.push(GroupContractPositionInfo { + position: position as u32, + group: GroupInfoResponse::from_group(&group), + }); + } + } + + results.push(GroupsDataContractInfo { + data_contract_id: contract_id_str, + groups, + }); + } + + let response = ProofMetadataResponse { + data: results, + metadata: combined_metadata.unwrap_or_else(|| ResponseMetadata { + height: 0, + core_chain_locked_height: 0, + epoch: 0, + time_ms: 0, + protocol_version: 0, + chain_id: String::new(), + }), + proof: combined_proof.unwrap_or_else(|| ProofInfo { + grovedb_proof: String::new(), + quorum_hash: String::new(), + signature: String::new(), + round: 0, + block_id_hash: String::new(), + quorum_type: 0, + }), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/identity.rs b/packages/wasm-sdk/src/queries/identity.rs index 3ca70fc9230..27f4ec3baa1 100644 --- a/packages/wasm-sdk/src/queries/identity.rs +++ b/packages/wasm-sdk/src/queries/identity.rs @@ -1,12 +1,18 @@ use crate::dpp::IdentityWasm; use crate::sdk::WasmSdk; -use dash_sdk::platform::{Fetch, FetchMany, Identifier, Identity}; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; +use dash_sdk::platform::{Fetch, FetchMany, FetchUnproved, Identifier, Identity}; use dash_sdk::dpp::identity::identity_public_key::IdentityPublicKey; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; use js_sys::Array; +use rs_dapi_client::IntoInner; +use std::collections::BTreeMap; + +// Proof info functions are now included below #[wasm_bindgen] pub async fn identity_fetch(sdk: &WasmSdk, base58_id: &str) -> Result { @@ -21,9 +27,81 @@ pub async fn identity_fetch(sdk: &WasmSdk, base58_id: &str) -> Result Result { + let id = Identifier::from_string( + base58_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (identity, metadata, proof) = Identity::fetch_with_metadata_and_proof(sdk, id, None) + .await?; + + match identity { + Some(identity) => { + // Convert identity to JSON value first + let identity_json = IdentityWasm::from(identity).to_json() + .map_err(|e| JsError::new(&format!("Failed to convert identity to JSON: {:?}", e)))?; + let identity_value: serde_json::Value = serde_wasm_bindgen::from_value(identity_json)?; + + let response = ProofMetadataResponse { + data: identity_value, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } + None => Err(JsError::new("Identity not found")), + } +} + +#[wasm_bindgen] +pub async fn identity_fetch_unproved(sdk: &WasmSdk, base58_id: &str) -> Result { + use dash_sdk::platform::proto::get_identity_request::{GetIdentityRequestV0, Version as GetIdentityRequestVersion}; + use dash_sdk::platform::proto::get_identity_response::{get_identity_response_v0, GetIdentityResponseV0, Version}; + use dash_sdk::platform::proto::{GetIdentityRequest, GetIdentityResponse}; + use rs_dapi_client::{DapiRequest, RequestSettings}; + + let id = Identifier::from_string( + base58_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let request = GetIdentityRequest { + version: Some(GetIdentityRequestVersion::V0(GetIdentityRequestV0 { + id: id.to_vec(), + prove: false, // Request without proof + })), + }; + + let response: GetIdentityResponse = request + .execute(sdk.as_ref(), RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity: {}", e)))? + .into_inner(); + + match response.version { + Some(Version::V0(GetIdentityResponseV0 { + result: Some(get_identity_response_v0::Result::Identity(identity_bytes)), + .. + })) => { + use dash_sdk::dpp::serialization::PlatformDeserializable; + let identity = Identity::deserialize_from_bytes( + identity_bytes.as_slice() + )?; + Ok(identity.into()) + } + _ => Err(JsError::new("Identity not found")), + } +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct IdentityKeyResponse { +pub(crate) struct IdentityKeyResponse { key_id: u32, key_type: String, public_key_data: String, @@ -45,6 +123,10 @@ pub async fn get_identity_keys( // DapiRequestExecutor not needed anymore + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + let id = Identifier::from_string( identity_id, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, @@ -98,19 +180,25 @@ pub async fn get_identity_keys( #[wasm_bindgen] pub async fn get_identity_nonce(sdk: &WasmSdk, identity_id: &str) -> Result { - use dash_sdk::dpp::prelude::IdentityNonce; + use drive_proof_verifier::types::IdentityNonceFetcher; use dash_sdk::platform::Fetch; + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + let id = Identifier::from_string( identity_id, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, )?; - let nonce_result = IdentityNonce::fetch(sdk.as_ref(), id) + let nonce_result = IdentityNonceFetcher::fetch(sdk.as_ref(), id) .await .map_err(|e| JsError::new(&format!("Failed to fetch identity nonce: {}", e)))?; - let nonce = nonce_result.ok_or_else(|| JsError::new("Identity nonce not found"))?; + let nonce = nonce_result + .map(|fetcher| fetcher.0) + .ok_or_else(|| JsError::new("Identity nonce not found"))?; // Return as a JSON object with nonce as string to avoid BigInt serialization issues #[derive(Serialize)] @@ -122,7 +210,47 @@ pub async fn get_identity_nonce(sdk: &WasmSdk, identity_id: &str) -> Result Result { + use drive_proof_verifier::types::IdentityNonceFetcher; + use dash_sdk::platform::Fetch; + + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (nonce_result, metadata, proof) = IdentityNonceFetcher::fetch_with_metadata_and_proof(sdk.as_ref(), id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity nonce with proof: {}", e)))?; + + let nonce = nonce_result + .map(|fetcher| fetcher.0) + .ok_or_else(|| JsError::new("Identity nonce not found"))?; + + let data = serde_json::json!({ + "nonce": nonce.to_string() + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } @@ -135,6 +263,14 @@ pub async fn get_identity_contract_nonce( use drive_proof_verifier::types::IdentityContractNonceFetcher; use dash_sdk::platform::Fetch; + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + if contract_id.is_empty() { + return Err(JsError::new("Contract ID is required")); + } + let identity_id = Identifier::from_string( identity_id, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, @@ -163,7 +299,64 @@ pub async fn get_identity_contract_nonce( nonce: nonce.to_string(), }; - serde_wasm_bindgen::to_value(&response) + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_contract_nonce_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + contract_id: &str, +) -> Result { + use drive_proof_verifier::types::IdentityContractNonceFetcher; + use dash_sdk::platform::Fetch; + + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + if contract_id.is_empty() { + return Err(JsError::new("Contract ID is required")); + } + + let identity_id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (nonce_result, metadata, proof) = IdentityContractNonceFetcher::fetch_with_metadata_and_proof( + sdk.as_ref(), + (identity_id, contract_id), + None + ) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity contract nonce with proof: {}", e)))?; + + let nonce = nonce_result + .map(|fetcher| fetcher.0) + .ok_or_else(|| JsError::new("Identity contract nonce not found"))?; + + let data = serde_json::json!({ + "nonce": nonce.to_string() + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } @@ -172,6 +365,10 @@ pub async fn get_identity_balance(sdk: &WasmSdk, id: &str) -> Result Result Result) - #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct IdentityBalanceAndRevisionResponse { +pub(crate) struct IdentityBalanceAndRevisionResponse { balance: String, // String to handle large numbers revision: u64, } @@ -255,6 +454,10 @@ pub async fn get_identity_balance_and_revision(sdk: &WasmSdk, identity_id: &str) use drive_proof_verifier::types::IdentityBalanceAndRevision; use dash_sdk::platform::Fetch; + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + let id = Identifier::from_string( identity_id, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, @@ -270,7 +473,9 @@ pub async fn get_identity_balance_and_revision(sdk: &WasmSdk, identity_id: &str) revision: balance_and_revision.1, }; - serde_wasm_bindgen::to_value(&response) + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } else { Err(JsError::new("Identity balance and revision not found")) @@ -303,7 +508,7 @@ pub async fn get_identity_by_public_key_hash(sdk: &WasmSdk, public_key_hash: &st #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct IdentityContractKeyResponse { +pub(crate) struct IdentityContractKeyResponse { identity_id: String, purpose: u32, key_id: u32, @@ -316,7 +521,7 @@ struct IdentityContractKeyResponse { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct IdentityContractKeysResponse { +pub(crate) struct IdentityContractKeysResponse { identity_id: String, keys: Vec, } @@ -478,7 +683,7 @@ pub async fn get_identity_by_non_unique_public_key_hash( #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct TokenBalanceResponse { +pub(crate) struct TokenBalanceResponse { token_id: String, balance: String, // String to handle large numbers } @@ -534,4 +739,494 @@ pub async fn get_identity_token_balances( serde_wasm_bindgen::to_value(&responses) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Proof info versions for identity queries + +#[wasm_bindgen] +pub async fn get_identity_keys_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + key_request_type: &str, + specific_key_ids: Option>, + limit: Option, + offset: Option, +) -> Result { + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all keys for now - TODO: implement specific key request once available in SDK + if key_request_type != "all" { + return Err(JsError::new("Currently only 'all' key request type is supported")); + } + + // Use FetchMany to get identity keys with proof + let (keys_result, metadata, proof) = IdentityPublicKey::fetch_many_with_metadata_and_proof(sdk.as_ref(), id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity keys with proof: {}", e)))?; + + // Convert keys to response format + let mut keys: Vec = Vec::new(); + + // Apply offset and limit if provided + let start = offset.unwrap_or(0) as usize; + let end = if let Some(lim) = limit { + start + lim as usize + } else { + usize::MAX + }; + + for (idx, (key_id, key_opt)) in keys_result.into_iter().enumerate() { + if idx < start { + continue; + } + if idx >= end { + break; + } + + if let Some(key) = key_opt { + keys.push(IdentityKeyResponse { + key_id: 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(), + }); + } + } + + let response = ProofMetadataResponse { + data: keys, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_balance_with_proof_info(sdk: &WasmSdk, id: &str) -> Result { + use drive_proof_verifier::types::IdentityBalance; + use dash_sdk::platform::Fetch; + + if id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + let identity_id = Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (balance_result, metadata, proof) = IdentityBalance::fetch_with_metadata_and_proof(sdk.as_ref(), identity_id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity balance with proof: {}", e)))?; + + if let Some(balance) = balance_result { + #[derive(Serialize)] + struct BalanceResponse { + balance: String, + } + + let data = BalanceResponse { + balance: balance.to_string(), + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new("Identity balance not found")) + } +} + +#[wasm_bindgen] +pub async fn get_identities_balances_with_proof_info(sdk: &WasmSdk, identity_ids: Vec) -> Result { + use drive_proof_verifier::types::IdentityBalance; + + // Convert string IDs to Identifiers + let identifiers: Vec = identity_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + let (balances_result, metadata, proof): (drive_proof_verifier::types::IdentityBalances, _, _) = IdentityBalance::fetch_many_with_metadata_and_proof(sdk.as_ref(), identifiers.clone(), None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities balances with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = identifiers + .into_iter() + .filter_map(|id| { + balances_result.get(&id).and_then(|balance_opt| { + balance_opt.map(|balance| { + IdentityBalanceResponse { + identity_id: id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + balance: balance.to_string(), + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_balance_and_revision_with_proof_info(sdk: &WasmSdk, identity_id: &str) -> Result { + use drive_proof_verifier::types::IdentityBalanceAndRevision; + use dash_sdk::platform::Fetch; + + if identity_id.is_empty() { + return Err(JsError::new("Identity ID is required")); + } + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let (result, metadata, proof) = IdentityBalanceAndRevision::fetch_with_metadata_and_proof(sdk.as_ref(), id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity balance and revision with proof: {}", e)))?; + + if let Some(balance_and_revision) = result { + let data = IdentityBalanceAndRevisionResponse { + balance: balance_and_revision.0.to_string(), + revision: balance_and_revision.1, + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new("Identity balance and revision not found")) + } +} + +#[wasm_bindgen] +pub async fn get_identity_by_public_key_hash_with_proof_info(sdk: &WasmSdk, public_key_hash: &str) -> Result { + use dash_sdk::platform::types::identity::PublicKeyHash; + + // Parse the hex-encoded public key hash + let hash_bytes = hex::decode(public_key_hash) + .map_err(|e| JsError::new(&format!("Invalid public key hash hex: {}", e)))?; + + if hash_bytes.len() != 20 { + return Err(JsError::new("Public key hash must be 20 bytes (40 hex characters)")); + } + + let mut hash_array = [0u8; 20]; + hash_array.copy_from_slice(&hash_bytes); + + let (result, metadata, proof) = Identity::fetch_with_metadata_and_proof(sdk.as_ref(), PublicKeyHash(hash_array), None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity by public key hash with proof: {}", e)))?; + + match result { + Some(identity) => { + let identity_json = IdentityWasm::from(identity).to_json() + .map_err(|e| JsError::new(&format!("Failed to convert identity to JSON: {:?}", e)))?; + let identity_value: serde_json::Value = serde_wasm_bindgen::from_value(identity_json)?; + + let response = ProofMetadataResponse { + data: identity_value, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } + None => Err(JsError::new("Identity not found for public key hash")), + } +} + +#[wasm_bindgen] +pub async fn get_identity_by_non_unique_public_key_hash_with_proof_info( + sdk: &WasmSdk, + public_key_hash: &str, + start_after: Option, +) -> Result { + // Parse the hex-encoded public key hash + let hash_bytes = hex::decode(public_key_hash) + .map_err(|e| JsError::new(&format!("Invalid public key hash hex: {}", e)))?; + + if hash_bytes.len() != 20 { + return Err(JsError::new("Public key hash must be 20 bytes (40 hex characters)")); + } + + let mut hash_array = [0u8; 20]; + hash_array.copy_from_slice(&hash_bytes); + + // Convert start_after if provided + let start_id = if let Some(start) = start_after { + Some(Identifier::from_string( + &start, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?) + } else { + None + }; + + use dash_sdk::platform::types::identity::NonUniquePublicKeyHashQuery; + + let query = NonUniquePublicKeyHashQuery { + key_hash: hash_array, + after: start_id.map(|id| *id.as_bytes()), + }; + + // Fetch identity by non-unique public key hash with proof + let (identity, metadata, proof) = Identity::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities by non-unique public key hash with proof: {}", e)))?; + + // Return array with single identity if found + let results = if let Some(id) = identity { + vec![id] + } else { + vec![] + }; + + // Convert results to JSON + let identities_json: Vec = results + .into_iter() + .map(|identity| { + let identity_wasm: IdentityWasm = identity.into(); + let json = identity_wasm.to_json() + .map_err(|_| serde_wasm_bindgen::Error::new("Failed to convert identity to JSON"))?; + serde_wasm_bindgen::from_value(json) + }) + .collect::, _>>()?; + + let response = ProofMetadataResponse { + data: identities_json, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identities_contract_keys_with_proof_info( + sdk: &WasmSdk, + identities_ids: Vec, + contract_id: &str, + _document_type_name: Option, + purposes: Option>, +) -> Result { + use dash_sdk::dpp::identity::Purpose; + + // Convert string IDs to Identifiers + let _identity_ids: Vec = identities_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + // Contract ID is not used in the individual key queries, but we validate it + let _contract_identifier = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Convert purposes if provided + let purposes_opt = purposes.map(|p| { + p.into_iter() + .filter_map(|purpose_int| match purpose_int { + 0 => Some(Purpose::AUTHENTICATION as u32), + 1 => Some(Purpose::ENCRYPTION as u32), + 2 => Some(Purpose::DECRYPTION as u32), + 3 => Some(Purpose::TRANSFER as u32), + 4 => Some(Purpose::SYSTEM as u32), + 5 => Some(Purpose::VOTING as u32), + _ => None, + }) + .collect::>() + }); + + // For now, we'll implement this by fetching keys for each identity individually with proof + // The SDK doesn't fully expose the batch query with proof yet + let mut all_responses: Vec = Vec::new(); + let mut combined_metadata: Option = None; + let mut combined_proof: Option = None; + + for identity_id_str in identities_ids { + let identity_id = Identifier::from_string( + &identity_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Get keys for this identity using the regular identity keys query with proof + let (keys_result, metadata, proof) = IdentityPublicKey::fetch_many_with_metadata_and_proof(sdk.as_ref(), identity_id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch keys for identity {} with proof: {}", identity_id_str, e)))?; + + // Store first metadata and proof + if combined_metadata.is_none() { + combined_metadata = Some(metadata.into()); + combined_proof = Some(proof.into()); + } + + let mut identity_keys = Vec::new(); + + // Filter keys by purpose if specified + for (key_id, key_opt) in keys_result { + if let Some(key) = key_opt { + // Check if this key matches the requested purposes + if let Some(ref purposes) = purposes_opt { + if !purposes.contains(&(key.purpose() as u32)) { + continue; + } + } + + let key_response = IdentityKeyResponse { + key_id: 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(), + }; + identity_keys.push(key_response); + } + } + + if !identity_keys.is_empty() { + all_responses.push(IdentityContractKeysResponse { + identity_id: identity_id_str, + keys: identity_keys, + }); + } + } + + let response = ProofMetadataResponse { + data: all_responses, + metadata: combined_metadata.unwrap_or_else(|| ResponseMetadata { + height: 0, + core_chain_locked_height: 0, + epoch: 0, + time_ms: 0, + protocol_version: 0, + chain_id: String::new(), + }), + proof: combined_proof.unwrap_or_else(|| ProofInfo { + grovedb_proof: String::new(), + quorum_hash: String::new(), + signature: String::new(), + round: 0, + block_id_hash: String::new(), + quorum_type: 0, + }), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_token_balances_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + token_ids: Vec, +) -> Result { + use dash_sdk::platform::tokens::identity_token_balances::IdentityTokenBalancesQuery; + use dash_sdk::dpp::balances::credits::TokenAmount; + + let identity_id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Convert token IDs to Identifiers + let token_identifiers: Vec = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + let query = IdentityTokenBalancesQuery { + identity_id, + token_ids: token_identifiers.clone(), + }; + + // Use FetchMany trait to fetch token balances with proof + let (balances, metadata, proof): (dash_sdk::query_types::identity_token_balance::IdentityTokenBalances, _, _) = TokenAmount::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity token balances with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = token_identifiers + .into_iter() + .zip(token_ids.into_iter()) + .filter_map(|(token_id, token_id_str)| { + balances.get(&token_id).and_then(|balance_opt| { + balance_opt.map(|balance| TokenBalanceResponse { + token_id: token_id_str, + balance: balance.to_string(), + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index 512666d2c34..2eece79bfff 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -1,6 +1,7 @@ pub mod identity; pub mod data_contract; pub mod document; +pub mod dpns; pub mod protocol; pub mod epoch; pub mod token; @@ -12,9 +13,71 @@ pub mod system; pub use identity::*; pub use data_contract::*; pub use document::*; +pub use dpns::*; pub use protocol::*; pub use epoch::*; pub use token::*; pub use voting::*; pub use group::*; -pub use system::*; \ No newline at end of file +pub use system::*; + +use serde::{Serialize, Deserialize}; + +// Common response structure for queries with proof and metadata +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ProofMetadataResponse { + pub data: T, + pub metadata: ResponseMetadata, + pub proof: ProofInfo, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ResponseMetadata { + pub height: u64, + pub core_chain_locked_height: u32, + pub epoch: u32, + pub time_ms: u64, + pub protocol_version: u32, + pub chain_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ProofInfo { + pub grovedb_proof: String, // Base64 encoded + pub quorum_hash: String, // Hex encoded + pub signature: String, // Base64 encoded + pub round: u32, + pub block_id_hash: String, // Hex encoded + pub quorum_type: u32, +} + +// Helper function to convert platform ResponseMetadata to our ResponseMetadata +impl From for ResponseMetadata { + fn from(metadata: dash_sdk::platform::proto::ResponseMetadata) -> Self { + ResponseMetadata { + height: metadata.height, + core_chain_locked_height: metadata.core_chain_locked_height, + epoch: metadata.epoch, + time_ms: metadata.time_ms, + protocol_version: metadata.protocol_version, + chain_id: metadata.chain_id, + } + } +} + +// Helper function to convert platform Proof to our ProofInfo +impl From for ProofInfo { + fn from(proof: dash_sdk::platform::proto::Proof) -> Self { + ProofInfo { + grovedb_proof: base64::encode(&proof.grovedb_proof), + quorum_hash: hex::encode(&proof.quorum_hash), + signature: base64::encode(&proof.signature), + round: proof.round, + block_id_hash: hex::encode(&proof.block_id_hash), + quorum_type: proof.quorum_type, + } + } +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs index e25690463ae..93304e460f4 100644 --- a/packages/wasm-sdk/src/queries/protocol.rs +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -2,6 +2,7 @@ use crate::sdk::WasmSdk; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -99,4 +100,67 @@ pub async fn get_protocol_version_upgrade_vote_status( serde_wasm_bindgen::to_value(&votes) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Proof versions for protocol queries + +#[wasm_bindgen] +pub async fn get_protocol_version_upgrade_state_with_proof_info(sdk: &WasmSdk) -> Result { + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::ProtocolVersionVoteCount; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + let (upgrade_result, metadata, proof): (drive_proof_verifier::types::ProtocolVersionUpgrades, _, _) = ProtocolVersionVoteCount::fetch_many_with_metadata_and_proof(sdk.as_ref(), (), None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch protocol version upgrade state with proof: {}", e)))?; + + // Get the current protocol version from the SDK + let current_version = sdk.version(); + + // Find the next version with votes + let mut next_version = None; + let mut activation_height = None; + let mut vote_count = None; + let mut threshold_reached = false; + + for (version, height_opt) in upgrade_result.iter() { + if *version > current_version { + next_version = Some(*version); + activation_height = *height_opt; + vote_count = None; + threshold_reached = height_opt.is_some(); + break; + } + } + + let state = ProtocolVersionUpgradeState { + current_protocol_version: current_version, + next_protocol_version: next_version, + activation_height, + vote_count, + threshold_reached, + }; + + let response = ProofMetadataResponse { + data: state, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_protocol_version_upgrade_vote_status_with_proof_info( + sdk: &WasmSdk, + start_pro_tx_hash: &str, + count: u32, +) -> Result { + // TODO: Implement once a proper fetch_many_with_metadata_and_proof method is available for MasternodeProtocolVote + // The fetch_votes method has different parameters than fetch_many + let _ = (sdk, start_pro_tx_hash, count); // Parameters will be used when implemented + Err(JsError::new("get_protocol_version_upgrade_vote_status_with_proof_info is not yet implemented")) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 41a18a654e3..0ec3763c046 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -2,6 +2,7 @@ use crate::sdk::WasmSdk; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; use dash_sdk::dpp::core_types::validator_set::v0::ValidatorSetV0Getters; #[derive(Serialize, Deserialize, Debug)] @@ -200,7 +201,9 @@ pub async fn get_total_credits_in_platform(sdk: &WasmSdk) -> Result Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{TotalCreditsInPlatform as TotalCreditsQuery, NoParamQuery}; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + let (total_credits_result, metadata, proof) = TotalCreditsQuery::fetch_with_metadata_and_proof(sdk.as_ref(), NoParamQuery {}, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch total credits with proof: {}", e)))?; + + let data = if let Some(credits) = total_credits_result { + Some(TotalCreditsResponse { + total_credits_in_platform: credits.0.to_string(), + }) + } else { + None + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_prefunded_specialized_balance_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, +) -> Result { + use dash_sdk::platform::{Identifier, Fetch}; + use drive_proof_verifier::types::PrefundedSpecializedBalance as PrefundedBalance; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse identity ID + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch prefunded specialized balance with proof + let (balance_result, metadata, proof) = PrefundedBalance::fetch_with_metadata_and_proof(sdk.as_ref(), identity_identifier, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch prefunded specialized balance with proof: {}", e)))?; + + let data = PrefundedSpecializedBalance { + identity_id: identity_id.to_string(), + balance: balance_result.map(|b| b.0).unwrap_or(0), + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_path_elements_with_proof_info( + sdk: &WasmSdk, + keys: Vec, +) -> Result { + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::{KeysInPath, Elements}; + use dash_sdk::drive::grovedb::Element; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Convert string keys to byte vectors + let key_bytes: Vec> = keys.iter() + .map(|k| k.as_bytes().to_vec()) + .collect(); + + // Create the query + let query = KeysInPath { + path: vec![], // Root path + keys: key_bytes, + }; + + // Fetch path elements with proof + let (path_elements_result, metadata, proof) = Element::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch path elements with proof: {}", e)))?; + + // Convert the result to our response format + let elements: Vec = keys.into_iter() + .map(|key| { + let value = path_elements_result.get(key.as_bytes()) + .and_then(|element_opt| element_opt.as_ref()) + .and_then(|element| { + element.as_item_bytes().ok().map(|bytes| { + use base64::Engine; + base64::engine::general_purpose::STANDARD.encode(bytes) + }) + }); + + PathElement { + path: vec![key], + value, + } + }) + .collect(); + + let response = ProofMetadataResponse { + data: elements, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/token.rs b/packages/wasm-sdk/src/queries/token.rs index 16def2d6f56..c8b0f131332 100644 --- a/packages/wasm-sdk/src/queries/token.rs +++ b/packages/wasm-sdk/src/queries/token.rs @@ -2,6 +2,7 @@ use crate::sdk::WasmSdk; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; use dash_sdk::platform::{Identifier, FetchMany}; use dash_sdk::dpp::balances::credits::TokenAmount; use dash_sdk::dpp::tokens::status::TokenStatus; @@ -386,7 +387,9 @@ pub async fn get_token_contract_info(sdk: &WasmSdk, data_contract_id: &str) -> R token_contract_position: position, }; - serde_wasm_bindgen::to_value(&response) + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } else { Ok(JsValue::NULL) @@ -453,7 +456,9 @@ pub async fn get_token_perpetual_distribution_last_claim( last_claim_block_height, }; - serde_wasm_bindgen::to_value(&response) + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } else { Ok(JsValue::NULL) @@ -487,9 +492,516 @@ pub async fn get_token_total_supply(sdk: &WasmSdk, token_id: &str) -> Result, + token_id: &str, +) -> Result { + use dash_sdk::platform::tokens::identity_token_balances::IdentitiesTokenBalancesQuery; + use drive_proof_verifier::types::identity_token_balance::IdentitiesTokenBalances; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse identity IDs + let identities: Result, _> = identity_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identities = identities?; + + // Create query + let query = IdentitiesTokenBalancesQuery { + identity_ids: identities.clone(), + token_id: token_identifier, + }; + + // Fetch balances with proof + let (balances_result, metadata, proof): (drive_proof_verifier::types::identity_token_balance::IdentitiesTokenBalances, _, _) = TokenAmount::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities token balances with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = identity_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + balances_result.get(&id).and_then(|balance_opt| { + balance_opt.map(|balance| { + IdentityTokenBalanceResponse { + identity_id: id_str, + balance: balance.to_string(), + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_token_statuses_with_proof_info(sdk: &WasmSdk, token_ids: Vec) -> Result { + use drive_proof_verifier::types::token_status::TokenStatuses; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse token IDs + let tokens: Result, _> = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Fetch token statuses with proof + let (statuses_result, metadata, proof) = TokenStatus::fetch_many_with_metadata_and_proof(sdk.as_ref(), tokens.clone(), None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token statuses with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = token_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + statuses_result.get(&id).and_then(|status_opt| { + status_opt.as_ref().map(|status| { + TokenStatusResponse { + token_id: id_str, + is_paused: status.paused(), + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_token_total_supply_with_proof_info(sdk: &WasmSdk, token_id: &str) -> Result { + use dash_sdk::dpp::balances::total_single_token_balance::TotalSingleTokenBalance; + use dash_sdk::platform::Fetch; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch total supply with proof + let (supply_result, metadata, proof) = TotalSingleTokenBalance::fetch_with_metadata_and_proof(sdk.as_ref(), token_identifier, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token total supply with proof: {}", e)))?; + + let data = if let Some(supply) = supply_result { + Some(TokenTotalSupplyResponse { + total_supply: supply.token_supply.to_string(), + }) + } else { + None + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Additional proof info versions for remaining token queries + +#[wasm_bindgen] +pub async fn get_identity_token_infos_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + token_ids: Option>, + _limit: Option, + _offset: Option, +) -> Result { + use dash_sdk::platform::tokens::token_info::IdentityTokenInfosQuery; + use drive_proof_verifier::types::token_info::IdentityTokenInfos; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse identity ID + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // If no token IDs specified, we can't query (SDK requires specific token IDs) + let token_id_strings = token_ids.ok_or_else(|| JsError::new("token_ids are required for this query"))?; + + // Parse token IDs + let tokens: Result, _> = token_id_strings + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Create query + let query = IdentityTokenInfosQuery { + identity_id: identity_identifier, + token_ids: tokens.clone(), + }; + + // Fetch token infos with proof + let (infos_result, metadata, proof): (IdentityTokenInfos, _, _) = IdentityTokenInfo::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity token infos with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = token_id_strings + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + infos_result.get(&id).and_then(|info_opt| { + info_opt.as_ref().map(|info| { + use dash_sdk::dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; + + // IdentityTokenInfo only contains frozen status + let is_frozen = match &info { + dash_sdk::dpp::tokens::info::IdentityTokenInfo::V0(v0) => v0.frozen(), + }; + + TokenInfoResponse { + token_id: id_str, + is_frozen, + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identities_token_infos_with_proof_info( + sdk: &WasmSdk, + identity_ids: Vec, + token_id: &str, +) -> Result { + use dash_sdk::platform::tokens::token_info::IdentitiesTokenInfosQuery; + use drive_proof_verifier::types::token_info::IdentitiesTokenInfos; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse identity IDs + let identities: Result, _> = identity_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identities = identities?; + + // Create query + let query = IdentitiesTokenInfosQuery { + identity_ids: identities.clone(), + token_id: token_identifier, + }; + + // Fetch token infos with proof + let (infos_result, metadata, proof): (IdentitiesTokenInfos, _, _) = IdentityTokenInfo::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities token infos with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = identity_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + infos_result.get(&id).and_then(|info_opt| { + info_opt.as_ref().map(|info| { + use dash_sdk::dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; + + // IdentityTokenInfo only contains frozen status + let is_frozen = match &info { + dash_sdk::dpp::tokens::info::IdentityTokenInfo::V0(v0) => v0.frozen(), + }; + + IdentityTokenInfoResponse { + identity_id: id_str, + is_frozen, + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_token_direct_purchase_prices_with_proof_info(sdk: &WasmSdk, token_ids: Vec) -> Result { + use drive_proof_verifier::types::TokenDirectPurchasePrices; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse token IDs + let tokens: Result, _> = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Fetch token prices with proof - use slice reference + let (prices_result, metadata, proof): (TokenDirectPurchasePrices, _, _) = TokenPricingSchedule::fetch_many_with_metadata_and_proof(sdk.as_ref(), &tokens[..], None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token direct purchase prices with proof: {}", e)))?; + + // Convert to response format + let responses: Vec = token_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + prices_result.get(&id).and_then(|price_opt| { + price_opt.as_ref().map(|schedule| { + // Get prices based on the schedule type + let (base_price, current_price) = match &schedule { + dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule::SinglePrice(price) => { + (price.to_string(), price.to_string()) + }, + dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule::SetPrices(prices) => { + // Use first price as base, last as current + let base = prices.first_key_value() + .map(|(_, p)| p.to_string()) + .unwrap_or_else(|| "0".to_string()); + let current = prices.last_key_value() + .map(|(_, p)| p.to_string()) + .unwrap_or_else(|| "0".to_string()); + (base, current) + }, + }; + + TokenPriceResponse { + token_id: id_str, + current_price, + base_price, + } + }) + }) + }) + .collect(); + + let response = ProofMetadataResponse { + data: responses, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_token_contract_info_with_proof_info(sdk: &WasmSdk, data_contract_id: &str) -> Result { + use dash_sdk::dpp::tokens::contract_info::TokenContractInfo; + use dash_sdk::platform::Fetch; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch token contract info with proof + let (info_result, metadata, proof) = TokenContractInfo::fetch_with_metadata_and_proof(sdk.as_ref(), contract_id, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token contract info with proof: {}", e)))?; + + let data = if let Some(info) = info_result { + use dash_sdk::dpp::tokens::contract_info::v0::TokenContractInfoV0Accessors; + + // Extract fields based on the enum variant + let (contract_id, position) = match &info { + dash_sdk::dpp::tokens::contract_info::TokenContractInfo::V0(v0) => { + (v0.contract_id(), v0.token_contract_position()) + }, + }; + + Some(TokenContractInfoResponse { + contract_id: contract_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + token_contract_position: position, + }) + } else { + None + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_token_perpetual_distribution_last_claim_with_proof_info( + sdk: &WasmSdk, + identity_id: &str, + token_id: &str, +) -> Result { + use dash_sdk::platform::query::TokenLastClaimQuery; + use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; + use dash_sdk::platform::Fetch; + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse IDs + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create query + let query = TokenLastClaimQuery { + token_id: token_identifier, + identity_id: identity_identifier, + }; + + // Fetch last claim info with proof + let (claim_result, metadata, proof) = RewardDistributionMoment::fetch_with_metadata_and_proof(sdk.as_ref(), query, None) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token perpetual distribution last claim with proof: {}", e)))?; + + let data = if let Some(moment) = claim_result { + // Extract timestamp and block height based on the moment type + // Since we need both timestamp and block height in the response, + // we'll return the moment value and type + let (last_claim_timestamp_ms, last_claim_block_height) = match moment { + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::BlockBasedMoment(height) => { + (0, height) // No timestamp available for block-based + }, + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::TimeBasedMoment(timestamp) => { + (timestamp, 0) // No block height available for time-based + }, + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::EpochBasedMoment(epoch) => { + (0, epoch as u64) // Convert epoch to u64, no timestamp available + }, + }; + + Some(LastClaimResponse { + last_claim_timestamp_ms, + last_claim_block_height, + }) + } else { + None + }; + + let response = ProofMetadataResponse { + data, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/voting.rs b/packages/wasm-sdk/src/queries/voting.rs index a391b3a8a33..5376de58e17 100644 --- a/packages/wasm-sdk/src/queries/voting.rs +++ b/packages/wasm-sdk/src/queries/voting.rs @@ -25,7 +25,7 @@ pub async fn get_contested_resources( document_type_name: &str, data_contract_id: &str, index_name: &str, - result_type: &str, + _result_type: &str, _allow_include_locked_and_abstaining_vote_tally: Option, start_at_value: Option>, limit: Option, @@ -92,10 +92,162 @@ pub async fn get_contested_resources( .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } +#[wasm_bindgen] +pub async fn get_contested_resource_voters_for_identity( + sdk: &WasmSdk, + contract_id: &str, + document_type_name: &str, + index_name: &str, + index_values: Vec, + contestant_id: &str, + start_at_voter_info: Option, + limit: Option, + order_ascending: Option, +) -> Result { + // TODO: Implement get_contested_resource_voters_for_identity + // This function should return voters for a specific identity in a contested resource + let _ = (sdk, contract_id, document_type_name, index_name, index_values, + contestant_id, start_at_voter_info, limit, order_ascending); + + // Return empty result for now + let result = serde_json::json!({ + "voters": [], + "metadata": {} + }); + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} +#[wasm_bindgen] +pub async fn get_contested_resource_identity_votes( + sdk: &WasmSdk, + identity_id: &str, + limit: Option, + start_at_vote_poll_id_info: Option, + order_ascending: Option, +) -> Result { + // TODO: Implement get_contested_resource_identity_votes + // This function should return all votes made by a specific identity + let _ = (sdk, identity_id, limit, start_at_vote_poll_id_info, order_ascending); + + // Return empty result for now + let result = serde_json::json!({ + "votes": [], + "metadata": {} + }); + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} #[wasm_bindgen] -pub async fn get_contested_resource_vote_state( +pub async fn get_vote_polls_by_end_date( + sdk: &WasmSdk, + start_time_info: Option, + end_time_info: Option, + limit: Option, + order_ascending: Option, +) -> Result { + // TODO: Implement get_vote_polls_by_end_date + // This function should return vote polls filtered by end date + let _ = (sdk, start_time_info, end_time_info, limit, order_ascending); + + // Return empty result for now + let result = serde_json::json!({ + "votePolls": [], + "metadata": {} + }); + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +// Proof info versions for voting queries + +#[wasm_bindgen] +pub async fn get_contested_resources_with_proof_info( + sdk: &WasmSdk, + document_type_name: &str, + data_contract_id: &str, + index_name: &str, + _result_type: &str, + _allow_include_locked_and_abstaining_vote_tally: Option, + start_at_value: Option>, + limit: Option, + _offset: Option, + order_ascending: Option, +) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse result_type to get start_index_values + // For now, we'll use the standard "dash" parent domain + let start_index_values = vec!["dash".as_bytes().to_vec()]; + + // Create start_at_value_info if provided + let start_at_value_info = start_at_value.map(|bytes| { + get_contested_resources_request::get_contested_resources_request_v0::StartAtValueInfo { + start_value: bytes, + start_value_included: true, + } + }); + + // Create the gRPC request directly - force prove=true for proof info + let request = GetContestedResourcesRequest { + version: Some(get_contested_resources_request::Version::V0( + GetContestedResourcesRequestV0 { + contract_id: contract_id.to_vec(), + document_type_name: document_type_name.to_string(), + index_name: index_name.to_string(), + start_index_values, + end_index_values: vec![], + start_at_value_info, + count: limit, + order_ascending: order_ascending.unwrap_or(true), + prove: true, // Always true for proof info version + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get contested resources with proof: {}", e)))?; + + // Extract metadata and proof from response + let metadata = response.inner.metadata() + .map_err(|e| JsError::new(&format!("Failed to get metadata: {:?}", e)))?; + + let proof = response.inner.proof() + .map_err(|e| JsError::new(&format!("Failed to get proof: {:?}", e)))?; + + // For now, return a simple response structure + let data = serde_json::json!({ + "contestedResources": [] + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.clone().into(), + proof: proof.clone().into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_contested_resource_vote_state_with_proof_info( sdk: &WasmSdk, data_contract_id: &str, document_type_name: &str, @@ -106,6 +258,8 @@ pub async fn get_contested_resource_vote_state( count: Option, _order_ascending: Option, ) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + // Parse contract ID let contract_id = Identifier::from_string( data_contract_id, @@ -141,7 +295,7 @@ pub async fn get_contested_resource_vote_state( _ => vec!["dash".as_bytes().to_vec()], // Default to dash }; - // Create the gRPC request directly + // Create the gRPC request directly - force prove=true let request = GetContestedResourceVoteStateRequest { version: Some(get_contested_resource_vote_state_request::Version::V0( GetContestedResourceVoteStateRequestV0 { @@ -153,7 +307,7 @@ pub async fn get_contested_resource_vote_state( allow_include_locked_and_abstaining_vote_tally: allow_include_locked_and_abstaining_vote_tally.unwrap_or(false), start_at_identifier_info, count, - prove: sdk.prove(), + prove: true, // Always true for proof info version }, )), }; @@ -163,32 +317,37 @@ pub async fn get_contested_resource_vote_state( .as_ref() .execute(request, RequestSettings::default()) .await - .map_err(|e| JsError::new(&format!("Failed to get contested resource vote state: {}", e)))?; + .map_err(|e| JsError::new(&format!("Failed to get contested resource vote state with proof: {}", e)))?; + + // Extract metadata and proof from response + let metadata = response.inner.metadata() + .map_err(|e| JsError::new(&format!("Failed to get metadata: {:?}", e)))?; + + let proof = response.inner.proof() + .map_err(|e| JsError::new(&format!("Failed to get proof: {:?}", e)))?; // Return a simple response structure - let result = serde_json::json!({ + let data = serde_json::json!({ "contenders": [], "abstainVoteTally": null, "lockVoteTally": null, - "finishedVoteInfo": null, - "metadata": { - "height": response.inner.metadata().ok().map(|m| m.height), - "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), - "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), - "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), - } + "finishedVoteInfo": null }); + let response = ProofMetadataResponse { + data, + metadata: metadata.clone().into(), + proof: proof.clone().into(), + }; + // Use json_compatible serializer let serializer = serde_wasm_bindgen::Serializer::json_compatible(); - result.serialize(&serializer) + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } - - #[wasm_bindgen] -pub async fn get_contested_resource_voters_for_identity( +pub async fn get_contested_resource_voters_for_identity_with_proof_info( sdk: &WasmSdk, data_contract_id: &str, document_type_name: &str, @@ -198,6 +357,8 @@ pub async fn get_contested_resource_voters_for_identity( count: Option, order_ascending: Option, ) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + // Parse IDs let contract_id = Identifier::from_string( data_contract_id, @@ -232,7 +393,7 @@ pub async fn get_contested_resource_voters_for_identity( None }; - // Create the gRPC request directly + // Create the gRPC request directly - force prove=true let request = GetContestedResourceVotersForIdentityRequest { version: Some(get_contested_resource_voters_for_identity_request::Version::V0( GetContestedResourceVotersForIdentityRequestV0 { @@ -244,7 +405,7 @@ pub async fn get_contested_resource_voters_for_identity( start_at_identifier_info, count, order_ascending: order_ascending.unwrap_or(true), - prove: sdk.prove(), + prove: true, // Always true for proof info version }, )), }; @@ -254,43 +415,50 @@ pub async fn get_contested_resource_voters_for_identity( .as_ref() .execute(request, RequestSettings::default()) .await - .map_err(|e| JsError::new(&format!("Failed to get contested resource voters: {}", e)))?; + .map_err(|e| JsError::new(&format!("Failed to get contested resource voters with proof: {}", e)))?; + + // Extract metadata and proof from response + let metadata = response.inner.metadata() + .map_err(|e| JsError::new(&format!("Failed to get metadata: {:?}", e)))?; + + let proof = response.inner.proof() + .map_err(|e| JsError::new(&format!("Failed to get proof: {:?}", e)))?; // Return a simple response structure - let result = serde_json::json!({ + let data = serde_json::json!({ "voters": [], - "finishedResults": false, - "metadata": { - "height": response.inner.metadata().ok().map(|m| m.height), - "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), - "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), - "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), - } + "finishedResults": false }); + let response = ProofMetadataResponse { + data, + metadata: metadata.clone().into(), + proof: proof.clone().into(), + }; + // Use json_compatible serializer let serializer = serde_wasm_bindgen::Serializer::json_compatible(); - result.serialize(&serializer) + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } - - #[wasm_bindgen] -pub async fn get_contested_resource_identity_votes( +pub async fn get_contested_resource_identity_votes_with_proof_info( sdk: &WasmSdk, identity_id: &str, limit: Option, offset: Option, order_ascending: Option, ) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + // Parse identity ID let identity_identifier = Identifier::from_string( identity_id, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, )?; - // Create the gRPC request directly + // Create the gRPC request directly - force prove=true let request = GetContestedResourceIdentityVotesRequest { version: Some(get_contested_resource_identity_votes_request::Version::V0( GetContestedResourceIdentityVotesRequestV0 { @@ -299,7 +467,7 @@ pub async fn get_contested_resource_identity_votes( offset, order_ascending: order_ascending.unwrap_or(true), start_at_vote_poll_id_info: None, - prove: sdk.prove(), + prove: true, // Always true for proof info version }, )), }; @@ -309,30 +477,35 @@ pub async fn get_contested_resource_identity_votes( .as_ref() .execute(request, RequestSettings::default()) .await - .map_err(|e| JsError::new(&format!("Failed to get contested resource identity votes: {}", e)))?; + .map_err(|e| JsError::new(&format!("Failed to get contested resource identity votes with proof: {}", e)))?; + + // Extract metadata and proof from response + let metadata = response.inner.metadata() + .map_err(|e| JsError::new(&format!("Failed to get metadata: {:?}", e)))?; + + let proof = response.inner.proof() + .map_err(|e| JsError::new(&format!("Failed to get proof: {:?}", e)))?; // Return a simple response structure - let result = serde_json::json!({ + let data = serde_json::json!({ "votes": [], - "finishedResults": false, - "metadata": { - "height": response.inner.metadata().ok().map(|m| m.height), - "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), - "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), - "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), - } + "finishedResults": false }); + let response = ProofMetadataResponse { + data, + metadata: metadata.clone().into(), + proof: proof.clone().into(), + }; + // Use json_compatible serializer let serializer = serde_wasm_bindgen::Serializer::json_compatible(); - result.serialize(&serializer) + response.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) } - - #[wasm_bindgen] -pub async fn get_vote_polls_by_end_date( +pub async fn get_vote_polls_by_end_date_with_proof_info( sdk: &WasmSdk, start_time_ms: Option, end_time_ms: Option, @@ -340,9 +513,11 @@ pub async fn get_vote_polls_by_end_date( offset: Option, order_ascending: Option, ) -> Result { + use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; + // Note: GetVotePollsByEndDateRequestV0 doesn't have start_at_poll_info, only offset - // Create the gRPC request directly + // Create the gRPC request directly - force prove=true let request = GetVotePollsByEndDateRequest { version: Some(get_vote_polls_by_end_date_request::Version::V0( GetVotePollsByEndDateRequestV0 { @@ -361,6 +536,104 @@ pub async fn get_vote_polls_by_end_date( limit, offset, ascending: order_ascending.unwrap_or(true), + prove: true, // Always true for proof info version + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get vote polls by end date with proof: {}", e)))?; + + // Extract metadata and proof from response + let metadata = response.inner.metadata() + .map_err(|e| JsError::new(&format!("Failed to get metadata: {:?}", e)))?; + + let proof = response.inner.proof() + .map_err(|e| JsError::new(&format!("Failed to get proof: {:?}", e)))?; + + // Return a simple response structure + let data = serde_json::json!({ + "votePollsByTimestamps": {}, + "finishedResults": false + }); + + let response = ProofMetadataResponse { + data, + metadata: metadata.clone().into(), + proof: proof.clone().into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + + + +#[wasm_bindgen] +pub async fn get_contested_resource_vote_state( + sdk: &WasmSdk, + data_contract_id: &str, + document_type_name: &str, + index_name: &str, + result_type: &str, + allow_include_locked_and_abstaining_vote_tally: Option, + start_at_identifier_info: Option, + count: Option, + _order_ascending: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse start_at_identifier_info if provided + let start_at_identifier_info = if let Some(info_str) = start_at_identifier_info { + let info: serde_json::Value = serde_json::from_str(&info_str) + .map_err(|e| JsError::new(&format!("Invalid start_at_identifier_info JSON: {}", e)))?; + + if let (Some(start_id), Some(included)) = (info.get("startIdentifier"), info.get("startIdentifierIncluded")) { + let start_identifier = start_id.as_str() + .ok_or_else(|| JsError::new("startIdentifier must be a string"))? + .as_bytes() + .to_vec(); + let start_identifier_included = included.as_bool().unwrap_or(true); + + Some(get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo { + start_identifier, + start_identifier_included, + }) + } else { + None + } + } else { + None + }; + + // Parse result_type to determine resource path + let index_values = match result_type { + "documentTypeName" => vec!["dash".as_bytes().to_vec()], + _ => vec!["dash".as_bytes().to_vec()], // Default to dash + }; + + // Create the gRPC request directly + let request = GetContestedResourceVoteStateRequest { + version: Some(get_contested_resource_vote_state_request::Version::V0( + GetContestedResourceVoteStateRequestV0 { + contract_id: contract_id.to_vec(), + document_type_name: document_type_name.to_string(), + index_name: index_name.to_string(), + index_values, + result_type: if allow_include_locked_and_abstaining_vote_tally.unwrap_or(false) { 0 } else { 1 }, + allow_include_locked_and_abstaining_vote_tally: allow_include_locked_and_abstaining_vote_tally.unwrap_or(false), + start_at_identifier_info, + count, prove: sdk.prove(), }, )), @@ -371,12 +644,14 @@ pub async fn get_vote_polls_by_end_date( .as_ref() .execute(request, RequestSettings::default()) .await - .map_err(|e| JsError::new(&format!("Failed to get vote polls by end date: {}", e)))?; + .map_err(|e| JsError::new(&format!("Failed to get contested resource vote state: {}", e)))?; // Return a simple response structure let result = serde_json::json!({ - "votePollsByTimestamps": {}, - "finishedResults": false, + "contenders": [], + "abstainVoteTally": null, + "lockVoteTally": null, + "finishedVoteInfo": null, "metadata": { "height": response.inner.metadata().ok().map(|m| m.height), "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), @@ -389,4 +664,5 @@ pub async fn get_vote_polls_by_end_date( let serializer = serde_wasm_bindgen::Serializer::json_compatible(); result.serialize(&serializer) .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) -} \ No newline at end of file +} + diff --git a/packages/wasm-sdk/src/sdk.rs b/packages/wasm-sdk/src/sdk.rs index 469df2ea3b9..b48f2204830 100644 --- a/packages/wasm-sdk/src/sdk.rs +++ b/packages/wasm-sdk/src/sdk.rs @@ -654,6 +654,11 @@ impl WasmSdkBuilder { pub fn with_context_provider(self, context_provider: WasmContext) -> Self { WasmSdkBuilder(self.0.with_context_provider(context_provider)) } + + // TODO: Add with_proofs method when it's available in the SDK builder + // pub fn with_proofs(self, enable_proofs: bool) -> Self { + // WasmSdkBuilder(self.0.with_proofs(enable_proofs)) + // } } // Store shared trusted contexts From 5d71bd178a207974dd0d745e8ebf01f6f6324dec Mon Sep 17 00:00:00 2001 From: quantum Date: Fri, 11 Jul 2025 14:35:43 -0500 Subject: [PATCH 12/30] more fixes --- packages/wasm-sdk/src/queries/document.rs | 2 +- packages/wasm-sdk/src/queries/epoch.rs | 30 +++-------------------- packages/wasm-sdk/src/queries/group.rs | 10 ++------ packages/wasm-sdk/src/queries/mod.rs | 6 +++-- packages/wasm-sdk/src/queries/token.rs | 9 +------ 5 files changed, 12 insertions(+), 45 deletions(-) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index be4e1d16f6d..84bcd9064d9 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -376,7 +376,7 @@ pub async fn get_documents_with_proof_info( for clause_json in clauses { let where_clause = parse_where_clause(&clause_json)?; - query.where_clauses.push(where_clause); + query = query.with_where(where_clause); } } diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs index f244375d1f0..96f4fb2e020 100644 --- a/packages/wasm-sdk/src/queries/epoch.rs +++ b/packages/wasm-sdk/src/queries/epoch.rs @@ -163,31 +163,21 @@ struct ProposerBlockCount { #[wasm_bindgen] pub async fn get_evonodes_proposed_epoch_blocks_by_ids( sdk: &WasmSdk, - epoch: u32, + epoch: u16, ids: Vec, ) -> Result { - use dash_sdk::platform::FetchMany; use drive_proof_verifier::types::ProposerBlockCountById; // Silence unused variables since this function is not yet implemented let _ = (sdk, ids); - // Check if epoch fits in u16 before casting - if epoch > u16::MAX as u32 { - return Err(JsError::new(&format!( - "Epoch value {} is invalid: must be less than or equal to {}", - epoch, - u16::MAX - ))); - } - // TODO: Use the SDK's FetchMany trait to get proposer block counts // This would automatically handle proof verification when sdk.prove() is true // Currently commented out due to query format issues - needs investigation /* let proposer_block_counts = ProposerBlockCountById::fetch_many( sdk.as_ref(), - (Some(epoch as u16), pro_tx_hashes), + (Some(epoch), pro_tx_hashes), ) .await .map_err(|e| JsError::new(&format!("Failed to fetch evonode proposed blocks by ids: {}", e)))?; @@ -218,7 +208,7 @@ pub async fn get_evonodes_proposed_epoch_blocks_by_ids( #[wasm_bindgen] pub async fn get_evonodes_proposed_epoch_blocks_by_range( sdk: &WasmSdk, - epoch: u32, + epoch: u16, limit: Option, start_after: Option, order_ascending: Option, @@ -239,18 +229,9 @@ pub async fn get_evonodes_proposed_epoch_blocks_by_range( None }; - // Check if epoch fits in u16 before casting - if epoch > u16::MAX as u32 { - return Err(JsError::new(&format!( - "Epoch value {} is invalid: must be less than or equal to {}", - epoch, - u16::MAX - ))); - } - let counts_result = ProposerBlockCounts::fetch_proposed_blocks_by_range( sdk.as_ref(), - Some(epoch as u16), + Some(epoch), limit, start_info, ) @@ -306,7 +287,6 @@ pub async fn get_epochs_info_with_proof_info( ascending: Option, ) -> Result { use dash_sdk::platform::types::epoch::EpochQuery; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; let query = LimitQuery { query: EpochQuery { @@ -341,8 +321,6 @@ pub async fn get_epochs_info_with_proof_info( #[wasm_bindgen] pub async fn get_current_epoch_with_proof_info(sdk: &WasmSdk) -> Result { - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; - let (epoch, metadata, proof) = ExtendedEpochInfo::fetch_current_with_metadata_and_proof(sdk.as_ref()) .await .map_err(|e| JsError::new(&format!("Failed to fetch current epoch with proof: {}", e)))?; diff --git a/packages/wasm-sdk/src/queries/group.rs b/packages/wasm-sdk/src/queries/group.rs index 23243152e3b..b29e92012c7 100644 --- a/packages/wasm-sdk/src/queries/group.rs +++ b/packages/wasm-sdk/src/queries/group.rs @@ -2,6 +2,8 @@ use crate::sdk::WasmSdk; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; use dash_sdk::platform::{Fetch, FetchMany, Identifier}; use dash_sdk::dpp::data_contract::group::Group; use dash_sdk::dpp::data_contract::GroupContractPosition; @@ -629,7 +631,6 @@ pub async fn get_group_infos_with_proof_info( } // Additional proof info versions for remaining group queries -use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; #[wasm_bindgen] pub async fn get_group_members_with_proof_info( @@ -657,13 +658,6 @@ pub async fn get_group_members_with_proof_info( .await .map_err(|e| JsError::new(&format!("Failed to fetch group with proof: {}", e)))?; - #[derive(Serialize, Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct GroupMember { - member_id: String, - power: u32, - } - let data = match group_result { Some(group) => { let mut members: Vec = Vec::new(); diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index 2eece79bfff..8e9d6f70c04 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -71,10 +71,12 @@ impl From for ResponseMetadata { // Helper function to convert platform Proof to our ProofInfo impl From for ProofInfo { fn from(proof: dash_sdk::platform::proto::Proof) -> Self { + use base64::{Engine as _, engine::general_purpose}; + ProofInfo { - grovedb_proof: base64::encode(&proof.grovedb_proof), + grovedb_proof: general_purpose::STANDARD.encode(&proof.grovedb_proof), quorum_hash: hex::encode(&proof.quorum_hash), - signature: base64::encode(&proof.signature), + signature: general_purpose::STANDARD.encode(&proof.signature), round: proof.round, block_id_hash: hex::encode(&proof.block_id_hash), quorum_type: proof.quorum_type, diff --git a/packages/wasm-sdk/src/queries/token.rs b/packages/wasm-sdk/src/queries/token.rs index c8b0f131332..107734e8445 100644 --- a/packages/wasm-sdk/src/queries/token.rs +++ b/packages/wasm-sdk/src/queries/token.rs @@ -3,6 +3,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; use serde::{Serialize, Deserialize}; use serde::ser::Serialize as _; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; use dash_sdk::platform::{Identifier, FetchMany}; use dash_sdk::dpp::balances::credits::TokenAmount; use dash_sdk::dpp::tokens::status::TokenStatus; @@ -511,7 +512,6 @@ pub async fn get_identities_token_balances_with_proof_info( ) -> Result { use dash_sdk::platform::tokens::identity_token_balances::IdentitiesTokenBalancesQuery; use drive_proof_verifier::types::identity_token_balance::IdentitiesTokenBalances; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse token ID let token_identifier = Identifier::from_string( @@ -575,7 +575,6 @@ pub async fn get_identities_token_balances_with_proof_info( #[wasm_bindgen] pub async fn get_token_statuses_with_proof_info(sdk: &WasmSdk, token_ids: Vec) -> Result { use drive_proof_verifier::types::token_status::TokenStatuses; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse token IDs let tokens: Result, _> = token_ids @@ -628,7 +627,6 @@ pub async fn get_token_statuses_with_proof_info(sdk: &WasmSdk, token_ids: Vec Result { use dash_sdk::dpp::balances::total_single_token_balance::TotalSingleTokenBalance; use dash_sdk::platform::Fetch; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse token ID let token_identifier = Identifier::from_string( @@ -673,7 +671,6 @@ pub async fn get_identity_token_infos_with_proof_info( ) -> Result { use dash_sdk::platform::tokens::token_info::IdentityTokenInfosQuery; use drive_proof_verifier::types::token_info::IdentityTokenInfos; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse identity ID let identity_identifier = Identifier::from_string( @@ -752,7 +749,6 @@ pub async fn get_identities_token_infos_with_proof_info( ) -> Result { use dash_sdk::platform::tokens::token_info::IdentitiesTokenInfosQuery; use drive_proof_verifier::types::token_info::IdentitiesTokenInfos; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse token ID let token_identifier = Identifier::from_string( @@ -823,7 +819,6 @@ pub async fn get_identities_token_infos_with_proof_info( #[wasm_bindgen] pub async fn get_token_direct_purchase_prices_with_proof_info(sdk: &WasmSdk, token_ids: Vec) -> Result { use drive_proof_verifier::types::TokenDirectPurchasePrices; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse token IDs let tokens: Result, _> = token_ids @@ -894,7 +889,6 @@ pub async fn get_token_direct_purchase_prices_with_proof_info(sdk: &WasmSdk, tok pub async fn get_token_contract_info_with_proof_info(sdk: &WasmSdk, data_contract_id: &str) -> Result { use dash_sdk::dpp::tokens::contract_info::TokenContractInfo; use dash_sdk::platform::Fetch; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse contract ID let contract_id = Identifier::from_string( @@ -946,7 +940,6 @@ pub async fn get_token_perpetual_distribution_last_claim_with_proof_info( use dash_sdk::platform::query::TokenLastClaimQuery; use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use dash_sdk::platform::Fetch; - use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; // Parse IDs let identity_identifier = Identifier::from_string( From 26e4d259ac185441f3a8e5dde97942c0d96ade68 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 11 Jul 2025 22:53:58 +0300 Subject: [PATCH 13/30] fix --- packages/rs-dpp/Cargo.toml | 3 ++- packages/rs-dpp/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index ec7cbf0da2a..b3236b8ea11 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -270,8 +270,9 @@ core-types = ["bls-signatures"] core-types-serialization = ["core-types"] core-types-serde-conversion = ["core-types"] state-transitions = [] +system_contracts = ["data-contracts", "factories"] # All system data contracts -all-system_contracts = ["data-contracts", "data-contracts/all-contracts", "dpns-contract", "dashpay-contract", "withdrawals-contract", "masternode-rewards-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] +all-system_contracts = ["system_contracts", "data-contracts/all-contracts", "dpns-contract", "dashpay-contract", "withdrawals-contract", "masternode-rewards-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] # Individual data contract features dpns-contract = ["data-contracts", "data-contracts/dpns"] diff --git a/packages/rs-dpp/src/lib.rs b/packages/rs-dpp/src/lib.rs index 6e4ae0467b0..fed054861a9 100644 --- a/packages/rs-dpp/src/lib.rs +++ b/packages/rs-dpp/src/lib.rs @@ -48,7 +48,7 @@ pub mod serialization; feature = "message-signature-verification" ))] pub mod signing; -#[cfg(feature = "data-contracts")] +#[cfg(feature = "system_contracts")] pub mod system_data_contracts; pub mod tokens; From 37a3ca45c1267835c29daa83475a87b9c3a49151 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 11 Jul 2025 23:00:27 +0300 Subject: [PATCH 14/30] fix --- packages/rs-drive/src/util/object_size_info/contract_info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-drive/src/util/object_size_info/contract_info.rs b/packages/rs-drive/src/util/object_size_info/contract_info.rs index 7fe5cc58392..9cf79d5f35f 100644 --- a/packages/rs-drive/src/util/object_size_info/contract_info.rs +++ b/packages/rs-drive/src/util/object_size_info/contract_info.rs @@ -46,7 +46,7 @@ pub enum DataContractInfo<'a> { OwnedDataContract(DataContract), } -impl<'a> DataContractInfo<'a> { +impl DataContractInfo<'_> { #[cfg(feature = "server")] /// Resolve the data contract info into an object that contains the data contract pub(crate) fn resolve( From 30215dfde16b3330c73d293e8104ddbfde796729 Mon Sep 17 00:00:00 2001 From: quantum Date: Fri, 11 Jul 2025 15:04:23 -0500 Subject: [PATCH 15/30] more fixes --- packages/rs-sdk/src/platform/dpns_usernames.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/rs-sdk/src/platform/dpns_usernames.rs b/packages/rs-sdk/src/platform/dpns_usernames.rs index c00aedec21a..65d22db4787 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -116,6 +116,9 @@ fn hash_double(data: Vec) -> [u8; 32] { hash.to_byte_array() } +/// Callback type for preorder document +pub type PreorderCallback = Box; + /// Input for registering a DPNS name pub struct RegisterDpnsNameInput { /// The label for the domain (e.g., "alice" for "alice.dash") @@ -127,7 +130,7 @@ pub struct RegisterDpnsNameInput { /// The signer for the identity pub signer: S, /// Optional callback to be called with the preorder document result - pub preorder_callback: Option>, + pub preorder_callback: Option, } /// Result of a DPNS name registration @@ -376,7 +379,7 @@ impl Sdk { // Query for existing domain with this label let query = DocumentQuery { - data_contract: dpns_contract.into(), + data_contract: dpns_contract, document_type_name: "domain".to_string(), where_clauses: vec![ WhereClause { @@ -442,7 +445,7 @@ impl Sdk { // Query for domain with this label let query = DocumentQuery { - data_contract: dpns_contract.into(), + data_contract: dpns_contract, document_type_name: "domain".to_string(), where_clauses: vec![ WhereClause { From a53b9cfaaec4ce4e3f589e714aed5c6ce748e452 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 11 Jul 2025 23:10:47 +0300 Subject: [PATCH 16/30] another fix --- packages/rs-drive/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index b6db06a002b..dd83b8cf6bc 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -122,5 +122,7 @@ verify = [ "grovedb/verify", "grovedb-costs", "dpp/state-transitions", - "dpp/data-contracts" + "dpp/system_contracts", + "dpp/token-history-contract", + "dpp/withdrawals-contract" ] From 6aac9cfceae0e6bbcfebe0e39289a4fd83e36758 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Sat, 12 Jul 2025 07:27:51 +0300 Subject: [PATCH 17/30] Potential fix for code scanning alert no. 24: DOM text reinterpreted as HTML Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- package.json | 3 ++- packages/wasm-sdk/index.html | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 657244e994b..31b66ca66ff 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "brace-expansion": "^2.0.2" }, "dependencies": { - "node-gyp": "^10.0.1" + "node-gyp": "^10.0.1", + "dompurify": "^3.2.6" } } diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 71785ef278d..16a183785a6 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -1,5 +1,6 @@ + @@ -2526,7 +2527,8 @@

Results

if (isError) { // For errors, show in single view - splitContainer.innerHTML = `
${data}
`; + const safeData = DOMPurify.sanitize(data); + splitContainer.innerHTML = `
${safeData}
`; currentResult = null; } else { // Parse JSON string if necessary From 0f6be17d49ab3269b2cc3710b57f42e9d14c9865 Mon Sep 17 00:00:00 2001 From: quantum Date: Fri, 11 Jul 2025 23:34:20 -0500 Subject: [PATCH 18/30] fixes --- packages/wasm-sdk/index.html | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 71785ef278d..5b139b1fae4 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -2526,7 +2526,9 @@

Results

if (isError) { // For errors, show in single view - splitContainer.innerHTML = `
${data}
`; + splitContainer.innerHTML = '
'; + const errorDiv = document.getElementById('identityInfo'); + errorDiv.textContent = data; currentResult = null; } else { // Parse JSON string if necessary @@ -2589,6 +2591,12 @@

Results

} } + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + function formatResultWithCredits(data) { const CREDITS_PER_DASH = 100000000000; // 100 billion credits = 1 Dash @@ -2596,7 +2604,7 @@

Results

const creditsNum = BigInt(credits); const dashValue = Number(creditsNum) / CREDITS_PER_DASH; const dashFormatted = dashValue.toFixed(8).replace(/\.?0+$/, ''); - return `${credits}`; + return `${escapeHtml(credits)}`; } function formatNonceValue(nonce, key) { @@ -2688,7 +2696,7 @@

Results

Used for: Identity state transitions that use balance or create data contracts`; } - return `${nonce}`; + return `${escapeHtml(nonce.toString())}`; } function processValue(value, key, indent = 0) { @@ -2717,7 +2725,8 @@

Results

const innerIndentStr = ' '.repeat(indent + 1); const entries = Object.entries(obj).map(([key, value]) => { const formattedValue = processValue(value, key, indent + 1); - return `${innerIndentStr}"${escapeHtml(key)}": ${formattedValue}`; + const escapedKey = escapeHtml(key); + return `${innerIndentStr}"${escapedKey}": ${formattedValue}`; }); return `{ ${entries.join(',\n')} @@ -2740,12 +2749,6 @@

Results

${indentStr}]`; } - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - return `
${processValue(data, '')}
`; } From 4a1cf37d348af88810e20a1230c19f586293923f Mon Sep 17 00:00:00 2001 From: quantum Date: Fri, 11 Jul 2025 23:45:17 -0500 Subject: [PATCH 19/30] more work --- .../rs-drive/src/util/object_size_info/contract_info.rs | 2 +- packages/wasm-sdk/index.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive/src/util/object_size_info/contract_info.rs b/packages/rs-drive/src/util/object_size_info/contract_info.rs index 9cf79d5f35f..984f53e9b45 100644 --- a/packages/rs-drive/src/util/object_size_info/contract_info.rs +++ b/packages/rs-drive/src/util/object_size_info/contract_info.rs @@ -49,7 +49,7 @@ pub enum DataContractInfo<'a> { impl DataContractInfo<'_> { #[cfg(feature = "server")] /// Resolve the data contract info into an object that contains the data contract - pub(crate) fn resolve( + pub(crate) fn resolve<'a>( self, drive: &Drive, block_info: &BlockInfo, diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 7db2bc3137b..039db82dd79 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -2564,7 +2564,7 @@

Results

// Display data in top section const dataContent = document.getElementById('identityInfo'); - dataContent.innerHTML = formatResultWithCredits(dataToFormat.data); + dataContent.innerHTML = DOMPurify.sanitize(formatResultWithCredits(dataToFormat.data)); // Display proof and metadata in bottom section const proofContent = document.getElementById('proofInfo'); @@ -2579,12 +2579,12 @@

Results

quorumType: dataToFormat.proof.quorumType } }; - proofContent.innerHTML = formatResultWithCredits(proofDisplay); + proofContent.innerHTML = DOMPurify.sanitize(formatResultWithCredits(proofDisplay)); } else { // Single view for non-proof mode or non-proof responses splitContainer.innerHTML = `
`; const resultContent = document.getElementById('identityInfo'); - resultContent.innerHTML = formatResultWithCredits(dataToFormat); + resultContent.innerHTML = DOMPurify.sanitize(formatResultWithCredits(dataToFormat)); } currentResult = data; From a0867a96bc84f01fcbb9f41b35e5a8270c4bf047 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 07:55:18 +0300 Subject: [PATCH 20/30] improvement --- packages/rs-dpp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index b3236b8ea11..61e34c3b678 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -270,7 +270,7 @@ core-types = ["bls-signatures"] core-types-serialization = ["core-types"] core-types-serde-conversion = ["core-types"] state-transitions = [] -system_contracts = ["data-contracts", "factories"] +system_contracts = ["data-contracts", "factories", "platform-value-json"] # All system data contracts all-system_contracts = ["system_contracts", "data-contracts/all-contracts", "dpns-contract", "dashpay-contract", "withdrawals-contract", "masternode-rewards-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] From ba65988753c6034f0c5015a4d23b8a2b57be4dfc Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 07:58:28 +0300 Subject: [PATCH 21/30] improvement --- .../document_type/class_methods/try_from_schema/v1/mod.rs | 2 ++ packages/rs-dpp/src/tokens/token_pricing_schedule.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index 0c4da311f10..4d96e901634 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -28,11 +28,13 @@ use crate::consensus::basic::data_contract::ContestedUniqueIndexOnMutableDocumen use crate::consensus::basic::data_contract::ContestedUniqueIndexWithUniqueIndexError; #[cfg(any(test, feature = "validation"))] use crate::consensus::basic::data_contract::InvalidDocumentTypeNameError; +#[cfg(feature = "validation")] use crate::consensus::basic::data_contract::RedundantDocumentPaidForByTokenWithContractId; #[cfg(feature = "validation")] use crate::consensus::basic::data_contract::TokenPaymentByBurningOnlyAllowedOnInternalTokenError; #[cfg(feature = "validation")] use crate::consensus::basic::document::MissingPositionsInDocumentTypePropertiesError; +#[cfg(feature = "validation")] use crate::consensus::basic::token::InvalidTokenPositionError; #[cfg(feature = "validation")] use crate::consensus::basic::BasicError; diff --git a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs index ea77b9d7e83..504837c2294 100644 --- a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs +++ b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs @@ -3,6 +3,7 @@ use crate::errors::ProtocolError; use crate::fee::Credits; use bincode_derive::{Decode, Encode}; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +#[cfg(feature = "state-transition-serde-conversion")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::{self, Display, Formatter}; From 36c3a4f5b2670e7a3a33845ef465224caca760bd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 08:03:10 +0300 Subject: [PATCH 22/30] fix --- packages/rs-drive/src/util/object_size_info/contract_info.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive/src/util/object_size_info/contract_info.rs b/packages/rs-drive/src/util/object_size_info/contract_info.rs index 984f53e9b45..1afe618ca2f 100644 --- a/packages/rs-drive/src/util/object_size_info/contract_info.rs +++ b/packages/rs-drive/src/util/object_size_info/contract_info.rs @@ -46,10 +46,10 @@ pub enum DataContractInfo<'a> { OwnedDataContract(DataContract), } -impl DataContractInfo<'_> { +impl <'a> DataContractInfo<'a> { #[cfg(feature = "server")] /// Resolve the data contract info into an object that contains the data contract - pub(crate) fn resolve<'a>( + pub(crate) fn resolve( self, drive: &Drive, block_info: &BlockInfo, From b2c07f5587c7d3734e77866f1646e2eaa4c5e37b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 08:16:59 +0300 Subject: [PATCH 23/30] fix --- packages/masternode-reward-shares-contract/src/lib.rs | 4 ++-- packages/rs-drive/src/util/object_size_info/contract_info.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/masternode-reward-shares-contract/src/lib.rs b/packages/masternode-reward-shares-contract/src/lib.rs index 7563fada094..62434285a12 100644 --- a/packages/masternode-reward-shares-contract/src/lib.rs +++ b/packages/masternode-reward-shares-contract/src/lib.rs @@ -18,10 +18,10 @@ pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); pub fn load_definitions(platform_version: &PlatformVersion) -> Result, Error> { match platform_version.system_data_contracts.withdrawals { - 0 => Ok(None), + 1 => Ok(None), version => Err(Error::UnknownVersionMismatch { method: "masternode_reward_shares_contract::load_definitions".to_string(), - known_versions: vec![0], + known_versions: vec![1], received: version, }), } diff --git a/packages/rs-drive/src/util/object_size_info/contract_info.rs b/packages/rs-drive/src/util/object_size_info/contract_info.rs index 1afe618ca2f..7fe5cc58392 100644 --- a/packages/rs-drive/src/util/object_size_info/contract_info.rs +++ b/packages/rs-drive/src/util/object_size_info/contract_info.rs @@ -46,7 +46,7 @@ pub enum DataContractInfo<'a> { OwnedDataContract(DataContract), } -impl <'a> DataContractInfo<'a> { +impl<'a> DataContractInfo<'a> { #[cfg(feature = "server")] /// Resolve the data contract info into an object that contains the data contract pub(crate) fn resolve( From 1b55f539ffac4e0897892709d5f976541c43dc9a Mon Sep 17 00:00:00 2001 From: quantum Date: Sat, 12 Jul 2025 00:23:39 -0500 Subject: [PATCH 24/30] fix --- packages/wasm-sdk/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 039db82dd79..9761553fa4a 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -2803,10 +2803,10 @@

Results

console.log(`Initialized ${network} SDK (${modeStr} mode):`, sdk); updateStatus(`WASM SDK successfully loaded on ${network.toUpperCase()} (${modeStr} mode)`, 'success'); - // Proof support is now available for identity queries - // When the proof toggle is ON (checked), queries return raw JSON without proof verification - // When the proof toggle is OFF (unchecked), queries return verified data with proofs - // Note: Most SDK queries don't yet support unproved mode, only identity_fetch_unproved is implemented + // Proof support is available for all queries - internally all queries use proof verification + // When the proof toggle is ON (checked), queries return verified data WITH proof information displayed + // When the proof toggle is OFF (unchecked), queries return verified data WITHOUT proof information displayed + // Note: Both modes use proof verification internally; the toggle only controls whether proof details are shown } } catch (error) { if (currentRequestToken === initRequestCounter) { From 8a3c9310f2c3a458080290248e8e2a18794434b2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 08:25:04 +0300 Subject: [PATCH 25/30] more work --- .pnp.cjs | 21 ++++++++++++++++++ ...-types-npm-2.0.7-a07fc44f59-8e4202766a.zip | Bin 0 -> 4330 bytes ...purify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip | Bin 0 -> 217347 bytes package.json | 4 ++-- yarn.lock | 20 +++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 .yarn/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-8e4202766a.zip create mode 100644 .yarn/cache/dompurify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip diff --git a/.pnp.cjs b/.pnp.cjs index 777540e7e04..d47f66d73cf 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -130,6 +130,7 @@ const RAW_RUNTIME_STATE = ["add-stream", "npm:1.0.0"],\ ["conventional-changelog", "npm:3.1.24"],\ ["conventional-changelog-dash", "https://github.com/dashevo/conventional-changelog-dash.git#commit=3d4d77e2cea876a27b92641c28b15aedf13eb788"],\ + ["dompurify", "npm:3.2.6"],\ ["node-gyp", "npm:10.0.1"],\ ["semver", "npm:7.5.3"],\ ["tempfile", "npm:3.0.0"],\ @@ -2804,6 +2805,7 @@ const RAW_RUNTIME_STATE = ["add-stream", "npm:1.0.0"],\ ["conventional-changelog", "npm:3.1.24"],\ ["conventional-changelog-dash", "https://github.com/dashevo/conventional-changelog-dash.git#commit=3d4d77e2cea876a27b92641c28b15aedf13eb788"],\ + ["dompurify", "npm:3.2.6"],\ ["node-gyp", "npm:10.0.1"],\ ["semver", "npm:7.5.3"],\ ["tempfile", "npm:3.0.0"],\ @@ -4816,6 +4818,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/trusted-types", [\ + ["npm:2.0.7", {\ + "packageLocation": "./.yarn/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-8e4202766a.zip/node_modules/@types/trusted-types/",\ + "packageDependencies": [\ + ["@types/trusted-types", "npm:2.0.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/vinyl", [\ ["npm:2.0.6", {\ "packageLocation": "./.yarn/cache/@types-vinyl-npm-2.0.6-62fe43810b-924415fe13.zip/node_modules/@types/vinyl/",\ @@ -9288,6 +9299,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dompurify", [\ + ["npm:3.2.6", {\ + "packageLocation": "./.yarn/cache/dompurify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip/node_modules/dompurify/",\ + "packageDependencies": [\ + ["dompurify", "npm:3.2.6"],\ + ["@types/trusted-types", "npm:2.0.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dot", [\ ["npm:1.1.3", {\ "packageLocation": "./.yarn/cache/dot-npm-1.1.3-a570dedf33-a2738bbe08.zip/node_modules/dot/",\ diff --git a/.yarn/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-8e4202766a.zip b/.yarn/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-8e4202766a.zip new file mode 100644 index 0000000000000000000000000000000000000000..3d2f216ed3bcf7e750855a1fc6f227344980c349 GIT binary patch literal 4330 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 literal 0 HcmV?d00001 diff --git a/.yarn/cache/dompurify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip b/.yarn/cache/dompurify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip new file mode 100644 index 0000000000000000000000000000000000000000..a4c24ad37e55d07f85b208a9962705a63b0c5a3e GIT binary patch literal 217347 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%0DkW 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

qiVsM$- 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

Kg*rbDb5cCkWONwa*fb8I?}3r{-}BB(L$2WBQK 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 31b66ca66ff..c3790938ae8 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "brace-expansion": "^2.0.2" }, "dependencies": { - "node-gyp": "^10.0.1", - "dompurify": "^3.2.6" + "dompurify": "^3.2.6", + "node-gyp": "^10.0.1" } } diff --git a/yarn.lock b/yarn.lock index 63f03c69aeb..238c2cb6114 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1838,6 +1838,7 @@ __metadata: add-stream: "npm:^1.0.0" conventional-changelog: "npm:^3.1.24" conventional-changelog-dash: "github:dashevo/conventional-changelog-dash" + dompurify: "npm:^3.2.6" node-gyp: "npm:^10.0.1" semver: "npm:^7.5.3" tempfile: "npm:^3.0.0" @@ -3534,6 +3535,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 + languageName: node + linkType: hard + "@types/vinyl@npm:^2.0.4": version: 2.0.6 resolution: "@types/vinyl@npm:2.0.6" @@ -7095,6 +7103,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.2.6": + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: b91631ed0e4d17fae950ef53613cc009ed7e73adc43ac94a41dd52f35483f7538d13caebdafa7626e0da145fc8184e7ac7935f14f25b7e841b32fda777e40447 + languageName: node + linkType: hard + "dot-case@npm:^3.0.4": version: 3.0.4 resolution: "dot-case@npm:3.0.4" From 61e4d9ca2f0ed7bee429403b66fb19ee72696138 Mon Sep 17 00:00:00 2001 From: quantum Date: Sat, 12 Jul 2025 00:35:44 -0500 Subject: [PATCH 26/30] fix --- packages/wasm-sdk/index.html | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index 9761553fa4a..c926425379c 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -2602,8 +2602,29 @@

Results

function formatCreditsValue(credits) { const creditsNum = BigInt(credits); - const dashValue = Number(creditsNum) / CREDITS_PER_DASH; - const dashFormatted = dashValue.toFixed(8).replace(/\.?0+$/, ''); + const CREDITS_PER_DASH_BIGINT = BigInt(CREDITS_PER_DASH); + + // Perform division using BigInt to get whole DASH + const wholeDash = creditsNum / CREDITS_PER_DASH_BIGINT; + // Get remainder for fractional part + const remainder = creditsNum % CREDITS_PER_DASH_BIGINT; + + // Convert remainder to 8 decimal places + // Multiply by 100000000 (8 decimal places) and divide by CREDITS_PER_DASH + const fractionalPart = (remainder * BigInt(100000000)) / CREDITS_PER_DASH_BIGINT; + + // Construct the decimal string + let dashFormatted; + if (fractionalPart === BigInt(0)) { + dashFormatted = wholeDash.toString(); + } else { + // Pad fractional part to 8 digits + const fractionalStr = fractionalPart.toString().padStart(8, '0'); + // Remove trailing zeros + const trimmedFractional = fractionalStr.replace(/0+$/, ''); + dashFormatted = `${wholeDash}.${trimmedFractional}`; + } + return `${escapeHtml(credits)}`; } From da06af1305f7c36935a506d2e3162e7a956ec964 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 12 Jul 2025 09:36:29 +0300 Subject: [PATCH 27/30] fix --- packages/rs-dpp/src/withdrawal/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index 570d13b40fb..3a4feda9992 100644 --- a/packages/rs-dpp/src/withdrawal/mod.rs +++ b/packages/rs-dpp/src/withdrawal/mod.rs @@ -1,5 +1,5 @@ pub mod daily_withdrawal_limit; -#[cfg(feature = "withdrawals-contract")] +#[cfg(all(feature = "withdrawals-contract", feature = "system_contracts"))] mod document_try_into_asset_unlock_base_transaction_info; use bincode::{Decode, Encode}; From a64b739931aa05aa284f9a347bd0059a20901392 Mon Sep 17 00:00:00 2001 From: quantum Date: Sat, 12 Jul 2025 01:52:18 -0500 Subject: [PATCH 28/30] fixes --- packages/wasm-sdk/build.sh | 3 +-- packages/wasm-sdk/index.html | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/wasm-sdk/build.sh b/packages/wasm-sdk/build.sh index b4e1e47461a..82dda6e01d4 100755 --- a/packages/wasm-sdk/build.sh +++ b/packages/wasm-sdk/build.sh @@ -16,6 +16,5 @@ if [ "${CARGO_BUILD_PROFILE:-}" = "dev" ] || [ "${CI:-}" != "true" ]; then OPT_LEVEL="minimal" fi -# Call unified build script with only the contracts we need -export CARGO_BUILD_FEATURES="dpns-contract,dashpay-contract,wallet-utils-contract,keywords-contract" +# Call unified build script with default features (no need to specify) exec "$SCRIPT_DIR/../scripts/build-wasm.sh" --package wasm-sdk --opt-level "$OPT_LEVEL" diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index c926425379c..61adc5181d4 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -1,6 +1,8 @@ - + From d47bfd86ac4ff84a2a4720f72b1b2db85d148804 Mon Sep 17 00:00:00 2001 From: quantum Date: Sat, 12 Jul 2025 02:12:18 -0500 Subject: [PATCH 29/30] fixes --- packages/scripts/build-wasm.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/scripts/build-wasm.sh b/packages/scripts/build-wasm.sh index 8172df61ed9..cdde279578e 100755 --- a/packages/scripts/build-wasm.sh +++ b/packages/scripts/build-wasm.sh @@ -110,9 +110,15 @@ if [ "$USE_WASM_PACK" = true ]; then # Add features if specified FEATURES_ARG="" if [ -n "${CARGO_BUILD_FEATURES:-}" ]; then + echo "CARGO_BUILD_FEATURES is set to: '$CARGO_BUILD_FEATURES'" FEATURES_ARG="--features $CARGO_BUILD_FEATURES" + else + echo "CARGO_BUILD_FEATURES is not set, using default features" + # Explicitly pass default features to ensure they're used + FEATURES_ARG="--features default" fi + echo "Running: wasm-pack build --target $TARGET_TYPE --release --no-opt $FEATURES_ARG" wasm-pack build --target "$TARGET_TYPE" --release --no-opt $FEATURES_ARG else # Build using cargo directly From 2b804473cb745a063dc4ba5d87b506d990a2e6e4 Mon Sep 17 00:00:00 2001 From: quantum Date: Sat, 12 Jul 2025 02:20:45 -0500 Subject: [PATCH 30/30] fixes --- packages/wasm-sdk/src/queries/dpns.rs | 156 ++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 packages/wasm-sdk/src/queries/dpns.rs diff --git a/packages/wasm-sdk/src/queries/dpns.rs b/packages/wasm-sdk/src/queries/dpns.rs new file mode 100644 index 00000000000..39ef08d67c3 --- /dev/null +++ b/packages/wasm-sdk/src/queries/dpns.rs @@ -0,0 +1,156 @@ +use crate::sdk::WasmSdk; +use crate::queries::{ProofMetadataResponse, ResponseMetadata, ProofInfo}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use serde::ser::Serialize as _; +use dash_sdk::platform::{Fetch, FetchMany, Document}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::document::DocumentV0Getters; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct DpnsUsernameInfo { + username: String, + identity_id: String, + document_id: String, +} + +#[wasm_bindgen] +pub async fn get_dpns_username_by_name( + sdk: &WasmSdk, + username: &str, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use drive::query::{WhereClause, WhereOperator}; + use dash_sdk::dpp::platform_value::Value; + + // DPNS contract ID on testnet + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + const DPNS_DOCUMENT_TYPE: &str = "domain"; + + // Parse username into label and domain + let parts: Vec<&str> = username.split('.').collect(); + if parts.len() != 2 { + return Err(JsError::new("Invalid username format. Expected format: label.dash")); + } + let label = parts[0]; + let domain = parts[1]; + + // Parse DPNS contract ID + let contract_id = dash_sdk::dpp::prelude::Identifier::from_string( + DPNS_CONTRACT_ID, + Encoding::Base58, + )?; + + // Create document query + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + DPNS_DOCUMENT_TYPE, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Query by label and normalizedParentDomainName + query = query.with_where(WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(label.to_lowercase()), + }); + + query = query.with_where(WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(domain.to_lowercase()), + }); + + let documents = Document::fetch_many(sdk.as_ref(), query).await?; + + if let Some((_, Some(document))) = documents.into_iter().next() { + let result = DpnsUsernameInfo { + username: username.to_string(), + identity_id: document.owner_id().to_string(Encoding::Base58), + document_id: document.id().to_string(Encoding::Base58), + }; + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new(&format!("Username '{}' not found", username))) + } +} + +#[wasm_bindgen] +pub async fn get_dpns_username_by_name_with_proof_info( + sdk: &WasmSdk, + username: &str, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use drive::query::{WhereClause, WhereOperator}; + use dash_sdk::dpp::platform_value::Value; + + // DPNS contract ID on testnet + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + const DPNS_DOCUMENT_TYPE: &str = "domain"; + + // Parse username into label and domain + let parts: Vec<&str> = username.split('.').collect(); + if parts.len() != 2 { + return Err(JsError::new("Invalid username format. Expected format: label.dash")); + } + let label = parts[0]; + let domain = parts[1]; + + // Parse DPNS contract ID + let contract_id = dash_sdk::dpp::prelude::Identifier::from_string( + DPNS_CONTRACT_ID, + Encoding::Base58, + )?; + + // Create document query + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + DPNS_DOCUMENT_TYPE, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Query by label and normalizedParentDomainName + query = query.with_where(WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(label.to_lowercase()), + }); + + query = query.with_where(WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(domain.to_lowercase()), + }); + + let (documents, metadata, proof) = Document::fetch_many_with_metadata_and_proof(sdk.as_ref(), query, None).await?; + + if let Some((_, Some(document))) = documents.into_iter().next() { + let result = DpnsUsernameInfo { + username: username.to_string(), + identity_id: document.owner_id().to_string(Encoding::Base58), + document_id: document.id().to_string(Encoding::Base58), + }; + + let response = ProofMetadataResponse { + data: result, + metadata: metadata.into(), + proof: proof.into(), + }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new(&format!("Username '{}' not found", username))) + } +} +