Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/wasm-sdk/QUERY_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# wasm-sdk query review (fetch/fetch_many, proofs, efficiency)

Context: review of query implementations to verify we use fetch/fetch_many correctly, avoid inefficient patterns, and handle proofs.

## Findings

- `src/queries/token.rs:getTokenPerpetualDistributionLastClaim`
- Uses a direct gRPC call with `prove: false` to avoid context/provider issues. Result is unverified and may diverge from the proof-backed variant. Should either default to the proofable path or document that this endpoint is best-effort/non-verified.
- `src/queries/protocol.rs:getProtocolVersionUpgradeVoteStatus` (and proof variant TODO)
- Uses `fetch_votes` without a proof-capable path; proof variant remains unimplemented. Protocol vote status cannot currently be verified.
- `src/queries/epoch.rs:getEvonodesProposedEpochBlocksBy*WithProofInfo`
- Proof-supporting variants are stubbed (return errors). Evonode proposed block counts cannot be fetched with proofs yet.

## Suggested follow-ups

1) Add/enable batch identity key queries (or parallelize) and return combined proofs for `getIdentitiesContractKeys*`.
2) Decide whether `getTokenPerpetualDistributionLastClaim` should default to the proof-backed path or be clearly marked as best-effort.
3) Implement proof-capable variants for protocol vote status and evonode proposed block counts once underlying fetch/proof traits land.
6 changes: 4 additions & 2 deletions packages/wasm-sdk/src/context_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ impl WasmTrustedContext {
std::num::NonZeroUsize::new(100).unwrap(),
)
.map_err(|e| ContextProviderError::Generic(e.to_string()))?
.with_refetch_if_not_found(false); // Disable refetch since we'll pre-fetch
// Enable refetch so token configs (and other context) are pulled on demand, same as rs-sdk
.with_refetch_if_not_found(true);

Ok(Self {
inner: std::sync::Arc::new(inner),
Expand All @@ -120,7 +121,8 @@ impl WasmTrustedContext {
std::num::NonZeroUsize::new(100).unwrap(),
)
.map_err(|e| ContextProviderError::Generic(e.to_string()))?
.with_refetch_if_not_found(false); // Disable refetch since we'll pre-fetch
// Enable refetch so token configs (and other context) are pulled on demand, same as rs-sdk
.with_refetch_if_not_found(true);

Ok(Self {
inner: std::sync::Arc::new(inner),
Expand Down
240 changes: 83 additions & 157 deletions packages/wasm-sdk/src/queries/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,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 wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
Expand Down Expand Up @@ -480,172 +482,96 @@ impl WasmSdk {
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
},
)),
// Create query and fetch via SDK (proofless variant)
let query = TokenLastClaimQuery {
token_id: token_identifier,
identity_id: identity_identifier,
};

// 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)),
// Try proof-backed fetch first; fall back to direct gRPC with prove=false if context lacks token config
let claim_result = match RewardDistributionMoment::fetch(self.as_ref(), query.clone()).await
{
Ok(res) => res,
Err(err) => {
// If proof verification failed due to missing token distribution type, retry without proofs
if err
.to_string()
.contains("Token distribution type not found with get_token_distribution")
{
use dapi_grpc::platform::v0::{
get_token_perpetual_distribution_last_claim_request::{
GetTokenPerpetualDistributionLastClaimRequestV0, Version,
},
get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::last_claim_info::PaidAt,
GetTokenPerpetualDistributionLastClaimRequest,
};
use rs_dapi_client::DapiRequestExecutor;

let request = GetTokenPerpetualDistributionLastClaimRequest {
version: Some(Version::V0(
GetTokenPerpetualDistributionLastClaimRequestV0 {
token_id: query.token_id.to_vec(),
identity_id: query.identity_id.to_vec(),
contract_info: None,
prove: false,
},
)),
};

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 (fallback): {}",
e
))
})?;

let claim = 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::last_claim_info::PaidAt::RawBytes(
bytes,
dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::get_token_perpetual_distribution_last_claim_response_v0::Result::LastClaim(
claim,
),
) => {
// 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
) => claim.paid_at,
_ => None,
},
_ => None,
};

match claim {
Some(PaidAt::TimestampMs(ts)) => Some(RewardDistributionMoment::TimeBasedMoment(ts)),
Some(PaidAt::BlockHeight(height)) => {
Some(RewardDistributionMoment::BlockBasedMoment(height))
}
Some(PaidAt::Epoch(epoch)) => {
Some(RewardDistributionMoment::EpochBasedMoment(epoch as u64))
}
_ => None,
}
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
} else {
return Err(err.into());
}
}
None => return Err(WasmSdkError::generic("Invalid response version")),
};

Ok(claim_result.map(|(timestamp_ms, block_height)| {
TokenLastClaimWasm::new(timestamp_ms, block_height)
}))
let data = claim_result.map(|moment| {
let (last_claim_timestamp_ms, last_claim_block_height) = match moment {
RewardDistributionMoment::BlockBasedMoment(height) => (0, height),
RewardDistributionMoment::TimeBasedMoment(timestamp) => (timestamp, 0),
RewardDistributionMoment::EpochBasedMoment(epoch) => (0, epoch as u64),
};

TokenLastClaimWasm::new(last_claim_timestamp_ms, last_claim_block_height)
});

Ok(data)
}

#[wasm_bindgen(js_name = "getTokenTotalSupply")]
Expand Down