Skip to main content

dash_sdk/
sdk.rs

1//! [Sdk] entrypoint to Dash Platform.
2
3use crate::error::{Error, StaleNodeError};
4use crate::internal_cache::NonceCache;
5use crate::mock::MockResponse;
6#[cfg(feature = "mocks")]
7use crate::mock::{provider::GrpcContextProvider, MockDashPlatformSdk};
8use crate::platform::transition::put_settings::PutSettings;
9use crate::platform::Identifier;
10use arc_swap::ArcSwapOption;
11use dapi_grpc::mock::Mockable;
12use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
13#[cfg(not(target_arch = "wasm32"))]
14use dapi_grpc::tonic::transport::Certificate;
15use dash_context_provider::ContextProvider;
16#[cfg(feature = "mocks")]
17use dash_context_provider::MockContextProvider;
18use dpp::bincode;
19use dpp::bincode::error::DecodeError;
20use dpp::dashcore::Network;
21use dpp::prelude::IdentityNonce;
22use dpp::version::PlatformVersion;
23use drive::grovedb::operations::proof::GroveDBProof;
24use drive_proof_verifier::FromProof;
25pub use http::Uri;
26#[cfg(feature = "mocks")]
27use rs_dapi_client::mock::MockDapiClient;
28pub use rs_dapi_client::AddressList;
29pub use rs_dapi_client::RequestSettings;
30use rs_dapi_client::{
31    transport::TransportRequest, DapiClient, DapiClientError, DapiRequestExecutor, ExecutionResult,
32};
33use std::fmt::Debug;
34#[cfg(feature = "mocks")]
35use std::num::NonZeroUsize;
36use std::path::Path;
37#[cfg(feature = "mocks")]
38use std::path::PathBuf;
39use std::sync::atomic::Ordering;
40use std::sync::{atomic, Arc};
41#[cfg(feature = "mocks")]
42use tokio::sync::{Mutex, MutexGuard};
43use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
44use zeroize::Zeroizing;
45
46/// How many data contracts fit in the cache.
47pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100;
48/// How many token configs fit in the cache.
49pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100;
50/// How many quorum public keys fit in the cache.
51pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100;
52/// The default metadata time tolerance for checkpoint queries in milliseconds
53const ADDRESS_STATE_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
54
55/// The default request settings for the SDK, used when the user does not provide any.
56///
57/// Use [SdkBuilder::with_settings] to set custom settings.
58const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
59    retries: Some(3),
60    timeout: None,
61    ban_failed_address: None,
62    connect_timeout: None,
63    max_decoding_message_size: None,
64};
65
66/// Dash Platform SDK
67///
68/// This is the main entry point for interacting with Dash Platform.
69/// It can be initialized in two modes:
70/// - `Normal`: Connects to a remote Dash Platform node.
71/// - `Mock`: Uses a mock implementation of Dash Platform.
72///
73/// Recommended method of initialization is to use [`SdkBuilder`]. There are also some helper
74/// methods:
75///
76/// * [`SdkBuilder::new_testnet()`] Create a [SdkBuilder] that connects to testnet.
77/// * [`SdkBuilder::new_mainnet()`] Create a [SdkBuilder] that connects to mainnet.
78/// * [`SdkBuilder::new_mock()`] Create a mock [SdkBuilder].
79/// * [`Sdk::new_mock()`] Create a mock [Sdk].
80///
81/// ## Thread safety
82///
83/// Sdk is thread safe and can be shared between threads.
84/// It uses internal locking when needed.
85///
86/// It is also safe to clone the Sdk.
87///
88/// ## Examples
89///
90/// See tests/ for examples of using the SDK.
91pub struct Sdk {
92    /// The network that the sdk is configured for (Dash (mainnet), Testnet, Devnet, Regtest)
93    pub network: Network,
94    inner: SdkInstance,
95    /// Use proofs when retrieving data from Platform.
96    ///
97    /// This is set to `true` by default. `false` is not implemented yet.
98    proofs: bool,
99
100    /// Nonce cache managed exclusively by the SDK.
101    nonce_cache: Arc<NonceCache>,
102
103    /// Context provider used by the SDK.
104    ///
105    /// ## Panics
106    ///
107    /// Note that setting this to None can panic.
108    context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
109
110    /// Protocol version number detected from the network. Shared between clones.
111    protocol_version: Arc<atomic::AtomicU32>,
112
113    /// Whether to auto-detect protocol version from network response metadata.
114    /// Set to `false` when the user explicitly calls [`SdkBuilder::with_version()`].
115    auto_detect_protocol_version: bool,
116
117    /// Last seen height; used to determine if the remote node is stale.
118    ///
119    /// This is clone-able and can be shared between threads.
120    metadata_last_seen_height: Arc<atomic::AtomicU64>,
121
122    /// How many blocks difference is allowed between the last height and the current height received in metadata.
123    ///
124    /// See [SdkBuilder::with_height_tolerance] for more information.
125    metadata_height_tolerance: Option<u64>,
126
127    /// How many milliseconds difference is allowed between the time received in response and current local time.
128    ///
129    /// See [SdkBuilder::with_time_tolerance] for more information.
130    metadata_time_tolerance_ms: Option<u64>,
131
132    /// Cancellation token; once cancelled, all pending requests should be aborted.
133    pub(crate) cancel_token: CancellationToken,
134
135    /// Global settings of dapi client
136    pub(crate) dapi_client_settings: RequestSettings,
137
138    #[cfg(feature = "mocks")]
139    dump_dir: Option<PathBuf>,
140}
141impl Clone for Sdk {
142    fn clone(&self) -> Self {
143        Self {
144            network: self.network,
145            inner: self.inner.clone(),
146            proofs: self.proofs,
147            nonce_cache: Arc::clone(&self.nonce_cache),
148            context_provider: ArcSwapOption::new(self.context_provider.load_full()),
149            cancel_token: self.cancel_token.clone(),
150            protocol_version: Arc::clone(&self.protocol_version),
151            auto_detect_protocol_version: self.auto_detect_protocol_version,
152            metadata_last_seen_height: Arc::clone(&self.metadata_last_seen_height),
153            metadata_height_tolerance: self.metadata_height_tolerance,
154            metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
155            dapi_client_settings: self.dapi_client_settings,
156            #[cfg(feature = "mocks")]
157            dump_dir: self.dump_dir.clone(),
158        }
159    }
160}
161
162impl Debug for Sdk {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match &self.inner {
165            SdkInstance::Dapi { dapi, .. } => f
166                .debug_struct("Sdk")
167                .field("dapi", dapi)
168                .field("proofs", &self.proofs)
169                .finish(),
170            #[cfg(feature = "mocks")]
171            SdkInstance::Mock { mock, .. } => f
172                .debug_struct("Sdk")
173                .field("mock", mock)
174                .field("proofs", &self.proofs)
175                .finish(),
176        }
177    }
178}
179
180/// Internal Sdk instance.
181///
182/// This is used to store the actual Sdk instance, which can be either a real Sdk or a mock Sdk.
183/// We use it to avoid exposing internals defined below to the public.
184#[derive(Debug, Clone)]
185enum SdkInstance {
186    /// Real Sdk, using DAPI with gRPC transport
187    Dapi {
188        /// DAPI client used to communicate with Dash Platform.
189        dapi: DapiClient,
190    },
191    /// Mock SDK
192    #[cfg(feature = "mocks")]
193    Mock {
194        /// Mock DAPI client used to communicate with Dash Platform.
195        ///
196        /// Dapi client is wrapped in a tokio [Mutex](tokio::sync::Mutex) as it's used in async context.
197        dapi: Arc<Mutex<MockDapiClient>>,
198        /// Mock SDK implementation processing mock expectations and responses.
199        mock: Arc<Mutex<MockDashPlatformSdk>>,
200        address_list: AddressList,
201    },
202}
203
204impl Sdk {
205    /// Initialize Dash Platform SDK in mock mode.
206    ///
207    /// This is a helper method that uses [`SdkBuilder`] to initialize the SDK in mock mode.
208    ///
209    /// See also [`SdkBuilder`].
210    pub fn new_mock() -> Self {
211        SdkBuilder::default()
212            .build()
213            .expect("mock should be created")
214    }
215
216    /// Return freshness criteria (height tolerance and time tolerance) for given request method.
217    ///
218    /// Note that if self.metadata_height_tolerance or self.metadata_time_tolerance_ms is None,
219    /// respective tolerance will be None regardless of method, to allow disabling staleness checks globally.
220    fn freshness_criteria(&self, method_name: &str) -> (Option<u64>, Option<u64>) {
221        match method_name {
222            "get_addresses_trunk_state"
223            | "get_addresses_branch_state"
224            | "get_nullifiers_trunk_state"
225            | "get_nullifiers_branch_state" => (
226                None,
227                self.metadata_time_tolerance_ms
228                    .and(Some(ADDRESS_STATE_TIME_TOLERANCE_MS)),
229            ),
230            _ => (
231                self.metadata_height_tolerance,
232                self.metadata_time_tolerance_ms,
233            ),
234        }
235    }
236
237    /// Verify response metadata against the current state of the SDK.
238    pub fn verify_response_metadata(
239        &self,
240        method_name: &str,
241        metadata: &ResponseMetadata,
242    ) -> Result<(), Error> {
243        let (metadata_height_tolerance, metadata_time_tolerance_ms) =
244            self.freshness_criteria(method_name);
245        if let Some(height_tolerance) = metadata_height_tolerance {
246            verify_metadata_height(
247                metadata,
248                height_tolerance,
249                Arc::clone(&(self.metadata_last_seen_height)),
250            )?;
251        };
252        if let Some(time_tolerance) = metadata_time_tolerance_ms {
253            let now = chrono::Utc::now().timestamp_millis() as u64;
254            verify_metadata_time(metadata, now, time_tolerance)?;
255        };
256
257        self.maybe_update_protocol_version(metadata.protocol_version);
258
259        Ok(())
260    }
261
262    /// Update the stored protocol version if `received_version` is newer and known.
263    ///
264    /// Uses `fetch_max` so the highest version always wins under concurrent updates.
265    /// The version is stored per-SDK instance (not in the process-wide global),
266    /// so multiple SDK instances can track different networks independently.
267    fn maybe_update_protocol_version(&self, received_version: u32) {
268        if !self.auto_detect_protocol_version {
269            return;
270        }
271
272        if received_version == 0 {
273            return;
274        }
275
276        let current = self.protocol_version.load(Ordering::Relaxed);
277
278        if received_version <= current {
279            return;
280        }
281
282        // Validate that we know this version before accepting it
283        if PlatformVersion::get(received_version).is_err() {
284            tracing::warn!(
285                received_version,
286                current_version = current,
287                "received unknown protocol version from network; keeping current"
288            );
289            return;
290        }
291
292        let previous = self
293            .protocol_version
294            .fetch_max(received_version, Ordering::Relaxed);
295        if previous < received_version {
296            tracing::info!(
297                old_version = previous,
298                new_version = received_version,
299                "protocol version updated from network metadata"
300            );
301        }
302    }
303
304    // TODO: Changed to public for tests
305    /// Retrieve object `O` from proof contained in `request` (of type `R`) and `response`.
306    ///
307    /// This method is used to retrieve objects from proofs returned by Dash Platform.
308    ///
309    /// ## Generic Parameters
310    ///
311    /// - `R`: Type of the request that was used to fetch the proof.
312    /// - `O`: Type of the object to be retrieved from the proof.
313    ///
314    /// ## Protocol version bootstrapping
315    ///
316    /// On a fresh auto-detect SDK (i.e. one built without [`SdkBuilder::with_version()`]), the
317    /// first call to this method uses [`PlatformVersion::latest()`] as a fallback because no
318    /// network response has been received yet to teach the SDK the real network version.
319    ///
320    /// The actual network version is learned only *after* proof parsing succeeds, when
321    /// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`.  If the
322    /// connected network runs an older protocol version **and** proof interpretation differs
323    /// between that version and `latest()`, the very first request may fail before the SDK can
324    /// correct itself.  Subsequent requests will use the correct version.
325    ///
326    /// This is a known bootstrap limitation.  Callers that must guarantee correct version
327    /// behaviour on the first request should pin the version explicitly via
328    /// [`SdkBuilder::with_version()`].
329    pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
330        &self,
331        request: O::Request,
332        response: O::Response,
333    ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
334    where
335        O::Request: Mockable + TransportRequest,
336    {
337        let provider = self
338            .context_provider()
339            .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
340        let method_name = request.method_name();
341
342        let (object, metadata, proof) = match self.inner {
343            SdkInstance::Dapi { .. } => O::maybe_from_proof_with_metadata(
344                request,
345                response,
346                self.network,
347                self.version(),
348                &provider,
349            ),
350            #[cfg(feature = "mocks")]
351            SdkInstance::Mock { ref mock, .. } => {
352                let guard = mock.lock().await;
353                guard.parse_proof_with_metadata(request, response)
354            }
355        }?;
356
357        self.verify_response_metadata(method_name, &metadata)
358            .inspect_err(|err| {
359                tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server");
360            })?;
361
362        Ok((object, metadata, proof))
363    }
364
365    /// Return [ContextProvider] used by the SDK.
366    pub fn context_provider(&self) -> Option<impl ContextProvider> {
367        let provider_guard = self.context_provider.load();
368        let provider = provider_guard.as_ref().map(Arc::clone);
369
370        provider
371    }
372
373    /// Returns a mutable reference to the `MockDashPlatformSdk` instance.
374    ///
375    /// Use returned object to configure mock responses with methods like `expect_fetch`.
376    ///
377    /// # Panics
378    ///
379    /// Panics when:
380    ///
381    /// * the `self` instance is not a `Mock` variant,
382    /// * the `self` instance is in use by another thread.
383    #[cfg(feature = "mocks")]
384    pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
385        if let Sdk {
386            inner: SdkInstance::Mock { ref mock, .. },
387            ..
388        } = self
389        {
390            mock.try_lock()
391                .expect("mock sdk is in use by another thread and cannot be reconfigured")
392        } else {
393            panic!("not a mock")
394        }
395    }
396
397    /// Get or fetch identity nonce, querying Platform when stale or absent.
398    /// Treats a missing nonce as `0` before applying the optional bump; on first
399    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
400    /// verify identity existence.
401    pub async fn get_identity_nonce(
402        &self,
403        identity_id: Identifier,
404        bump_first: bool,
405        settings: Option<PutSettings>,
406    ) -> Result<IdentityNonce, Error> {
407        let settings = settings.unwrap_or_default();
408        let nonce = self
409            .nonce_cache
410            .get_identity_nonce(self, identity_id, bump_first, &settings)
411            .await?;
412
413        tracing::trace!(
414            identity_id = %identity_id,
415            bump_first,
416            nonce,
417            "Fetched identity nonce"
418        );
419
420        Ok(nonce)
421    }
422
423    /// Get or fetch identity-contract nonce, querying Platform when stale or absent.
424    /// Treats a missing nonce as `0` before applying the optional bump; on first
425    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
426    /// verify identity or contract existence.
427    pub async fn get_identity_contract_nonce(
428        &self,
429        identity_id: Identifier,
430        contract_id: Identifier,
431        bump_first: bool,
432        settings: Option<PutSettings>,
433    ) -> Result<IdentityNonce, Error> {
434        let settings = settings.unwrap_or_default();
435        self.nonce_cache
436            .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
437            .await
438    }
439
440    /// Marks identity nonce cache entries as stale so they are re-fetched from
441    /// Platform on the next call to [`get_identity_nonce`] or
442    /// [`get_identity_contract_nonce`].
443    pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
444        self.nonce_cache.refresh(identity_id).await;
445    }
446
447    /// Return [Dash Platform version](PlatformVersion) information used by this SDK.
448    ///
449    /// When auto-detection is enabled (default), returns [`PlatformVersion::latest()`]
450    /// until the first network response is received, then tracks the network's version.
451    /// When pinned via [`SdkBuilder::with_version()`], always returns the pinned version.
452    pub fn version<'v>(&self) -> &'v PlatformVersion {
453        let v = self.protocol_version.load(Ordering::Relaxed);
454        PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
455    }
456
457    /// Return the raw protocol version number currently used by this SDK.
458    pub fn protocol_version_number(&self) -> u32 {
459        self.protocol_version.load(Ordering::Relaxed)
460    }
461
462    // TODO: Move to settings
463    /// Indicate if the sdk should request and verify proofs.
464    pub fn prove(&self) -> bool {
465        self.proofs
466    }
467
468    // TODO: If we remove this setter we don't need to use ArcSwap.
469    //   It's good enough to set Context once when you initialize the SDK.
470    /// Set the [ContextProvider] to use.
471    ///
472    /// [ContextProvider] is used to access state information, like data contracts and quorum public keys.
473    ///
474    /// Note that this will overwrite any previous context provider.
475    pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
476        self.context_provider
477            .swap(Some(Arc::new(Box::new(context_provider))));
478    }
479
480    /// Returns a future that resolves when the Sdk is cancelled (e.g. shutdown was requested).
481    pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
482        self.cancel_token.cancelled()
483    }
484
485    /// Request shutdown of the Sdk and all related operations.
486    pub fn shutdown(&self) {
487        self.cancel_token.cancel();
488    }
489
490    /// Return the [DapiClient] address list
491    pub fn address_list(&self) -> &AddressList {
492        match &self.inner {
493            SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
494            #[cfg(feature = "mocks")]
495            SdkInstance::Mock { address_list, .. } => address_list,
496        }
497    }
498}
499
500/// If received metadata time differs from local time by more than `tolerance`, the remote node is considered stale.
501///
502/// ## Parameters
503///
504/// - `metadata`: Metadata of the received response
505/// - `now_ms`: Current local time in milliseconds
506/// - `tolerance_ms`: Tolerance in milliseconds
507pub(crate) fn verify_metadata_time(
508    metadata: &ResponseMetadata,
509    now_ms: u64,
510    tolerance_ms: u64,
511) -> Result<(), Error> {
512    let metadata_time = metadata.time_ms;
513
514    // metadata_time - tolerance_ms <= now_ms <= metadata_time + tolerance_ms
515    if now_ms.abs_diff(metadata_time) > tolerance_ms {
516        return Err(StaleNodeError::Time {
517            expected_timestamp_ms: now_ms,
518            received_timestamp_ms: metadata_time,
519            tolerance_ms,
520        }
521        .into());
522    }
523
524    tracing::trace!(
525        expected_time = now_ms,
526        received_time = metadata_time,
527        tolerance_ms,
528        "received response with valid time"
529    );
530    Ok(())
531}
532
533/// If current metadata height is behind previously seen height by more than `tolerance`, the remote node
534///  is considered stale.
535fn verify_metadata_height(
536    metadata: &ResponseMetadata,
537    tolerance: u64,
538    last_seen_height: Arc<atomic::AtomicU64>,
539) -> Result<(), Error> {
540    let mut expected_height = last_seen_height.load(Ordering::Relaxed);
541    let received_height = metadata.height;
542
543    // Same height, no need to update.
544    if received_height == expected_height {
545        tracing::trace!(
546            expected_height,
547            received_height,
548            tolerance,
549            "received message has the same height as previously seen"
550        );
551        return Ok(());
552    }
553
554    // If expected_height <= tolerance, then Sdk just started, so we just assume what we got is correct.
555    if expected_height > tolerance && received_height < expected_height - tolerance {
556        return Err(StaleNodeError::Height {
557            expected_height,
558            received_height,
559            tolerance_blocks: tolerance,
560        }
561        .into());
562    }
563
564    // New height is ahead of the last seen height, so we update the last seen height.
565    tracing::trace!(
566        expected_height = expected_height,
567        received_height = received_height,
568        tolerance,
569        "received message with new height"
570    );
571    while let Err(stored_height) = last_seen_height.compare_exchange(
572        expected_height,
573        received_height,
574        Ordering::SeqCst,
575        Ordering::Relaxed,
576    ) {
577        // The value was changed to a higher value by another thread, so we need to retry.
578        if stored_height >= metadata.height {
579            break;
580        }
581        expected_height = stored_height;
582    }
583
584    Ok(())
585}
586
587#[async_trait::async_trait]
588impl DapiRequestExecutor for Sdk {
589    async fn execute<R: TransportRequest>(
590        &self,
591        request: R,
592        settings: RequestSettings,
593    ) -> ExecutionResult<R::Response, DapiClientError> {
594        match self.inner {
595            SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
596            #[cfg(feature = "mocks")]
597            SdkInstance::Mock { ref dapi, .. } => {
598                let dapi_guard = dapi.lock().await;
599                dapi_guard.execute(request, settings).await
600            }
601        }
602    }
603}
604
605/// Dash Platform SDK Builder, used to configure and [`SdkBuilder::build()`] the [Sdk].
606///
607/// [SdkBuilder] implements a "builder" design pattern to allow configuration of the Sdk before it is instantiated.
608/// It allows creation of Sdk in two modes:
609/// - `Normal`: Connects to a remote Dash Platform node.
610/// - `Mock`: Uses a mock implementation of Dash Platform.
611///
612/// Mandatory steps of initialization in normal mode are:
613///
614/// 1. Create an instance of [SdkBuilder] with [`SdkBuilder::new()`]
615/// 2. Configure the builder with [`SdkBuilder::with_core()`]
616/// 3. Call [`SdkBuilder::build()`] to create the [Sdk] instance.
617pub struct SdkBuilder {
618    /// List of addresses to connect to.
619    ///
620    /// If `None`, a mock client will be created.
621    addresses: Option<AddressList>,
622    settings: Option<RequestSettings>,
623
624    network: Network,
625
626    core_ip: String,
627    core_port: u16,
628    core_user: String,
629    core_password: Zeroizing<String>,
630
631    /// If true, request and verify proofs of the responses.
632    proofs: bool,
633
634    /// Platform version to use in this Sdk
635    version: &'static PlatformVersion,
636
637    /// Whether the user explicitly called `with_version()`.
638    /// When true, auto-detection of protocol version from network metadata is disabled.
639    version_explicit: bool,
640
641    /// Cache size for data contracts. Used by mock [GrpcContextProvider].
642    #[cfg(feature = "mocks")]
643    data_contract_cache_size: NonZeroUsize,
644
645    /// Cache size for token configs. Used by mock [GrpcContextProvider].
646    #[cfg(feature = "mocks")]
647    token_config_cache_size: NonZeroUsize,
648
649    /// Cache size for quorum public keys. Used by mock [GrpcContextProvider].
650    #[cfg(feature = "mocks")]
651    quorum_public_keys_cache_size: NonZeroUsize,
652
653    /// Context provider used by the SDK.
654    context_provider: Option<Box<dyn ContextProvider>>,
655
656    /// How many blocks difference is allowed between the last seen metadata height and the height received in response
657    /// metadata.
658    ///
659    /// See [SdkBuilder::with_height_tolerance] for more information.
660    metadata_height_tolerance: Option<u64>,
661
662    /// How many milliseconds difference is allowed between the time received in response metadata and current local time.
663    ///
664    /// See [SdkBuilder::with_time_tolerance] for more information.
665    metadata_time_tolerance_ms: Option<u64>,
666
667    /// directory where dump files will be stored
668    #[cfg(feature = "mocks")]
669    dump_dir: Option<PathBuf>,
670
671    /// Cancellation token; once cancelled, all pending requests should be aborted.
672    pub(crate) cancel_token: CancellationToken,
673
674    /// CA certificate to use for TLS connections.
675    #[cfg(not(target_arch = "wasm32"))]
676    ca_certificate: Option<Certificate>,
677}
678
679impl Default for SdkBuilder {
680    /// Create default SdkBuilder that will create a mock client.
681    fn default() -> Self {
682        Self {
683            addresses: None,
684            settings: None,
685            network: Network::Mainnet,
686            core_ip: "".to_string(),
687            core_port: 0,
688            core_password: "".to_string().into(),
689            core_user: "".to_string(),
690
691            proofs: true,
692            metadata_height_tolerance: Some(1),
693            metadata_time_tolerance_ms: None,
694
695            #[cfg(feature = "mocks")]
696            data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
697                .expect("data contract cache size must be positive"),
698
699            #[cfg(feature = "mocks")]
700            token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
701                .expect("token config cache size must be positive"),
702
703            #[cfg(feature = "mocks")]
704            quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
705                .expect("quorum public keys cache size must be positive"),
706
707            context_provider: None,
708
709            cancel_token: CancellationToken::new(),
710
711            version: PlatformVersion::latest(),
712            version_explicit: false,
713            #[cfg(not(target_arch = "wasm32"))]
714            ca_certificate: None,
715
716            #[cfg(feature = "mocks")]
717            dump_dir: None,
718        }
719    }
720}
721
722impl SdkBuilder {
723    /// Enable or disable proofs on requests.
724    ///
725    /// In mock/offline testing with recorded vectors, set to false to match dumps
726    /// that were captured without proofs.
727    pub fn with_proofs(mut self, proofs: bool) -> Self {
728        self.proofs = proofs;
729        self
730    }
731    /// Create a new SdkBuilder with provided address list.
732    pub fn new(addresses: AddressList) -> Self {
733        Self {
734            addresses: Some(addresses),
735            ..Default::default()
736        }
737    }
738
739    /// Replace the address list on this builder.
740    pub fn with_address_list(mut self, addresses: AddressList) -> Self {
741        self.addresses = Some(addresses);
742        self
743    }
744
745    /// Create a new SdkBuilder that will generate mock client.
746    pub fn new_mock() -> Self {
747        Self::default()
748    }
749
750    /// Create a new SdkBuilder instance preconfigured for testnet. NOT IMPLEMENTED YET.
751    ///
752    /// This is a helper method that preconfigures [SdkBuilder] for testnet use.
753    /// Use this method if you want to connect to Dash Platform testnet during development and testing
754    /// of your solution.
755    pub fn new_testnet() -> Self {
756        unimplemented!(
757            "Testnet address list not implemented yet. Use new() and provide address list."
758        )
759    }
760
761    /// Create a new SdkBuilder instance preconfigured for mainnet (production network). NOT IMPLEMENTED YET.
762    ///
763    /// This is a helper method that preconfigures [SdkBuilder] for production use.
764    /// Use this method if you want to connect to Dash Platform mainnet with production-ready product.
765    ///
766    /// ## Panics
767    ///
768    /// This method panics if the mainnet configuration cannot be loaded.
769    ///
770    /// ## Unstable
771    ///
772    /// This method is unstable and can be changed in the future.
773    pub fn new_mainnet() -> Self {
774        unimplemented!(
775            "Mainnet address list not implemented yet. Use new() and provide address list."
776        )
777    }
778
779    /// Configure network type.
780    ///
781    /// Defaults to Network::Mainnet which is mainnet.
782    pub fn with_network(mut self, network: Network) -> Self {
783        self.network = network;
784        self
785    }
786
787    /// Configure CA certificate to use when verifying TLS connections.
788    ///
789    /// Used mainly for testing purposes and local networks.
790    ///
791    /// If not set, uses standard system CA certificates.
792    ///
793    /// ## Parameters
794    ///
795    /// - `pem_certificate`: PEM-encoded CA certificate. User must ensure that the certificate is valid.
796    #[cfg(not(target_arch = "wasm32"))]
797    pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
798        self.ca_certificate = Some(pem_certificate);
799        self
800    }
801
802    /// Load CA certificate from a PEM-encoded file.
803    ///
804    /// This is a convenience method that reads the certificate from a file and sets it using
805    /// [SdkBuilder::with_ca_certificate()].
806    #[cfg(not(target_arch = "wasm32"))]
807    pub fn with_ca_certificate_file(
808        self,
809        certificate_file_path: impl AsRef<Path>,
810    ) -> std::io::Result<Self> {
811        let pem = std::fs::read(certificate_file_path)?;
812        let cert = Certificate::from_pem(pem);
813
814        Ok(self.with_ca_certificate(cert))
815    }
816
817    /// Configure request settings.
818    ///
819    /// Tune request settings used to connect to the Dash Platform.
820    ///
821    /// Defaults to [`DEFAULT_REQUEST_SETTINGS`], which sets retries to 3.
822    ///
823    /// See [`RequestSettings`] for more information.
824    pub fn with_settings(mut self, settings: RequestSettings) -> Self {
825        self.settings = Some(settings);
826        self
827    }
828
829    /// Configure platform version.
830    ///
831    /// Select specific version of Dash Platform to use.
832    ///
833    /// Defaults to [PlatformVersion::latest()].
834    pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
835        self.version = version;
836        self.version_explicit = true;
837        self
838    }
839
840    /// Configure context provider to use.
841    ///
842    /// Context provider is used to retrieve data contracts and quorum public keys from application state.
843    /// It should be implemented by the user of this SDK to provide stateful information about the application.
844    ///
845    /// See [ContextProvider] for more information and [GrpcContextProvider] for an example implementation.
846    pub fn with_context_provider<C: ContextProvider + 'static>(
847        mut self,
848        context_provider: C,
849    ) -> Self {
850        self.context_provider = Some(Box::new(context_provider));
851
852        self
853    }
854
855    /// Set cancellation token that will be used by the Sdk.
856    ///
857    /// Once that cancellation token is cancelled, all pending requests shall terminate.
858    pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
859        self.cancel_token = cancel_token;
860        self
861    }
862
863    /// Use Dash Core as a wallet and context provider.
864    ///
865    /// This is a convenience method that configures the SDK to use Dash Core as a wallet and context provider.
866    ///
867    /// For more control over the configuration, use [`SdkBuilder::with_context_provider()`].
868    ///
869    /// This is temporary implementation, intended for development purposes.
870    pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
871        self.core_ip = ip.to_string();
872        self.core_port = port;
873        self.core_user = user.to_string();
874        self.core_password = Zeroizing::from(password.to_string());
875
876        self
877    }
878
879    /// Change number of blocks difference allowed between the last height and the height received in current response.
880    ///
881    /// If height received in response metadata is behind previously seen height by more than this value, the node
882    /// is considered stale, and the request will fail.
883    ///
884    /// If None, the height is not checked.
885    ///
886    /// Note that this feature doesn't guarantee that you are getting latest data, but it significantly decreases
887    /// probability of getting old data.
888    ///
889    /// This is set to `1` by default.
890    pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
891        self.metadata_height_tolerance = tolerance;
892        self
893    }
894
895    /// How many milliseconds difference is allowed between the time received in response and current local time.
896    /// If the received time differs from local time by more than this value, the remote node is stale.
897    ///
898    /// If None, the time is not checked.
899    ///
900    /// This is set to `None` by default.
901    ///
902    /// Note that enabling this check can cause issues if the local time is not synchronized with the network time,
903    /// when the network is stalled or time between blocks increases significantly.
904    ///
905    /// Selecting a safe value for this parameter depends on maximum time between blocks mined on the network.
906    /// For example, if the network is configured to mine a block every maximum 3 minutes, setting this value
907    /// to a bit more than 6 minutes (to account for misbehaving proposers, network delays and local time
908    /// synchronization issues) should be safe.
909    pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
910        self.metadata_time_tolerance_ms = tolerance_ms;
911        self
912    }
913
914    /// Configure directory where dumps of all requests and responses will be saved.
915    /// Useful for debugging.
916    ///
917    /// This function will create the directory if it does not exist and save dumps of
918    /// * all requests and responses - in files named `msg-*.json`
919    /// * retrieved quorum public keys - in files named `quorum_pubkey-*.json`
920    /// * retrieved data contracts - in files named `data_contract-*.json`
921    ///
922    /// These files can be used together with [MockDashPlatformSdk] to replay the requests and responses.
923    /// See [MockDashPlatformSdk::load_expectations_sync()] for more information.
924    ///
925    /// Available only when `mocks` feature is enabled.
926    #[cfg(feature = "mocks")]
927    pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
928        self.dump_dir = Some(dump_dir.to_path_buf());
929        self
930    }
931
932    /// Build the Sdk instance.
933    ///
934    /// This method will create the Sdk instance based on the configuration provided to the builder.
935    ///
936    /// # Errors
937    ///
938    /// This method will return an error if the Sdk cannot be created.
939    pub fn build(self) -> Result<Sdk, Error> {
940        let dapi_client_settings = match self.settings {
941            Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
942            None => DEFAULT_REQUEST_SETTINGS,
943        };
944
945        let sdk= match self.addresses {
946            // non-mock mode
947            Some(addresses) => {
948                #[allow(unused_mut)] // needs to be mutable for features other than wasm
949                let mut dapi = DapiClient::new(addresses, dapi_client_settings);
950                #[cfg(not(target_arch = "wasm32"))]
951                if let Some(pem) = self.ca_certificate {
952                    dapi = dapi.with_ca_certificate(pem);
953                }
954
955                #[cfg(feature = "mocks")]
956                let dapi = dapi.dump_dir(self.dump_dir.clone());
957
958                #[allow(unused_mut)] // needs to be mutable for #[cfg(feature = "mocks")]
959                let mut sdk= Sdk{
960                    network: self.network,
961                    dapi_client_settings,
962                    inner:SdkInstance::Dapi { dapi },
963                    proofs:self.proofs,
964                    context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
965                    cancel_token: self.cancel_token,
966                    nonce_cache: Default::default(),
967                    // When auto-detecting, seed with 0 (uninitialized) so the first
968                    // network response sets the actual version — even if it's lower
969                    // than the binary's latest. When pinned, use the explicit version.
970                    protocol_version: Arc::new(atomic::AtomicU32::new(
971                        if self.version_explicit { self.version.protocol_version } else { 0 },
972                    )),
973                    auto_detect_protocol_version: !self.version_explicit,
974                    // Note: in the future, we need to securely initialize initial height during Sdk bootstrap or first request.
975                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(0)),
976                    metadata_height_tolerance: self.metadata_height_tolerance,
977                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
978                    #[cfg(feature = "mocks")]
979                    dump_dir: self.dump_dir,
980                };
981                // if context provider is not set correctly (is None), it means we need to fall back to core wallet
982                if  sdk.context_provider.load().is_none() {
983                    #[cfg(feature = "mocks")]
984                    if !self.core_ip.is_empty() {
985                        tracing::warn!(
986                            "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
987                        let mut context_provider = GrpcContextProvider::new(None,
988                            &self.core_ip, self.core_port, &self.core_user, &self.core_password,
989                            self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
990                        #[cfg(feature = "mocks")]
991                        if sdk.dump_dir.is_some() {
992                            context_provider.set_dump_dir(sdk.dump_dir.clone());
993                        }
994                        // We have cyclical dependency Sdk <-> GrpcContextProvider, so we just do some
995                        // workaround using additional Arc.
996                        let context_provider= Arc::new(context_provider);
997                        sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
998                        context_provider.set_sdk(Some(sdk.clone()));
999                    } else{
1000                        return Err(Error::Config(concat!(
1001                            "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1002                            "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1003                            .to_string()));
1004                    }
1005                    #[cfg(not(feature = "mocks"))]
1006                    return Err(Error::Config(concat!(
1007                        "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1008                        "or enable `mocks` feature to use mock context provider")
1009                        .to_string()));
1010                };
1011
1012                sdk
1013            },
1014            #[cfg(feature = "mocks")]
1015            // mock mode
1016            None => {
1017                let dapi =Arc::new(Mutex::new(  MockDapiClient::new()));
1018                // We create mock context provider that will use the mock DAPI client to retrieve data contracts.
1019                let  context_provider = self.context_provider.unwrap_or_else(||{
1020                    let mut cp=MockContextProvider::new();
1021                    if let Some(ref dump_dir) = self.dump_dir {
1022                        cp.quorum_keys_dir(Some(dump_dir.clone()));
1023                    }
1024                    Box::new(cp)
1025                }
1026                );
1027                let mock_sdk = MockDashPlatformSdk::new(self.version, Arc::clone(&dapi));
1028                let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1029                let sdk= Sdk {
1030                    network: self.network,
1031                    dapi_client_settings,
1032                    inner:SdkInstance::Mock {
1033                        mock:mock_sdk.clone(),
1034                        dapi,
1035                        address_list: AddressList::new(),
1036                    },
1037                    dump_dir: self.dump_dir.clone(),
1038                    proofs:self.proofs,
1039                    nonce_cache: Default::default(),
1040                    protocol_version: Arc::new(atomic::AtomicU32::new(
1041                        if self.version_explicit { self.version.protocol_version } else { 0 },
1042                    )),
1043                    auto_detect_protocol_version: !self.version_explicit,
1044                    context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1045                    cancel_token: self.cancel_token,
1046                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(0)),
1047                    metadata_height_tolerance: self.metadata_height_tolerance,
1048                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1049                };
1050                let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1051                guard.set_sdk(sdk.clone());
1052                if let Some(ref dump_dir) = self.dump_dir {
1053                    guard.load_expectations_sync(dump_dir)?;
1054                };
1055
1056                sdk
1057            },
1058            #[cfg(not(feature = "mocks"))]
1059            None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1060        };
1061
1062        Ok(sdk)
1063    }
1064}
1065
1066pub fn prettify_proof(proof: &Proof) -> String {
1067    let config = bincode::config::standard()
1068        .with_big_endian()
1069        .with_no_limit();
1070    let grovedb_proof: Result<GroveDBProof, DecodeError> =
1071        bincode::decode_from_slice(&proof.grovedb_proof, config).map(|(a, _)| a);
1072
1073    let grovedb_proof_string = match grovedb_proof {
1074        Ok(proof) => format!("{}", proof),
1075        Err(_) => "Invalid GroveDBProof".to_string(),
1076    };
1077    format!(
1078        "Proof {{
1079            grovedb_proof: {},
1080            quorum_hash: 0x{},
1081            signature: 0x{},
1082            round: {},
1083            block_id_hash: 0x{},
1084            quorum_type: {},
1085        }}",
1086        grovedb_proof_string,
1087        hex::encode(&proof.quorum_hash),
1088        hex::encode(&proof.signature),
1089        proof.round,
1090        hex::encode(&proof.block_id_hash),
1091        proof.quorum_type,
1092    )
1093}
1094
1095#[cfg(test)]
1096mod test {
1097    use std::sync::Arc;
1098
1099    use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1100    use rs_dapi_client::transport::TransportRequest;
1101    use test_case::test_matrix;
1102
1103    use crate::SdkBuilder;
1104
1105    #[test_matrix(97..102, 100, 2, false; "valid height")]
1106    #[test_case(103, 100, 2, true; "invalid height")]
1107    fn test_verify_metadata_height(
1108        expected_height: u64,
1109        received_height: u64,
1110        tolerance: u64,
1111        expect_err: bool,
1112    ) {
1113        let metadata = ResponseMetadata {
1114            height: received_height,
1115            ..Default::default()
1116        };
1117
1118        let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
1119
1120        let result =
1121            super::verify_metadata_height(&metadata, tolerance, Arc::clone(&last_seen_height));
1122
1123        assert_eq!(result.is_err(), expect_err);
1124        if result.is_ok() {
1125            assert_eq!(
1126                last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
1127                received_height,
1128                "previous height should be updated"
1129            );
1130        }
1131    }
1132
1133    #[test]
1134    fn cloned_sdk_verify_metadata_height() {
1135        let sdk1 = SdkBuilder::new_mock()
1136            .build()
1137            .expect("mock Sdk should be created");
1138
1139        // First message verified, height 1.
1140        let metadata = ResponseMetadata {
1141            height: 1,
1142            ..Default::default()
1143        };
1144
1145        // use dummy request type to satisfy generic parameter
1146        let request = GetIdentityRequest::default();
1147        sdk1.verify_response_metadata(request.method_name(), &metadata)
1148            .expect("metadata should be valid");
1149
1150        assert_eq!(
1151            sdk1.metadata_last_seen_height
1152                .load(std::sync::atomic::Ordering::Relaxed),
1153            metadata.height,
1154            "initial height"
1155        );
1156
1157        // now, we clone sdk and do two requests.
1158        let sdk2 = sdk1.clone();
1159        let sdk3 = sdk1.clone();
1160
1161        // Second message verified, height 2.
1162        let metadata = ResponseMetadata {
1163            height: 2,
1164            ..Default::default()
1165        };
1166        // use dummy request type to satisfy generic parameter
1167        let request = GetIdentityRequest::default();
1168        sdk2.verify_response_metadata(request.method_name(), &metadata)
1169            .expect("metadata should be valid");
1170
1171        assert_eq!(
1172            sdk1.metadata_last_seen_height
1173                .load(std::sync::atomic::Ordering::Relaxed),
1174            metadata.height,
1175            "first sdk should see height from second sdk"
1176        );
1177        assert_eq!(
1178            sdk3.metadata_last_seen_height
1179                .load(std::sync::atomic::Ordering::Relaxed),
1180            metadata.height,
1181            "third sdk should see height from second sdk"
1182        );
1183
1184        // Third message verified, height 3.
1185        let metadata = ResponseMetadata {
1186            height: 3,
1187            ..Default::default()
1188        };
1189        // use dummy request type to satisfy generic parameter
1190        let request = GetIdentityRequest::default();
1191        sdk3.verify_response_metadata(request.method_name(), &metadata)
1192            .expect("metadata should be valid");
1193
1194        assert_eq!(
1195            sdk1.metadata_last_seen_height
1196                .load(std::sync::atomic::Ordering::Relaxed),
1197            metadata.height,
1198            "first sdk should see height from third sdk"
1199        );
1200
1201        assert_eq!(
1202            sdk2.metadata_last_seen_height
1203                .load(std::sync::atomic::Ordering::Relaxed),
1204            metadata.height,
1205            "second sdk should see height from third sdk"
1206        );
1207
1208        // Now, using sdk1 for height 1 again should fail, as we are already at 3, with default tolerance 1.
1209        let metadata = ResponseMetadata {
1210            height: 1,
1211            ..Default::default()
1212        };
1213
1214        let request = GetIdentityRequest::default();
1215        sdk1.verify_response_metadata(request.method_name(), &metadata)
1216            .expect_err("metadata should be invalid");
1217    }
1218
1219    /// Helper: build a mock SDK with auto-detect enabled and a specific starting version.
1220    /// Does NOT call `with_version()` (which would disable auto-detect).
1221    fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
1222        use std::sync::atomic::Ordering;
1223
1224        let sdk = SdkBuilder::new_mock()
1225            .build()
1226            .expect("mock Sdk should be created");
1227        sdk.protocol_version
1228            .store(starting_version, Ordering::Relaxed);
1229        sdk
1230    }
1231
1232    #[test]
1233    fn test_version_update_from_metadata() {
1234        let sdk = mock_sdk_with_auto_detect(1);
1235
1236        assert_eq!(sdk.protocol_version_number(), 1);
1237
1238        let metadata = ResponseMetadata {
1239            protocol_version: 2,
1240            height: 1,
1241            ..Default::default()
1242        };
1243
1244        sdk.verify_response_metadata("test", &metadata)
1245            .expect("metadata should be valid");
1246
1247        assert_eq!(sdk.protocol_version_number(), 2);
1248        assert_eq!(sdk.version().protocol_version, 2);
1249    }
1250
1251    #[test]
1252    fn test_unknown_version_ignored() {
1253        use dpp::version::PlatformVersion;
1254
1255        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1256        let original_version = sdk.protocol_version_number();
1257
1258        let metadata = ResponseMetadata {
1259            protocol_version: 999,
1260            height: 1,
1261            ..Default::default()
1262        };
1263
1264        sdk.verify_response_metadata("test", &metadata)
1265            .expect("metadata should be valid");
1266
1267        assert_eq!(sdk.protocol_version_number(), original_version);
1268        assert_eq!(sdk.version().protocol_version, original_version);
1269    }
1270
1271    #[test]
1272    fn test_version_shared_between_clones() {
1273        let sdk = mock_sdk_with_auto_detect(1);
1274
1275        let clone = sdk.clone();
1276
1277        let metadata = ResponseMetadata {
1278            protocol_version: 2,
1279            height: 1,
1280            ..Default::default()
1281        };
1282
1283        clone
1284            .verify_response_metadata("test", &metadata)
1285            .expect("metadata should be valid");
1286
1287        assert_eq!(
1288            sdk.protocol_version_number(),
1289            2,
1290            "original should see update from clone"
1291        );
1292    }
1293
1294    #[test]
1295    fn test_version_downgrade_ignored() {
1296        let sdk = mock_sdk_with_auto_detect(2);
1297
1298        assert_eq!(sdk.protocol_version_number(), 2);
1299
1300        let metadata = ResponseMetadata {
1301            protocol_version: 1,
1302            height: 1,
1303            ..Default::default()
1304        };
1305
1306        sdk.verify_response_metadata("test", &metadata)
1307            .expect("metadata should be valid");
1308
1309        assert_eq!(sdk.protocol_version_number(), 2);
1310    }
1311
1312    #[test]
1313    fn test_version_zero_ignored() {
1314        use dpp::version::PlatformVersion;
1315
1316        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1317        let original_version = sdk.protocol_version_number();
1318
1319        let metadata = ResponseMetadata {
1320            protocol_version: 0,
1321            height: 1,
1322            ..Default::default()
1323        };
1324
1325        sdk.verify_response_metadata("test", &metadata)
1326            .expect("metadata should be valid");
1327
1328        assert_eq!(sdk.protocol_version_number(), original_version);
1329    }
1330
1331    #[test]
1332    fn test_concurrent_updates_converge_to_highest() {
1333        use std::thread;
1334
1335        let sdk = mock_sdk_with_auto_detect(1);
1336
1337        assert_eq!(sdk.protocol_version_number(), 1);
1338
1339        let mut handles = Vec::new();
1340        // Spawn threads that race to update to version 2 and version 3
1341        for version in [2u32, 3, 2, 3, 2, 3] {
1342            let sdk_clone = sdk.clone();
1343            handles.push(thread::spawn(move || {
1344                let metadata = ResponseMetadata {
1345                    protocol_version: version,
1346                    height: 1,
1347                    ..Default::default()
1348                };
1349                sdk_clone
1350                    .verify_response_metadata("test", &metadata)
1351                    .expect("metadata should be valid");
1352            }));
1353        }
1354
1355        for h in handles {
1356            h.join().expect("thread should not panic");
1357        }
1358
1359        // Highest known version (3) must win regardless of thread ordering
1360        assert_eq!(
1361            sdk.protocol_version_number(),
1362            3,
1363            "concurrent updates must converge to highest version"
1364        );
1365    }
1366
1367    // TC-7 (global DPP version sync) removed — set_current() is no longer called
1368    // from the SDK. Version is stored per-instance, not in the process-wide global.
1369
1370    #[test]
1371    fn test_explicit_version_disables_auto_detect() {
1372        use dpp::version::PlatformVersion;
1373
1374        // Explicitly pin to version 1 via with_version()
1375        let sdk = SdkBuilder::new_mock()
1376            .with_version(PlatformVersion::get(1).unwrap())
1377            .build()
1378            .expect("mock Sdk should be created");
1379
1380        assert_eq!(sdk.protocol_version_number(), 1);
1381        assert!(!sdk.auto_detect_protocol_version);
1382
1383        // Network reports version 2 — should be ignored because version is pinned
1384        let metadata = ResponseMetadata {
1385            protocol_version: 2,
1386            height: 1,
1387            ..Default::default()
1388        };
1389
1390        sdk.verify_response_metadata("test", &metadata)
1391            .expect("metadata should be valid");
1392
1393        assert_eq!(
1394            sdk.protocol_version_number(),
1395            1,
1396            "pinned version must not be auto-updated"
1397        );
1398    }
1399
1400    #[test]
1401    fn test_default_sdk_detects_older_network_version() {
1402        use dpp::version::PlatformVersion;
1403
1404        // Default SDK: auto-detect enabled, seeded at 0 (uninitialized)
1405        let sdk = SdkBuilder::new_mock()
1406            .build()
1407            .expect("mock Sdk should be created");
1408
1409        // Before any network response, version() falls back to latest()
1410        assert_eq!(
1411            sdk.version().protocol_version,
1412            PlatformVersion::latest().protocol_version,
1413            "before first response, should fall back to latest"
1414        );
1415        assert_eq!(sdk.protocol_version_number(), 0, "should be uninitialized");
1416
1417        // Network reports version 1 (older than latest) — should be accepted
1418        let metadata = ResponseMetadata {
1419            protocol_version: 1,
1420            height: 1,
1421            ..Default::default()
1422        };
1423
1424        sdk.verify_response_metadata("test", &metadata)
1425            .expect("metadata should be valid");
1426
1427        assert_eq!(
1428            sdk.protocol_version_number(),
1429            1,
1430            "default SDK must detect older network version"
1431        );
1432        assert_eq!(sdk.version().protocol_version, 1);
1433    }
1434
1435    #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
1436    #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
1437    #[test_matrix([0,100], [0,100], 100, false; "zero time")]
1438    #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
1439    fn test_verify_metadata_time(
1440        received_time: u64,
1441        now_time: u64,
1442        tolerance: u64,
1443        expect_err: bool,
1444    ) {
1445        let metadata = ResponseMetadata {
1446            time_ms: received_time,
1447            ..Default::default()
1448        };
1449
1450        let result = super::verify_metadata_time(&metadata, now_time, tolerance);
1451
1452        assert_eq!(result.is_err(), expect_err);
1453    }
1454}