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