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                "GetDataContractHistoryRequest" => {
160                    load_expectation::<proto::GetDataContractHistoryRequest>(&mut dapi, filename)?
161                }
162                "GetDocumentHistoryRequest" => {
163                    load_expectation::<proto::GetDocumentHistoryRequest>(&mut dapi, filename)?
164                }
165                "IdentityRequest" => load_expectation::<IdentityRequest>(&mut dapi, filename)?,
166                "GetIdentityRequest" => {
167                    load_expectation::<proto::GetIdentityRequest>(&mut dapi, filename)?
168                }
169
170                "GetIdentityBalanceRequest" => {
171                    load_expectation::<proto::GetIdentityBalanceRequest>(&mut dapi, filename)?
172                }
173                "GetIdentityContractNonceRequest" => {
174                    load_expectation::<proto::GetIdentityContractNonceRequest>(&mut dapi, filename)?
175                }
176                "GetIdentityBalanceAndRevisionRequest" => load_expectation::<
177                    proto::GetIdentityBalanceAndRevisionRequest,
178                >(&mut dapi, filename)?,
179                "GetAddressInfoRequest" => {
180                    load_expectation::<proto::GetAddressInfoRequest>(&mut dapi, filename)?
181                }
182                "GetAddressesInfosRequest" => {
183                    load_expectation::<proto::GetAddressesInfosRequest>(&mut dapi, filename)?
184                }
185                "GetIdentityKeysRequest" => {
186                    load_expectation::<proto::GetIdentityKeysRequest>(&mut dapi, filename)?
187                }
188                "GetProtocolVersionUpgradeStateRequest" => load_expectation::<
189                    proto::GetProtocolVersionUpgradeStateRequest,
190                >(&mut dapi, filename)?,
191                "GetProtocolVersionUpgradeVoteStatusRequest" => {
192                    load_expectation::<proto::GetProtocolVersionUpgradeVoteStatusRequest>(
193                        &mut dapi, filename,
194                    )?
195                }
196                "GetContestedResourcesRequest" => {
197                    load_expectation::<proto::GetContestedResourcesRequest>(&mut dapi, filename)?
198                }
199                "GetContestedResourceVoteStateRequest" => load_expectation::<
200                    proto::GetContestedResourceVoteStateRequest,
201                >(&mut dapi, filename)?,
202                "GetContestedResourceVotersForIdentityRequest" => {
203                    load_expectation::<proto::GetContestedResourceVotersForIdentityRequest>(
204                        &mut dapi, filename,
205                    )?
206                }
207                "GetContestedResourceIdentityVotesRequest" => {
208                    load_expectation::<proto::GetContestedResourceIdentityVotesRequest>(
209                        &mut dapi, filename,
210                    )?
211                }
212                "GetVotePollsByEndDateRequest" => {
213                    load_expectation::<proto::GetVotePollsByEndDateRequest>(&mut dapi, filename)?
214                }
215                "GetPrefundedSpecializedBalanceRequest" => load_expectation::<
216                    proto::GetPrefundedSpecializedBalanceRequest,
217                >(&mut dapi, filename)?,
218                "GetPathElementsRequest" => {
219                    load_expectation::<proto::GetPathElementsRequest>(&mut dapi, filename)?
220                }
221                "GetTotalCreditsInPlatformRequest" => load_expectation::<
222                    proto::GetTotalCreditsInPlatformRequest,
223                >(&mut dapi, filename)?,
224                "GetIdentityTokenBalancesRequest" => {
225                    load_expectation::<proto::GetIdentityTokenBalancesRequest>(&mut dapi, filename)?
226                }
227                "GetIdentitiesTokenBalancesRequest" => load_expectation::<
228                    proto::GetIdentitiesTokenBalancesRequest,
229                >(&mut dapi, filename)?,
230                "GetIdentityTokenInfosRequest" => {
231                    load_expectation::<proto::GetIdentityTokenInfosRequest>(&mut dapi, filename)?
232                }
233                "GetIdentitiesTokenInfosRequest" => {
234                    load_expectation::<proto::GetIdentitiesTokenInfosRequest>(&mut dapi, filename)?
235                }
236                "GetTokenStatusesRequest" => {
237                    load_expectation::<proto::GetTokenStatusesRequest>(&mut dapi, filename)?
238                }
239                "GetTokenTotalSupplyRequest" => {
240                    load_expectation::<proto::GetTokenTotalSupplyRequest>(&mut dapi, filename)?
241                }
242                "GetGroupInfoRequest" => {
243                    load_expectation::<proto::GetGroupInfoRequest>(&mut dapi, filename)?
244                }
245                "GetGroupInfosRequest" => {
246                    load_expectation::<proto::GetGroupInfosRequest>(&mut dapi, filename)?
247                }
248                "GetGroupActionsRequest" => {
249                    load_expectation::<proto::GetGroupActionsRequest>(&mut dapi, filename)?
250                }
251                "GetGroupActionSignersRequest" => {
252                    load_expectation::<proto::GetGroupActionSignersRequest>(&mut dapi, filename)?
253                }
254                "EvoNode" => load_expectation::<EvoNode>(&mut dapi, filename)?,
255                "GetTokenDirectPurchasePricesRequest" => load_expectation::<
256                    proto::GetTokenDirectPurchasePricesRequest,
257                >(&mut dapi, filename)?,
258                "GetTokenPerpetualDistributionLastClaimRequest" => {
259                    load_expectation::<proto::GetTokenPerpetualDistributionLastClaimRequest>(
260                        &mut dapi, filename,
261                    )?
262                }
263                "GetTokenPreProgrammedDistributionsRequest" => {
264                    load_expectation::<proto::GetTokenPreProgrammedDistributionsRequest>(
265                        &mut dapi, filename,
266                    )?
267                }
268                "GetAddressesTrunkStateRequest" => {
269                    load_expectation::<proto::GetAddressesTrunkStateRequest>(&mut dapi, filename)?
270                }
271                _ => {
272                    return Err(Error::Config(format!(
273                        "unknown request type {} in {}, missing match arm in load_expectations?",
274                        request_type,
275                        filename.display()
276                    )))
277                }
278            };
279        }
280
281        Ok(self)
282    }
283
284    /// Expect a [Fetch] request and return provided object.
285    ///
286    /// This method is used to define mock expectations for [Fetch] requests.
287    ///
288    /// ## Generic Parameters
289    ///
290    /// - `O`: Type of the object that will be returned in response to the query. Must implement [Fetch] and [MockResponse].
291    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
292    ///
293    /// ## Arguments
294    ///
295    /// - `query`: Query that will be sent to Platform.
296    /// - `object`: Object that will be returned in response to `query`, or None if the object is expected to not exist.
297    ///
298    /// ## Returns
299    ///
300    /// * Reference to self on success, to allow chaining
301    /// * Error when expectation cannot be set or is already defined for this request
302    ///
303    /// ## Panics
304    ///
305    /// Can panic on errors.
306    ///
307    /// ## Example
308    ///
309    /// ```no_run
310    /// # let r = tokio::runtime::Runtime::new().unwrap();
311    /// #
312    /// # r.block_on(async {
313    ///     use dash_sdk::{Sdk, platform::{Identity, Fetch, dpp::identity::accessors::IdentityGettersV0}};
314    ///
315    ///     let mut api = Sdk::new_mock();
316    ///     // Define expected response
317    ///     let expected: Identity = Identity::random_identity(1, None, api.version())
318    ///         .expect("create expected identity");
319    ///     // Define query that will be sent
320    ///     let query = expected.id();
321    ///     // Expect that in response to `query`, `expected` will be returned
322    ///     api.mock().expect_fetch(query, Some(expected.clone())).await.unwrap();
323    ///
324    ///     // Fetch the identity
325    ///     let retrieved = dpp::prelude::Identity::fetch(&api, query)
326    ///         .await
327    ///         .unwrap()
328    ///         .expect("object should exist");
329    ///
330    ///     // Check that the identity is the same as expected
331    ///     assert_eq!(retrieved, expected);
332    /// # });
333    /// ```
334    pub async fn expect_fetch<O: Fetch + MockResponse, Q: Query<<O as Fetch>::Query>>(
335        &mut self,
336        query: Q,
337        object: Option<O>,
338    ) -> Result<&mut Self, Error>
339    where
340        <<O as Fetch>::Request as TransportRequest>::Response: Default,
341    {
342        let (rich, wire) =
343            self.encode_rich_to_wire::<Q, <O as Fetch>::Query, <O as Fetch>::Request>(query);
344        self.expect(&rich, wire, object).await?;
345
346        Ok(self)
347    }
348
349    /// Remove previously defined expectation for a [Fetch] request.
350    ///
351    /// Returns `true` if any expectation was removed.
352    pub async fn remove_fetch_expectation<O, Q>(&mut self, query: Q) -> bool
353    where
354        O: Fetch,
355        Q: Query<<O as Fetch>::Query>,
356    {
357        let (rich, wire) =
358            self.encode_rich_to_wire::<Q, <O as Fetch>::Query, <O as Fetch>::Request>(query);
359        self.remove(&rich, wire).await
360    }
361
362    /// Expect a [FetchMany] request and return provided object.
363    ///
364    /// This method is used to define mock expectations for [FetchMany] requests.
365    ///
366    /// ## Generic Parameters
367    ///
368    /// - `O`: Type of the object that will be returned in response to the query.
369    ///   Must implement [FetchMany].
370    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
371    /// - `R`: Collection type for the results. Must implement [MockResponse].
372    ///
373    /// ## Arguments
374    ///
375    /// - `query`: Query that will be sent to Platform.
376    /// - `objects`: Collection of objects that will be returned in response to `query`, or None if no objects are expected.
377    ///
378    /// ## Returns
379    ///
380    /// * Reference to self on success, to allow chaining
381    /// * Error when expectation cannot be set or is already defined for this request
382    ///
383    /// ## Panics
384    ///
385    /// Can panic on errors.
386    ///
387    /// ## Example
388    ///
389    /// Usage example is similar to
390    /// [MockDashPlatformSdk::expect_fetch()], but the expected
391    /// object must be a vector of objects.
392    pub async fn expect_fetch_many<
393        K: Ord,
394        O: FetchMany<K, R>,
395        Q: Query<<O as FetchMany<K, R>>::Query>,
396        R,
397    >(
398        &mut self,
399        query: Q,
400        objects: Option<R>,
401    ) -> Result<&mut Self, Error>
402    where
403        R: FromIterator<(K, Option<O>)>
404            + MockResponse
405            + FromProof<
406                <O as FetchMany<K, R>>::Query,
407                Request = <O as FetchMany<K, R>>::Query,
408                Response = <<O as FetchMany<K, R>>::Request as TransportRequest>::Response,
409            > + Sync
410            + Send
411            + Default,
412        <<O as FetchMany<K, R>>::Request as TransportRequest>::Response: Default,
413    {
414        let (rich, wire) = self
415            .encode_rich_to_wire::<Q, <O as FetchMany<K, R>>::Query, <O as FetchMany<K, R>>::Request>(
416                query,
417            );
418        self.expect(&rich, wire, objects).await?;
419
420        Ok(self)
421    }
422
423    /// Encode a user-facing `query` first into its rich form (`R`) and
424    /// then into its wire form (`W`), both against the SDK's current
425    /// `QuerySettings`. Returns `(rich, wire)` for use as proof-mock /
426    /// DAPI-mock expectation keys.
427    ///
428    /// ## Panics
429    ///
430    /// INTENTIONAL(SEC-001): test-harness fail-fast — encoder errors
431    /// for V1-only `DocumentQuery` features against a V0
432    /// `PlatformVersion` crash the test setup loudly rather than
433    /// silently propagate. Panics also if `set_sdk` was not called.
434    fn encode_rich_to_wire<Q, R, W>(&self, query: Q) -> (R, W)
435    where
436        Q: Query<R>,
437        R: Query<W> + Mockable,
438        W: TransportRequest,
439    {
440        let sdk_guard = self.sdk.load();
441        let sdk = sdk_guard
442            .as_ref()
443            .expect("sdk must be set when creating mock");
444        let settings = sdk.query_settings();
445        let rich: R = query.query(&settings).expect("query must be correct");
446        let wire: W = rich.query(&settings).expect("wire encoding must succeed");
447        (rich, wire)
448    }
449
450    /// Save expectations for a request.
451    ///
452    /// `rich_request` is the user-facing query (what [`FromProof`] binds to) and seeds
453    /// the proof-mock cache key. `wire_request` is the proto that flows over the wire
454    /// and seeds the DAPI executor mock. For non-versioned operations both arguments
455    /// are the same value; for documents the rich form is [`DocumentQuery`] and the
456    /// wire is [`GetDocumentsRequest`].
457    async fn expect<R: Mockable + std::fmt::Debug, W: TransportRequest, O: MockResponse>(
458        &mut self,
459        rich_request: &R,
460        wire_request: W,
461        returned_object: Option<O>,
462    ) -> Result<(), Error>
463    where
464        W::Response: Default,
465    {
466        let key = Key::new(rich_request);
467
468        if self.from_proof_expectations.contains_key(&key) {
469            return Err(MockError::MockExpectationConflict(format!(
470                "proof expectation key {} already defined for {} request: {:?}",
471                key,
472                std::any::type_name::<R>(),
473                rich_request
474            ))
475            .into());
476        }
477
478        self.from_proof_expectations
479            .insert(key, returned_object.mock_serialize(self));
480
481        let mut dapi_guard = self.dapi.lock().await;
482        dapi_guard.expect(
483            &wire_request,
484            &Ok(ExecutionResponse {
485                inner: Default::default(),
486                retries: 0,
487                address: "http://127.0.0.1".parse().expect("failed to parse address"),
488            }),
489        )?;
490
491        Ok(())
492    }
493
494    /// Remove expectations for a request.
495    async fn remove<R: Mockable, W: TransportRequest>(
496        &mut self,
497        rich_request: &R,
498        wire_request: W,
499    ) -> bool {
500        let key = Key::new(rich_request);
501        let removed_from_proof = self.from_proof_expectations.remove(&key).is_some();
502
503        let mut dapi_guard = self.dapi.lock().await;
504        let removed_from_dapi = dapi_guard.remove(&wire_request);
505
506        removed_from_proof || removed_from_dapi
507    }
508
509    /// Wrapper around [FromProof] that uses mock expectations, falling back to [FromProof] if no expectation is found.
510    pub(crate) fn parse_proof_with_metadata<I, O: FromProof<I>>(
511        &self,
512        request: O::Request,
513        response: O::Response,
514    ) -> Result<(Option<O>, ResponseMetadata, Proof), drive_proof_verifier::Error>
515    where
516        O::Request: Mockable,
517        Option<O>: MockResponse,
518        // O: FromProof<<O as FromProof<I>>::Request>,
519    {
520        let key = Key::new(&request);
521
522        let data = match self.from_proof_expectations.get(&key) {
523            Some(d) => (
524                Option::<O>::mock_deserialize(self, d),
525                ResponseMetadata::default(),
526                Proof::default(),
527            ),
528            None => {
529                let version = self.version();
530                let provider = self.context_provider()
531                    .ok_or(ContextProviderError::InvalidQuorum(
532                        "expectation not found and quorum info provider not initialized with sdk.mock().quorum_info_dir()".to_string()
533                    ))?;
534                O::maybe_from_proof_with_metadata(
535                    request,
536                    response,
537                    Network::Regtest,
538                    version,
539                    &provider,
540                )?
541            }
542        };
543
544        Ok(data)
545    }
546    /// Return context provider implementation defined for upstream Sdk object.
547    fn context_provider(&self) -> Option<impl ContextProvider> {
548        if let Some(sdk) = self.sdk.load_full() {
549            sdk.clone().context_provider()
550        } else {
551            None
552        }
553    }
554}
555
556/// Load expectation from file and save it to `dapi_guard` mock Dapi client.
557///
558/// This function is used to load expectations from files in a directory.
559/// It is implemented without reference to the `MockDashPlatformSdk` object
560/// to make it easier to use in async context.
561fn load_expectation<T: TransportRequest>(
562    dapi_guard: &mut OwnedMutexGuard<MockDapiClient>,
563    path: &PathBuf,
564) -> Result<(), Error> {
565    let data = DumpData::<T>::load(path)
566        .map_err(|e| {
567            Error::Config(format!(
568                "cannot load mock expectations from {}: {}",
569                path.display(),
570                e
571            ))
572        })?
573        .deserialize();
574    dapi_guard.expect(&data.0, &data.1)?;
575    Ok(())
576}