dash_sdk/mock/
provider.rs

1//! Example ContextProvider that uses the Core RPC API and the Sdk to fetch data.
2
3use crate::core::LowLevelDashCoreClient;
4use crate::platform::Fetch;
5use crate::sync::block_on;
6use crate::{Error, Sdk};
7use arc_swap::ArcSwapAny;
8use dash_context_provider::{ContextProvider, ContextProviderError};
9use dpp::data_contract::TokenConfiguration;
10use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
11use dpp::version::PlatformVersion;
12use std::hash::Hash;
13use std::num::NonZeroUsize;
14use std::sync::Arc;
15
16/// Context provider that uses the Core RPC API and the Sdk to fetch data.
17///
18/// Example [ContextProvider] used by the Sdk for testing purposes.
19pub struct GrpcContextProvider {
20    /// Core client
21    core: LowLevelDashCoreClient,
22    /// [Sdk] to use when fetching data from Platform
23    ///
24    /// Note that if the `sdk` is `None`, the context provider will not be able to fetch data itself and will rely on
25    /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`.
26    ///
27    /// We use [Arc] as we have circular dependencies between Sdk and ContextProvider.
28    sdk: ArcSwapAny<Arc<Option<Sdk>>>,
29
30    /// Data contracts cache.
31    ///
32    /// Users can insert new data contracts into the cache using [`Cache::put`].
33    pub data_contracts_cache: Cache<Identifier, DataContract>,
34
35    /// Token configurations cache.
36    ///
37    /// Users can insert new token configurations into the cache using [`Cache::put`].
38    pub token_configurations_cache: Cache<Identifier, TokenConfiguration>,
39
40    /// Quorum public keys cache.
41    ///
42    /// Key is a tuple of quorum hash and quorum type. Value is a quorum public key.
43    ///
44    /// Users can insert new quorum public keys into the cache using [`Cache::put`].
45    pub quorum_public_keys_cache: Cache<([u8; 32], u32), [u8; 48]>,
46
47    /// Directory where to store dumped data.
48    ///
49    /// This is used to store data that is fetched from Platform and can be used for testing purposes.
50    #[cfg(feature = "mocks")]
51    pub dump_dir: Option<std::path::PathBuf>,
52}
53
54impl GrpcContextProvider {
55    /// Create new context provider.
56    ///
57    /// Note that if the `sdk` is `None`, the context provider will not be able to fetch data itself and will rely on
58    /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`.
59    ///
60    /// Sdk can be set later with [`GrpcContextProvider::set_sdk`].
61    #[allow(clippy::too_many_arguments)]
62    pub fn new(
63        sdk: Option<Sdk>,
64        core_ip: &str,
65        core_port: u16,
66        core_user: &str,
67        core_password: &str,
68
69        data_contracts_cache_size: NonZeroUsize,
70        token_config_cache_size: NonZeroUsize,
71        quorum_public_keys_cache_size: NonZeroUsize,
72    ) -> Result<Self, Error> {
73        let core_client =
74            LowLevelDashCoreClient::new(core_ip, core_port, core_user, core_password)?;
75        Ok(Self {
76            core: core_client,
77            sdk: ArcSwapAny::new(Arc::new(sdk)),
78            data_contracts_cache: Cache::new(data_contracts_cache_size),
79            token_configurations_cache: Cache::new(token_config_cache_size),
80            quorum_public_keys_cache: Cache::new(quorum_public_keys_cache_size),
81            #[cfg(feature = "mocks")]
82            dump_dir: None,
83        })
84    }
85
86    /// Set the Sdk to use when fetching data from Platform.
87    /// This is useful when the Sdk is created after the ContextProvider.
88    ///
89    /// Note that if the `sdk` is `None`, the context provider will not be able to fetch data itself and will rely on
90    /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`.
91    pub fn set_sdk(&self, sdk: Option<Sdk>) {
92        self.sdk.store(Arc::new(sdk));
93    }
94    /// Set the directory where to store dumped data.
95    ///
96    /// When set, the context provider will store data fetched from Platform into this directory.
97    #[cfg(feature = "mocks")]
98    pub fn set_dump_dir(&mut self, dump_dir: Option<std::path::PathBuf>) {
99        self.dump_dir = dump_dir;
100    }
101
102    /// Save quorum public key to disk.
103    ///
104    /// Files are named: `quorum_pubkey-<int_quorum_type>-<hex_quorum_hash>.json`
105    ///
106    /// Note that this will overwrite files with the same quorum type and quorum hash.
107    ///
108    /// Any errors are logged on `warn` level and ignored.
109    #[cfg(feature = "mocks")]
110    fn dump_quorum_public_key(
111        &self,
112        quorum_type: u32,
113        quorum_hash: [u8; 32],
114        _core_chain_locked_height: u32,
115        public_key: &[u8],
116    ) {
117        use hex::ToHex;
118
119        let path = match &self.dump_dir {
120            Some(p) => p,
121            None => return,
122        };
123
124        let encoded = hex::encode(public_key);
125
126        let file = path.join(format!(
127            "quorum_pubkey-{}-{}.json",
128            quorum_type,
129            quorum_hash.encode_hex::<String>()
130        ));
131
132        if let Err(e) = std::fs::write(file, encoded) {
133            tracing::warn!("Unable to write dump file {:?}: {}", path, e);
134        }
135    }
136
137    /// Save data contract to disk.
138    ///
139    /// Files are named: `data_contract-<hex_data_contract_id>.json`
140    ///
141    /// Note that this will overwrite files with the same data contract ID.
142    ///
143    /// Any errors are logged on `warn` level and ignored.
144    #[cfg(feature = "mocks")]
145    fn dump_data_contract(&self, data_contract: &DataContract) {
146        use dpp::data_contract::accessors::v0::DataContractV0Getters;
147        use hex::ToHex;
148
149        let path = match &self.dump_dir {
150            Some(p) => p,
151            None => return,
152        };
153        let id = data_contract.id();
154
155        let file = path.join(format!("data_contract-{}.json", id.encode_hex::<String>()));
156
157        let encoded = serde_json::to_vec(data_contract).expect("serialize data contract");
158        if let Err(e) = std::fs::write(file, encoded) {
159            tracing::warn!("Unable to write dump file {:?}: {}", path, e);
160        }
161    }
162}
163
164impl ContextProvider for GrpcContextProvider {
165    fn get_quorum_public_key(
166        &self,
167        quorum_type: u32,
168        quorum_hash: [u8; 32], // quorum hash is 32 bytes
169        core_chain_locked_height: u32,
170    ) -> Result<[u8; 48], ContextProviderError> {
171        if let Some(key) = self
172            .quorum_public_keys_cache
173            .get(&(quorum_hash, quorum_type))
174        {
175            return Ok(*key);
176        };
177
178        let key = self.core.get_quorum_public_key(quorum_type, quorum_hash)?;
179
180        self.quorum_public_keys_cache
181            .put((quorum_hash, quorum_type), key);
182
183        #[cfg(feature = "mocks")]
184        self.dump_quorum_public_key(quorum_type, quorum_hash, core_chain_locked_height, &key);
185
186        Ok(key)
187    }
188
189    fn get_data_contract(
190        &self,
191        data_contract_id: &Identifier,
192        _platform_version: &PlatformVersion,
193    ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
194        if let Some(contract) = self.data_contracts_cache.get(data_contract_id) {
195            return Ok(Some(contract));
196        };
197        let sdk_guard = self.sdk.load();
198
199        let sdk = match sdk_guard.as_ref() {
200            Some(sdk) => sdk,
201            None => {
202                tracing::warn!("data contract cache miss and no sdk provided, skipping fetch");
203                return Ok(None);
204            }
205        };
206
207        let contract_id = *data_contract_id;
208
209        let sdk_cloned = sdk.clone();
210
211        let data_contract: Option<DataContract> =
212            block_on(async move { DataContract::fetch(&sdk_cloned, contract_id).await })?
213                .map_err(|e| ContextProviderError::DataContractFailure(e.to_string()))?;
214
215        if let Some(ref dc) = data_contract {
216            self.data_contracts_cache.put(*data_contract_id, dc.clone());
217        };
218
219        #[cfg(feature = "mocks")]
220        if let Some(ref dc) = data_contract {
221            self.dump_data_contract(dc);
222        }
223
224        Ok(data_contract.map(Arc::new))
225    }
226
227    fn get_token_configuration(
228        &self,
229        token_id: &Identifier,
230    ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
231        if let Some(config) = self.token_configurations_cache.get(token_id) {
232            Ok(Some((*config).clone()))
233        } else {
234            tracing::warn!("token config cache miss");
235            Ok(None)
236        }
237    }
238
239    fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
240        self.core.get_platform_activation_height()
241    }
242}
243
244/// Thread-safe cache of various objects inside the SDK.
245///
246/// This is used to cache objects that are expensive to fetch from Platform, like data contracts.
247pub struct Cache<K: Hash + Eq, V> {
248    // We use a RwLock to allow access to the cache when we don't have mutable &self
249    // And we use Arc to allow multiple threads to access the cache without having to clone it
250    inner: std::sync::RwLock<lru::LruCache<K, Arc<V>>>,
251}
252
253impl<K: Hash + Eq, V> Cache<K, V> {
254    /// Create new cache
255    pub fn new(capacity: NonZeroUsize) -> Self {
256        Self {
257            // inner: std::sync::Mutex::new(lru::LruCache::new(capacity)),
258            inner: std::sync::RwLock::new(lru::LruCache::new(capacity)),
259        }
260    }
261
262    /// Get a reference to the value stored under `k`.
263    pub fn get(&self, k: &K) -> Option<Arc<V>> {
264        let mut guard = self.inner.write().expect("cache lock poisoned");
265        guard.get(k).map(Arc::clone)
266    }
267
268    /// Insert a new value into the cache.
269    pub fn put(&self, k: K, v: V) {
270        let mut guard = self.inner.write().expect("cache lock poisoned");
271        guard.put(k, Arc::new(v));
272    }
273}