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