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