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;
28use 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                old_version = previous,
334                new_version = received_version,
335                "protocol version updated from network metadata"
336            );
337        }
338    }
339
340    // TODO: Changed to public for tests
341    /// Retrieve object `O` from proof contained in `request` (of type `R`) and `response`.
342    ///
343    /// This method is used to retrieve objects from proofs returned by Dash Platform.
344    ///
345    /// ## Generic Parameters
346    ///
347    /// - `R`: Type of the request that was used to fetch the proof.
348    /// - `O`: Type of the object to be retrieved from the proof.
349    ///
350    /// ## Protocol version bootstrapping
351    ///
352    /// On a fresh auto-detect SDK (i.e. one built without [`SdkBuilder::with_version()`]), the
353    /// first call to this method uses [`PlatformVersion::latest()`] as a fallback because no
354    /// network response has been received yet to teach the SDK the real network version.
355    ///
356    /// The actual network version is learned only *after* proof parsing succeeds, when
357    /// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`.  If the
358    /// connected network runs an older protocol version **and** proof interpretation differs
359    /// between that version and `latest()`, the very first request may fail before the SDK can
360    /// correct itself.  Subsequent requests will use the correct version.
361    ///
362    /// This is a known bootstrap limitation.  Callers that must guarantee correct version
363    /// behaviour on the first request should pin the version explicitly via
364    /// [`SdkBuilder::with_version()`].
365    pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
366        &self,
367        request: O::Request,
368        response: O::Response,
369    ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
370    where
371        O::Request: Mockable + TransportRequest,
372    {
373        let provider = self
374            .context_provider()
375            .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
376        let method_name = request.method_name();
377
378        let (object, metadata, proof) = match self.inner {
379            SdkInstance::Dapi { .. } => O::maybe_from_proof_with_metadata(
380                request,
381                response,
382                self.network,
383                self.version(),
384                &provider,
385            ),
386            #[cfg(feature = "mocks")]
387            SdkInstance::Mock { ref mock, .. } => {
388                let guard = mock.lock().await;
389                guard.parse_proof_with_metadata(request, response)
390            }
391        }?;
392
393        self.verify_response_metadata(method_name, &metadata)
394            .inspect_err(|err| {
395                tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server");
396            })?;
397
398        Ok((object, metadata, proof))
399    }
400
401    /// Return [ContextProvider] used by the SDK.
402    pub fn context_provider(&self) -> Option<impl ContextProvider> {
403        let provider_guard = self.context_provider.load();
404        let provider = provider_guard.as_ref().map(Arc::clone);
405
406        provider
407    }
408
409    /// Returns a mutable reference to the `MockDashPlatformSdk` instance.
410    ///
411    /// Use returned object to configure mock responses with methods like `expect_fetch`.
412    ///
413    /// # Panics
414    ///
415    /// Panics when:
416    ///
417    /// * the `self` instance is not a `Mock` variant,
418    /// * the `self` instance is in use by another thread.
419    #[cfg(feature = "mocks")]
420    pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
421        if let Sdk {
422            inner: SdkInstance::Mock { ref mock, .. },
423            ..
424        } = self
425        {
426            mock.try_lock()
427                .expect("mock sdk is in use by another thread and cannot be reconfigured")
428        } else {
429            panic!("not a mock")
430        }
431    }
432
433    /// Get or fetch identity nonce, querying Platform when stale or absent.
434    /// Treats a missing nonce as `0` before applying the optional bump; on first
435    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
436    /// verify identity existence.
437    pub async fn get_identity_nonce(
438        &self,
439        identity_id: Identifier,
440        bump_first: bool,
441        settings: Option<PutSettings>,
442    ) -> Result<IdentityNonce, Error> {
443        let settings = settings.unwrap_or_default();
444        let nonce = self
445            .nonce_cache
446            .get_identity_nonce(self, identity_id, bump_first, &settings)
447            .await?;
448
449        tracing::trace!(
450            identity_id = %identity_id,
451            bump_first,
452            nonce,
453            "Fetched identity nonce"
454        );
455
456        Ok(nonce)
457    }
458
459    /// Get or fetch identity-contract nonce, querying Platform when stale or absent.
460    /// Treats a missing nonce as `0` before applying the optional bump; on first
461    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
462    /// verify identity or contract existence.
463    pub async fn get_identity_contract_nonce(
464        &self,
465        identity_id: Identifier,
466        contract_id: Identifier,
467        bump_first: bool,
468        settings: Option<PutSettings>,
469    ) -> Result<IdentityNonce, Error> {
470        let settings = settings.unwrap_or_default();
471        self.nonce_cache
472            .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
473            .await
474    }
475
476    /// Marks identity nonce cache entries as stale so they are re-fetched from
477    /// Platform on the next call to [`get_identity_nonce`] or
478    /// [`get_identity_contract_nonce`].
479    pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
480        self.nonce_cache.refresh(identity_id).await;
481    }
482
483    /// Return [Dash Platform version](PlatformVersion) information used by this SDK.
484    ///
485    /// When auto-detection is enabled (default), returns [`PlatformVersion::latest()`]
486    /// until the first network response is received, then tracks the network's version.
487    /// When pinned via [`SdkBuilder::with_version()`], always returns the pinned version.
488    pub fn version<'v>(&self) -> &'v PlatformVersion {
489        let v = self.protocol_version.load(Ordering::Relaxed);
490        PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
491    }
492
493    /// Return the raw protocol version number currently used by this SDK.
494    pub fn protocol_version_number(&self) -> u32 {
495        self.protocol_version.load(Ordering::Relaxed)
496    }
497
498    // TODO: Move to settings
499    /// Indicate if the sdk should request and verify proofs.
500    pub fn prove(&self) -> bool {
501        self.proofs
502    }
503
504    // TODO: If we remove this setter we don't need to use ArcSwap.
505    //   It's good enough to set Context once when you initialize the SDK.
506    /// Set the [ContextProvider] to use.
507    ///
508    /// [ContextProvider] is used to access state information, like data contracts and quorum public keys.
509    ///
510    /// Note that this will overwrite any previous context provider.
511    pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
512        self.context_provider
513            .swap(Some(Arc::new(Box::new(context_provider))));
514    }
515
516    /// Returns a future that resolves when the Sdk is cancelled (e.g. shutdown was requested).
517    pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
518        self.cancel_token.cancelled()
519    }
520
521    /// Request shutdown of the Sdk and all related operations.
522    pub fn shutdown(&self) {
523        self.cancel_token.cancel();
524    }
525
526    /// Return the [DapiClient] address list
527    pub fn address_list(&self) -> &AddressList {
528        match &self.inner {
529            SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
530            #[cfg(feature = "mocks")]
531            SdkInstance::Mock { address_list, .. } => address_list,
532        }
533    }
534}
535
536/// If received metadata time differs from local time by more than `tolerance`, the remote node is considered stale.
537///
538/// ## Parameters
539///
540/// - `metadata`: Metadata of the received response
541/// - `now_ms`: Current local time in milliseconds
542/// - `tolerance_ms`: Tolerance in milliseconds
543pub(crate) fn verify_metadata_time(
544    metadata: &ResponseMetadata,
545    now_ms: u64,
546    tolerance_ms: u64,
547) -> Result<(), Error> {
548    let metadata_time = metadata.time_ms;
549
550    // metadata_time - tolerance_ms <= now_ms <= metadata_time + tolerance_ms
551    if now_ms.abs_diff(metadata_time) > tolerance_ms {
552        return Err(StaleNodeError::Time {
553            expected_timestamp_ms: now_ms,
554            received_timestamp_ms: metadata_time,
555            tolerance_ms,
556        }
557        .into());
558    }
559
560    tracing::trace!(
561        expected_time = now_ms,
562        received_time = metadata_time,
563        tolerance_ms,
564        "received response with valid time"
565    );
566    Ok(())
567}
568
569/// If current metadata height is behind previously seen height by more than `tolerance`, the remote node
570///  is considered stale.
571fn verify_metadata_height(
572    metadata: &ResponseMetadata,
573    tolerance: u64,
574    last_seen_height: Arc<atomic::AtomicU64>,
575) -> Result<(), Error> {
576    let mut expected_height = last_seen_height.load(Ordering::Relaxed);
577    let received_height = metadata.height;
578
579    // Same height, no need to update.
580    if received_height == expected_height {
581        tracing::trace!(
582            expected_height,
583            received_height,
584            tolerance,
585            "received message has the same height as previously seen"
586        );
587        return Ok(());
588    }
589
590    // If expected_height <= tolerance, then Sdk just started, so we just assume what we got is correct.
591    if expected_height > tolerance && received_height < expected_height - tolerance {
592        return Err(StaleNodeError::Height {
593            expected_height,
594            received_height,
595            tolerance_blocks: tolerance,
596        }
597        .into());
598    }
599
600    // New height is ahead of the last seen height, so we update the last seen height.
601    tracing::trace!(
602        expected_height = expected_height,
603        received_height = received_height,
604        tolerance,
605        "received message with new height"
606    );
607    while let Err(stored_height) = last_seen_height.compare_exchange(
608        expected_height,
609        received_height,
610        Ordering::SeqCst,
611        Ordering::Relaxed,
612    ) {
613        // The value was changed to a higher value by another thread, so we need to retry.
614        if stored_height >= metadata.height {
615            break;
616        }
617        expected_height = stored_height;
618    }
619
620    Ok(())
621}
622
623#[async_trait::async_trait]
624impl DapiRequestExecutor for Sdk {
625    async fn execute<R: TransportRequest>(
626        &self,
627        request: R,
628        settings: RequestSettings,
629    ) -> ExecutionResult<R::Response, DapiClientError> {
630        match self.inner {
631            SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
632            #[cfg(feature = "mocks")]
633            SdkInstance::Mock { ref dapi, .. } => {
634                let dapi_guard = dapi.lock().await;
635                dapi_guard.execute(request, settings).await
636            }
637        }
638    }
639}
640
641/// Dash Platform SDK Builder, used to configure and [`SdkBuilder::build()`] the [Sdk].
642///
643/// [SdkBuilder] implements a "builder" design pattern to allow configuration of the Sdk before it is instantiated.
644/// It allows creation of Sdk in two modes:
645/// - `Normal`: Connects to a remote Dash Platform node.
646/// - `Mock`: Uses a mock implementation of Dash Platform.
647///
648/// Mandatory steps of initialization in normal mode are:
649///
650/// 1. Create an instance of [SdkBuilder] with [`SdkBuilder::new()`]
651/// 2. Configure the builder with [`SdkBuilder::with_core()`]
652/// 3. Call [`SdkBuilder::build()`] to create the [Sdk] instance.
653pub struct SdkBuilder {
654    /// List of addresses to connect to.
655    ///
656    /// If `None`, a mock client will be created.
657    addresses: Option<AddressList>,
658    settings: Option<RequestSettings>,
659
660    network: Network,
661
662    core_ip: String,
663    core_port: u16,
664    core_user: String,
665    core_password: Zeroizing<String>,
666
667    /// If true, request and verify proofs of the responses.
668    proofs: bool,
669
670    /// Platform version to use in this Sdk
671    version: &'static PlatformVersion,
672
673    /// Whether the user explicitly called `with_version()`.
674    /// When true, auto-detection of protocol version from network metadata is disabled.
675    version_explicit: bool,
676
677    /// Cache size for data contracts. Used by mock [GrpcContextProvider].
678    #[cfg(feature = "mocks")]
679    data_contract_cache_size: NonZeroUsize,
680
681    /// Cache size for token configs. Used by mock [GrpcContextProvider].
682    #[cfg(feature = "mocks")]
683    token_config_cache_size: NonZeroUsize,
684
685    /// Cache size for quorum public keys. Used by mock [GrpcContextProvider].
686    #[cfg(feature = "mocks")]
687    quorum_public_keys_cache_size: NonZeroUsize,
688
689    /// Context provider used by the SDK.
690    context_provider: Option<Box<dyn ContextProvider>>,
691
692    /// How many blocks difference is allowed between the last seen metadata height and the height received in response
693    /// metadata.
694    ///
695    /// See [SdkBuilder::with_height_tolerance] for more information.
696    metadata_height_tolerance: Option<u64>,
697
698    /// How many milliseconds difference is allowed between the time received in response metadata and current local time.
699    ///
700    /// See [SdkBuilder::with_time_tolerance] for more information.
701    metadata_time_tolerance_ms: Option<u64>,
702
703    /// directory where dump files will be stored
704    #[cfg(feature = "mocks")]
705    dump_dir: Option<PathBuf>,
706
707    /// Cancellation token; once cancelled, all pending requests should be aborted.
708    pub(crate) cancel_token: CancellationToken,
709
710    /// CA certificate to use for TLS connections.
711    #[cfg(not(target_arch = "wasm32"))]
712    ca_certificate: Option<Certificate>,
713}
714
715impl Default for SdkBuilder {
716    /// Create default SdkBuilder that will create a mock client.
717    fn default() -> Self {
718        Self {
719            addresses: None,
720            settings: None,
721            network: Network::Mainnet,
722            core_ip: "".to_string(),
723            core_port: 0,
724            core_password: "".to_string().into(),
725            core_user: "".to_string(),
726
727            proofs: true,
728            metadata_height_tolerance: Some(1),
729            metadata_time_tolerance_ms: None,
730
731            #[cfg(feature = "mocks")]
732            data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
733                .expect("data contract cache size must be positive"),
734
735            #[cfg(feature = "mocks")]
736            token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
737                .expect("token config cache size must be positive"),
738
739            #[cfg(feature = "mocks")]
740            quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
741                .expect("quorum public keys cache size must be positive"),
742
743            context_provider: None,
744
745            cancel_token: CancellationToken::new(),
746
747            version: PlatformVersion::latest(),
748            version_explicit: false,
749            #[cfg(not(target_arch = "wasm32"))]
750            ca_certificate: None,
751
752            #[cfg(feature = "mocks")]
753            dump_dir: None,
754        }
755    }
756}
757
758impl SdkBuilder {
759    /// Enable or disable proofs on requests.
760    ///
761    /// In mock/offline testing with recorded vectors, set to false to match dumps
762    /// that were captured without proofs.
763    pub fn with_proofs(mut self, proofs: bool) -> Self {
764        self.proofs = proofs;
765        self
766    }
767    /// Create a new SdkBuilder with provided address list.
768    pub fn new(addresses: AddressList) -> Self {
769        Self {
770            addresses: Some(addresses),
771            ..Default::default()
772        }
773    }
774
775    /// Replace the address list on this builder.
776    pub fn with_address_list(mut self, addresses: AddressList) -> Self {
777        self.addresses = Some(addresses);
778        self
779    }
780
781    /// Create a new SdkBuilder that will generate mock client.
782    pub fn new_mock() -> Self {
783        Self::default()
784    }
785
786    /// Create a new SdkBuilder instance preconfigured for testnet.
787    ///
788    /// This is a helper method that preconfigures [SdkBuilder] for testnet use.
789    /// Use this method if you want to connect to Dash Platform testnet during development and testing
790    /// of your solution.
791    pub fn new_testnet() -> Self {
792        let address_list = default_address_list_for_network(Network::Testnet);
793
794        Self::new(address_list).with_network(Network::Testnet)
795    }
796
797    /// Create a new SdkBuilder instance preconfigured for mainnet (production network).
798    ///
799    /// This is a helper method that preconfigures [SdkBuilder] for production use.
800    /// Use this method if you want to connect to Dash Platform mainnet with production-ready product.
801    ///
802    /// ## Panics
803    ///
804    /// This method panics if the mainnet configuration cannot be loaded.
805    ///
806    /// ## Unstable
807    ///
808    /// This method is unstable and can be changed in the future.
809    pub fn new_mainnet() -> Self {
810        let address_list = default_address_list_for_network(Network::Mainnet);
811
812        Self::new(address_list).with_network(Network::Mainnet)
813    }
814
815    /// Configure network type.
816    ///
817    /// Defaults to Network::Mainnet which is mainnet.
818    pub fn with_network(mut self, network: Network) -> Self {
819        self.network = network;
820        self
821    }
822
823    /// Configure CA certificate to use when verifying TLS connections.
824    ///
825    /// Used mainly for testing purposes and local networks.
826    ///
827    /// If not set, uses standard system CA certificates.
828    ///
829    /// ## Parameters
830    ///
831    /// - `pem_certificate`: PEM-encoded CA certificate. User must ensure that the certificate is valid.
832    #[cfg(not(target_arch = "wasm32"))]
833    pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
834        self.ca_certificate = Some(pem_certificate);
835        self
836    }
837
838    /// Load CA certificate from a PEM-encoded file.
839    ///
840    /// This is a convenience method that reads the certificate from a file and sets it using
841    /// [SdkBuilder::with_ca_certificate()].
842    #[cfg(not(target_arch = "wasm32"))]
843    pub fn with_ca_certificate_file(
844        self,
845        certificate_file_path: impl AsRef<Path>,
846    ) -> std::io::Result<Self> {
847        let pem = std::fs::read(certificate_file_path)?;
848        let cert = Certificate::from_pem(pem);
849
850        Ok(self.with_ca_certificate(cert))
851    }
852
853    /// Configure request settings.
854    ///
855    /// Tune request settings used to connect to the Dash Platform.
856    ///
857    /// Defaults to [`DEFAULT_REQUEST_SETTINGS`], which sets retries to 3.
858    ///
859    /// See [`RequestSettings`] for more information.
860    pub fn with_settings(mut self, settings: RequestSettings) -> Self {
861        self.settings = Some(settings);
862        self
863    }
864
865    /// Configure platform version.
866    ///
867    /// Select specific version of Dash Platform to use.
868    ///
869    /// Defaults to [PlatformVersion::latest()].
870    pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
871        self.version = version;
872        self.version_explicit = true;
873        self
874    }
875
876    /// Configure context provider to use.
877    ///
878    /// Context provider is used to retrieve data contracts and quorum public keys from application state.
879    /// It should be implemented by the user of this SDK to provide stateful information about the application.
880    ///
881    /// See [ContextProvider] for more information and [GrpcContextProvider] for an example implementation.
882    pub fn with_context_provider<C: ContextProvider + 'static>(
883        mut self,
884        context_provider: C,
885    ) -> Self {
886        self.context_provider = Some(Box::new(context_provider));
887
888        self
889    }
890
891    /// Set cancellation token that will be used by the Sdk.
892    ///
893    /// Once that cancellation token is cancelled, all pending requests shall terminate.
894    pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
895        self.cancel_token = cancel_token;
896        self
897    }
898
899    /// Use Dash Core as a wallet and context provider.
900    ///
901    /// This is a convenience method that configures the SDK to use Dash Core as a wallet and context provider.
902    ///
903    /// For more control over the configuration, use [`SdkBuilder::with_context_provider()`].
904    ///
905    /// This is temporary implementation, intended for development purposes.
906    pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
907        self.core_ip = ip.to_string();
908        self.core_port = port;
909        self.core_user = user.to_string();
910        self.core_password = Zeroizing::from(password.to_string());
911
912        self
913    }
914
915    /// Change number of blocks difference allowed between the last height and the height received in current response.
916    ///
917    /// If height received in response metadata is behind previously seen height by more than this value, the node
918    /// is considered stale, and the request will fail.
919    ///
920    /// If None, the height is not checked.
921    ///
922    /// Note that this feature doesn't guarantee that you are getting latest data, but it significantly decreases
923    /// probability of getting old data.
924    ///
925    /// This is set to `1` by default.
926    pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
927        self.metadata_height_tolerance = tolerance;
928        self
929    }
930
931    /// How many milliseconds difference is allowed between the time received in response and current local time.
932    /// If the received time differs from local time by more than this value, the remote node is stale.
933    ///
934    /// If None, the time is not checked.
935    ///
936    /// This is set to `None` by default.
937    ///
938    /// Note that enabling this check can cause issues if the local time is not synchronized with the network time,
939    /// when the network is stalled or time between blocks increases significantly.
940    ///
941    /// Selecting a safe value for this parameter depends on maximum time between blocks mined on the network.
942    /// For example, if the network is configured to mine a block every maximum 3 minutes, setting this value
943    /// to a bit more than 6 minutes (to account for misbehaving proposers, network delays and local time
944    /// synchronization issues) should be safe.
945    pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
946        self.metadata_time_tolerance_ms = tolerance_ms;
947        self
948    }
949
950    /// Configure directory where dumps of all requests and responses will be saved.
951    /// Useful for debugging.
952    ///
953    /// This function will create the directory if it does not exist and save dumps of
954    /// * all requests and responses - in files named `msg-*.json`
955    /// * retrieved quorum public keys - in files named `quorum_pubkey-*.json`
956    /// * retrieved data contracts - in files named `data_contract-*.json`
957    ///
958    /// These files can be used together with [MockDashPlatformSdk] to replay the requests and responses.
959    /// See [MockDashPlatformSdk::load_expectations_sync()] for more information.
960    ///
961    /// Available only when `mocks` feature is enabled.
962    #[cfg(feature = "mocks")]
963    pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
964        self.dump_dir = Some(dump_dir.to_path_buf());
965        self
966    }
967
968    /// Build the Sdk instance.
969    ///
970    /// This method will create the Sdk instance based on the configuration provided to the builder.
971    ///
972    /// # Errors
973    ///
974    /// This method will return an error if the Sdk cannot be created.
975    pub fn build(self) -> Result<Sdk, Error> {
976        let dapi_client_settings = match self.settings {
977            Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
978            None => DEFAULT_REQUEST_SETTINGS,
979        };
980
981        let sdk= match self.addresses {
982            // non-mock mode
983            Some(addresses) => {
984                #[allow(unused_mut)] // needs to be mutable for features other than wasm
985                let mut dapi = DapiClient::new(addresses, dapi_client_settings);
986                #[cfg(not(target_arch = "wasm32"))]
987                if let Some(pem) = self.ca_certificate {
988                    dapi = dapi.with_ca_certificate(pem);
989                }
990
991                #[cfg(feature = "mocks")]
992                let dapi = dapi.dump_dir(self.dump_dir.clone());
993
994                #[allow(unused_mut)] // needs to be mutable for #[cfg(feature = "mocks")]
995                let mut sdk= Sdk{
996                    network: self.network,
997                    dapi_client_settings,
998                    inner:SdkInstance::Dapi { dapi },
999                    proofs:self.proofs,
1000                    context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
1001                    cancel_token: self.cancel_token,
1002                    nonce_cache: Default::default(),
1003                    // When auto-detecting, seed with 0 (uninitialized) so the first
1004                    // network response sets the actual version — even if it's lower
1005                    // than the binary's latest. When pinned, use the explicit version.
1006                    protocol_version: Arc::new(atomic::AtomicU32::new(
1007                        if self.version_explicit { self.version.protocol_version } else { 0 },
1008                    )),
1009                    auto_detect_protocol_version: !self.version_explicit,
1010                    // Note: in the future, we need to securely initialize initial height during Sdk bootstrap or first request.
1011                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(0)),
1012                    metadata_height_tolerance: self.metadata_height_tolerance,
1013                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1014                    #[cfg(feature = "mocks")]
1015                    dump_dir: self.dump_dir,
1016                };
1017                // if context provider is not set correctly (is None), it means we need to fall back to core wallet
1018                if  sdk.context_provider.load().is_none() {
1019                    #[cfg(feature = "mocks")]
1020                    if !self.core_ip.is_empty() {
1021                        tracing::warn!(
1022                            "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
1023                        let mut context_provider = GrpcContextProvider::new(None,
1024                            &self.core_ip, self.core_port, &self.core_user, &self.core_password,
1025                            self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
1026                        #[cfg(feature = "mocks")]
1027                        if sdk.dump_dir.is_some() {
1028                            context_provider.set_dump_dir(sdk.dump_dir.clone());
1029                        }
1030                        // We have cyclical dependency Sdk <-> GrpcContextProvider, so we just do some
1031                        // workaround using additional Arc.
1032                        let context_provider= Arc::new(context_provider);
1033                        sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
1034                        context_provider.set_sdk(Some(sdk.clone()));
1035                    } else{
1036                        return Err(Error::Config(concat!(
1037                            "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1038                            "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1039                            .to_string()));
1040                    }
1041                    #[cfg(not(feature = "mocks"))]
1042                    return Err(Error::Config(concat!(
1043                        "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1044                        "or enable `mocks` feature to use mock context provider")
1045                        .to_string()));
1046                };
1047
1048                sdk
1049            },
1050            #[cfg(feature = "mocks")]
1051            // mock mode
1052            None => {
1053                let dapi =Arc::new(Mutex::new(  MockDapiClient::new()));
1054                // We create mock context provider that will use the mock DAPI client to retrieve data contracts.
1055                let  context_provider = self.context_provider.unwrap_or_else(||{
1056                    let mut cp=MockContextProvider::new();
1057                    if let Some(ref dump_dir) = self.dump_dir {
1058                        cp.quorum_keys_dir(Some(dump_dir.clone()));
1059                    }
1060                    Box::new(cp)
1061                }
1062                );
1063                let mock_sdk = MockDashPlatformSdk::new(self.version, Arc::clone(&dapi));
1064                let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1065                let sdk= Sdk {
1066                    network: self.network,
1067                    dapi_client_settings,
1068                    inner:SdkInstance::Mock {
1069                        mock:mock_sdk.clone(),
1070                        dapi,
1071                        address_list: AddressList::new(),
1072                    },
1073                    dump_dir: self.dump_dir.clone(),
1074                    proofs:self.proofs,
1075                    nonce_cache: Default::default(),
1076                    protocol_version: Arc::new(atomic::AtomicU32::new(
1077                        if self.version_explicit { self.version.protocol_version } else { 0 },
1078                    )),
1079                    auto_detect_protocol_version: !self.version_explicit,
1080                    context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1081                    cancel_token: self.cancel_token,
1082                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(0)),
1083                    metadata_height_tolerance: self.metadata_height_tolerance,
1084                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1085                };
1086                let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1087                guard.set_sdk(sdk.clone());
1088                if let Some(ref dump_dir) = self.dump_dir {
1089                    guard.load_expectations_sync(dump_dir)?;
1090                };
1091
1092                sdk
1093            },
1094            #[cfg(not(feature = "mocks"))]
1095            None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1096        };
1097
1098        Ok(sdk)
1099    }
1100}
1101
1102pub fn prettify_proof(proof: &Proof) -> String {
1103    let config = bincode::config::standard()
1104        .with_big_endian()
1105        .with_no_limit();
1106    let grovedb_proof: Result<GroveDBProof, DecodeError> =
1107        bincode::decode_from_slice(&proof.grovedb_proof, config).map(|(a, _)| a);
1108
1109    let grovedb_proof_string = match grovedb_proof {
1110        Ok(proof) => format!("{}", proof),
1111        Err(_) => "Invalid GroveDBProof".to_string(),
1112    };
1113    format!(
1114        "Proof {{
1115            grovedb_proof: {},
1116            quorum_hash: 0x{},
1117            signature: 0x{},
1118            round: {},
1119            block_id_hash: 0x{},
1120            quorum_type: {},
1121        }}",
1122        grovedb_proof_string,
1123        hex::encode(&proof.quorum_hash),
1124        hex::encode(&proof.signature),
1125        proof.round,
1126        hex::encode(&proof.block_id_hash),
1127        proof.quorum_type,
1128    )
1129}
1130
1131#[cfg(test)]
1132mod test {
1133    use std::sync::Arc;
1134
1135    use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1136    use rs_dapi_client::transport::TransportRequest;
1137    use test_case::test_matrix;
1138
1139    use crate::SdkBuilder;
1140
1141    use super::Network;
1142
1143    /// Mainnet Evo masternodes expose the Platform HTTP endpoint on 443.
1144    const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1145    /// Testnet Evo masternodes expose the Platform HTTP endpoint on 1443.
1146    const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;
1147
1148    #[test]
1149    fn new_testnet_sources_bootstrap_from_seeds() {
1150        let builder = SdkBuilder::new_testnet();
1151        let address_list = builder
1152            .addresses
1153            .as_ref()
1154            .expect("testnet builder should configure default addresses");
1155
1156        assert_eq!(builder.network, Network::Testnet);
1157        assert!(
1158            !address_list.is_empty(),
1159            "testnet must have at least one bootstrap address"
1160        );
1161        for address in address_list.get_live_addresses() {
1162            assert_eq!(
1163                address.uri().port_u16(),
1164                Some(TESTNET_PLATFORM_HTTP_PORT),
1165                "testnet bootstrap address must use the platform HTTP port",
1166            );
1167        }
1168    }
1169
1170    #[test]
1171    fn new_mainnet_sources_bootstrap_from_seeds() {
1172        let builder = SdkBuilder::new_mainnet();
1173        let address_list = builder
1174            .addresses
1175            .as_ref()
1176            .expect("mainnet builder should configure default addresses");
1177
1178        assert_eq!(builder.network, Network::Mainnet);
1179        assert!(
1180            !address_list.is_empty(),
1181            "mainnet must have at least one bootstrap address"
1182        );
1183        for address in address_list.get_live_addresses() {
1184            assert_eq!(
1185                address.uri().port_u16(),
1186                Some(MAINNET_PLATFORM_HTTP_PORT),
1187                "mainnet bootstrap address must use the platform HTTP port",
1188            );
1189        }
1190    }
1191
1192    /// Smoke signal: the upstream seed lists are far larger than 10 entries on
1193    /// both networks. If parsing drops most of them we want a loud test
1194    /// failure rather than silently shipping a near-empty bootstrap list.
1195    #[test]
1196    fn bootstrap_counts_reasonable() {
1197        let mainnet = SdkBuilder::new_mainnet()
1198            .addresses
1199            .expect("mainnet builder should configure default addresses");
1200        let testnet = SdkBuilder::new_testnet()
1201            .addresses
1202            .expect("testnet builder should configure default addresses");
1203        assert!(
1204            mainnet.len() >= 10,
1205            "expected >=10 mainnet bootstrap addresses, got {}",
1206            mainnet.len()
1207        );
1208        assert!(
1209            testnet.len() >= 10,
1210            "expected >=10 testnet bootstrap addresses, got {}",
1211            testnet.len()
1212        );
1213    }
1214
1215    #[test_matrix(97..102, 100, 2, false; "valid height")]
1216    #[test_case(103, 100, 2, true; "invalid height")]
1217    fn test_verify_metadata_height(
1218        expected_height: u64,
1219        received_height: u64,
1220        tolerance: u64,
1221        expect_err: bool,
1222    ) {
1223        let metadata = ResponseMetadata {
1224            height: received_height,
1225            ..Default::default()
1226        };
1227
1228        let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
1229
1230        let result =
1231            super::verify_metadata_height(&metadata, tolerance, Arc::clone(&last_seen_height));
1232
1233        assert_eq!(result.is_err(), expect_err);
1234        if result.is_ok() {
1235            assert_eq!(
1236                last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
1237                received_height,
1238                "previous height should be updated"
1239            );
1240        }
1241    }
1242
1243    #[test]
1244    fn cloned_sdk_verify_metadata_height() {
1245        let sdk1 = SdkBuilder::new_mock()
1246            .build()
1247            .expect("mock Sdk should be created");
1248
1249        // First message verified, height 1.
1250        let metadata = ResponseMetadata {
1251            height: 1,
1252            ..Default::default()
1253        };
1254
1255        // use dummy request type to satisfy generic parameter
1256        let request = GetIdentityRequest::default();
1257        sdk1.verify_response_metadata(request.method_name(), &metadata)
1258            .expect("metadata should be valid");
1259
1260        assert_eq!(
1261            sdk1.metadata_last_seen_height
1262                .load(std::sync::atomic::Ordering::Relaxed),
1263            metadata.height,
1264            "initial height"
1265        );
1266
1267        // now, we clone sdk and do two requests.
1268        let sdk2 = sdk1.clone();
1269        let sdk3 = sdk1.clone();
1270
1271        // Second message verified, height 2.
1272        let metadata = ResponseMetadata {
1273            height: 2,
1274            ..Default::default()
1275        };
1276        // use dummy request type to satisfy generic parameter
1277        let request = GetIdentityRequest::default();
1278        sdk2.verify_response_metadata(request.method_name(), &metadata)
1279            .expect("metadata should be valid");
1280
1281        assert_eq!(
1282            sdk1.metadata_last_seen_height
1283                .load(std::sync::atomic::Ordering::Relaxed),
1284            metadata.height,
1285            "first sdk should see height from second sdk"
1286        );
1287        assert_eq!(
1288            sdk3.metadata_last_seen_height
1289                .load(std::sync::atomic::Ordering::Relaxed),
1290            metadata.height,
1291            "third sdk should see height from second sdk"
1292        );
1293
1294        // Third message verified, height 3.
1295        let metadata = ResponseMetadata {
1296            height: 3,
1297            ..Default::default()
1298        };
1299        // use dummy request type to satisfy generic parameter
1300        let request = GetIdentityRequest::default();
1301        sdk3.verify_response_metadata(request.method_name(), &metadata)
1302            .expect("metadata should be valid");
1303
1304        assert_eq!(
1305            sdk1.metadata_last_seen_height
1306                .load(std::sync::atomic::Ordering::Relaxed),
1307            metadata.height,
1308            "first sdk should see height from third sdk"
1309        );
1310
1311        assert_eq!(
1312            sdk2.metadata_last_seen_height
1313                .load(std::sync::atomic::Ordering::Relaxed),
1314            metadata.height,
1315            "second sdk should see height from third sdk"
1316        );
1317
1318        // Now, using sdk1 for height 1 again should fail, as we are already at 3, with default tolerance 1.
1319        let metadata = ResponseMetadata {
1320            height: 1,
1321            ..Default::default()
1322        };
1323
1324        let request = GetIdentityRequest::default();
1325        sdk1.verify_response_metadata(request.method_name(), &metadata)
1326            .expect_err("metadata should be invalid");
1327    }
1328
1329    /// Helper: build a mock SDK with auto-detect enabled and a specific starting version.
1330    /// Does NOT call `with_version()` (which would disable auto-detect).
1331    fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
1332        use std::sync::atomic::Ordering;
1333
1334        let sdk = SdkBuilder::new_mock()
1335            .build()
1336            .expect("mock Sdk should be created");
1337        sdk.protocol_version
1338            .store(starting_version, Ordering::Relaxed);
1339        sdk
1340    }
1341
1342    #[test]
1343    fn test_version_update_from_metadata() {
1344        let sdk = mock_sdk_with_auto_detect(1);
1345
1346        assert_eq!(sdk.protocol_version_number(), 1);
1347
1348        let metadata = ResponseMetadata {
1349            protocol_version: 2,
1350            height: 1,
1351            ..Default::default()
1352        };
1353
1354        sdk.verify_response_metadata("test", &metadata)
1355            .expect("metadata should be valid");
1356
1357        assert_eq!(sdk.protocol_version_number(), 2);
1358        assert_eq!(sdk.version().protocol_version, 2);
1359    }
1360
1361    #[test]
1362    fn test_unknown_version_ignored() {
1363        use dpp::version::PlatformVersion;
1364
1365        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1366        let original_version = sdk.protocol_version_number();
1367
1368        let metadata = ResponseMetadata {
1369            protocol_version: 999,
1370            height: 1,
1371            ..Default::default()
1372        };
1373
1374        sdk.verify_response_metadata("test", &metadata)
1375            .expect("metadata should be valid");
1376
1377        assert_eq!(sdk.protocol_version_number(), original_version);
1378        assert_eq!(sdk.version().protocol_version, original_version);
1379    }
1380
1381    #[test]
1382    fn test_version_shared_between_clones() {
1383        let sdk = mock_sdk_with_auto_detect(1);
1384
1385        let clone = sdk.clone();
1386
1387        let metadata = ResponseMetadata {
1388            protocol_version: 2,
1389            height: 1,
1390            ..Default::default()
1391        };
1392
1393        clone
1394            .verify_response_metadata("test", &metadata)
1395            .expect("metadata should be valid");
1396
1397        assert_eq!(
1398            sdk.protocol_version_number(),
1399            2,
1400            "original should see update from clone"
1401        );
1402    }
1403
1404    #[test]
1405    fn test_version_downgrade_ignored() {
1406        let sdk = mock_sdk_with_auto_detect(2);
1407
1408        assert_eq!(sdk.protocol_version_number(), 2);
1409
1410        let metadata = ResponseMetadata {
1411            protocol_version: 1,
1412            height: 1,
1413            ..Default::default()
1414        };
1415
1416        sdk.verify_response_metadata("test", &metadata)
1417            .expect("metadata should be valid");
1418
1419        assert_eq!(sdk.protocol_version_number(), 2);
1420    }
1421
1422    #[test]
1423    fn test_version_zero_ignored() {
1424        use dpp::version::PlatformVersion;
1425
1426        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1427        let original_version = sdk.protocol_version_number();
1428
1429        let metadata = ResponseMetadata {
1430            protocol_version: 0,
1431            height: 1,
1432            ..Default::default()
1433        };
1434
1435        sdk.verify_response_metadata("test", &metadata)
1436            .expect("metadata should be valid");
1437
1438        assert_eq!(sdk.protocol_version_number(), original_version);
1439    }
1440
1441    #[test]
1442    fn test_concurrent_updates_converge_to_highest() {
1443        use std::thread;
1444
1445        let sdk = mock_sdk_with_auto_detect(1);
1446
1447        assert_eq!(sdk.protocol_version_number(), 1);
1448
1449        let mut handles = Vec::new();
1450        // Spawn threads that race to update to version 2 and version 3
1451        for version in [2u32, 3, 2, 3, 2, 3] {
1452            let sdk_clone = sdk.clone();
1453            handles.push(thread::spawn(move || {
1454                let metadata = ResponseMetadata {
1455                    protocol_version: version,
1456                    height: 1,
1457                    ..Default::default()
1458                };
1459                sdk_clone
1460                    .verify_response_metadata("test", &metadata)
1461                    .expect("metadata should be valid");
1462            }));
1463        }
1464
1465        for h in handles {
1466            h.join().expect("thread should not panic");
1467        }
1468
1469        // Highest known version (3) must win regardless of thread ordering
1470        assert_eq!(
1471            sdk.protocol_version_number(),
1472            3,
1473            "concurrent updates must converge to highest version"
1474        );
1475    }
1476
1477    // TC-7 (global DPP version sync) removed — set_current() is no longer called
1478    // from the SDK. Version is stored per-instance, not in the process-wide global.
1479
1480    #[test]
1481    fn test_explicit_version_disables_auto_detect() {
1482        use dpp::version::PlatformVersion;
1483
1484        // Explicitly pin to version 1 via with_version()
1485        let sdk = SdkBuilder::new_mock()
1486            .with_version(PlatformVersion::get(1).unwrap())
1487            .build()
1488            .expect("mock Sdk should be created");
1489
1490        assert_eq!(sdk.protocol_version_number(), 1);
1491        assert!(!sdk.auto_detect_protocol_version);
1492
1493        // Network reports version 2 — should be ignored because version is pinned
1494        let metadata = ResponseMetadata {
1495            protocol_version: 2,
1496            height: 1,
1497            ..Default::default()
1498        };
1499
1500        sdk.verify_response_metadata("test", &metadata)
1501            .expect("metadata should be valid");
1502
1503        assert_eq!(
1504            sdk.protocol_version_number(),
1505            1,
1506            "pinned version must not be auto-updated"
1507        );
1508    }
1509
1510    #[test]
1511    fn test_default_sdk_detects_older_network_version() {
1512        use dpp::version::PlatformVersion;
1513
1514        // Default SDK: auto-detect enabled, seeded at 0 (uninitialized)
1515        let sdk = SdkBuilder::new_mock()
1516            .build()
1517            .expect("mock Sdk should be created");
1518
1519        // Before any network response, version() falls back to latest()
1520        assert_eq!(
1521            sdk.version().protocol_version,
1522            PlatformVersion::latest().protocol_version,
1523            "before first response, should fall back to latest"
1524        );
1525        assert_eq!(sdk.protocol_version_number(), 0, "should be uninitialized");
1526
1527        // Network reports version 1 (older than latest) — should be accepted
1528        let metadata = ResponseMetadata {
1529            protocol_version: 1,
1530            height: 1,
1531            ..Default::default()
1532        };
1533
1534        sdk.verify_response_metadata("test", &metadata)
1535            .expect("metadata should be valid");
1536
1537        assert_eq!(
1538            sdk.protocol_version_number(),
1539            1,
1540            "default SDK must detect older network version"
1541        );
1542        assert_eq!(sdk.version().protocol_version, 1);
1543    }
1544
1545    #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
1546    #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
1547    #[test_matrix([0,100], [0,100], 100, false; "zero time")]
1548    #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
1549    fn test_verify_metadata_time(
1550        received_time: u64,
1551        now_time: u64,
1552        tolerance: u64,
1553        expect_err: bool,
1554    ) {
1555        let metadata = ResponseMetadata {
1556            time_ms: received_time,
1557            ..Default::default()
1558        };
1559
1560        let result = super::verify_metadata_time(&metadata, now_time, tolerance);
1561
1562        assert_eq!(result.is_err(), expect_err);
1563    }
1564}