diff --git a/packages/rs-context-provider/src/provider.rs b/packages/rs-context-provider/src/provider.rs index 83377e0dd53..d8843b48bee 100644 --- a/packages/rs-context-provider/src/provider.rs +++ b/packages/rs-context-provider/src/provider.rs @@ -88,13 +88,6 @@ pub trait ContextProvider: Send + Sync { /// * `Ok(CoreBlockHeight)`: On success, returns the platform activation height as defined by mn_rr /// * `Err(Error)`: On failure, returns an error indicating why the operation failed. fn get_platform_activation_height(&self) -> Result; - - /// Updates the cached data contract with a fresh version. - /// - /// Called when the SDK detects that a cached contract is stale (e.g., after a - /// document deserialization failure due to a contract schema update). - /// The default implementation is a no-op. - fn update_data_contract(&self, _contract: Arc) {} } impl + Send + Sync> ContextProvider for C { @@ -126,10 +119,6 @@ impl + Send + Sync> ContextProvider for C { fn get_platform_activation_height(&self) -> Result { self.as_ref().get_platform_activation_height() } - - fn update_data_contract(&self, contract: Arc) { - self.as_ref().update_data_contract(contract) - } } impl ContextProvider for std::sync::Mutex @@ -167,11 +156,6 @@ where let lock = self.lock().expect("lock poisoned"); lock.get_platform_activation_height() } - - fn update_data_contract(&self, contract: Arc) { - let lock = self.lock().expect("lock poisoned"); - lock.update_data_contract(contract) - } } /// A trait that provides a function that can be used to look up a [DataContract] by its [Identifier]. diff --git a/packages/rs-drive-proof-verifier/src/error.rs b/packages/rs-drive-proof-verifier/src/error.rs index 15e5f4050da..98f1991f073 100644 --- a/packages/rs-drive-proof-verifier/src/error.rs +++ b/packages/rs-drive-proof-verifier/src/error.rs @@ -29,8 +29,8 @@ pub enum Error { }, /// Dash Protocol error - #[error("protocol: {0}")] - ProtocolError(ProtocolError), + #[error("dash protocol: {error}")] + ProtocolError { error: String }, /// Empty response metadata #[error("empty response metadata")] @@ -99,18 +99,17 @@ pub enum Error { impl From for Error { fn from(error: drive::error::Error) -> Self { - match error { - drive::error::Error::Protocol(protocol_err) => Self::ProtocolError(*protocol_err), - other => Self::DriveError { - error: other.to_string(), - }, + Self::DriveError { + error: error.to_string(), } } } impl From for Error { fn from(error: ProtocolError) -> Self { - Self::ProtocolError(error) + Self::ProtocolError { + error: error.to_string(), + } } } @@ -139,10 +138,6 @@ impl MapGroveDbError for Result { }) } - Err(drive::error::Error::Protocol(protocol_err)) => { - Err(Error::ProtocolError(*protocol_err)) - } - Err(other) => Err(other.into()), } } diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 401dd87e0c5..f3a9127d2cb 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -277,7 +277,9 @@ impl FromProof for Identity { let id = match request.version.ok_or(Error::EmptyVersion)? { get_identity_request::Version::V0(v0) => { - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into()))? + Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + })? } }; @@ -472,7 +474,9 @@ impl FromProof for IdentityPublicKeys { get_identity_keys_request::Version::V0(v0) => { let request_type = v0.request_type; let identity_id = Identifier::from_bytes(&v0.identity_id) - .map_err(|e| Error::ProtocolError(e.into()))? + .map_err(|e| Error::ProtocolError { + error: e.to_string(), + })? .into_buffer(); let limit = v0.limit.map(|i| i as u16); let offset = v0.offset.map(|i| i as u16); @@ -610,12 +614,14 @@ impl FromProof for IdentityNonceFetcher { let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let identity_id = match request.version.ok_or(Error::EmptyVersion)? { - get_identity_nonce_request::Version::V0(v0) => Ok::( - Identifier::from_bytes(&v0.identity_id) - .map_err(|e| Error::ProtocolError(e.into()))?, - ), - }?; + let identity_id = + match request.version.ok_or(Error::EmptyVersion)? { + get_identity_nonce_request::Version::V0(v0) => Ok::( + Identifier::from_bytes(&v0.identity_id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?, + ), + }?; // Extract content from proof and verify Drive/GroveDB proofs let (root_hash, maybe_nonce) = Drive::verify_identity_nonce( @@ -661,10 +667,12 @@ impl FromProof for IdentityContractNo let (identity_id, contract_id) = match request.version.ok_or(Error::EmptyVersion)? { get_identity_contract_nonce_request::Version::V0(v0) => { Ok::<(Identifier, Identifier), Error>(( - Identifier::from_bytes(&v0.identity_id) - .map_err(|e| Error::ProtocolError(e.into()))?, - Identifier::from_bytes(&v0.contract_id) - .map_err(|e| Error::ProtocolError(e.into()))?, + Identifier::from_bytes(&v0.identity_id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?, + Identifier::from_bytes(&v0.contract_id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?, )) } }?; @@ -712,9 +720,10 @@ impl FromProof for IdentityBalance { let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; let id = match request.version.ok_or(Error::EmptyVersion)? { - get_identity_balance_request::Version::V0(v0) => { - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into())) - } + get_identity_balance_request::Version::V0(v0) => Identifier::from_bytes(&v0.id) + .map_err(|e| Error::ProtocolError { + error: e.to_string(), + }), }?; // Extract content from proof and verify Drive/GroveDB proofs @@ -805,7 +814,9 @@ impl FromProof for IdentityBalan let id = match request.version.ok_or(Error::EmptyVersion)? { get_identity_balance_and_revision_request::Version::V0(v0) => { - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into())) + Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + }) } }?; @@ -1123,7 +1134,9 @@ impl FromProof for DataContract { let id = match request.version.ok_or(Error::EmptyVersion)? { get_data_contract_request::Version::V0(v0) => { - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into())) + Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + }) } }?; @@ -1168,7 +1181,9 @@ impl FromProof for (DataContract, Vec) { let id = match request.version.ok_or(Error::EmptyVersion)? { get_data_contract_request::Version::V0(v0) => { - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into())) + Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + }) } }?; @@ -1279,8 +1294,9 @@ impl FromProof for DataContractHistory let (id, limit, offset, start_at_ms) = match request.version.ok_or(Error::EmptyVersion)? { get_data_contract_history_request::Version::V0(v0) => { - let id = - Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError(e.into()))?; + let id = Identifier::from_bytes(&v0.id).map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?; let limit = u32_to_u16_opt(v0.limit.unwrap_or_default())?; let offset = u32_to_u16_opt(v0.offset.unwrap_or_default())?; let start_at_ms = v0.start_at_ms; @@ -1330,7 +1346,9 @@ impl FromProof for StateTransitionPro let proof = response.proof().or(Err(Error::NoProofInResult))?; let state_transition = StateTransition::deserialize_from_bytes(&request.state_transition) - .map_err(Error::ProtocolError)?; + .map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; @@ -1755,14 +1773,20 @@ impl FromProof for IdentitiesContrac Ok(identifier.to_buffer()) }) .collect::, platform_value::Error>>() - .map_err(|e| Error::ProtocolError(e.into()))?; + .map_err(|e| Error::ProtocolError { + error: e.to_string(), + })?; let contract_id = Identifier::from_vec(contract_id) - .map_err(|e| Error::ProtocolError(e.into()))? + .map_err(|e| Error::ProtocolError { + error: e.to_string(), + })? .into_buffer(); let purposes = purposes .into_iter() .map(|purpose| { - Purpose::try_from(purpose).map_err(|e| Error::ProtocolError(e.into())) + Purpose::try_from(purpose).map_err(|e| Error::ProtocolError { + error: e.to_string(), + }) }) .collect::, Error>>()?; (identifiers, contract_id, document_type_name, purposes) diff --git a/packages/rs-drive-proof-verifier/src/unproved.rs b/packages/rs-drive-proof-verifier/src/unproved.rs index cb28a3db4a7..dc8fdabc25d 100644 --- a/packages/rs-drive-proof-verifier/src/unproved.rs +++ b/packages/rs-drive-proof-verifier/src/unproved.rs @@ -184,7 +184,7 @@ impl FromUnproved for CurrentQuorumsInfo .map(|q_hash| { let mut q_hash_array = [0u8; 32]; if q_hash.len() != 32 { - return Err(Error::ResponseDecodeError { + return Err(Error::ProtocolError { error: "Invalid quorum_hash length".to_string(), }); } @@ -196,7 +196,7 @@ impl FromUnproved for CurrentQuorumsInfo // Extract current quorum hash let mut current_quorum_hash = [0u8; 32]; if v0.current_quorum_hash.len() != 32 { - return Err(Error::ResponseDecodeError { + return Err(Error::ProtocolError { error: "Invalid current_quorum_hash length".to_string(), }); } @@ -204,7 +204,7 @@ impl FromUnproved for CurrentQuorumsInfo let mut last_block_proposer = [0u8; 32]; if v0.last_block_proposer.len() != 32 { - return Err(Error::ResponseDecodeError { + return Err(Error::ProtocolError { error: "Invalid last_block_proposer length".to_string(), }); } @@ -225,7 +225,7 @@ impl FromUnproved for CurrentQuorumsInfo .into_iter() .map(|member| { let pro_tx_hash = ProTxHash::from_slice(&member.pro_tx_hash) - .map_err(|_| Error::ResponseDecodeError { + .map_err(|_| Error::ProtocolError { error: "Invalid ProTxHash format".to_string(), })?; let validator = ValidatorV0 { @@ -244,7 +244,7 @@ impl FromUnproved for CurrentQuorumsInfo Ok(ValidatorSet::V0(ValidatorSetV0 { quorum_hash: QuorumHash::from_slice(quorum_hash.as_slice()) - .map_err(|_| Error::ResponseDecodeError { + .map_err(|_| Error::ProtocolError { error: "Invalid Quorum Hash format".to_string(), })?, quorum_index: None, // Assuming it's not provided here @@ -253,7 +253,7 @@ impl FromUnproved for CurrentQuorumsInfo threshold_public_key: BlsPublicKey::try_from( vs.threshold_public_key.as_slice(), ) - .map_err(|_| Error::ResponseDecodeError { + .map_err(|_| Error::ProtocolError { error: "Invalid BlsPublicKey format".to_string(), })?, })) diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index 0f7847b65a6..91ad6eaa677 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -810,10 +810,6 @@ impl ContextProvider for TrustedHttpContextProvider { )), } } - - fn update_data_contract(&self, contract: Arc) { - self.add_known_contract((*contract).clone()); - } } #[cfg(test)] diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index 6680d6b28eb..88327ff5564 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -6,7 +6,6 @@ use crate::sync::block_on; use crate::{Error, Sdk}; use arc_swap::ArcSwapAny; use dash_context_provider::{ContextProvider, ContextProviderError}; -use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::TokenConfiguration; use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; use dpp::version::PlatformVersion; @@ -240,11 +239,6 @@ impl ContextProvider for GrpcContextProvider { fn get_platform_activation_height(&self) -> Result { self.core.get_platform_activation_height() } - - fn update_data_contract(&self, contract: Arc) { - self.data_contracts_cache - .put(contract.id(), (*contract).clone()); - } } /// Thread-safe cache of various objects inside the SDK. diff --git a/packages/rs-sdk/src/mock/sdk.rs b/packages/rs-sdk/src/mock/sdk.rs index c7904f05d4b..a6d75d829f1 100644 --- a/packages/rs-sdk/src/mock/sdk.rs +++ b/packages/rs-sdk/src/mock/sdk.rs @@ -26,11 +26,7 @@ use rs_dapi_client::{ transport::TransportRequest, DapiClient, DumpData, ExecutionResponse, }; -use std::{ - collections::{BTreeMap, BTreeSet}, - path::PathBuf, - sync::Arc, -}; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; use tokio::sync::{Mutex, OwnedMutexGuard}; /// Mechanisms to mock Dash Platform SDK. @@ -46,9 +42,6 @@ use tokio::sync::{Mutex, OwnedMutexGuard}; #[derive(Debug)] pub struct MockDashPlatformSdk { from_proof_expectations: BTreeMap>, - /// Keys for which proof parsing should return a `CorruptedSerialization` error. - /// Used to test contract-refresh retry logic. - proof_error_expectations: BTreeSet, platform_version: &'static PlatformVersion, dapi: Arc>, sdk: ArcSwapOption, @@ -76,7 +69,6 @@ impl MockDashPlatformSdk { pub(crate) fn new(version: &'static PlatformVersion, dapi: Arc>) -> Self { Self { from_proof_expectations: Default::default(), - proof_error_expectations: Default::default(), platform_version: version, dapi, sdk: ArcSwapOption::new(None), @@ -402,39 +394,6 @@ impl MockDashPlatformSdk { Ok(self) } - /// Expect a [Fetch] request to fail with a document deserialization (CorruptedSerialization) error. - /// - /// This sets up the mock so that proof parsing for the given query returns a - /// `ProtocolError(DataContractError::CorruptedSerialization(...))` error. - /// Used to test the contract-refresh retry logic. - pub async fn expect_fetch_proof_error::Request>>( - &mut self, - query: Q, - ) -> Result<&mut Self, Error> - where - <::Request as TransportRequest>::Response: Default, - { - let grpc_request = query.query(self.prove()).expect("query must be correct"); - let key = Key::new(&grpc_request); - - self.proof_error_expectations.insert(key); - - // Also set up DAPI mock for execute so the transport layer returns a default response - { - let mut dapi_guard = self.dapi.lock().await; - dapi_guard.expect( - &grpc_request, - &Ok(ExecutionResponse { - inner: Default::default(), - retries: 0, - address: "http://127.0.0.1".parse().expect("failed to parse address"), - }), - )?; - } - - Ok(self) - } - /// Save expectations for a request. async fn expect( &mut self, @@ -500,17 +459,6 @@ impl MockDashPlatformSdk { { let key = Key::new(&request); - // Check if this request should simulate a deserialization error - if self.proof_error_expectations.contains(&key) { - return Err(drive_proof_verifier::Error::ProtocolError( - dpp::ProtocolError::DataContractError( - dpp::data_contract::errors::DataContractError::CorruptedSerialization( - "mock: simulated stale contract deserialization error".to_string(), - ), - ), - )); - } - let data = match self.from_proof_expectations.get(&key) { Some(d) => ( Option::::mock_deserialize(self, d), diff --git a/packages/rs-sdk/src/platform/documents/document_query.rs b/packages/rs-sdk/src/platform/documents/document_query.rs index 7808977e2aa..3a669e5c42c 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/rs-sdk/src/platform/documents/document_query.rs @@ -129,20 +129,6 @@ impl DocumentQuery { self } - - /// Create a clone of this query with a different data contract. - /// - /// Preserves all where/order_by/limit/start clauses. - pub fn clone_with_contract(&self, contract: Arc) -> Self { - Self { - data_contract: contract, - document_type_name: self.document_type_name.clone(), - where_clauses: self.where_clauses.clone(), - order_by_clauses: self.order_by_clauses.clone(), - limit: self.limit, - start: self.start.clone(), - } - } } impl TransportRequest for DocumentQuery { diff --git a/packages/rs-sdk/src/platform/fetch.rs b/packages/rs-sdk/src/platform/fetch.rs index a80eaacb97b..27c6394dfe5 100644 --- a/packages/rs-sdk/src/platform/fetch.rs +++ b/packages/rs-sdk/src/platform/fetch.rs @@ -11,9 +11,7 @@ use crate::mock::MockResponse; use crate::sync::retry; use crate::{error::Error, platform::query::Query, Sdk}; -use dash_context_provider::ContextProvider; use dapi_grpc::platform::v0::{self as platform_proto, Proof, ResponseMetadata}; -use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use dpp::identity::identities_contract_keys::IdentitiesContractKeys; use dpp::voting::votes::Vote; @@ -159,8 +157,47 @@ where query: Q, settings: Option, ) -> Result<(Option, ResponseMetadata, Proof), Error> { - let request = query.query(sdk.prove())?; - fetch_request(sdk, &request, settings).await + let request: &::Request = &query.query(sdk.prove())?; + + let fut = |settings: RequestSettings| async move { + let ExecutionResponse { + address, + retries, + inner: response, + } = request + .clone() + .execute(sdk, settings) + .await + .map_err(|execution_error| execution_error.inner_into())?; + + let object_type = std::any::type_name::().to_string(); + tracing::trace!(request = ?request, response = ?response, ?address, retries, object_type, "fetched object from platform"); + + let (object, response_metadata, proof): (Option, ResponseMetadata, Proof) = sdk + .parse_proof_with_metadata_and_proof(request.clone(), response) + .await + .map_err(|e| ExecutionError { + inner: e, + address: Some(address.clone()), + retries, + })?; + + match object { + Some(item) => Ok((item.into(), response_metadata, proof)), + None => Ok((None, response_metadata, proof)), + } + .map(|x| ExecutionResponse { + inner: x, + address, + retries, + }) + }; + + let settings = sdk + .dapi_client_settings + .override_by(settings.unwrap_or_default()); + + retry(sdk.address_list(), settings, fut).await.into_inner() } /// Fetch single object from Platform. @@ -223,85 +260,8 @@ impl Fetch for (dpp::prelude::DataContract, Vec) { type Request = platform_proto::GetDataContractRequest; } -#[async_trait::async_trait] impl Fetch for Document { type Request = DocumentQuery; - - async fn fetch_with_metadata_and_proof::Request>>( - sdk: &Sdk, - query: Q, - settings: Option, - ) -> Result<(Option, ResponseMetadata, Proof), Error> { - let document_query: DocumentQuery = query.query(sdk.prove())?; - - // First attempt with current (possibly cached) contract - match fetch_request(sdk, &document_query, settings).await { - Ok(result) => Ok(result), - Err(e) if is_document_deserialization_error(&e) => { - let fresh_query = refetch_contract_for_query(sdk, &document_query).await?; - fetch_request(sdk, &fresh_query, settings).await - } - Err(e) => Err(e), - } - } -} - -/// Execute a fetch request with node-level retry logic. -/// -/// Shared implementation used by both the default [Fetch::fetch_with_metadata_and_proof] -/// and the [Document]-specific override. -async fn fetch_request( - sdk: &Sdk, - request: &R, - settings: Option, -) -> Result<(Option, ResponseMetadata, Proof), Error> -where - O: Sized - + Send - + Debug - + MockResponse - + FromProof::Response>, - R: TransportRequest + Into<>::Request> + Clone + Debug, -{ - let fut = |settings: RequestSettings| async move { - let ExecutionResponse { - address, - retries, - inner: response, - } = request - .clone() - .execute(sdk, settings) - .await - .map_err(|execution_error| execution_error.inner_into())?; - - let object_type = std::any::type_name::().to_string(); - tracing::trace!(request = ?request, response = ?response, ?address, retries, object_type, "fetched object from platform"); - - let (object, response_metadata, proof): (Option, ResponseMetadata, Proof) = sdk - .parse_proof_with_metadata_and_proof(request.clone(), response) - .await - .map_err(|e| ExecutionError { - inner: e, - address: Some(address.clone()), - retries, - })?; - - match object { - Some(item) => Ok((item.into(), response_metadata, proof)), - None => Ok((None, response_metadata, proof)), - } - .map(|x| ExecutionResponse { - inner: x, - address, - retries, - }) - }; - - let settings = sdk - .dapi_client_settings - .override_by(settings.unwrap_or_default()); - - retry(sdk.address_list(), settings, fut).await.into_inner() } impl Fetch for drive_proof_verifier::types::IdentityBalance { @@ -368,90 +328,3 @@ impl Fetch for drive_proof_verifier::types::RecentCompactedAddressBalanceChanges impl Fetch for drive_proof_verifier::types::PlatformAddressTrunkState { type Request = platform_proto::GetAddressesTrunkStateRequest; } - -/// Refetch the data contract from the network, update the context provider -/// cache, and return a new [DocumentQuery] with the fresh contract. -/// -/// Used by document fetch retry logic when a deserialization error indicates -/// a stale cached contract. -pub(super) async fn refetch_contract_for_query( - sdk: &Sdk, - document_query: &DocumentQuery, -) -> Result { - tracing::debug!( - contract_id = ?document_query.data_contract.id(), - "refetching contract for document query after deserialization failure" - ); - - let fresh_contract = dpp::prelude::DataContract::fetch(sdk, document_query.data_contract.id()) - .await? - .ok_or(Error::MissingDependency( - "DataContract".to_string(), - format!( - "data contract {} not found during refetch", - document_query.data_contract.id() - ), - ))?; - - let fresh_contract = std::sync::Arc::new(fresh_contract); - - // Update the cached contract in the context provider - if let Some(context_provider) = sdk.context_provider() { - context_provider.update_data_contract(fresh_contract.clone()); - } - - Ok(document_query.clone_with_contract(fresh_contract)) -} - -/// Returns true if the error indicates a document deserialization failure -/// that could be caused by a stale/outdated data contract schema. -pub(super) fn is_document_deserialization_error(error: &Error) -> bool { - use dpp::data_contract::errors::DataContractError; - - matches!( - error, - Error::Proof(drive_proof_verifier::Error::ProtocolError( - dpp::ProtocolError::DataContractError(DataContractError::CorruptedSerialization(_)) - )) - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use dpp::data_contract::errors::DataContractError; - - #[test] - fn test_corrupted_serialization_is_detected() { - let error = Error::Proof(drive_proof_verifier::Error::ProtocolError( - dpp::ProtocolError::DataContractError(DataContractError::CorruptedSerialization( - "test error".to_string(), - )), - )); - - assert!(is_document_deserialization_error(&error)); - } - - #[test] - fn test_other_protocol_error_is_not_detected() { - let error = Error::Proof(drive_proof_verifier::Error::ProtocolError( - dpp::ProtocolError::DecodingError("some decoding error".to_string()), - )); - - assert!(!is_document_deserialization_error(&error)); - } - - #[test] - fn test_other_proof_error_is_not_detected() { - let error = Error::Proof(drive_proof_verifier::Error::EmptyVersion); - - assert!(!is_document_deserialization_error(&error)); - } - - #[test] - fn test_non_proof_error_is_not_detected() { - let error = Error::Generic("some error".to_string()); - - assert!(!is_document_deserialization_error(&error)); - } -} diff --git a/packages/rs-sdk/src/platform/fetch_many.rs b/packages/rs-sdk/src/platform/fetch_many.rs index bf20f76abc3..c0e4947b1e2 100644 --- a/packages/rs-sdk/src/platform/fetch_many.rs +++ b/packages/rs-sdk/src/platform/fetch_many.rs @@ -45,7 +45,6 @@ use rs_dapi_client::{ transport::TransportRequest, DapiRequest, ExecutionError, ExecutionResponse, InnerInto, IntoInner, RequestSettings, }; -use std::fmt::Debug; /// Fetch multiple objects from Platform. /// @@ -208,8 +207,51 @@ where query: Q, settings: Option, ) -> Result<(O, ResponseMetadata, Proof), Error> { - let request = query.query(sdk.prove())?; - fetch_many_request(sdk, &request, settings).await + let request = &query.query(sdk.prove())?; + + let fut = |settings: RequestSettings| async move { + let ExecutionResponse { + address, + retries, + inner: response, + } = request + .clone() + .execute(sdk, settings) + .await + .map_err(|e| e.inner_into())?; + + let object_type = std::any::type_name::().to_string(); + tracing::trace!( + request = ?request, + response = ?response, + ?address, + retries, + object_type, + "fetched objects from platform" + ); + + sdk.parse_proof_with_metadata_and_proof::<>::Request, O>( + request.clone(), + response, + ) + .await + .map_err(|e| ExecutionError { + inner: e, + address: Some(address.clone()), + retries, + }) + .map(|(o, metadata, proof)| ExecutionResponse { + inner: (o.unwrap_or_default(), metadata, proof), + retries, + address: address.clone(), + }) + }; + + let settings = sdk + .dapi_client_settings + .override_by(settings.unwrap_or_default()); + + retry(sdk.address_list(), settings, fut).await.into_inner() } /// Fetch multiple objects from Platform by their identifiers. @@ -278,85 +320,43 @@ impl FetchMany for Document { // type stores full contract, which is missing in the GetDocumentsRequest type. // TODO: Refactor to use ContextProvider type Request = DocumentQuery; - - async fn fetch_many_with_metadata_and_proof< - Q: Query<>::Request>, - >( + async fn fetch_many>::Request>>( sdk: &Sdk, query: Q, - settings: Option, - ) -> Result<(Documents, ResponseMetadata, Proof), Error> { - let document_query: DocumentQuery = query.query(sdk.prove())?; - - // First attempt with current (possibly cached) contract - match fetch_many_request(sdk, &document_query, settings).await { - Ok(result) => Ok(result), - Err(e) if super::fetch::is_document_deserialization_error(&e) => { - let fresh_query = - super::fetch::refetch_contract_for_query(sdk, &document_query).await?; - fetch_many_request(sdk, &fresh_query, settings).await - } - Err(e) => Err(e), - } - } -} + ) -> Result { + let document_query: &DocumentQuery = &query.query(sdk.prove())?; -/// Execute a fetch-many request with node-level retry logic. -/// -/// Shared implementation used by both the default [FetchMany::fetch_many_with_metadata_and_proof] -/// and the [Document]-specific override. -async fn fetch_many_request( - sdk: &Sdk, - request: &R, - settings: Option, -) -> Result<(O, ResponseMetadata, Proof), Error> -where - O: Send - + Default - + MockResponse - + FromProof::Response>, - R: TransportRequest + Into<>::Request> + Clone + Debug, -{ - let fut = |settings: RequestSettings| async move { - let ExecutionResponse { - address, - retries, - inner: response, - } = request - .clone() - .execute(sdk, settings) - .await - .map_err(|e| e.inner_into())?; - - let object_type = std::any::type_name::().to_string(); - tracing::trace!( - request = ?request, - response = ?response, - ?address, - retries, - object_type, - "fetched objects from platform" - ); - - sdk.parse_proof_with_metadata_and_proof::(request.clone(), response) - .await - .map_err(|e| ExecutionError { - inner: e, - address: Some(address.clone()), + retry(sdk.address_list(), sdk.dapi_client_settings, |settings| async move { + let request = document_query.clone(); + + let ExecutionResponse { + address, retries, - }) - .map(|(o, metadata, proof)| ExecutionResponse { - inner: (o.unwrap_or_default(), metadata, proof), + inner: response + } = request.execute(sdk, settings).await.map_err(|e| e.inner_into())?; + + tracing::trace!(request=?document_query, response=?response, ?address, retries, "fetch multiple documents"); + + // let object: Option> = sdk + let documents = sdk + .parse_proof::(document_query.clone(), response) + .await + .map_err(|e| ExecutionError { + inner: e, + retries, + address: Some(address.clone()), + })? + .unwrap_or_default(); + + Ok(ExecutionResponse { + inner: documents, retries, - address: address.clone(), + address, }) - }; - - let settings = sdk - .dapi_client_settings - .override_by(settings.unwrap_or_default()); - - retry(sdk.address_list(), settings, fut).await.into_inner() + }) + .await + .into_inner() + } } /// Retrieve public keys for a given identity. diff --git a/packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs b/packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs deleted file mode 100644 index 9449a4cfb20..00000000000 --- a/packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Tests for document fetch retry on stale contract deserialization error. -//! -//! When a data contract is updated on the network, the SDK may hold a cached (old) version. -//! Documents serialized with the new schema fail to deserialize with the old contract. -//! The SDK should detect this and retry once with a freshly-fetched contract. - -use super::common::{mock_data_contract, mock_document_type}; -use dash_sdk::{ - platform::{DocumentQuery, Fetch, FetchMany}, - Sdk, -}; -use dpp::{ - data_contract::{ - accessors::v0::DataContractV0Getters, - document_type::{ - accessors::DocumentTypeV0Getters, random_document::CreateRandomDocument, DocumentType, - }, - }, - document::{Document, DocumentV0Getters}, - prelude::DataContract, -}; -use drive_proof_verifier::types::Documents; - -/// Helper: create two data contracts sharing the same identity and ID, but the -/// "new" contract has an extra field. Both contracts use the same document type name. -/// -/// Returns (old_contract, new_contract, new_document_type). -fn make_old_and_new_contracts() -> (DataContract, DataContract, DocumentType) { - use dpp::{ - data_contract::{config::DataContractConfig, DataContractFactory}, - platform_value::{platform_value, Value}, - prelude::Identifier, - version::PlatformVersion, - }; - use std::collections::BTreeMap; - - let platform_version = PlatformVersion::latest(); - let protocol_version = platform_version.protocol_version; - let owner_id = Identifier::new([7u8; 32]); - - // --- "old" contract: single field --- - let old_schema = platform_value!({ - "type": "object", - "properties": { - "a": { - "type": "string", - "maxLength": 10, - "position": 0 - } - }, - "additionalProperties": false, - }); - - let mut old_types: BTreeMap = BTreeMap::new(); - old_types.insert("document_type_name".to_string(), old_schema); - - let old_contract = DataContractFactory::new(protocol_version) - .unwrap() - .create(owner_id, 0, platform_value!(old_types), None, None) - .expect("create old data contract") - .data_contract_owned(); - - // --- "new" contract: two fields (simulates a schema update) --- - let new_schema = platform_value!({ - "type": "object", - "properties": { - "a": { - "type": "string", - "maxLength": 10, - "position": 0 - }, - "b": { - "type": "integer", - "position": 1 - } - }, - "additionalProperties": false, - }); - - let config = - DataContractConfig::default_for_version(platform_version).expect("create a default config"); - - let new_document_type = DocumentType::try_from_schema( - old_contract.id(), - 1, - config.version(), - "document_type_name", - new_schema.clone(), - None, - &BTreeMap::new(), - &config, - true, - &mut vec![], - platform_version, - ) - .expect("create new document type"); - - let mut new_types: BTreeMap = BTreeMap::new(); - new_types.insert("document_type_name".to_string(), new_schema); - - let mut new_contract = DataContractFactory::new(protocol_version) - .unwrap() - .create(owner_id, 0, platform_value!(new_types), None, None) - .expect("create new data contract") - .data_contract_owned(); - - // Make the new contract share the same ID as the old one - use dpp::data_contract::accessors::v0::DataContractV0Setters; - new_contract.set_id(old_contract.id()); - - (old_contract, new_contract, new_document_type) -} - -/// Test: Document::fetch retries once when the first attempt fails with a deserialization error. -/// -/// Setup: -/// 1. Create old and new versions of a data contract -/// 2. Build a DocumentQuery using the OLD contract -/// 3. Mock the document query with old contract to return a CorruptedSerialization error -/// 4. Mock DataContract::fetch to return the NEW contract -/// 5. Mock the document query with new contract to return the expected document -/// 6. Verify that Document::fetch succeeds (retry worked) -#[tokio::test] -async fn test_fetch_document_retries_on_stale_contract() { - let mut sdk = Sdk::new_mock(); - - let (old_contract, new_contract, new_doc_type) = make_old_and_new_contracts(); - - let expected_document = new_doc_type - .random_document(None, sdk.version()) - .expect("create document"); - let document_id = expected_document.id(); - - // Build a query using the old contract (simulates stale cache) - let old_query = DocumentQuery::new(old_contract.clone(), "document_type_name") - .expect("create document query with old contract") - .with_document_id(&document_id); - - // 1) Old query should fail with a deserialization error - sdk.mock() - .expect_fetch_proof_error::(old_query.clone()) - .await - .expect("set error expectation"); - - // 2) DataContract::fetch should return the new contract (refetch) - sdk.mock() - .expect_fetch(new_contract.id(), Some(new_contract.clone())) - .await - .expect("set contract refetch expectation"); - - // 3) New query (with fresh contract) should succeed - let new_query = old_query.clone_with_contract(std::sync::Arc::new(new_contract)); - sdk.mock() - .expect_fetch(new_query, Some(expected_document.clone())) - .await - .expect("set document fetch expectation with new contract"); - - // Execute: the retry logic should handle the error and succeed - let result = Document::fetch(&sdk, old_query) - .await - .expect("fetch should succeed after retry"); - - let fetched = result.expect("document should be returned"); - assert_eq!(fetched.id(), expected_document.id()); -} - -/// Test: Document::fetch_many retries once when the first attempt fails with a deserialization error. -#[tokio::test] -async fn test_fetch_many_documents_retries_on_stale_contract() { - let mut sdk = Sdk::new_mock(); - - let (old_contract, new_contract, new_doc_type) = make_old_and_new_contracts(); - - let expected_document = new_doc_type - .random_document(None, sdk.version()) - .expect("create document"); - - let expected_documents = - Documents::from([(expected_document.id(), Some(expected_document.clone()))]); - - // Build a query using the old contract (simulates stale cache) - let old_query = DocumentQuery::new(old_contract.clone(), "document_type_name") - .expect("create document query with old contract"); - - // 1) Old query should fail with a deserialization error - // We use expect_fetch_proof_error with Document since DocumentQuery is the - // request type for both Fetch and FetchMany - sdk.mock() - .expect_fetch_proof_error::(old_query.clone()) - .await - .expect("set error expectation"); - - // 2) DataContract::fetch should return the new contract (refetch) - sdk.mock() - .expect_fetch(new_contract.id(), Some(new_contract.clone())) - .await - .expect("set contract refetch expectation"); - - // 3) New query (with fresh contract) should succeed - let new_query = old_query.clone_with_contract(std::sync::Arc::new(new_contract)); - sdk.mock() - .expect_fetch_many(new_query, Some(expected_documents.clone())) - .await - .expect("set document fetch_many expectation with new contract"); - - // Execute: the retry logic should handle the error and succeed - let result = Document::fetch_many(&sdk, old_query) - .await - .expect("fetch_many should succeed after retry"); - - assert_eq!(result.len(), 1); - let (doc_id, doc) = result.into_iter().next().unwrap(); - assert_eq!(doc_id, expected_document.id()); - assert!(doc.is_some()); -} - -/// Test: When the error is NOT a deserialization error, no retry happens and the error propagates. -#[tokio::test] -async fn test_fetch_document_does_not_retry_on_other_errors() { - let sdk = Sdk::new_mock(); - - let doc_type = mock_document_type(); - let contract = mock_data_contract(Some(&doc_type)); - - // Build a query but don't set any expectations — the mock will have no matching - // response for execute, which should result in an error that is NOT a deserialization error. - let query = DocumentQuery::new(contract, doc_type.name()).expect("create document query"); - - let result = Document::fetch(&sdk, query).await; - - // Should fail — non-deserialization errors propagate without retry - assert!( - result.is_err(), - "expected error when no mock expectations are set" - ); -} diff --git a/packages/rs-sdk/tests/fetch/mod.rs b/packages/rs-sdk/tests/fetch/mod.rs index 7de201ee4cb..83fc9a97b03 100644 --- a/packages/rs-sdk/tests/fetch/mod.rs +++ b/packages/rs-sdk/tests/fetch/mod.rs @@ -24,7 +24,6 @@ mod generated_data; mod group_actions; mod identity; mod identity_contract_nonce; -mod mock_document_contract_refresh; mod mock_fetch; mod mock_fetch_many; mod prefunded_specialized_balance; diff --git a/packages/wasm-sdk/src/context_provider.rs b/packages/wasm-sdk/src/context_provider.rs index 922f92e9a54..3bf5ce0fc6f 100644 --- a/packages/wasm-sdk/src/context_provider.rs +++ b/packages/wasm-sdk/src/context_provider.rs @@ -103,10 +103,6 @@ impl ContextProvider for WasmTrustedContext { fn get_platform_activation_height(&self) -> Result { self.inner.get_platform_activation_height() } - - fn update_data_contract(&self, contract: Arc) { - self.inner.update_data_contract(contract) - } } // JS-exported async factory methods