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 00000000000..3d2f216ed3b Binary files /dev/null and b/.yarn/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-8e4202766a.zip differ 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 00000000000..a4c24ad37e5 Binary files /dev/null and b/.yarn/cache/dompurify-npm-3.2.6-8d2a7542b7-b91631ed0e.zip differ 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/package.json b/package.json index 657244e994b..c3790938ae8 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "brace-expansion": "^2.0.2" }, "dependencies": { + "dompurify": "^3.2.6", "node-gyp": "^10.0.1" } } 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..d2e1f8b9cc2 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)] @@ -38,74 +55,160 @@ pub struct DataContractSource { impl SystemDataContract { pub fn id(&self) -> Identifier { let bytes = match self { + #[cfg(feature = "withdrawals")] SystemDataContract::Withdrawals => withdrawals_contract::ID_BYTES, + #[cfg(not(feature = "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 => [ + 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 => [ + 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 => [ + 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 => [ + 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 => [ + 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 => [ + 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 => [ + 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, + ], }; 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 +216,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/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-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 321194a4640..61e34c3b678 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" } @@ -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", @@ -126,34 +126,18 @@ 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", ] all_features_without_client = [ "json-object", "platform-value", - "system_contracts", + "all-system_contracts", "state-transitions", "extended-document", "cbor", @@ -286,8 +270,19 @@ 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"] -fixtures-and-mocks = ["system_contracts", "platform-value/json"] +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"] + +# Individual data contract features +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/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/lib.rs b/packages/rs-dpp/src/lib.rs index 66599277405..fed054861a9 100644 --- a/packages/rs-dpp/src/lib.rs +++ b/packages/rs-dpp/src/lib.rs @@ -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 92bb23364e4..13ed6cd214a 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -22,17 +22,42 @@ impl ConfigurationForSystemContract for SystemDataContract { platform_version: &PlatformVersion, ) -> Result { match self { - SystemDataContract::Withdrawals - | SystemDataContract::MasternodeRewards - | SystemDataContract::FeatureFlags - | SystemDataContract::DPNS - | SystemDataContract::Dashpay - | SystemDataContract::WalletUtils => { + SystemDataContract::Withdrawals => { let mut config = DataContractConfig::default_for_version(platform_version)?; config.set_sized_integer_types_enabled(false); Ok(config) } - SystemDataContract::TokenHistory | SystemDataContract::KeywordSearch => { + SystemDataContract::MasternodeRewards => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + SystemDataContract::FeatureFlags => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + SystemDataContract::DPNS => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + SystemDataContract::Dashpay => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + SystemDataContract::WalletUtils => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(false); + Ok(config) + } + SystemDataContract::TokenHistory => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(true); + Ok(config) + } + 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-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}; diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index f2031e68e0e..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 = "system_contracts")] +#[cfg(all(feature = "withdrawals-contract", feature = "system_contracts"))] 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 d03d583efff..dd83b8cf6bc 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 } @@ -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", @@ -123,4 +123,6 @@ verify = [ "grovedb-costs", "dpp/state-transitions", "dpp/system_contracts", + "dpp/token-history-contract", + "dpp/withdrawals-contract" ] diff --git a/packages/rs-sdk-trusted-context-provider/Cargo.toml b/packages/rs-sdk-trusted-context-provider/Cargo.toml index 18e83d229a1..082f11e7418 100644 --- a/packages/rs-sdk-trusted-context-provider/Cargo.toml +++ b/packages/rs-sdk-trusted-context-provider/Cargo.toml @@ -22,6 +22,28 @@ dashcore = { git = "https://github.com/dashpay/rust-dashcore", features = ["bls- futures = "0.3" url = "2.5" +[features] +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"] +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 b5caddb2516..6d75000516c 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -10,6 +10,16 @@ use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; type QuorumHash = [u8; 32]; use dpp::dashcore::Network; use dpp::data_contract::TokenConfiguration; +#[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 dpp::version::PlatformVersion; use lru::LruCache; @@ -139,7 +149,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(); @@ -486,7 +496,104 @@ 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/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 1e2a8a18d47..8f6c7b2e008 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" } @@ -116,7 +115,16 @@ 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) +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"] token_reward_explanations = ["dpp/token-reward-explanations"] 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..65d22db4787 --- /dev/null +++ b/packages/rs-sdk/src/platform/dpns_usernames.rs @@ -0,0 +1,584 @@ +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; +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::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 +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() +} + +/// 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 &ch in &chars[1..chars.len() - 1] { + if !ch.is_ascii_alphanumeric() && ch != '-' { + 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}; + // sha256d already does double SHA256 + let hash = sha256d::Hash::hash(&data); + 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") + 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, + /// Optional callback to be called with the preorder document result + pub preorder_callback: Option, +} + +/// 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 { + /// 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 = { + 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)))? + }; + + 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()))?; + + 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()))?; + 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 = + 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 + let platform_preorder_document = 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?; + + // Call the preorder callback if provided + if let Some(callback) = input.preorder_callback { + callback(&platform_preorder_document); + } + + // Submit domain document after preorder + let platform_domain_document = 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: platform_preorder_document, + domain_document: platform_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; + + let dpns_contract = self.fetch_dpns_contract().await?; + + let normalized_label = convert_to_homograph_safe_chars(label); + + // Query for existing domain with this label + let query = DocumentQuery { + data_contract: dpns_contract, + 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; + + let dpns_contract = self.fetch_dpns_contract().await?; + + // Extract label from full name if needed + // 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 + let query = DocumentQuery { + data_contract: dpns_contract, + 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"); + } + + #[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/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 { diff --git a/packages/scripts/build-wasm.sh b/packages/scripts/build-wasm.sh index 46934de5e98..cdde279578e 100755 --- a/packages/scripts/build-wasm.sh +++ b/packages/scripts/build-wasm.sh @@ -107,12 +107,30 @@ 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 + 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 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-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 7a7f5cd7b53..eb829cddbb1 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -6,10 +6,21 @@ publish = false crate-type = ["cdylib"] [features] -default = [] +default = ["dpns-contract", "dashpay-contract", "wallet-utils-contract", "token-history-contract", "keywords-contract"] mocks = ["dash-sdk/mocks"] -system-data-contracts = ["dash-sdk/system-data-contracts"] + +# 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", "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"] [dependencies] diff --git a/packages/wasm-sdk/build.sh b/packages/wasm-sdk/build.sh index 81af0bd508c..82dda6e01d4 100755 --- a/packages/wasm-sdk/build.sh +++ b/packages/wasm-sdk/build.sh @@ -16,5 +16,5 @@ if [ "${CARGO_BUILD_PROFILE:-}" = "dev" ] || [ "${CI:-}" != "true" ]; then OPT_LEVEL="minimal" fi -# Call unified build script +# 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 2bacaa7cef4..61adc5181d4 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -1,5 +1,8 @@ + @@ -60,7 +63,7 @@ .app-container { display: flex; - height: 100vh; + height: calc(100vh - 50px); /* Account for status banner height */ overflow: hidden; } @@ -104,6 +107,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 +292,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; @@ -280,12 +379,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 { @@ -349,6 +453,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; @@ -638,6 +781,7 @@ + @@ -658,7 +802,7 @@

Authentication

- +
@@ -670,6 +814,16 @@

Query Parameters

+ + @@ -683,7 +837,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.
+
@@ -806,14 +962,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, @@ -826,33 +987,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, @@ -861,9 +1036,18 @@

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 + 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 +1202,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 } + ] } } }, @@ -1216,7 +1407,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 } @@ -1224,7 +1415,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 } @@ -1248,22 +1439,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 } ] } } @@ -1330,12 +1517,32 @@

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: { + 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)" } ] } } @@ -2316,13 +2523,16 @@

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 + const safeData = DOMPurify.sanitize(data); + splitContainer.innerHTML = `
${safeData}
`; currentResult = null; } else { - resultContent.className = 'result-content'; // Parse JSON string if necessary let dataToFormat = data; if (typeof data === 'string') { @@ -2333,20 +2543,183 @@

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 = DOMPurify.sanitize(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 = DOMPurify.sanitize(formatResultWithCredits(proofDisplay)); + } else { + // Single view for non-proof mode or non-proof responses + splitContainer.innerHTML = `
`; + const resultContent = document.getElementById('identityInfo'); + resultContent.innerHTML = DOMPurify.sanitize(formatResultWithCredits(dataToFormat)); + } + currentResult = data; } } + 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 function formatCreditsValue(credits) { const creditsNum = BigInt(credits); - const dashValue = Number(creditsNum) / CREDITS_PER_DASH; - const dashFormatted = dashValue.toFixed(8).replace(/\.?0+$/, ''); - return `${credits}`; + 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)}`; + } + + 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 `${escapeHtml(nonce.toString())}`; } function processValue(value, key, indent = 0) { @@ -2354,8 +2727,13 @@

Results

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)) { @@ -2370,7 +2748,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')} @@ -2393,12 +2772,6 @@

Results

${indentStr}]`; } - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - return `
${processValue(data, '')}
`; } @@ -2452,6 +2825,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 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) { @@ -2487,8 +2865,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'); @@ -2782,10 +3209,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( @@ -2808,9 +3247,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 @@ -2836,8 +3283,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, @@ -2846,10 +3298,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') { @@ -2865,31 +3317,104 @@

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_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); + + 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') { @@ -2905,15 +3430,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( @@ -2945,7 +3483,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) { @@ -2971,7 +3513,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( @@ -2990,7 +3536,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); @@ -3006,7 +3556,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 @@ -3164,8 +3718,6 @@

Results

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

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`); + + // 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) + // 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! Both preorder and domain documents submitted.', 'success'); } else if (transitionType === 'documentPurchase') { // Handle document purchase result = await sdk.documentPurchase( @@ -3644,7 +4315,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; } @@ -3684,6 +4354,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'; }); @@ -3702,6 +4373,7 @@

