diff --git a/packages/js-evo-sdk/src/tokens/facade.ts b/packages/js-evo-sdk/src/tokens/facade.ts index ee2d3289e36..67d79ce37f1 100644 --- a/packages/js-evo-sdk/src/tokens/facade.ts +++ b/packages/js-evo-sdk/src/tokens/facade.ts @@ -100,12 +100,12 @@ export class TokensFacade { return w.getTokenContractInfoWithProofInfo(contractId); } - async perpetualDistributionLastClaim(identityId: wasm.IdentifierLike, tokenId: wasm.IdentifierLike): Promise { + async perpetualDistributionLastClaim(identityId: wasm.IdentifierLike, tokenId: wasm.IdentifierLike): Promise { const w = await this.sdk.getWasmSdkConnected(); return w.getTokenPerpetualDistributionLastClaim(identityId, tokenId); } - async perpetualDistributionLastClaimWithProof(identityId: wasm.IdentifierLike, tokenId: wasm.IdentifierLike): Promise> { + async perpetualDistributionLastClaimWithProof(identityId: wasm.IdentifierLike, tokenId: wasm.IdentifierLike): Promise> { const w = await this.sdk.getWasmSdkConnected(); return w.getTokenPerpetualDistributionLastClaimWithProofInfo(identityId, tokenId); } diff --git a/packages/js-evo-sdk/tests/functional/epoch.spec.mjs b/packages/js-evo-sdk/tests/functional/epoch.spec.mjs index f4d6ab0e6c0..47c6365a663 100644 --- a/packages/js-evo-sdk/tests/functional/epoch.spec.mjs +++ b/packages/js-evo-sdk/tests/functional/epoch.spec.mjs @@ -24,4 +24,24 @@ describe('Epoch', function epochSuite() { const res = await sdk.epoch.evonodesProposedBlocksByRange({ epoch: TEST_IDS.epoch, limit: 5 }); expect(res).to.exist(); }); + + it('evonodesProposedBlocksByIdsWithProof() returns results with proof', async () => { + const { epoch, proTxHash } = TEST_IDS; + const res = await sdk.epoch.evonodesProposedBlocksByIdsWithProof(epoch, [proTxHash]); + expect(res).to.exist(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + }); + + it('evonodesProposedBlocksByRangeWithProof() returns results with proof', async () => { + const res = await sdk.epoch.evonodesProposedBlocksByRangeWithProof({ + epoch: TEST_IDS.epoch, + limit: 5, + }); + expect(res).to.exist(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + }); }); diff --git a/packages/js-evo-sdk/tests/functional/protocol.spec.mjs b/packages/js-evo-sdk/tests/functional/protocol.spec.mjs index 3db4f508342..c89cf6466d9 100644 --- a/packages/js-evo-sdk/tests/functional/protocol.spec.mjs +++ b/packages/js-evo-sdk/tests/functional/protocol.spec.mjs @@ -15,8 +15,24 @@ describe('Protocol', function protocolSuite() { expect(res).to.exist(); }); + it('versionUpgradeStateWithProof() returns state with proof', async () => { + const res = await sdk.protocol.versionUpgradeStateWithProof(); + expect(res).to.exist(); + expect(res.data).to.exist(); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + }); + it('versionUpgradeVoteStatus() returns vote window status', async () => { const res = await sdk.protocol.versionUpgradeVoteStatus(TEST_IDS.proTxHash, 1); expect(res).to.exist(); }); + + it('versionUpgradeVoteStatusWithProof() returns vote status with proof', async () => { + const res = await sdk.protocol.versionUpgradeVoteStatusWithProof(TEST_IDS.proTxHash, 50); + expect(res).to.exist(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + }); }); diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index 0cde23cea20..43e137400d5 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -61,6 +61,9 @@ pub struct TrustedHttpContextProvider { /// Known contracts cache - contracts that are pre-loaded and can be served immediately known_contracts: Arc>>>, + /// Known token configurations cache - token configs that are pre-loaded for proof verification + known_token_configurations: Arc>>, + /// Whether to refetch quorums if not found in cache refetch_if_not_found: bool, } @@ -165,6 +168,7 @@ impl TrustedHttpContextProvider { last_previous_quorums: Arc::new(ArcSwap::new(Arc::new(None))), fallback_provider: None, known_contracts: Arc::new(Mutex::new(HashMap::new())), + known_token_configurations: Arc::new(Mutex::new(HashMap::new())), refetch_if_not_found: true, }) } @@ -208,6 +212,23 @@ impl TrustedHttpContextProvider { } } + /// Add a token configuration to the known token configurations cache + pub fn add_known_token_configuration(&self, token_id: Identifier, config: TokenConfiguration) { + let mut known = self.known_token_configurations.lock().unwrap(); + known.insert(token_id, config); + } + + /// Add multiple token configurations to the known token configurations cache + pub fn add_known_token_configurations( + &self, + configs: Vec<(Identifier, TokenConfiguration)>, + ) { + let mut known = self.known_token_configurations.lock().unwrap(); + for (token_id, config) in configs { + known.insert(token_id, config); + } + } + /// Update the quorum caches by fetching current and previous quorums pub async fn update_quorum_caches(&self) -> Result<(), TrustedContextProviderError> { // Fetch current quorums @@ -747,6 +768,13 @@ impl ContextProvider for TrustedHttpContextProvider { &self, token_id: &Identifier, ) -> Result, ContextProviderError> { + // First check known token configurations cache + let known = self.known_token_configurations.lock().unwrap(); + if let Some(config) = known.get(token_id) { + return Ok(Some(config.clone())); + } + drop(known); + // Delegate to fallback provider if available if let Some(ref provider) = self.fallback_provider { provider.get_token_configuration(token_id) diff --git a/packages/wasm-dpp2/TODO.md b/packages/wasm-dpp2/TODO.md new file mode 100644 index 00000000000..8f142e23ecc --- /dev/null +++ b/packages/wasm-dpp2/TODO.md @@ -0,0 +1,52 @@ +# wasm-dpp2 TODO + +## Type Wrappers for Flexible Input + +### ProTxHash Wrapper + +Create a wrapper around `ProTxHash` from dashcore with flexible input types. + +`ProTxHash` is a newtype wrapper around `sha256d::Hash`: + +```rust +pub struct ProTxHash(sha256d::Hash); +``` + +```typescript +type ProTxHashLike = ProTxHash | Uint8Array | string; +``` + +**Requirements:** + +- Create `ProTxHashWasm` wrapper in wasm-dpp2 +- Accept hex string, raw bytes (Uint8Array), or ProTxHash object +- Centralize parsing logic (currently duplicated in wasm-sdk methods) + +**Affected areas in wasm-sdk:** + +- `getProtocolVersionUpgradeVoteStatus` (startProTxHash parameter) +- `getProtocolVersionUpgradeVoteStatusWithProofInfo` (startProTxHash parameter) +- `getEvonodesProposedEpochBlocksByIds` (ids parameter) +- `getEvonodesProposedEpochBlocksByIdsWithProofInfo` (proTxHashes parameter) +- `EvonodeProposedBlocksRangeQuery.startAfter` field + +### Network Wrapper + +Create a wrapper around `Network` from dashcore with flexible input types: + +```typescript +type NetworkLike = Network | string; +``` + +**Requirements:** + +- Create `NetworkWasm` wrapper in wasm-dpp2 +- Accept string ("mainnet", "testnet", "devnet", "regtest") or Network object +- Centralize parsing logic +- Use everywhere we pass network parameter + +**Affected areas:** + +- SDK builder methods +- Wallet key derivation +- Address generation/validation diff --git a/packages/wasm-sdk/src/context_provider.rs b/packages/wasm-sdk/src/context_provider.rs index 481e2966ab5..093c45ad838 100644 --- a/packages/wasm-sdk/src/context_provider.rs +++ b/packages/wasm-sdk/src/context_provider.rs @@ -162,4 +162,9 @@ impl WasmTrustedContext { pub fn add_known_contract(&self, contract: DataContract) { self.inner.add_known_contract(contract); } + + /// Add a token configuration to the known token configurations cache + pub fn add_known_token_configuration(&self, token_id: Identifier, config: TokenConfiguration) { + self.inner.add_known_token_configuration(token_id, config); + } } diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs index 8fcfd8b4d38..e808423fec6 100644 --- a/packages/wasm-sdk/src/queries/epoch.rs +++ b/packages/wasm-sdk/src/queries/epoch.rs @@ -164,12 +164,6 @@ export interface EvonodeProposedBlocksRangeQuery { * @default undefined */ startAfter?: string; - - /** - * Sort order for results. - * @default undefined (server default) - */ - orderAscending?: boolean; } "#; @@ -187,15 +181,12 @@ struct EvonodeProposedBlocksRangeQueryInput { limit: Option, #[serde(default)] start_after: Option, - #[serde(default)] - order_ascending: Option, } struct EvonodeProposedBlocksRangeQueryParsed { epoch: u16, limit: Option, start_info: Option, - order_ascending: Option, } fn parse_evonode_range_query( @@ -223,7 +214,6 @@ fn parse_evonode_range_query( epoch: input.epoch, limit: input.limit, start_info, - order_ascending: input.order_ascending, }) } @@ -374,11 +364,8 @@ impl WasmSdk { epoch, limit, start_info, - order_ascending, } = parse_evonode_range_query(query)?; - let _ = order_ascending; - let counts_result = ProposerBlockCounts::fetch_proposed_blocks_by_range( self.as_ref(), Some(epoch), @@ -517,31 +504,88 @@ impl WasmSdk { )) } - #[wasm_bindgen(js_name = "getEvonodesProposedEpochBlocksByIdsWithProofInfo")] + #[wasm_bindgen( + js_name = "getEvonodesProposedEpochBlocksByIdsWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] pub async fn get_evonodes_proposed_epoch_blocks_by_ids_with_proof_info( &self, epoch: u16, #[wasm_bindgen(js_name = "proTxHashes")] pro_tx_hashes: Vec, - ) -> Result { - // TODO: Implement once SDK Query trait is implemented for ProposerBlockCountById - // Currently not supported due to query format issues - let _ = (self, epoch, pro_tx_hashes); // Parameters will be used when implemented - Err(WasmSdkError::generic( - "get_evonodes_proposed_epoch_blocks_by_ids_with_proof_info is not yet implemented", + ) -> Result { + use drive_proof_verifier::types::ProposerBlockCountById; + + // Parse the ProTxHash strings + let parsed_hashes: Vec = pro_tx_hashes + .into_iter() + .map(|hash_str| { + ProTxHash::from_str(&hash_str).map_err(|e| { + WasmSdkError::invalid_argument(format!( + "Invalid ProTxHash '{}': {}", + hash_str, e + )) + }) + }) + .collect::, WasmSdkError>>()?; + + // Use FetchMany with proof to get block counts for specific IDs + let (counts, metadata, proof) = ProposerBlockCountById::fetch_many_with_metadata_and_proof( + self.as_ref(), + (epoch, parsed_hashes), + None, + ) + .await?; + + let map = Map::new(); + for (identifier, count) in counts.0 { + let key = JsValue::from(IdentifierWasm::from(identifier)); + map.set(&key, &JsValue::from(BigInt::from(count))); + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + map, metadata, proof, )) } - #[wasm_bindgen(js_name = "getEvonodesProposedEpochBlocksByRangeWithProofInfo")] + #[wasm_bindgen( + js_name = "getEvonodesProposedEpochBlocksByRangeWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] pub async fn get_evonodes_proposed_epoch_blocks_by_range_with_proof_info( &self, query: EvonodeProposedBlocksRangeQueryJs, - ) -> Result { - // TODO: Implement once SDK Query trait is implemented for ProposerBlockCountByRange - // Currently not supported due to query format issues - let parsed = parse_evonode_range_query(query)?; - let _ = (self, parsed); // Parameters will be used when implemented - Err(WasmSdkError::generic( - "get_evonodes_proposed_epoch_blocks_by_range_with_proof_info is not yet implemented", + ) -> Result { + use drive_proof_verifier::types::ProposerBlockCountByRange; + + let EvonodeProposedBlocksRangeQueryParsed { + epoch, + limit, + start_info, + } = parse_evonode_range_query(query)?; + + // Create a LimitQuery for the range request + let limit_query = LimitQuery { + query: Some(epoch), + limit, + start_info, + }; + + let (counts, metadata, proof) = + ProposerBlockCountByRange::fetch_many_with_metadata_and_proof( + self.as_ref(), + limit_query, + None, + ) + .await?; + + let map = Map::new(); + for (identifier, count) in counts.0 { + let key = JsValue::from(IdentifierWasm::from(identifier)); + map.set(&key, &JsValue::from(BigInt::from(count))); + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + map, metadata, proof, )) } } diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs index 39b13978743..57b149e6dc2 100644 --- a/packages/wasm-sdk/src/queries/protocol.rs +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -251,19 +251,74 @@ impl WasmSdk { )) } - #[wasm_bindgen(js_name = "getProtocolVersionUpgradeVoteStatusWithProofInfo")] + #[wasm_bindgen( + js_name = "getProtocolVersionUpgradeVoteStatusWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] pub async fn get_protocol_version_upgrade_vote_status_with_proof_info( &self, #[wasm_bindgen(js_name = "startProTxHash")] #[wasm_bindgen(unchecked_param_type = "string | Uint8Array")] start_pro_tx_hash: JsValue, 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 _ = (self, start_pro_tx_hash, count); // Parameters will be used when implemented - Err(WasmSdkError::generic( - "get_protocol_version_upgrade_vote_status_with_proof_info is not yet implemented", + ) -> Result { + use dash_sdk::dpp::dashcore::ProTxHash; + use dash_sdk::platform::{FetchMany, LimitQuery}; + use drive_proof_verifier::types::MasternodeProtocolVote; + use std::str::FromStr; + + // Parse the ProTxHash + let start_hash: Option = if let Some(s) = start_pro_tx_hash.as_string() { + if s.is_empty() { + None + } else { + Some(ProTxHash::from_str(&s).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid ProTxHash: {}", e)) + })?) + } + } else { + let bytes = js_sys::Uint8Array::new(&start_pro_tx_hash).to_vec(); + if bytes.is_empty() { + None + } else { + if bytes.len() != 32 { + return Err(WasmSdkError::invalid_argument( + "ProTxHash must be 32 bytes or an empty value", + )); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + let raw = sha256d::Hash::from_byte_array(arr); + Some(ProTxHash::from_raw_hash(raw)) + } + }; + + // Create a LimitQuery with the start hash and count + let query = LimitQuery { + query: start_hash, + limit: Some(count), + start_info: None, + }; + + let (votes_result, metadata, proof) = + MasternodeProtocolVote::fetch_many_with_metadata_and_proof(self.as_ref(), query, None) + .await?; + + // Convert to our response format + let votes_map = Map::new(); + for (pro_tx_hash, vote_opt) in votes_result { + if let Some(vote) = vote_opt { + let key = JsValue::from_str(&pro_tx_hash.to_string()); + let value = JsValue::from(ProtocolVersionUpgradeVoteStatusWasm::new( + pro_tx_hash.to_string(), + vote.voted_version, + )); + votes_map.set(&key, &value); + } + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + votes_map, metadata, proof, )) } } diff --git a/packages/wasm-sdk/src/queries/token.rs b/packages/wasm-sdk/src/queries/token.rs index 44955912509..4bf322f9d0c 100644 --- a/packages/wasm-sdk/src/queries/token.rs +++ b/packages/wasm-sdk/src/queries/token.rs @@ -5,10 +5,12 @@ use crate::queries::ProofMetadataResponseWasm; use crate::sdk::WasmSdk; use dash_sdk::dpp::balances::credits::TokenAmount; use dash_sdk::dpp::tokens::calculate_token_id; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use dash_sdk::dpp::tokens::info::IdentityTokenInfo; use dash_sdk::dpp::tokens::status::TokenStatus; use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; -use dash_sdk::platform::{FetchMany, Identifier}; +use dash_sdk::platform::query::TokenLastClaimQuery; +use dash_sdk::platform::{Fetch, FetchMany, Identifier}; use js_sys::{BigInt, Map}; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; @@ -53,36 +55,54 @@ impl TokenPriceInfoWasm { } } -#[wasm_bindgen(js_name = "TokenLastClaim")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenLastClaimWasm { - last_claim_timestamp_ms: u64, - last_claim_block_height: u64, -} +#[wasm_bindgen(js_name = "RewardDistributionMoment")] +pub struct RewardDistributionMomentWasm(RewardDistributionMoment); + +#[wasm_bindgen(js_class = RewardDistributionMoment)] +impl RewardDistributionMomentWasm { + /// Returns the type: "block", "time", or "epoch" + #[wasm_bindgen(getter = "type")] + pub fn moment_type(&self) -> String { + match &self.0 { + RewardDistributionMoment::BlockBasedMoment(_) => "block".to_string(), + RewardDistributionMoment::TimeBasedMoment(_) => "time".to_string(), + RewardDistributionMoment::EpochBasedMoment(_) => "epoch".to_string(), + } + } -impl TokenLastClaimWasm { - fn new(last_claim_timestamp_ms: u64, last_claim_block_height: u64) -> Self { - Self { - last_claim_timestamp_ms, - last_claim_block_height, + /// Returns the block height (only valid when type is "block") + #[wasm_bindgen(getter = "blockHeight")] + pub fn block_height(&self) -> Option { + match &self.0 { + RewardDistributionMoment::BlockBasedMoment(height) => Some(*height), + _ => None, } } -} -#[wasm_bindgen(js_class = TokenLastClaim)] -impl TokenLastClaimWasm { - #[wasm_bindgen(getter = "lastClaimTimestampMs")] - pub fn last_claim_timestamp_ms(&self) -> u64 { - self.last_claim_timestamp_ms + /// Returns the timestamp in ms (only valid when type is "time") + #[wasm_bindgen(getter = "timestampMs")] + pub fn timestamp_ms(&self) -> Option { + match &self.0 { + RewardDistributionMoment::TimeBasedMoment(ts) => Some(*ts), + _ => None, + } } - #[wasm_bindgen(getter = "lastClaimBlockHeight")] - pub fn last_claim_block_height(&self) -> u64 { - self.last_claim_block_height + /// Returns the epoch index (only valid when type is "epoch") + #[wasm_bindgen(getter = "epochIndex")] + pub fn epoch_index(&self) -> Option { + match &self.0 { + RewardDistributionMoment::EpochBasedMoment(epoch) => Some(*epoch), + _ => None, + } + } +} + +impl From for RewardDistributionMomentWasm { + fn from(moment: RewardDistributionMoment) -> Self { + Self(moment) } } -impl_wasm_serde_conversions!(TokenLastClaimWasm); #[wasm_bindgen(js_name = "TokenTotalSupply")] #[derive(Clone, Serialize, Deserialize)] @@ -484,177 +504,24 @@ impl WasmSdk { #[wasm_bindgen(js_name = "tokenId")] #[wasm_bindgen(unchecked_param_type = "Identifier | Uint8Array | string")] token_id: JsValue, - ) -> Result, WasmSdkError> { + ) -> Result, WasmSdkError> { // Parse IDs let identity_identifier = identifier_from_js(&identity_id, "identity ID")?; let token_identifier = identifier_from_js(&token_id, "token ID")?; - // Use direct gRPC request instead of high-level SDK fetch to avoid proof verification issues - use dapi_grpc::platform::v0::{ - get_token_perpetual_distribution_last_claim_request::{ - GetTokenPerpetualDistributionLastClaimRequestV0, Version, - }, - GetTokenPerpetualDistributionLastClaimRequest, - }; - use rs_dapi_client::DapiRequestExecutor; - - // Create direct gRPC Request without proofs to avoid context provider issues - let request = GetTokenPerpetualDistributionLastClaimRequest { - version: Some(Version::V0( - GetTokenPerpetualDistributionLastClaimRequestV0 { - token_id: token_identifier.to_vec(), - identity_id: identity_identifier.to_vec(), - contract_info: None, // Not needed for this query - prove: false, // Use prove: false to avoid proof verification and context provider dependency - }, - )), - }; + // Prefetch token configuration and add to context provider cache + // This is required for proof verification to work + self.prefetch_token_configuration(token_identifier).await?; - // Execute the gRPC request - let response = self - .inner_sdk() - .execute(request, rs_dapi_client::RequestSettings::default()) - .await - .map_err(|e| { - WasmSdkError::generic(format!( - "Failed to fetch token perpetual distribution last claim: {}", - e - )) - })?; - - // Extract result from response and convert to our expected format - let claim_result = match response.inner.version { - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::Version::V0( - v0, - ), - ) => { - match v0.result { - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::Result::LastClaim( - claim, - ), - ) => { - match claim.paid_at { - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::last_claim_info::PaidAt::TimestampMs( - timestamp, - ), - ) => Some((timestamp, 0)), - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::last_claim_info::PaidAt::BlockHeight( - height, - ), - ) => Some((0, height)), - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::last_claim_info::PaidAt::Epoch( - epoch, - ), - ) => Some((0, epoch as u64)), - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::last_claim_info::PaidAt::RawBytes( - bytes, - ), - ) => { - // Raw bytes format specification (confirmed via server trace logs): - // - Total length: 8 bytes (big-endian encoding) - // - Bytes 0-3: Timestamp as u32 (seconds since Unix epoch, 0 = no timestamp recorded) - // - Bytes 4-7: Block height as u32 (Dash blockchain block number) - // - // Validation ranges: - // - Timestamp: 0 (unset) or >= 1609459200 (Jan 1, 2021 00:00:00 UTC, before Dash Platform mainnet) - // - Block height: 0 (invalid) or >= 1 (valid blockchain height) - if bytes.len() >= 8 { - let timestamp = u32::from_be_bytes([ - bytes[0], - bytes[1], - bytes[2], - bytes[3], - ]) as u64; - let block_height = u32::from_be_bytes([ - bytes[4], - bytes[5], - bytes[6], - bytes[7], - ]) as u64; - - // Validate timestamp: must be 0 (unset) or a reasonable Unix timestamp - let validated_timestamp = if timestamp != 0 - && timestamp < 1609459200 - { - tracing::warn!( - target = "wasm_sdk", timestamp, - "Invalid timestamp in raw bytes (too early)" - ); - 0 // Use 0 for invalid timestamps - } else { - timestamp - }; - - // Validate block height: must be a positive value - let validated_block_height = if block_height == 0 { - tracing::warn!( - target = "wasm_sdk", - "Invalid block height in raw bytes: 0 (genesis block not expected)" - ); - 1 // Use minimum valid block height - } else { - block_height - }; - - Some((validated_timestamp * 1000, validated_block_height)) - } else if bytes.len() >= 4 { - // Fallback: decode only the last 4 bytes as block height - let block_height = u32::from_be_bytes([ - bytes[bytes.len() - 4], - bytes[bytes.len() - 3], - bytes[bytes.len() - 2], - bytes[bytes.len() - 1], - ]) as u64; - - // Validate block height - let validated_block_height = if block_height == 0 { - tracing::warn!( - target = "wasm_sdk", - "Invalid block height in fallback parsing: 0" - ); - 1 // Use minimum valid block height - } else { - block_height - }; - - Some((0, validated_block_height)) - } else { - tracing::warn!( - target = "wasm_sdk", len = bytes.len(), - "Insufficient raw bytes length (expected 8 or 4)" - ); - Some((0, 0)) - } - } - None => None, // No paid_at info - } - } - Some( - dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::Result::Proof( - _, - ), - ) => { - return Err( - WasmSdkError::generic( - "Received proof instead of data - this should not happen with prove: false", - ), - ); - } - None => None, // No claim found - } - } - None => return Err(WasmSdkError::generic("Invalid response version")), + // Create query and fetch via SDK with proof verification + let query = TokenLastClaimQuery { + token_id: token_identifier, + identity_id: identity_identifier, }; - Ok(claim_result.map(|(timestamp_ms, block_height)| { - TokenLastClaimWasm::new(timestamp_ms, block_height) - })) + let claim_result = RewardDistributionMoment::fetch(self.as_ref(), query).await?; + + Ok(claim_result.map(RewardDistributionMomentWasm::from)) } #[wasm_bindgen(js_name = "getTokenTotalSupply")] @@ -1005,6 +872,10 @@ impl WasmSdk { let identity_identifier = identifier_from_js(&identity_id, "identity ID")?; let token_identifier = identifier_from_js(&token_id, "token ID")?; + // Prefetch token configuration and add to context provider cache + // This is required for proof verification to work + self.prefetch_token_configuration(token_identifier).await?; + // Create query let query = TokenLastClaimQuery { token_id: token_identifier, @@ -1016,34 +887,97 @@ impl WasmSdk { RewardDistributionMoment::fetch_with_metadata_and_proof(self.as_ref(), query, None) .await?; - 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(TokenLastClaimWasm::new( - last_claim_timestamp_ms, - last_claim_block_height, - )) - } else { - None - }; - - let data = data.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); + let data = claim_result + .map(RewardDistributionMomentWasm::from) + .map(JsValue::from) + .unwrap_or(JsValue::UNDEFINED); Ok(ProofMetadataResponseWasm::from_sdk_parts( data, metadata, proof, )) } } + +// Internal helper methods for token queries +impl WasmSdk { + /// Prefetch token configuration and add it to the context provider cache. + /// This is required for proof verification of token-related queries. + pub(crate) async fn prefetch_token_configuration( + &self, + token_id: Identifier, + ) -> Result<(), WasmSdkError> { + use crate::sdk::{MAINNET_TRUSTED_CONTEXT, TESTNET_TRUSTED_CONTEXT}; + use dash_sdk::dpp::dashcore::Network; + use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; + use dash_sdk::dpp::tokens::contract_info::v0::TokenContractInfoV0Accessors; + use dash_sdk::dpp::tokens::contract_info::TokenContractInfo; + use dash_sdk::platform::DataContract; + + // Step 1: Fetch TokenContractInfo to get contract_id and position + let token_contract_info = TokenContractInfo::fetch(self.as_ref(), token_id) + .await? + .ok_or_else(|| { + WasmSdkError::generic(format!( + "Token contract info not found for token ID: {}", + token_id + )) + })?; + + let contract_id = token_contract_info.contract_id(); + let token_position = token_contract_info.token_contract_position(); + + // Step 2: Fetch the DataContract + let data_contract = DataContract::fetch(self.as_ref(), contract_id) + .await? + .ok_or_else(|| { + WasmSdkError::generic(format!( + "Data contract not found for contract ID: {}", + contract_id + )) + })?; + + // Step 3: Extract the TokenConfiguration from the contract + let token_configuration = data_contract + .expected_token_configuration(token_position) + .map_err(|e| { + WasmSdkError::generic(format!( + "Failed to get token configuration at position {}: {}", + token_position, e + )) + })? + .clone(); + + // Step 4: Add the token configuration to the trusted context cache + let network = self.network(); + match network { + Network::Dash => { + let guard = MAINNET_TRUSTED_CONTEXT.lock().unwrap(); + let ctx = guard.as_ref().ok_or_else(|| { + WasmSdkError::generic(format!( + "Mainnet trusted context not initialized for token {}", + token_id + )) + })?; + ctx.add_known_token_configuration(token_id, token_configuration); + } + Network::Testnet => { + let guard = TESTNET_TRUSTED_CONTEXT.lock().unwrap(); + let ctx = guard.as_ref().ok_or_else(|| { + WasmSdkError::generic(format!( + "Testnet trusted context not initialized for token {}", + token_id + )) + })?; + ctx.add_known_token_configuration(token_id, token_configuration); + } + _ => { + return Err(WasmSdkError::generic(format!( + "Token configuration caching not implemented for network {:?}", + network + ))); + } + } + + Ok(()) + } +} diff --git a/packages/wasm-sdk/tests/functional/epochs-blocks.spec.mjs b/packages/wasm-sdk/tests/functional/epochs-blocks.spec.mjs index cb3765c414e..c0551d7ae6f 100644 --- a/packages/wasm-sdk/tests/functional/epochs-blocks.spec.mjs +++ b/packages/wasm-sdk/tests/functional/epochs-blocks.spec.mjs @@ -45,4 +45,24 @@ describe('Epochs and evonode blocks', function describeEpochs() { limit: 50, }); }); + + it('queries evonode proposed blocks by ids with proof', async () => { + const EVONODE_ID = '143dcd6a6b7684fde01e88a10e5d65de9a29244c5ecd586d14a342657025f113'; + const res = await client.getEvonodesProposedEpochBlocksByIdsWithProofInfo(8635, [EVONODE_ID]); + expect(res).to.be.ok(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.be.ok(); + expect(res.metadata).to.be.ok(); + }); + + it('queries evonode proposed blocks by range with proof', async () => { + const res = await client.getEvonodesProposedEpochBlocksByRangeWithProofInfo({ + epoch: 8635, + limit: 50, + }); + expect(res).to.be.ok(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.be.ok(); + expect(res.metadata).to.be.ok(); + }); }); diff --git a/packages/wasm-sdk/tests/functional/protocol.spec.mjs b/packages/wasm-sdk/tests/functional/protocol.spec.mjs index ba9e3b35f4c..5f093058a66 100644 --- a/packages/wasm-sdk/tests/functional/protocol.spec.mjs +++ b/packages/wasm-sdk/tests/functional/protocol.spec.mjs @@ -22,9 +22,26 @@ describe('Protocol versions', function describeProtocolVersions() { expect(state).to.be.ok(); }); + it('fetches protocol upgrade state with proof', async () => { + const res = await client.getProtocolVersionUpgradeStateWithProofInfo(); + expect(res).to.be.ok(); + expect(res.data).to.be.ok(); + expect(res.proof).to.be.ok(); + expect(res.metadata).to.be.ok(); + }); + it('lists protocol upgrade vote statuses', async () => { const START_PROTX = '143dcd6a6b7684fde01e88a10e5d65de9a29244c5ecd586d14a342657025f113'; const res = await client.getProtocolVersionUpgradeVoteStatus(START_PROTX, 50); expect(res).to.be.instanceOf(Map); }); + + it('lists protocol upgrade vote statuses with proof', async () => { + const START_PROTX = '143dcd6a6b7684fde01e88a10e5d65de9a29244c5ecd586d14a342657025f113'; + const res = await client.getProtocolVersionUpgradeVoteStatusWithProofInfo(START_PROTX, 50); + expect(res).to.be.ok(); + expect(res.data).to.be.instanceOf(Map); + expect(res.proof).to.be.ok(); + expect(res.metadata).to.be.ok(); + }); });