Skip to main content

dash_sdk/mock/
sdk.rs

1//! Mocking mechanisms for Dash Platform SDK.
2//!
3//! See [MockDashPlatformSdk] for more details.
4use super::MockResponse;
5use crate::{
6    platform::{
7        types::{evonode::EvoNode, identity::IdentityRequest},
8        Fetch, FetchMany, Query,
9    },
10    sync::block_on,
11    Error, Sdk,
12};
13use arc_swap::ArcSwapOption;
14use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
15use dapi_grpc::{
16    mock::Mockable,
17    platform::v0::{self as proto},
18};
19use dash_context_provider::{ContextProvider, ContextProviderError};
20use dpp::dashcore::Network;
21use dpp::version::PlatformVersion;
22use drive_proof_verifier::FromProof;
23use rs_dapi_client::mock::MockError;
24use rs_dapi_client::{
25    mock::{Key, MockDapiClient},
26    transport::TransportRequest,
27    DapiClient, DumpData, ExecutionResponse,
28};
29use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
30use tokio::sync::{Mutex, OwnedMutexGuard};
31
32/// Mechanisms to mock Dash Platform SDK.
33///
34/// This object is returned by [Sdk::mock()](crate::Sdk::mock()) and is used to define mock expectations.
35///
36/// Use [MockDashPlatformSdk::expect_fetch_many()] to define expectations for [FetchMany] requests
37/// and [MockDashPlatformSdk::expect_fetch()] for [Fetch] requests.
38///
39/// ## Panics
40///
41/// Can panic on errors.
42#[derive(Debug)]
43pub struct MockDashPlatformSdk {
44    from_proof_expectations: BTreeMap<Key, Vec<u8>>,
45    dapi: Arc<Mutex<MockDapiClient>>,
46    sdk: ArcSwapOption<Sdk>,
47}
48
49impl MockDashPlatformSdk {
50    /// Returns true when requests should use proofs.
51    ///
52    /// ## Panics
53    ///
54    /// Panics when sdk is not set during initialization.
55    pub fn prove(&self) -> bool {
56        if let Some(sdk) = self.sdk.load().as_ref() {
57            sdk.prove()
58        } else {
59            panic!("sdk must be set when creating mock ")
60        }
61    }
62
63    /// Create new mock SDK.
64    ///
65    /// ## Note
66    ///
67    /// You have to call [MockDashPlatformSdk::set_sdk()] to set sdk, otherwise Mock SDK will panic.
68    pub(crate) fn new(dapi: Arc<Mutex<MockDapiClient>>) -> Self {
69        Self {
70            from_proof_expectations: Default::default(),
71            dapi,
72            sdk: ArcSwapOption::new(None),
73        }
74    }
75
76    pub(crate) fn set_sdk(&mut self, sdk: Sdk) {
77        self.sdk.store(Some(Arc::new(sdk)));
78    }
79
80    /// Returns the current `PlatformVersion` from the outer [`Sdk`]'s
81    /// auto-detect-aware atomic. Both request-encode (`sdk.query_settings()`)
82    /// and proof-decode (`parse_proof_with_metadata`) read through this
83    /// single source, so a mock ratchet from response metadata is visible
84    /// to both paths.
85    ///
86    /// ## Panics
87    ///
88    /// Panics when sdk is not set during initialization.
89    pub(crate) fn version<'v>(&self) -> &'v PlatformVersion {
90        if let Some(sdk) = self.sdk.load().as_ref() {
91            sdk.version()
92        } else {
93            panic!("sdk must be set when creating mock ")
94        }
95    }
96
97    /// Load all expectations from files in a directory asynchronously.
98    ///
99    /// See [MockDashPlatformSdk::load_expectations_sync()] for more details.
100    #[deprecated(since = "1.4.0", note = "use load_expectations_sync")]
101    pub async fn load_expectations<P: AsRef<std::path::Path> + Send + 'static>(
102        &mut self,
103        dir: P,
104    ) -> Result<&mut Self, Error> {
105        self.load_expectations_sync(dir)
106    }
107
108    /// Load all expectations from files in a directory.
109    ///
110    ///
111    /// By default, mock expectations are loaded when Sdk is built with [SdkBuilder::build()](crate::SdkBuilder::build()).
112    /// This function can be used to load expectations after the Sdk is created, or use alternative location.
113    /// Expectation files must be prefixed with [DapiClient::DUMP_FILE_PREFIX] and
114    /// have `.json` extension.
115    pub fn load_expectations_sync<P: AsRef<std::path::Path>>(
116        &mut self,
117        dir: P,
118    ) -> Result<&mut Self, Error> {
119        let prefix = DapiClient::DUMP_FILE_PREFIX;
120
121        let entries = dir.as_ref().read_dir().map_err(|e| {
122            Error::Config(format!(
123                "cannot load mock expectations from {}: {}",
124                dir.as_ref().display(),
125                e
126            ))
127        })?;
128
129        let files: Vec<PathBuf> = entries
130            .into_iter()
131            .filter_map(|x| x.ok())
132            .filter(|f| {
133                f.file_type().is_ok_and(|t| t.is_file())
134                    && f.file_name().to_string_lossy().starts_with(prefix)
135                    && f.file_name().to_string_lossy().ends_with(".json")
136            })
137            .map(|f| f.path())
138            .collect();
139
140        let mut dapi = block_on(self.dapi.clone().lock_owned())?;
141
142        for filename in &files {
143            let basename = filename.file_name().unwrap().to_str().unwrap();
144            let request_type = basename.split('_').nth(1).unwrap_or_default();
145
146            match request_type {
147                "GetDocumentsRequest" => {
148                    load_expectation::<proto::GetDocumentsRequest>(&mut dapi, filename)?
149                }
150                "GetEpochsInfoRequest" => {
151                    load_expectation::<proto::GetEpochsInfoRequest>(&mut dapi, filename)?
152                }
153                "GetDataContractRequest" => {
154                    load_expectation::<proto::GetDataContractRequest>(&mut dapi, filename)?
155                }
156                "GetDataContractsRequest" => {
157                    load_expectation::<proto::GetDataContractsRequest>(&mut dapi, filename)?
158                }
159                "GetDataContractsByRangeRequest" => {
160                    load_expectation::<proto::GetDataContractsByRangeRequest>(&mut dapi, filename)?
161                }
162                "GetDataContractsLatestVersionsRequest" => load_expectation::<
163                    proto::GetDataContractsLatestVersionsRequest,
164                >(&mut dapi, filename)?,
165                "GetDataContractHistoryRequest" => {
166                    load_expectation::<proto::GetDataContractHistoryRequest>(&mut dapi, filename)?
167                }
168                "GetContractGroupInfoRequest" => {
169                    load_expectation::<proto::GetContractGroupInfoRequest>(&mut dapi, filename)?
170                }
171                "GetContractGroupMembersRequest" => {
172                    load_expectation::<proto::GetContractGroupMembersRequest>(&mut dapi, filename)?
173                }
174                "GetContractGroupsForContractRequest" => load_expectation::<
175                    proto::GetContractGroupsForContractRequest,
176                >(&mut dapi, filename)?,
177                "GetDocumentHistoryRequest" => {
178                    load_expectation::<proto::GetDocumentHistoryRequest>(&mut dapi, filename)?
179                }
180                "IdentityRequest" => load_expectation::<IdentityRequest>(&mut dapi, filename)?,
181                "GetIdentityRequest" => {
182                    load_expectation::<proto::GetIdentityRequest>(&mut dapi, filename)?
183                }
184
185                "GetIdentityBalanceRequest" => {
186                    load_expectation::<proto::GetIdentityBalanceRequest>(&mut dapi, filename)?
187                }
188                "GetIdentityContractNonceRequest" => {
189                    load_expectation::<proto::GetIdentityContractNonceRequest>(&mut dapi, filename)?
190                }
191                "GetIdentityBalanceAndRevisionRequest" => load_expectation::<
192                    proto::GetIdentityBalanceAndRevisionRequest,
193                >(&mut dapi, filename)?,
194                "GetAddressInfoRequest" => {
195                    load_expectation::<proto::GetAddressInfoRequest>(&mut dapi, filename)?
196                }
197                "GetAddressesInfosRequest" => {
198                    load_expectation::<proto::GetAddressesInfosRequest>(&mut dapi, filename)?
199                }
200                "GetIdentityKeysRequest" => {
201                    load_expectation::<proto::GetIdentityKeysRequest>(&mut dapi, filename)?
202                }
203                "GetProtocolVersionUpgradeStateRequest" => load_expectation::<
204                    proto::GetProtocolVersionUpgradeStateRequest,
205                >(&mut dapi, filename)?,
206                "GetProtocolVersionUpgradeVoteStatusRequest" => {
207                    load_expectation::<proto::GetProtocolVersionUpgradeVoteStatusRequest>(
208                        &mut dapi, filename,
209                    )?
210                }
211                "GetContestedResourcesRequest" => {
212                    load_expectation::<proto::GetContestedResourcesRequest>(&mut dapi, filename)?
213                }
214                "GetContestedResourceVoteStateRequest" => load_expectation::<
215                    proto::GetContestedResourceVoteStateRequest,
216                >(&mut dapi, filename)?,
217                "GetContestedResourceVotersForIdentityRequest" => {
218                    load_expectation::<proto::GetContestedResourceVotersForIdentityRequest>(
219                        &mut dapi, filename,
220                    )?
221                }
222                "GetContestedResourceIdentityVotesRequest" => {
223                    load_expectation::<proto::GetContestedResourceIdentityVotesRequest>(
224                        &mut dapi, filename,
225                    )?
226                }
227                "GetVotePollsByEndDateRequest" => {
228                    load_expectation::<proto::GetVotePollsByEndDateRequest>(&mut dapi, filename)?
229                }
230                "GetPrefundedSpecializedBalanceRequest" => load_expectation::<
231                    proto::GetPrefundedSpecializedBalanceRequest,
232                >(&mut dapi, filename)?,
233                "GetPathElementsRequest" => {
234                    load_expectation::<proto::GetPathElementsRequest>(&mut dapi, filename)?
235                }
236                "GetTotalCreditsInPlatformRequest" => load_expectation::<
237                    proto::GetTotalCreditsInPlatformRequest,
238                >(&mut dapi, filename)?,
239                "GetIdentityKeysRemainingBudgetsRequest" => load_expectation::<
240                    proto::GetIdentityKeysRemainingBudgetsRequest,
241                >(&mut dapi, filename)?,
242                "GetIdentityTokenBalancesRequest" => {
243                    load_expectation::<proto::GetIdentityTokenBalancesRequest>(&mut dapi, filename)?
244                }
245                "GetIdentitiesTokenBalancesRequest" => load_expectation::<
246                    proto::GetIdentitiesTokenBalancesRequest,
247                >(&mut dapi, filename)?,
248                "GetIdentityTokenInfosRequest" => {
249                    load_expectation::<proto::GetIdentityTokenInfosRequest>(&mut dapi, filename)?
250                }
251                "GetIdentitiesTokenInfosRequest" => {
252                    load_expectation::<proto::GetIdentitiesTokenInfosRequest>(&mut dapi, filename)?
253                }
254                "GetTokenStatusesRequest" => {
255                    load_expectation::<proto::GetTokenStatusesRequest>(&mut dapi, filename)?
256                }
257                "GetTokenTotalSupplyRequest" => {
258                    load_expectation::<proto::GetTokenTotalSupplyRequest>(&mut dapi, filename)?
259                }
260                "GetGroupInfoRequest" => {
261                    load_expectation::<proto::GetGroupInfoRequest>(&mut dapi, filename)?
262                }
263                "GetGroupInfosRequest" => {
264                    load_expectation::<proto::GetGroupInfosRequest>(&mut dapi, filename)?
265                }
266                "GetGroupActionsRequest" => {
267                    load_expectation::<proto::GetGroupActionsRequest>(&mut dapi, filename)?
268                }
269                "GetGroupActionSignersRequest" => {
270                    load_expectation::<proto::GetGroupActionSignersRequest>(&mut dapi, filename)?
271                }
272                "EvoNode" => load_expectation::<EvoNode>(&mut dapi, filename)?,
273                "GetTokenDirectPurchasePricesRequest" => load_expectation::<
274                    proto::GetTokenDirectPurchasePricesRequest,
275                >(&mut dapi, filename)?,
276                "GetTokenPerpetualDistributionLastClaimRequest" => {
277                    load_expectation::<proto::GetTokenPerpetualDistributionLastClaimRequest>(
278                        &mut dapi, filename,
279                    )?
280                }
281                "GetTokenPreProgrammedDistributionsRequest" => {
282                    load_expectation::<proto::GetTokenPreProgrammedDistributionsRequest>(
283                        &mut dapi, filename,
284                    )?
285                }
286                "GetAddressesTrunkStateRequest" => {
287                    load_expectation::<proto::GetAddressesTrunkStateRequest>(&mut dapi, filename)?
288                }
289                _ => {
290                    return Err(Error::Config(format!(
291                        "unknown request type {} in {}, missing match arm in load_expectations?",
292                        request_type,
293                        filename.display()
294                    )))
295                }
296            };
297        }
298
299        Ok(self)
300    }
301
302    /// Expect a [Fetch] request and return provided object.
303    ///
304    /// This method is used to define mock expectations for [Fetch] requests.
305    ///
306    /// ## Generic Parameters
307    ///
308    /// - `O`: Type of the object that will be returned in response to the query. Must implement [Fetch] and [MockResponse].
309    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
310    ///
311    /// ## Arguments
312    ///
313    /// - `query`: Query that will be sent to Platform.
314    /// - `object`: Object that will be returned in response to `query`, or None if the object is expected to not exist.
315    ///
316    /// ## Returns
317    ///
318    /// * Reference to self on success, to allow chaining
319    /// * Error when expectation cannot be set or is already defined for this request
320    ///
321    /// ## Panics
322    ///
323    /// Can panic on errors.
324    ///
325    /// ## Example
326    ///
327    /// ```no_run
328    /// # let r = tokio::runtime::Runtime::new().unwrap();
329    /// #
330    /// # r.block_on(async {
331    ///     use dash_sdk::{Sdk, platform::{Identity, Fetch, dpp::identity::accessors::IdentityGettersV0}};
332    ///
333    ///     let mut api = Sdk::new_mock();
334    ///     // Define expected response
335    ///     let expected: Identity = Identity::random_identity(1, None, api.version())
336    ///         .expect("create expected identity");
337    ///     // Define query that will be sent
338    ///     let query = expected.id();
339    ///     // Expect that in response to `query`, `expected` will be returned
340    ///     api.mock().expect_fetch(query, Some(expected.clone())).await.unwrap();
341    ///
342    ///     // Fetch the identity
343    ///     let retrieved = dpp::prelude::Identity::fetch(&api, query)
344    ///         .await
345    ///         .unwrap()
346    ///         .expect("object should exist");
347    ///
348    ///     // Check that the identity is the same as expected
349    ///     assert_eq!(retrieved, expected);
350    /// # });
351    /// ```
352    pub async fn expect_fetch<O: Fetch + MockResponse, Q: Query<<O as Fetch>::Query>>(
353        &mut self,
354        query: Q,
355        object: Option<O>,
356    ) -> Result<&mut Self, Error>
357    where
358        <<O as Fetch>::Request as TransportRequest>::Response: Default,
359    {
360        let (rich, wire) =
361            self.encode_rich_to_wire::<Q, <O as Fetch>::Query, <O as Fetch>::Request>(query);
362        self.expect(&rich, wire, object).await?;
363
364        Ok(self)
365    }
366
367    /// Remove previously defined expectation for a [Fetch] request.
368    ///
369    /// Returns `true` if any expectation was removed.
370    pub async fn remove_fetch_expectation<O, Q>(&mut self, query: Q) -> bool
371    where
372        O: Fetch,
373        Q: Query<<O as Fetch>::Query>,
374    {
375        let (rich, wire) =
376            self.encode_rich_to_wire::<Q, <O as Fetch>::Query, <O as Fetch>::Request>(query);
377        self.remove(&rich, wire).await
378    }
379
380    /// Expect a [FetchMany] request and return provided object.
381    ///
382    /// This method is used to define mock expectations for [FetchMany] requests.
383    ///
384    /// ## Generic Parameters
385    ///
386    /// - `O`: Type of the object that will be returned in response to the query.
387    ///   Must implement [FetchMany].
388    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
389    /// - `R`: Collection type for the results. Must implement [MockResponse].
390    ///
391    /// ## Arguments
392    ///
393    /// - `query`: Query that will be sent to Platform.
394    /// - `objects`: Collection of objects that will be returned in response to `query`, or None if no objects are expected.
395    ///
396    /// ## Returns
397    ///
398    /// * Reference to self on success, to allow chaining
399    /// * Error when expectation cannot be set or is already defined for this request
400    ///
401    /// ## Panics
402    ///
403    /// Can panic on errors.
404    ///
405    /// ## Example
406    ///
407    /// Usage example is similar to
408    /// [MockDashPlatformSdk::expect_fetch()], but the expected
409    /// object must be a vector of objects.
410    pub async fn expect_fetch_many<
411        K: Ord,
412        O: FetchMany<K, R>,
413        Q: Query<<O as FetchMany<K, R>>::Query>,
414        R,
415    >(
416        &mut self,
417        query: Q,
418        objects: Option<R>,
419    ) -> Result<&mut Self, Error>
420    where
421        R: FromIterator<(K, Option<O>)>
422            + MockResponse
423            + FromProof<
424                <O as FetchMany<K, R>>::Query,
425                Request = <O as FetchMany<K, R>>::Query,
426                Response = <<O as FetchMany<K, R>>::Request as TransportRequest>::Response,
427            > + Sync
428            + Send
429            + Default,
430        <<O as FetchMany<K, R>>::Request as TransportRequest>::Response: Default,
431    {
432        let (rich, wire) = self
433            .encode_rich_to_wire::<Q, <O as FetchMany<K, R>>::Query, <O as FetchMany<K, R>>::Request>(
434                query,
435            );
436        self.expect(&rich, wire, objects).await?;
437
438        Ok(self)
439    }
440
441    /// Encode a user-facing `query` first into its rich form (`R`) and
442    /// then into its wire form (`W`), both against the SDK's current
443    /// `QuerySettings`. Returns `(rich, wire)` for use as proof-mock /
444    /// DAPI-mock expectation keys.
445    ///
446    /// ## Panics
447    ///
448    /// INTENTIONAL(SEC-001): test-harness fail-fast — encoder errors
449    /// for V1-only `DocumentQuery` features against a V0
450    /// `PlatformVersion` crash the test setup loudly rather than
451    /// silently propagate. Panics also if `set_sdk` was not called.
452    fn encode_rich_to_wire<Q, R, W>(&self, query: Q) -> (R, W)
453    where
454        Q: Query<R>,
455        R: Query<W> + Mockable,
456        W: TransportRequest,
457    {
458        let sdk_guard = self.sdk.load();
459        let sdk = sdk_guard
460            .as_ref()
461            .expect("sdk must be set when creating mock");
462        let settings = sdk.query_settings();
463        let rich: R = query.query(&settings).expect("query must be correct");
464        let wire: W = rich.query(&settings).expect("wire encoding must succeed");
465        (rich, wire)
466    }
467
468    /// Save expectations for a request.
469    ///
470    /// `rich_request` is the user-facing query (what [`FromProof`] binds to) and seeds
471    /// the proof-mock cache key. `wire_request` is the proto that flows over the wire
472    /// and seeds the DAPI executor mock. For non-versioned operations both arguments
473    /// are the same value; for documents the rich form is [`DocumentQuery`] and the
474    /// wire is [`GetDocumentsRequest`].
475    async fn expect<R: Mockable + std::fmt::Debug, W: TransportRequest, O: MockResponse>(
476        &mut self,
477        rich_request: &R,
478        wire_request: W,
479        returned_object: Option<O>,
480    ) -> Result<(), Error>
481    where
482        W::Response: Default,
483    {
484        let key = Key::new(rich_request);
485
486        if self.from_proof_expectations.contains_key(&key) {
487            return Err(MockError::MockExpectationConflict(format!(
488                "proof expectation key {} already defined for {} request: {:?}",
489                key,
490                std::any::type_name::<R>(),
491                rich_request
492            ))
493            .into());
494        }
495
496        self.from_proof_expectations
497            .insert(key, returned_object.mock_serialize(self));
498
499        let mut dapi_guard = self.dapi.lock().await;
500        dapi_guard.expect(
501            &wire_request,
502            &Ok(ExecutionResponse {
503                inner: Default::default(),
504                retries: 0,
505                address: "http://127.0.0.1".parse().expect("failed to parse address"),
506            }),
507        )?;
508
509        Ok(())
510    }
511
512    /// Remove expectations for a request.
513    async fn remove<R: Mockable, W: TransportRequest>(
514        &mut self,
515        rich_request: &R,
516        wire_request: W,
517    ) -> bool {
518        let key = Key::new(rich_request);
519        let removed_from_proof = self.from_proof_expectations.remove(&key).is_some();
520
521        let mut dapi_guard = self.dapi.lock().await;
522        let removed_from_dapi = dapi_guard.remove(&wire_request);
523
524        removed_from_proof || removed_from_dapi
525    }
526
527    /// Wrapper around [FromProof] that uses mock expectations, falling back to [FromProof] if no expectation is found.
528    pub(crate) fn parse_proof_with_metadata<I, O: FromProof<I>>(
529        &self,
530        request: O::Request,
531        response: O::Response,
532    ) -> Result<(Option<O>, ResponseMetadata, Proof), drive_proof_verifier::Error>
533    where
534        O::Request: Mockable,
535        Option<O>: MockResponse,
536        // O: FromProof<<O as FromProof<I>>::Request>,
537    {
538        let key = Key::new(&request);
539
540        let data = match self.from_proof_expectations.get(&key) {
541            // Report the latest protocol version so the proof path's ratchet
542            // (`maybe_update_protocol_version`) fires as it would against a real
543            // network; `default()` reports 0, which the ratchet ignores.
544            Some(d) => (
545                Option::<O>::mock_deserialize(self, d),
546                ResponseMetadata {
547                    protocol_version: dpp::version::LATEST_VERSION,
548                    ..Default::default()
549                },
550                Proof::default(),
551            ),
552            None => {
553                let version = self.version();
554                let provider = self.context_provider()
555                    .ok_or(ContextProviderError::InvalidQuorum(
556                        "expectation not found and quorum info provider not initialized with sdk.mock().quorum_info_dir()".to_string()
557                    ))?;
558                O::maybe_from_proof_with_metadata(
559                    request,
560                    response,
561                    Network::Regtest,
562                    version,
563                    &provider,
564                )?
565            }
566        };
567
568        Ok(data)
569    }
570    /// Return context provider implementation defined for upstream Sdk object.
571    fn context_provider(&self) -> Option<impl ContextProvider> {
572        if let Some(sdk) = self.sdk.load_full() {
573            sdk.clone().context_provider()
574        } else {
575            None
576        }
577    }
578}
579
580/// Load expectation from file and save it to `dapi_guard` mock Dapi client.
581///
582/// This function is used to load expectations from files in a directory.
583/// It is implemented without reference to the `MockDashPlatformSdk` object
584/// to make it easier to use in async context.
585fn load_expectation<T: TransportRequest>(
586    dapi_guard: &mut OwnedMutexGuard<MockDapiClient>,
587    path: &PathBuf,
588) -> Result<(), Error> {
589    let data = DumpData::<T>::load(path)
590        .map_err(|e| {
591            Error::Config(format!(
592                "cannot load mock expectations from {}: {}",
593                path.display(),
594                e
595            ))
596        })?
597        .deserialize();
598    dapi_guard.expect(&data.0, &data.1)?;
599    Ok(())
600}