Results

// Hide inputs and button queryInputs.style.display = 'none'; + document.getElementById('proofToggleContainer').style.display = 'none'; executeButton.style.display = 'none'; queryDescription.style.display = 'none'; @@ -3788,6 +4460,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'; @@ -3832,6 +4512,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/dpns.rs b/packages/wasm-sdk/src/dpns.rs new file mode 100644 index 00000000000..2e96eeb35da --- /dev/null +++ b/packages/wasm-sdk/src/dpns.rs @@ -0,0 +1,177 @@ +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::{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")] +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, + preorder_callback: Option, +) -> 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(); + + // 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 + let result = sdk.as_ref() + .register_dpns_name(input) + .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( + 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; 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 aa6260c917f..84bcd9064d9 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}; +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 = query.with_where(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, @@ -371,9 +487,87 @@ pub async fn get_document( } #[wasm_bindgen] -pub async fn get_dpns_username( +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, identity_id: &str, + limit: Option, ) -> Result { use dash_sdk::platform::documents::document_query::DocumentQuery; use dash_sdk::platform::FetchMany; @@ -412,41 +606,173 @@ 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"))?; + 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)); + } + } + + 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(); - // Construct the full username - let username = format!("{}.{}", label, parent_domain); + 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 the username as a JSON string - return Ok(JsValue::from_str(&username)); + return serde_wasm_bindgen::to_value(&modified_result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))); } } - // No DPNS name found for this identity - Ok(JsValue::NULL) + // 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/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))) + } +} + diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs index 0ddf1d455a0..96f4fb2e020 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 { @@ -152,30 +163,13 @@ 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::dpp::dashcore::ProTxHash; - use std::str::FromStr; - 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)))?; - - // 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 - ))); - } + // Silence unused variables since this function is not yet implemented + let _ = (sdk, ids); // TODO: Use the SDK's FetchMany trait to get proposer block counts // This would automatically handle proof verification when sdk.prove() is true @@ -183,7 +177,7 @@ pub async fn get_evonodes_proposed_epoch_blocks_by_ids( /* 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)))?; @@ -214,16 +208,14 @@ 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, ) -> 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 { @@ -237,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, ) @@ -294,4 +277,179 @@ 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; + + 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 { + 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..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; @@ -12,9 +14,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 +248,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 +518,544 @@ 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 + +#[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)))?; + + 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..8e9d6f70c04 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,73 @@ 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 { + use base64::{Engine as _, engine::general_purpose}; + + ProofInfo { + grovedb_proof: general_purpose::STANDARD.encode(&proof.grovedb_proof), + quorum_hash: hex::encode(&proof.quorum_hash), + signature: general_purpose::STANDARD.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..107734e8445 100644 --- a/packages/wasm-sdk/src/queries/token.rs +++ b/packages/wasm-sdk/src/queries/token.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::{Identifier, FetchMany}; use dash_sdk::dpp::balances::credits::TokenAmount; use dash_sdk::dpp::tokens::status::TokenStatus; @@ -386,7 +388,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 +457,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 +493,508 @@ 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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 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"