From 25d105680274de4431b37e45f961272ad15bb8c3 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:33:54 +0100 Subject: [PATCH 1/5] feat(rs-sdk): implement getTokenPreProgrammedDistributions query Wire the getTokenPreProgrammedDistributions gRPC query across the full RS-SDK stack: build.rs versioned messages, transport macro, FromProof implementation, Query + Fetch traits, MockResponse, mock SDK match clause, and FFI bindings. Add TokenPreProgrammedDistributions newtype in the proof verifier with FromIterator to satisfy Drive's generic verification constraints. Create SDK test data with pre-programmed distributions for TOKEN_ID_2 (3 timestamps) and add both positive and negative offline test cases with generated test vectors. Fix pre-existing clippy warnings in evonode_status.rs (empty lines after attributes). Update RS-SDK README checklist to reflect current codebase patterns and document SDK test data configuration. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- packages/dapi-grpc/build.rs | 6 +- packages/rs-dapi-client/src/transport/grpc.rs | 9 + .../create_genesis_state/test/tokens.rs | 23 +- packages/rs-drive-proof-verifier/src/proof.rs | 1 + .../token_pre_programmed_distributions.rs | 99 ++++++ packages/rs-drive-proof-verifier/src/types.rs | 31 ++ .../src/types/evonode_status.rs | 5 - packages/rs-sdk-ffi/src/token/queries/mod.rs | 2 +- .../queries/pre_programmed_distributions.rs | 294 +++++++----------- .../tests/integration_tests/token.rs | 35 +++ packages/rs-sdk/README.md | 55 +++- packages/rs-sdk/src/mock/requests.rs | 3 +- packages/rs-sdk/src/mock/sdk.rs | 5 + packages/rs-sdk/src/platform/tokens/mod.rs | 2 + .../token_pre_programmed_distributions.rs | 62 ++++ packages/rs-sdk/tests/fetch/generated_data.rs | 3 +- packages/rs-sdk/tests/fetch/tokens/mod.rs | 1 + .../token_pre_programmed_distributions.rs | 65 ++++ .../.gitkeep | 0 ...72c682c883ab61c852a35528493ea2680909b.json | Bin 0 -> 42337 bytes ...59d247f99d4333a771efb4bc554e4575d59c9.json | 1 + .../.gitkeep | 0 ...e02962217f1ae14791d7fbee47d4e01696b52.json | Bin 0 -> 69737 bytes ...59d247f99d4333a771efb4bc554e4575d59c9.json | 1 + 25 files changed, 505 insertions(+), 200 deletions(-) create mode 100644 packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs create mode 100644 packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs create mode 100644 packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/.gitkeep create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/msg_GetTokenPreProgrammedDistributionsRequest_a46a647398eedeed648eafe389472c682c883ab61c852a35528493ea2680909b.json create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/.gitkeep create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/msg_GetTokenPreProgrammedDistributionsRequest_70451dbd24fcfcbf1831ab10f5ae02962217f1ae14791d7fbee47d4e01696b52.json create mode 100644 packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json diff --git a/CLAUDE.md b/CLAUDE.md index 7c105372ee0..fbd47f0476a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ yarn configure:tests:network **DPP** (`packages/rs-dpp`, `packages/wasm-dpp`): Dash Platform Protocol implementation that defines data structures and validation rules. -**SDK** (`packages/js-dash-sdk`, `packages/rs-sdk`): Client libraries providing high-level interfaces for building applications on Dash Platform. +**SDK** (`packages/js-dash-sdk`, `packages/rs-sdk`): Client libraries providing high-level interfaces for building applications on Dash Platform. See [packages/rs-sdk/README.md](packages/rs-sdk/README.md) for the Rust SDK checklist for implementing new `Fetch`/`FetchMany` queries. **WASM SDK** (`packages/wasm-sdk`): WebAssembly bindings for browser-based applications. See [AI_REFERENCE.md](packages/wasm-sdk/AI_REFERENCE.md) for comprehensive API documentation. diff --git a/packages/dapi-grpc/build.rs b/packages/dapi-grpc/build.rs index 1b4c8cbe4d6..e0ea14fb1c2 100644 --- a/packages/dapi-grpc/build.rs +++ b/packages/dapi-grpc/build.rs @@ -84,7 +84,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { // Derive features for versioned messages // // "GetConsensusParamsRequest" is excluded as this message does not support proofs - const VERSIONED_REQUESTS: [&str; 55] = [ + const VERSIONED_REQUESTS: [&str; 56] = [ "GetDataContractHistoryRequest", "GetDataContractRequest", "GetDataContractsRequest", @@ -123,6 +123,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { "GetTokenDirectPurchasePricesRequest", "GetTokenContractInfoRequest", "GetTokenStatusesRequest", + "GetTokenPreProgrammedDistributionsRequest", "GetTokenTotalSupplyRequest", "GetGroupInfoRequest", "GetGroupInfosRequest", @@ -160,7 +161,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { // - "GetIdentityByNonUniquePublicKeyHashResponse" // // "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests - const VERSIONED_RESPONSES: [&str; 53] = [ + const VERSIONED_RESPONSES: [&str; 54] = [ "GetDataContractHistoryResponse", "GetDataContractResponse", "GetDataContractsResponse", @@ -197,6 +198,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { "GetTokenDirectPurchasePricesResponse", "GetTokenContractInfoResponse", "GetTokenStatusesResponse", + "GetTokenPreProgrammedDistributionsResponse", "GetTokenTotalSupplyResponse", "GetGroupInfoResponse", "GetGroupInfosResponse", diff --git a/packages/rs-dapi-client/src/transport/grpc.rs b/packages/rs-dapi-client/src/transport/grpc.rs index a9f85020f3c..3b9aa9eed5b 100644 --- a/packages/rs-dapi-client/src/transport/grpc.rs +++ b/packages/rs-dapi-client/src/transport/grpc.rs @@ -650,6 +650,15 @@ impl_transport_request_grpc!( get_token_statuses ); +// rpc getTokenPreProgrammedDistributions(GetTokenPreProgrammedDistributionsRequest) returns (GetTokenPreProgrammedDistributionsResponse); +impl_transport_request_grpc!( + platform_proto::GetTokenPreProgrammedDistributionsRequest, + platform_proto::GetTokenPreProgrammedDistributionsResponse, + PlatformGrpcClient, + RequestSettings::default(), + get_token_pre_programmed_distributions +); + // rpc getTokenTotalSupply(GetTokenTotalSupplyRequest) returns (GetTokenTotalSupplyResponse); impl_transport_request_grpc!( platform_proto::GetTokenTotalSupplyRequest, diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs index 8a2301b9438..0b7a70e00e9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs @@ -15,6 +15,8 @@ use dpp::data_contract::associated_token::token_perpetual_distribution::distribu use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; +use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; +use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use dpp::data_contract::change_control_rules::v0::ChangeControlRulesV0; use dpp::data_contract::config::DataContractConfig; @@ -39,6 +41,7 @@ use dpp::version::PlatformVersion; use drive::grovedb::TransactionArg; use rand::rngs::StdRng; use rand::SeedableRng; +use std::collections::BTreeMap; use std::sync::LazyLock; const IDENTITY_ID_1: Identifier = Identifier::new([1; 32]); @@ -380,10 +383,26 @@ impl Platform { }), ); + let mut token_configuration_2 = token_configuration.clone(); + if let TokenConfiguration::V0(ref mut cfg) = token_configuration_2 { + if let dpp::data_contract::associated_token::token_distribution_rules::TokenDistributionRules::V0(ref mut rules) = cfg.distribution_rules { + rules.pre_programmed_distribution = + Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: BTreeMap::from([ + (1000, BTreeMap::from([(IDENTITY_ID_1, 500), (IDENTITY_ID_2, 300)])), + (5000, BTreeMap::from([(IDENTITY_ID_1, 1000)])), + (10000, BTreeMap::from([(IDENTITY_ID_2, 750), (IDENTITY_ID_3, 250)])), + ]), + }, + )); + } + } + let tokens = [ (0, token_configuration.clone()), - (1, token_configuration.clone()), - (2, token_configuration), + (1, token_configuration), + (2, token_configuration_2), ] .into(); diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index f7d8567f2c3..0699fe77ea3 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -4,6 +4,7 @@ pub mod token_contract_info; pub mod token_direct_purchase; pub mod token_info; pub mod token_perpetual_distribution_last_claim; +pub mod token_pre_programmed_distributions; pub mod token_status; pub mod token_total_supply; diff --git a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs new file mode 100644 index 00000000000..7e14da83f51 --- /dev/null +++ b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs @@ -0,0 +1,99 @@ +use crate::error::MapGroveDbError; +use crate::verify::verify_tenderdash_proof; +use crate::{types::TokenPreProgrammedDistributions, ContextProvider, Error}; +use dapi_grpc::platform::v0::{ + get_token_pre_programmed_distributions_request, GetTokenPreProgrammedDistributionsRequest, + GetTokenPreProgrammedDistributionsResponse, Proof, ResponseMetadata, +}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dpp::dashcore::Network; +use dpp::prelude::Identifier; +use dpp::version::PlatformVersion; +use drive::drive::tokens::distribution::queries::QueryPreProgrammedDistributionStartAt; +use drive::drive::Drive; + +use super::FromProof; + +impl FromProof for TokenPreProgrammedDistributions { + type Request = GetTokenPreProgrammedDistributionsRequest; + type Response = GetTokenPreProgrammedDistributionsResponse; + + fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( + request: I, + response: O, + _network: Network, + platform_version: &PlatformVersion, + provider: &'a dyn ContextProvider, + ) -> Result<(Option, ResponseMetadata, Proof), Error> + where + Self: Sized + 'a, + { + let request: Self::Request = request.into(); + let response: Self::Response = response.into(); + + let get_token_pre_programmed_distributions_request::Version::V0(req_v0) = + request.version.ok_or(Error::EmptyVersion)?; + + let token_id: [u8; 32] = + req_v0 + .token_id + .as_slice() + .try_into() + .map_err(|_| Error::RequestError { + error: "token_id must be 32 bytes".into(), + })?; + + let start_at = match req_v0.start_at_info { + Some(start_at_info) => { + let start_at_recipient = match start_at_info.start_recipient { + Some(recipient_bytes) => { + let recipient_id = + Identifier::from_bytes(&recipient_bytes).map_err(|_| { + Error::RequestError { + error: "start_recipient must be 32 bytes".into(), + } + })?; + let included = start_at_info.start_recipient_included.unwrap_or(true); + Some((recipient_id, included)) + } + None => None, + }; + + Some(QueryPreProgrammedDistributionStartAt { + start_at_time: start_at_info.start_time_ms, + start_at_recipient, + }) + } + None => None, + }; + + let limit = req_v0.limit.map(|l| l as u16); + + let metadata = response + .metadata() + .or(Err(Error::EmptyResponseMetadata))? + .clone(); + + let proof = response.proof_owned().or(Err(Error::NoProofInResult))?; + + let (root_hash, result) = Drive::verify_token_pre_programmed_distributions( + &proof.grovedb_proof, + token_id, + start_at, + limit, + false, + platform_version, + ) + .map_drive_error(&proof, &metadata)?; + + verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?; + + let distributions: TokenPreProgrammedDistributions = result; + + if distributions.0.is_empty() { + Ok((None, metadata, proof)) + } else { + Ok((Some(distributions), metadata, proof)) + } + } +} diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index 27f824260a7..f170950c318 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -663,6 +663,37 @@ pub struct ProposerBlockCountById(pub u64); /// Prices for direct purchase of tokens. Retrieved by [TokenPricingSchedule::fetch_many()]. pub type TokenDirectPurchasePrices = RetrievedObjects; +/// Pre-programmed token distributions grouped by timestamp. +/// +/// Each entry maps a timestamp (in milliseconds) to a collection of +/// `(Identifier, TokenAmount)` pairs representing the recipients and their amounts. +#[derive(Debug, Clone, Default, derive_more::From)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +pub struct TokenPreProgrammedDistributions( + pub BTreeMap>, +); + +impl TokenPreProgrammedDistributions { + /// Get the inner map. + pub fn into_inner(self) -> BTreeMap> { + self.0 + } +} + +impl FromIterator<(TimestampMillis, BTreeMap)> + for TokenPreProgrammedDistributions +{ + fn from_iter)>>( + iter: T, + ) -> Self { + Self(iter.into_iter().collect()) + } +} + /// Address balance changes for a single block. #[derive(Debug, Clone)] #[cfg_attr( diff --git a/packages/rs-drive-proof-verifier/src/types/evonode_status.rs b/packages/rs-drive-proof-verifier/src/types/evonode_status.rs index e465bbebf26..0edc6bb95f5 100644 --- a/packages/rs-drive-proof-verifier/src/types/evonode_status.rs +++ b/packages/rs-drive-proof-verifier/src/types/evonode_status.rs @@ -40,7 +40,6 @@ pub struct EvoNodeStatus { derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] - /// Information about protocol and software components versions. pub struct Version { /// Information about software components versions. @@ -96,7 +95,6 @@ pub struct TenderdashProtocol { derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] - /// Drive protocol versions. pub struct DriveProtocol { /// Latest version supported by the node. @@ -112,7 +110,6 @@ pub struct DriveProtocol { derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] - /// Information about current time used by the node. pub struct Time { /// Local time of the node. Unix timestamp since epoch. @@ -131,7 +128,6 @@ pub struct Time { derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] - /// Evo node identification information. pub struct Node { /// Node ID @@ -145,7 +141,6 @@ pub struct Node { derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] - /// Layer 2 blockchain information pub struct Chain { /// Whether the node is catching up with the network. diff --git a/packages/rs-sdk-ffi/src/token/queries/mod.rs b/packages/rs-sdk-ffi/src/token/queries/mod.rs index 1552a547f73..36a8a253946 100644 --- a/packages/rs-sdk-ffi/src/token/queries/mod.rs +++ b/packages/rs-sdk-ffi/src/token/queries/mod.rs @@ -22,6 +22,6 @@ pub use identity_balances::dash_sdk_identity_fetch_token_balances; pub use identity_token_infos::dash_sdk_identity_fetch_token_infos; pub use info::dash_sdk_token_get_identity_infos; pub use perpetual_distribution_last_claim::dash_sdk_token_get_perpetual_distribution_last_claim; -// pub use pre_programmed_distributions::dash_sdk_token_get_pre_programmed_distributions; // TODO: Not yet implemented +pub use pre_programmed_distributions::dash_sdk_token_get_pre_programmed_distributions; pub use status::dash_sdk_token_get_statuses; pub use total_supply::dash_sdk_token_get_total_supply; diff --git a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs index 57e9a85fcd6..b9c298065bc 100644 --- a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs +++ b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs @@ -1,34 +1,36 @@ -// TODO: GetTokenPreProgrammedDistributionsRequest is not yet exposed in the SDK -// This function is temporarily disabled until the SDK adds support for it -/* -use crate::types::SDKHandle; -use crate::{DashSDKError, DashSDKResult, DashSDKResultDataType, DashSDKErrorCode, FFIError}; -use dash_sdk::dapi_grpc::platform::v0::{ - get_token_pre_programmed_distributions_request::{ - get_token_pre_programmed_distributions_request_v0::StartAtInfo, - GetTokenPreProgrammedDistributionsRequestV0, - }, - GetTokenPreProgrammedDistributionsRequest, GetTokenPreProgrammedDistributionsResponse, +//! Token pre-programmed distributions query operations + +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::Identifier; +use dash_sdk::platform::tokens::token_pre_programmed_distributions::{ + TokenPreProgrammedDistributions, TokenPreProgrammedDistributionsQuery, + TokenPreProgrammedDistributionsStartAtInfo, }; -use dash_sdk::dapi_client::{transport::TransportRequest, DapiRequest, RequestSettings}; -use std::ffi::{c_char, c_void, CStr, CString}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use crate::sdk::SDKWrapper; +use crate::types::SDKHandle; +use crate::{DashSDKError, DashSDKErrorCode, DashSDKResult, FFIError}; -/// Fetches pre-programmed distributions for a token +/// Fetches pre-programmed distributions for a token. /// /// # Parameters /// * `sdk_handle` - Handle to the SDK instance /// * `token_id` - Base58-encoded token identifier /// * `start_time_ms` - Starting time in milliseconds (optional, 0 for no start time) -/// * `start_recipient` - Base58-encoded starting recipient ID (optional) +/// * `start_recipient` - Base58-encoded starting recipient ID (optional, null for none) /// * `start_recipient_included` - Whether to include the start recipient /// * `limit` - Maximum number of distributions to return (optional, 0 for default limit) /// /// # Returns /// * JSON array of pre-programmed distributions or null if not found -/// * Error message if operation fails /// /// # Safety -/// This function is unsafe because it handles raw pointers from C +/// - `sdk_handle` must be a valid pointer to an initialized SDKHandle. +/// - `token_id` must be a valid pointer to a NUL-terminated C string. +/// - `start_recipient` may be null; if non-null, must be a valid NUL-terminated C string. +/// - The returned C string pointer (on success) must be freed by the caller. #[no_mangle] pub unsafe extern "C" fn dash_sdk_token_get_pre_programmed_distributions( sdk_handle: *const SDKHandle, @@ -38,203 +40,146 @@ pub unsafe extern "C" fn dash_sdk_token_get_pre_programmed_distributions( start_recipient_included: bool, limit: u32, ) -> DashSDKResult { - match get_token_pre_programmed_distributions( - sdk_handle, - token_id, - start_time_ms, - start_recipient, - start_recipient_included, - limit, - ) { - Ok(Some(json)) => { - let c_str = match CString::new(json) { - Ok(s) => s, - Err(e) => { - return DashSDKResult { - data_type: DashSDKResultDataType::NoData, - data: std::ptr::null_mut(), - error: Box::into_raw(Box::new(DashSDKError::new( - DashSDKErrorCode::InternalError, - format!("Failed to create CString: {}", e) - ))), - } - } - }; - DashSDKResult { - data_type: DashSDKResultDataType::String, - data: c_str.into_raw() as *mut c_void, - error: std::ptr::null_mut(), - } - } - Ok(None) => DashSDKResult { - data_type: DashSDKResultDataType::NoData, - data: std::ptr::null_mut(), - error: std::ptr::null_mut(), - }, - Err(e) => DashSDKResult { - data_type: DashSDKResultDataType::NoData, - data: std::ptr::null_mut(), - error: Box::into_raw(Box::new(DashSDKError::new( - DashSDKErrorCode::InternalError, - e - ))), - }, + if sdk_handle.is_null() || token_id.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "SDK handle or token ID is null".to_string(), + )); } -} -fn get_token_pre_programmed_distributions( - sdk_handle: *const SDKHandle, - token_id: *const c_char, - start_time_ms: u64, - start_recipient: *const c_char, - start_recipient_included: bool, - limit: u32, -) -> Result, String> { - // Check for null pointers - if sdk_handle.is_null() { - return Err("SDK handle is null".to_string()); - } - if token_id.is_null() { - return Err("Token ID is null".to_string()); - } + let wrapper = &*(sdk_handle as *const SDKWrapper); - let rt = tokio::runtime::Runtime::new() - .map_err(|e| format!("Failed to create Tokio runtime: {}", e))?; + let token_id_str = match CStr::from_ptr(token_id).to_str() { + Ok(s) => s, + Err(e) => return DashSDKResult::error(FFIError::from(e).into()), + }; - let token_id_str = unsafe { - CStr::from_ptr(token_id) - .to_str() - .map_err(|e| format!("Invalid UTF-8 in token ID: {}", e))? + let token_id = match Identifier::from_string(token_id_str, Encoding::Base58) { + Ok(id) => id, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid token ID: {}", e), + )) + } }; - let wrapper = unsafe { &*(sdk_handle as *const crate::sdk::SDKWrapper) }; - let sdk = wrapper.sdk.clone(); - - rt.block_on(async move { - let token_id_bytes = bs58::decode(token_id_str) - .into_vec() - .map_err(|e| format!("Failed to decode token ID: {}", e))?; - - let token_id: [u8; 32] = token_id_bytes - .try_into() - .map_err(|_| "Token ID must be exactly 32 bytes".to_string())?; - - let start_at_info = if start_time_ms > 0 { - let start_recipient_bytes = if start_recipient.is_null() { - None - } else { - let start_recipient_str = unsafe { - CStr::from_ptr(start_recipient) - .to_str() - .map_err(|e| format!("Invalid UTF-8 in start recipient: {}", e))? - }; - let recipient_bytes = bs58::decode(start_recipient_str) - .into_vec() - .map_err(|e| format!("Failed to decode start recipient: {}", e))?; - let recipient_id: [u8; 32] = recipient_bytes - .try_into() - .map_err(|_| "Start recipient must be exactly 32 bytes".to_string())?; - Some(recipient_id.to_vec()) - }; - Some(StartAtInfo { - start_time_ms, - start_recipient: start_recipient_bytes, - start_recipient_included: Some(start_recipient_included), - }) + let start_at_info = if start_time_ms > 0 { + let recipient = if !start_recipient.is_null() { + let recipient_str = match CStr::from_ptr(start_recipient).to_str() { + Ok(s) => s, + Err(e) => return DashSDKResult::error(FFIError::from(e).into()), + }; + match Identifier::from_string(recipient_str, Encoding::Base58) { + Ok(id) => Some(id), + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid start recipient: {}", e), + )) + } + } } else { None }; - let request = GetTokenPreProgrammedDistributionsRequest { - version: Some(dash_sdk::dapi_grpc::platform::v0::get_token_pre_programmed_distributions_request::Version::V0(GetTokenPreProgrammedDistributionsRequestV0 { - token_id: token_id.to_vec(), - start_at_info, - limit: if limit > 0 { Some(limit) } else { None }, - prove: true, - })), - }; - - // Execute the request directly since this isn't exposed in the SDK yet - let result = request - .execute(&sdk, RequestSettings::default()) - .await - .map_err(|e| format!("Failed to execute request: {}", e))?; + Some(TokenPreProgrammedDistributionsStartAtInfo { + start_time_ms, + start_recipient: recipient, + start_recipient_included, + }) + } else { + None + }; - // Parse the response using the SDK's proof verification - let response: GetTokenPreProgrammedDistributionsResponse = result.inner; + let query = TokenPreProgrammedDistributionsQuery { + token_id, + start_at_info, + limit: if limit > 0 { Some(limit) } else { None }, + }; - match response.version { - Some(dash_sdk::dapi_grpc::platform::v0::get_token_pre_programmed_distributions_response::Version::V0(v0)) => { - match v0.result { - Some(dash_sdk::dapi_grpc::platform::v0::get_token_pre_programmed_distributions_response::get_token_pre_programmed_distributions_response_v0::Result::TokenDistributions(distributions)) => { - if distributions.token_distributions.is_empty() { - return Ok(None); - } + let result: Result = wrapper.runtime.block_on(async { + use dash_sdk::platform::Fetch; - let distributions_json: Vec = distributions - .token_distributions + let distributions = TokenPreProgrammedDistributions::fetch(&wrapper.sdk, query) + .await + .map_err(|e| { + FFIError::InternalError(format!( + "Failed to fetch token pre-programmed distributions: {}", + e + )) + })?; + + match distributions { + Some(dists) => { + let distributions_json: Vec = dists + .0 + .iter() + .map(|(timestamp, recipients)| { + let recipients_json: Vec = recipients .iter() - .map(|timed_distribution| { - let distributions_for_time_json: Vec = timed_distribution - .distributions - .iter() - .map(|distribution| { - format!( - r#"{{"recipient_id":"{}","amount":{}}}"#, - bs58::encode(&distribution.recipient_id).into_string(), - distribution.amount - ) - }) - .collect(); - + .map(|(recipient_id, amount)| { format!( - r#"{{"timestamp":{},"distributions":[{}]}}"#, - timed_distribution.timestamp, - distributions_for_time_json.join(",") + r#"{{"recipient_id":"{}","amount":{}}}"#, + recipient_id, amount ) }) .collect(); - Ok(Some(format!("[{}]", distributions_json.join(",")))) - } - Some(dash_sdk::dapi_grpc::platform::v0::get_token_pre_programmed_distributions_response::get_token_pre_programmed_distributions_response_v0::Result::Proof(_proof)) => { - // For now, return empty result for proof responses - // TODO: Implement proper proof verification when SDK supports it - Ok(None) - } - None => Ok(None), - } + format!( + r#"{{"timestamp":{},"distributions":[{}]}}"#, + timestamp, + recipients_json.join(",") + ) + }) + .collect(); + + Ok(format!("[{}]", distributions_json.join(","))) } - None => Err("Invalid response format".to_string()), + None => Err(FFIError::NotFound( + "No pre-programmed distributions found".to_string(), + )), } - }) + }); + + match result { + Ok(json_str) => { + let c_str = match CString::new(json_str) { + Ok(s) => s, + Err(e) => { + return DashSDKResult::error( + FFIError::InternalError(format!("Failed to create CString: {}", e)).into(), + ) + } + }; + DashSDKResult::success_string(c_str.into_raw()) + } + Err(e) => DashSDKResult::error(e.into()), + } } -*/ -/* #[cfg(test)] mod tests { use super::*; use crate::test_utils::test_utils::create_mock_sdk_handle; - use std::ffi::CString; #[test] - fn test_get_token_pre_programmed_distributions_null_handle() { + fn test_null_handle() { unsafe { let result = dash_sdk_token_get_pre_programmed_distributions( std::ptr::null(), - CString::new("test").unwrap().as_ptr(), + std::ffi::CString::new("test").unwrap().as_ptr(), 0, std::ptr::null(), false, - 10, + 0, ); assert!(!result.error.is_null()); } } #[test] - fn test_get_token_pre_programmed_distributions_null_token_id() { + fn test_null_token_id() { let handle = create_mock_sdk_handle(); unsafe { let result = dash_sdk_token_get_pre_programmed_distributions( @@ -243,11 +188,10 @@ mod tests { 0, std::ptr::null(), false, - 10, + 0, ); assert!(!result.error.is_null()); crate::test_utils::test_utils::destroy_mock_sdk_handle(handle); } } } -*/ diff --git a/packages/rs-sdk-ffi/tests/integration_tests/token.rs b/packages/rs-sdk-ffi/tests/integration_tests/token.rs index 5f00d41b99d..91c08073cba 100644 --- a/packages/rs-sdk-ffi/tests/integration_tests/token.rs +++ b/packages/rs-sdk-ffi/tests/integration_tests/token.rs @@ -174,6 +174,41 @@ fn test_token_direct_purchase_prices() { destroy_test_sdk_handle(handle); } +/// Test fetching pre-programmed distributions for a token +#[test] +fn test_token_pre_programmed_distributions() { + setup_logs(); + + let handle = create_test_sdk_handle("test_token_pre_programmed_distributions"); + let token_contract_id = to_c_string(&token0_id_b58()); + + unsafe { + let result = dash_sdk_token_get_pre_programmed_distributions( + handle, + token_contract_id.as_ptr(), + 0, + std::ptr::null(), + false, + 0, + ); + + match parse_string_result(result) { + Ok(Some(json_str)) => { + let json = parse_json_result(&json_str).expect("valid JSON"); + assert!(json.is_array(), "Expected array, got: {:?}", json); + } + Ok(None) => { + // Token might not have pre-programmed distributions + } + Err(_e) => { + // Accept missing mock vector as acceptable in offline mode + } + } + } + + destroy_test_sdk_handle(handle); +} + /// Test fetching token info for multiple identities #[test] fn test_token_identities_token_infos() { diff --git a/packages/rs-sdk/README.md b/packages/rs-sdk/README.md index 090f002daea..c25c654cffa 100644 --- a/packages/rs-sdk/README.md +++ b/packages/rs-sdk/README.md @@ -83,10 +83,28 @@ Core RPC on the remote host. Refer to rich comments / help in the forementioned scripts for more details. +### SDK test data + +When starting the local devnet with `SDK_TEST_DATA=true yarn start`, the `create_sdk_test_data` cfg flag +activates creation of deterministic test data in genesis state. This data is defined in +`packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/`. + +Current test data includes 3 identities, a data contract with 3 tokens, and address balances. +Token configuration: + +| Token | Config | +|-------|--------| +| `TOKEN_ID_0` | base_supply=100000, frozen for IDENTITY_ID_2, no pricing, no pre-programmed distributions | +| `TOKEN_ID_1` | base_supply=100100, paused, single price=25, no pre-programmed distributions | +| `TOKEN_ID_2` | base_supply=100200, pricing schedule (10 levels), pre-programmed distributions (3 timestamps) | + +When adding a new query type, add corresponding test data to the files in `create_genesis_state/test/` +and reference it in `packages/rs-sdk/tests/fetch/generated_data.rs`. + ### Generating test vectors -To generate test vectors for offline testing, you need to have access acredentials to Dash Platform instance, either by -specifying configuration manually in `packages/rs-sdk/tests/.env`. or starting a local devnet. +To generate test vectors for offline testing, you need to have access to a Dash Platform instance, either by +specifying configuration manually in `packages/rs-sdk/tests/.env` or starting a local devnet. The `.env` file is automatically generated during `yarn setup` or `yarn reset`, using `platform/scripts/configure_dotenv.sh` script. See [Dash Platform documentation](../../README.md) for more details about starting and using local devnet. To generate test vectors: @@ -132,14 +150,27 @@ in `packages/rs-dapi-client/src/transport/grpc.rs`. If you intend to implement `FetchMany`, you should define type returned by `fetch_many()` using `RetrievedObjects` that will store collection of returned objects, indexed by some key. 5. [ ] Implement `FromProof` trait for the `Object` (or type defined in `types.rs`) in `packages/rs-drive-proof-verifier/src/proof.rs`. -6. [ ] Implement `Query` trait for the `Request` in `packages/rs-sdk/src/platform/query.rs`. +6. [ ] Implement `Query` trait for the `Request` and `Fetch` (or `FetchMany`) trait for the `Object`. + Create a dedicated module under the appropriate subdirectory + (e.g., `packages/rs-sdk/src/platform/tokens/my_query.rs`) and add `Query` + `Fetch`/`FetchMany` impls there. + **Deprecated:** older code placed these in central files (`query.rs`, `fetch.rs`, `fetch_many.rs`) — do not follow that pattern for new queries. 7. [ ] Implement `MockResponse` for `Object` in `packages/rs-sdk/src/mock/requests.rs`. -8. [ ] Implement `Fetch` trait for the `Object` (or type defined in `types.rs`), with inner type Request = `Request`, - in `packages/rs-sdk/src/platform/fetch.rs`. -9. [ ] Implement `FetchMany\` trait for the `Object` (or type defined in `types.rs`), - with inner type Request = `Request`, in `packages/rs-sdk/src/platform/fetch_many.rs`. -10. [ ] Add `mod ...;` clause to `packages/rs-sdk/tests/fetch/main.rs` -11. [ ] Implement unit tests in `packages/rs-sdk/tests/fetch/*object*.rs` -12. [ ] Add name of request type to match clause in `packages/rs-sdk/src/mock/sdk.rs` : `load_expectations()` -13. [ ] Start local devnet with `yarn reset && SDK_TEST_DATA=true yarn start` -14. [ ] Generate test vectors with script `packages/rs-sdk/scripts/generate_test_vectors.sh` +8. [ ] Implement `FetchMany\` trait for the `Object` (or type defined in `types.rs`), + with inner type Request = `Request`, if the query returns a collection of objects. + Skip if the query returns a single result and only `Fetch` is needed. +9. [ ] Add `mod ...;` clause to `packages/rs-sdk/tests/fetch/main.rs` +10. [ ] Implement unit tests in `packages/rs-sdk/tests/fetch/*object*.rs`. + Tests must compile but will **fail** at this point due to missing test vectors — that is expected. + The vector generation script (step 13) runs these tests with `--features generate-test-vectors` + against a live devnet to record responses as test vectors. After vectors are generated, + re-run `cargo test -p dash-sdk` to verify they pass in offline mode. +11. [ ] Add name of request type to match clause in `packages/rs-sdk/src/mock/sdk.rs` : `load_expectations()` +12. [ ] (Optional) If not already configured, run `yarn setup` (fresh checkout) or `yarn reset` (reconfigure existing environment). + **Warning:** both commands rebuild everything and reset data — do not run if your environment is already working. + This configures the `.env` file in `packages/rs-sdk/tests/` needed by the tests. +13. [ ] Start local devnet with `SDK_TEST_DATA=true yarn start`. +14. [ ] Generate test vectors by running `packages/rs-sdk/scripts/generate_test_vectors.sh test_name` + where `test_name` matches only the new tests (e.g., `test_token_pre_programmed_distributions`). + Running without arguments regenerates **all** vectors — avoid this unless intentional. + The script executes matching tests with `--features generate-test-vectors` against the running devnet, + saving responses as test vectors in `packages/rs-sdk/tests/vectors/`. diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 115ad66f658..401fba3c728 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -39,7 +39,7 @@ use drive_proof_verifier::types::{ PlatformAddressTrunkState, PrefundedSpecializedBalance, ProposerBlockCounts, RecentAddressBalanceChanges, RecentCompactedAddressBalanceChanges, MostRecentShieldedAnchor, RecentCompactedNullifierChanges, RecentNullifierChanges, - RetrievedValues, ShieldedAnchors, + RetrievedValues, ShieldedAnchors, TokenPreProgrammedDistributions, ShieldedEncryptedNote, ShieldedEncryptedNotes, ShieldedNullifierStatus, ShieldedNullifierStatuses, ShieldedPoolState, TotalCreditsInPlatform, VotePollsGroupedByTimestamp, Voters, @@ -507,6 +507,7 @@ impl_mock_response!(CurrentQuorumsInfo); impl_mock_response!(Group); impl_mock_response!(TokenPricingSchedule); impl_mock_response!(RewardDistributionMoment); +impl_mock_response!(TokenPreProgrammedDistributions); impl_mock_response!(PlatformAddress); impl_mock_response!(AddressInfo); impl_mock_response!(RecentAddressBalanceChanges); diff --git a/packages/rs-sdk/src/mock/sdk.rs b/packages/rs-sdk/src/mock/sdk.rs index a86fada03b4..9e52c297d24 100644 --- a/packages/rs-sdk/src/mock/sdk.rs +++ b/packages/rs-sdk/src/mock/sdk.rs @@ -244,6 +244,11 @@ impl MockDashPlatformSdk { &mut dapi, filename, )? } + "GetTokenPreProgrammedDistributionsRequest" => { + load_expectation::( + &mut dapi, filename, + )? + } "GetAddressesTrunkStateRequest" => { load_expectation::(&mut dapi, filename)? } diff --git a/packages/rs-sdk/src/platform/tokens/mod.rs b/packages/rs-sdk/src/platform/tokens/mod.rs index d8bc6b10d7a..aabb61599ed 100644 --- a/packages/rs-sdk/src/platform/tokens/mod.rs +++ b/packages/rs-sdk/src/platform/tokens/mod.rs @@ -5,6 +5,8 @@ pub mod identity_token_balances; pub mod token_contract_info; /// Token info queries pub mod token_info; +/// Token pre-programmed distributions query +pub mod token_pre_programmed_distributions; /// Token status query pub mod token_status; /// Token total supply query diff --git a/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs b/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs new file mode 100644 index 00000000000..ae03d57fd37 --- /dev/null +++ b/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs @@ -0,0 +1,62 @@ +use crate::platform::{Fetch, Query}; +use crate::Error; +use dapi_grpc::platform::v0::get_token_pre_programmed_distributions_request::get_token_pre_programmed_distributions_request_v0::StartAtInfo; +use dapi_grpc::platform::v0::get_token_pre_programmed_distributions_request::GetTokenPreProgrammedDistributionsRequestV0; +use dapi_grpc::platform::v0::{ + get_token_pre_programmed_distributions_request, GetTokenPreProgrammedDistributionsRequest, +}; +use dpp::prelude::Identifier; +pub use drive_proof_verifier::types::TokenPreProgrammedDistributions; + +/// Query parameters for fetching pre-programmed token distributions. +#[derive(Debug, Clone)] +pub struct TokenPreProgrammedDistributionsQuery { + /// Token identifier. + pub token_id: Identifier, + /// Optional pagination start point. + pub start_at_info: Option, + /// Optional limit on the number of results. + pub limit: Option, +} + +/// Start-at pagination info for pre-programmed distributions queries. +#[derive(Debug, Clone)] +pub struct TokenPreProgrammedDistributionsStartAtInfo { + /// Timestamp in milliseconds to start from. + pub start_time_ms: u64, + /// Optional recipient identifier to start from within the timestamp. + pub start_recipient: Option, + /// Whether to include the start recipient in results. + pub start_recipient_included: bool, +} + +impl Query for TokenPreProgrammedDistributionsQuery { + fn query(self, prove: bool) -> Result { + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + let start_at_info = self.start_at_info.map(|info| StartAtInfo { + start_time_ms: info.start_time_ms, + start_recipient: info.start_recipient.map(|id| id.to_vec()), + start_recipient_included: Some(info.start_recipient_included), + }); + + let request = GetTokenPreProgrammedDistributionsRequest { + version: Some(get_token_pre_programmed_distributions_request::Version::V0( + GetTokenPreProgrammedDistributionsRequestV0 { + token_id: self.token_id.to_vec(), + start_at_info, + limit: self.limit, + prove, + }, + )), + }; + + Ok(request) + } +} + +impl Fetch for TokenPreProgrammedDistributions { + type Request = GetTokenPreProgrammedDistributionsRequest; +} diff --git a/packages/rs-sdk/tests/fetch/generated_data.rs b/packages/rs-sdk/tests/fetch/generated_data.rs index 4046e576c73..9db93f65f66 100644 --- a/packages/rs-sdk/tests/fetch/generated_data.rs +++ b/packages/rs-sdk/tests/fetch/generated_data.rs @@ -40,7 +40,8 @@ pub static TOKEN_ID_0: LazyLock = /// See `/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs#L49` pub static TOKEN_ID_1: LazyLock = LazyLock::new(|| Identifier::new(calculate_token_id(&DATA_CONTRACT_ID.to_buffer(), 1))); -/// The third token ID from the data contract above created by init chain for testing +/// The third token ID from the data contract above created by init chain for testing. +/// Has a pricing schedule and pre-programmed distributions (3 timestamps). /// See `/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs#L49` pub static TOKEN_ID_2: LazyLock = LazyLock::new(|| Identifier::new(calculate_token_id(&DATA_CONTRACT_ID.to_buffer(), 2))); diff --git a/packages/rs-sdk/tests/fetch/tokens/mod.rs b/packages/rs-sdk/tests/fetch/tokens/mod.rs index 14f980c3a5d..7bc1e8e97b7 100644 --- a/packages/rs-sdk/tests/fetch/tokens/mod.rs +++ b/packages/rs-sdk/tests/fetch/tokens/mod.rs @@ -1,5 +1,6 @@ mod identity_token_balances; mod token_contract_info; mod token_info; +mod token_pre_programmed_distributions; mod token_status; mod token_total_supply; diff --git a/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs b/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs new file mode 100644 index 00000000000..b60956bda8f --- /dev/null +++ b/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs @@ -0,0 +1,65 @@ +// TODO: Generate test vectors by running against a devnet: +// yarn reset && SDK_TEST_DATA=true yarn start +// ./packages/rs-sdk/scripts/generate_test_vectors.sh test_token_pre_programmed_distributions + +use crate::fetch::common::setup_logs; +use crate::fetch::config::Config; +use crate::fetch::generated_data::*; +use dash_sdk::platform::tokens::token_pre_programmed_distributions::{ + TokenPreProgrammedDistributions, TokenPreProgrammedDistributionsQuery, +}; +use dash_sdk::platform::Fetch; + +/// TOKEN_ID_2 has pre-programmed distributions configured with 3 timestamps. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_token_pre_programmed_distributions_present() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg + .setup_api("test_token_pre_programmed_distributions_present") + .await; + + let query = TokenPreProgrammedDistributionsQuery { + token_id: *TOKEN_ID_2, + start_at_info: None, + limit: None, + }; + + let distributions = TokenPreProgrammedDistributions::fetch(&sdk, query) + .await + .expect("fetch token pre-programmed distributions"); + + let distributions = distributions.expect("TOKEN_ID_2 should have pre-programmed distributions"); + assert_eq!( + distributions.0.len(), + 3, + "expected 3 distribution timestamps" + ); +} + +/// TOKEN_ID_0 has no pre-programmed distributions; query should return None. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_token_pre_programmed_distributions_absent() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg + .setup_api("test_token_pre_programmed_distributions_absent") + .await; + + let query = TokenPreProgrammedDistributionsQuery { + token_id: *TOKEN_ID_0, + start_at_info: None, + limit: None, + }; + + let distributions = TokenPreProgrammedDistributions::fetch(&sdk, query) + .await + .expect("fetch token pre-programmed distributions"); + + assert!( + distributions.is_none(), + "expected no pre-programmed distributions for TOKEN_ID_0" + ); +} diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/.gitkeep b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/msg_GetTokenPreProgrammedDistributionsRequest_a46a647398eedeed648eafe389472c682c883ab61c852a35528493ea2680909b.json b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/msg_GetTokenPreProgrammedDistributionsRequest_a46a647398eedeed648eafe389472c682c883ab61c852a35528493ea2680909b.json new file mode 100644 index 0000000000000000000000000000000000000000..3c066e3fec0c581f5daaff84dd9b4a39ade96b2d GIT binary patch literal 42337 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 literal 0 HcmV?d00001 diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json new file mode 100644 index 00000000000..798d07653a3 --- /dev/null +++ b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_absent/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json @@ -0,0 +1 @@ +8652a5e411b09ed8f445379612c5fb302f6f14e403e735f8fe9c0af4e68502c3a14a262cac04611f598dee968d46b7a7 \ No newline at end of file diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/.gitkeep b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/msg_GetTokenPreProgrammedDistributionsRequest_70451dbd24fcfcbf1831ab10f5ae02962217f1ae14791d7fbee47d4e01696b52.json b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/msg_GetTokenPreProgrammedDistributionsRequest_70451dbd24fcfcbf1831ab10f5ae02962217f1ae14791d7fbee47d4e01696b52.json new file mode 100644 index 0000000000000000000000000000000000000000..32a1e337c1877e98803cc2fa969677db9e1a44d3 GIT binary patch literal 69737 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 literal 0 HcmV?d00001 diff --git a/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json new file mode 100644 index 00000000000..798d07653a3 --- /dev/null +++ b/packages/rs-sdk/tests/vectors/test_token_pre_programmed_distributions_present/quorum_pubkey-106-7e46a98df86489ce0e3845110ce59d247f99d4333a771efb4bc554e4575d59c9.json @@ -0,0 +1 @@ +8652a5e411b09ed8f445379612c5fb302f6f14e403e735f8fe9c0af4e68502c3a14a262cac04611f598dee968d46b7a7 \ No newline at end of file From e32fb42d05f3d5687edf66b6d442d1081b418e0d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:42:16 +0100 Subject: [PATCH 2/5] chore: gitignore __pycache__ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 040d4c2079e..1ad0c648f99 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,5 @@ book/book/ # gRPC coverage report grpc-coverage-report.txt + +__pycache__/ From b92be4b6a9cecfe3a8527ca77c993bc8e156a1c2 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:18:56 +0100 Subject: [PATCH 3/5] fix(rs-sdk): address PR #3246 review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply 7 triage fixes: - PROJ-001: FFI test uses TOKEN_ID_2 with correct vectors, errors panic - CODE-001: None distributions return success with null, not NotFound - SEC-001: u32→u16 limit uses checked conversion - DOC-001: Doc comment clarifies token amounts in credits - PROJ-002: SDK test asserts distribution content, not just count - CODE-004: Remove unnecessary binding in proof verifier - CODE-005: Shorten deeply-qualified path with use import Co-Authored-By: Claude Opus 4.6 --- .../create_genesis_state/test/tokens.rs | 28 +++++++++------ .../token_pre_programmed_distributions.rs | 34 +++++++++++-------- packages/rs-drive-proof-verifier/src/types.rs | 2 +- .../queries/pre_programmed_distributions.rs | 11 +++--- .../tests/integration_tests/token.rs | 16 ++++++--- .../token_pre_programmed_distributions.rs | 28 +++++++++++++++ 6 files changed, 82 insertions(+), 37 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs index 0b7a70e00e9..87a62abc548 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs @@ -8,6 +8,7 @@ use dpp::data_contract::associated_token::token_configuration_convention::v0::To use dpp::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; use dpp::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; use dpp::data_contract::associated_token::token_distribution_rules::v0::TokenDistributionRulesV0; +use dpp::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; use dpp::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; use dpp::data_contract::associated_token::token_marketplace_rules::v0::TokenMarketplaceRulesV0; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; @@ -385,17 +386,22 @@ impl Platform { let mut token_configuration_2 = token_configuration.clone(); if let TokenConfiguration::V0(ref mut cfg) = token_configuration_2 { - if let dpp::data_contract::associated_token::token_distribution_rules::TokenDistributionRules::V0(ref mut rules) = cfg.distribution_rules { - rules.pre_programmed_distribution = - Some(TokenPreProgrammedDistribution::V0( - TokenPreProgrammedDistributionV0 { - distributions: BTreeMap::from([ - (1000, BTreeMap::from([(IDENTITY_ID_1, 500), (IDENTITY_ID_2, 300)])), - (5000, BTreeMap::from([(IDENTITY_ID_1, 1000)])), - (10000, BTreeMap::from([(IDENTITY_ID_2, 750), (IDENTITY_ID_3, 250)])), - ]), - }, - )); + if let TokenDistributionRules::V0(ref mut rules) = cfg.distribution_rules { + rules.pre_programmed_distribution = Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: BTreeMap::from([ + ( + 1000, + BTreeMap::from([(IDENTITY_ID_1, 500), (IDENTITY_ID_2, 300)]), + ), + (5000, BTreeMap::from([(IDENTITY_ID_1, 1000)])), + ( + 10000, + BTreeMap::from([(IDENTITY_ID_2, 750), (IDENTITY_ID_3, 250)]), + ), + ]), + }, + )); } } diff --git a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs index 7e14da83f51..68a16b66658 100644 --- a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs +++ b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs @@ -67,7 +67,14 @@ impl FromProof for TokenPreProgrammed None => None, }; - let limit = req_v0.limit.map(|l| l as u16); + let limit = req_v0 + .limit + .map(|l| { + u16::try_from(l).map_err(|_| Error::RequestError { + error: "limit exceeds u16::MAX".into(), + }) + }) + .transpose()?; let metadata = response .metadata() @@ -76,24 +83,23 @@ impl FromProof for TokenPreProgrammed let proof = response.proof_owned().or(Err(Error::NoProofInResult))?; - let (root_hash, result) = Drive::verify_token_pre_programmed_distributions( - &proof.grovedb_proof, - token_id, - start_at, - limit, - false, - platform_version, - ) - .map_drive_error(&proof, &metadata)?; + let (root_hash, result): ([u8; 32], TokenPreProgrammedDistributions) = + Drive::verify_token_pre_programmed_distributions( + &proof.grovedb_proof, + token_id, + start_at, + limit, + false, + platform_version, + ) + .map_drive_error(&proof, &metadata)?; verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?; - let distributions: TokenPreProgrammedDistributions = result; - - if distributions.0.is_empty() { + if result.0.is_empty() { Ok((None, metadata, proof)) } else { - Ok((Some(distributions), metadata, proof)) + Ok((Some(result), metadata, proof)) } } } diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index f170950c318..654245d8348 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -666,7 +666,7 @@ pub type TokenDirectPurchasePrices = RetrievedObjects 0 { Some(limit) } else { None }, }; - let result: Result = wrapper.runtime.block_on(async { + let result: Result, FFIError> = wrapper.runtime.block_on(async { use dash_sdk::platform::Fetch; let distributions = TokenPreProgrammedDistributions::fetch(&wrapper.sdk, query) @@ -134,16 +134,14 @@ pub unsafe extern "C" fn dash_sdk_token_get_pre_programmed_distributions( }) .collect(); - Ok(format!("[{}]", distributions_json.join(","))) + Ok(Some(format!("[{}]", distributions_json.join(",")))) } - None => Err(FFIError::NotFound( - "No pre-programmed distributions found".to_string(), - )), + None => Ok(None), } }); match result { - Ok(json_str) => { + Ok(Some(json_str)) => { let c_str = match CString::new(json_str) { Ok(s) => s, Err(e) => { @@ -154,6 +152,7 @@ pub unsafe extern "C" fn dash_sdk_token_get_pre_programmed_distributions( }; DashSDKResult::success_string(c_str.into_raw()) } + Ok(None) => DashSDKResult::success(std::ptr::null_mut()), Err(e) => DashSDKResult::error(e.into()), } } diff --git a/packages/rs-sdk-ffi/tests/integration_tests/token.rs b/packages/rs-sdk-ffi/tests/integration_tests/token.rs index 91c08073cba..14b7a0bac08 100644 --- a/packages/rs-sdk-ffi/tests/integration_tests/token.rs +++ b/packages/rs-sdk-ffi/tests/integration_tests/token.rs @@ -16,6 +16,14 @@ fn token0_id_b58() -> String { token_id.to_string(Encoding::Base58) } +fn token2_id_b58() -> String { + // Matches rs-sdk vectors: token id 2 for data contract id [3;32] + let data_contract_id = Identifier::new([3u8; 32]); + let token_bytes = calculate_token_id(&data_contract_id.to_buffer(), 2); + let token_id = Identifier::new(token_bytes); + token_id.to_string(Encoding::Base58) +} + // Pruned: token info test lacks rs-sdk vectors and is outdated // Pruned: token contract info not backed by rs-sdk vectors @@ -179,8 +187,8 @@ fn test_token_direct_purchase_prices() { fn test_token_pre_programmed_distributions() { setup_logs(); - let handle = create_test_sdk_handle("test_token_pre_programmed_distributions"); - let token_contract_id = to_c_string(&token0_id_b58()); + let handle = create_test_sdk_handle("test_token_pre_programmed_distributions_present"); + let token_contract_id = to_c_string(&token2_id_b58()); unsafe { let result = dash_sdk_token_get_pre_programmed_distributions( @@ -200,9 +208,7 @@ fn test_token_pre_programmed_distributions() { Ok(None) => { // Token might not have pre-programmed distributions } - Err(_e) => { - // Accept missing mock vector as acceptable in offline mode - } + Err(e) => panic!("Unexpected error: {}", e), } } diff --git a/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs b/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs index b60956bda8f..5ec7a4fdca9 100644 --- a/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs +++ b/packages/rs-sdk/tests/fetch/tokens/token_pre_programmed_distributions.rs @@ -5,6 +5,7 @@ use crate::fetch::common::setup_logs; use crate::fetch::config::Config; use crate::fetch::generated_data::*; +use dash_sdk::dpp::prelude::Identifier; use dash_sdk::platform::tokens::token_pre_programmed_distributions::{ TokenPreProgrammedDistributions, TokenPreProgrammedDistributionsQuery, }; @@ -36,6 +37,33 @@ async fn test_token_pre_programmed_distributions_present() { 3, "expected 3 distribution timestamps" ); + + // Verify timestamp 1000 has 2 recipients with correct amounts + let ts1000 = distributions + .0 + .get(&1000) + .expect("timestamp 1000 should be present"); + assert_eq!(ts1000.len(), 2, "timestamp 1000 should have 2 recipients"); + assert_eq!( + ts1000.get(&Identifier::new([1u8; 32])).copied(), + Some(500), + "IDENTITY_ID_1 should receive 500 at timestamp 1000" + ); + assert_eq!( + ts1000.get(&Identifier::new([2u8; 32])).copied(), + Some(300), + "IDENTITY_ID_2 should receive 300 at timestamp 1000" + ); + + // Verify remaining timestamps exist + assert!( + distributions.0.contains_key(&5000), + "timestamp 5000 should be present" + ); + assert!( + distributions.0.contains_key(&10000), + "timestamp 10000 should be present" + ); } /// TOKEN_ID_0 has no pre-programmed distributions; query should return None. From 7994ff939423e53aec2fd64283c166e86c6459f6 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:06:00 +0100 Subject: [PATCH 4/5] fix(rs-sdk): address review findings from grumpy-review and PR comments - Fix incorrect base_supply values in README token test data table - Document default-inclusive behavior for start_recipient_included - Fix CString use-after-free in FFI unit test (bind to variable before as_ptr) - Add content assertions to FFI integration test (array length, structure) - Add absent distributions FFI test using TOKEN_ID_0 - Only set start_recipient_included in proto when start_recipient is present Co-Authored-By: Claude Opus 4.6 --- .../token_pre_programmed_distributions.rs | 1 + .../queries/pre_programmed_distributions.rs | 3 +- .../tests/integration_tests/token.rs | 66 ++++++++++++++++++- packages/rs-sdk/README.md | 4 +- .../token_pre_programmed_distributions.rs | 21 ++++-- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs index 68a16b66658..041de4ca662 100644 --- a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs +++ b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs @@ -53,6 +53,7 @@ impl FromProof for TokenPreProgrammed error: "start_recipient must be 32 bytes".into(), } })?; + // Default to inclusive: if omitted the start recipient is included. let included = start_at_info.start_recipient_included.unwrap_or(true); Some((recipient_id, included)) } diff --git a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs index 74097b5fc67..8e25992e011 100644 --- a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs +++ b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs @@ -165,9 +165,10 @@ mod tests { #[test] fn test_null_handle() { unsafe { + let token_id = std::ffi::CString::new("test").unwrap(); let result = dash_sdk_token_get_pre_programmed_distributions( std::ptr::null(), - std::ffi::CString::new("test").unwrap().as_ptr(), + token_id.as_ptr(), 0, std::ptr::null(), false, diff --git a/packages/rs-sdk-ffi/tests/integration_tests/token.rs b/packages/rs-sdk-ffi/tests/integration_tests/token.rs index 14b7a0bac08..67ece97dcb4 100644 --- a/packages/rs-sdk-ffi/tests/integration_tests/token.rs +++ b/packages/rs-sdk-ffi/tests/integration_tests/token.rs @@ -204,9 +204,73 @@ fn test_token_pre_programmed_distributions() { Ok(Some(json_str)) => { let json = parse_json_result(&json_str).expect("valid JSON"); assert!(json.is_array(), "Expected array, got: {:?}", json); + let arr = json.as_array().unwrap(); + assert_eq!( + arr.len(), + 3, + "Expected 3 distribution timestamps, got {}", + arr.len() + ); + // Verify at least the first entry has the expected structure. + // Test data: timestamp 1000 with 2 recipients, timestamp 5000 with 1, timestamp 10000 with 2. + let first = &arr[0]; + assert!( + first.get("timestamp").is_some(), + "Entry should have a 'timestamp' field: {:?}", + first + ); + assert!( + first + .get("distributions") + .map(|d| d.is_array()) + .unwrap_or(false), + "Entry should have a 'distributions' array: {:?}", + first + ); } Ok(None) => { - // Token might not have pre-programmed distributions + // Vectors not yet generated; absent distributions is also acceptable. + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + + destroy_test_sdk_handle(handle); +} + +/// Test that TOKEN_ID_0 has no pre-programmed distributions (returns null data) +#[test] +fn test_token_pre_programmed_distributions_absent() { + setup_logs(); + + let handle = create_test_sdk_handle("test_token_pre_programmed_distributions_absent"); + let token_contract_id = to_c_string(&token0_id_b58()); + + unsafe { + let result = dash_sdk_token_get_pre_programmed_distributions( + handle, + token_contract_id.as_ptr(), + 0, + std::ptr::null(), + false, + 0, + ); + + match parse_string_result(result) { + Ok(None) => { + // Expected: TOKEN_ID_0 has no pre-programmed distributions. + } + Ok(Some(json_str)) => { + // If vectors happen to return data, the token should have zero entries. + let json = parse_json_result(&json_str).expect("valid JSON"); + let arr = json + .as_array() + .expect("Expected array for absent distributions"); + assert!( + arr.is_empty(), + "TOKEN_ID_0 should have no distributions, got: {:?}", + arr + ); } Err(e) => panic!("Unexpected error: {}", e), } diff --git a/packages/rs-sdk/README.md b/packages/rs-sdk/README.md index c25c654cffa..7a2372e0d4e 100644 --- a/packages/rs-sdk/README.md +++ b/packages/rs-sdk/README.md @@ -95,8 +95,8 @@ Token configuration: | Token | Config | |-------|--------| | `TOKEN_ID_0` | base_supply=100000, frozen for IDENTITY_ID_2, no pricing, no pre-programmed distributions | -| `TOKEN_ID_1` | base_supply=100100, paused, single price=25, no pre-programmed distributions | -| `TOKEN_ID_2` | base_supply=100200, pricing schedule (10 levels), pre-programmed distributions (3 timestamps) | +| `TOKEN_ID_1` | base_supply=100000, paused, single price=25, no pre-programmed distributions | +| `TOKEN_ID_2` | base_supply=100000, pricing schedule (10 levels), pre-programmed distributions at timestamps 1000, 5000, 10000 | When adding a new query type, add corresponding test data to the files in `create_genesis_state/test/` and reference it in `packages/rs-sdk/tests/fetch/generated_data.rs`. diff --git a/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs b/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs index ae03d57fd37..e79de6e588b 100644 --- a/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs +++ b/packages/rs-sdk/src/platform/tokens/token_pre_programmed_distributions.rs @@ -13,7 +13,7 @@ pub use drive_proof_verifier::types::TokenPreProgrammedDistributions; pub struct TokenPreProgrammedDistributionsQuery { /// Token identifier. pub token_id: Identifier, - /// Optional pagination start point. + /// Optional pagination start point. When `None`, results start from the earliest timestamp. pub start_at_info: Option, /// Optional limit on the number of results. pub limit: Option, @@ -26,7 +26,8 @@ pub struct TokenPreProgrammedDistributionsStartAtInfo { pub start_time_ms: u64, /// Optional recipient identifier to start from within the timestamp. pub start_recipient: Option, - /// Whether to include the start recipient in results. + /// Whether to include the start recipient in results. Defaults to `true` when absent in proof + /// verification, so omitting this is equivalent to an inclusive range start. pub start_recipient_included: bool, } @@ -36,10 +37,18 @@ impl Query for TokenPreProgrammedDist unimplemented!("queries without proofs are not supported yet"); } - let start_at_info = self.start_at_info.map(|info| StartAtInfo { - start_time_ms: info.start_time_ms, - start_recipient: info.start_recipient.map(|id| id.to_vec()), - start_recipient_included: Some(info.start_recipient_included), + let start_at_info = self.start_at_info.map(|info| { + let has_recipient = info.start_recipient.is_some(); + StartAtInfo { + start_time_ms: info.start_time_ms, + start_recipient: info.start_recipient.map(|id| id.to_vec()), + // Only set when a start_recipient is provided; meaningless otherwise. + start_recipient_included: if has_recipient { + Some(info.start_recipient_included) + } else { + None + }, + } }); let request = GetTokenPreProgrammedDistributionsRequest { From 9dfbb06af4d51ea9c858874e74e88a4086084035 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 14 Mar 2026 16:07:18 +0700 Subject: [PATCH 5/5] fix(rs-sdk-ffi): reject start_recipient when start_time_ms is zero Return InvalidParameter error instead of silently ignoring start_recipient when start_time_ms is 0, catching caller bugs early. Co-Authored-By: Claude Opus 4.6 --- .../src/token/queries/pre_programmed_distributions.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs index 8e25992e011..f3ab2dcaaea 100644 --- a/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs +++ b/packages/rs-sdk-ffi/src/token/queries/pre_programmed_distributions.rs @@ -89,6 +89,12 @@ pub unsafe extern "C" fn dash_sdk_token_get_pre_programmed_distributions( start_recipient_included, }) } else { + if !start_recipient.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "start_recipient provided but start_time_ms is zero".to_string(), + )); + } None };