openzeppelin_relayer/repositories/network/
network_in_memory.rs

1//! This module defines an in-memory network repository for managing
2//! network configurations. It provides functionality to create and retrieve
3//! network configurations, while update and delete operations are not supported.
4//! The repository is implemented using a `Mutex`-protected `HashMap` to
5//! ensure thread safety in asynchronous contexts.
6
7use crate::{
8    models::{NetworkRepoModel, NetworkType, RepositoryError},
9    repositories::{NetworkRepository, PaginatedResult, PaginationQuery, Repository},
10};
11use async_trait::async_trait;
12use eyre::Result;
13use std::collections::HashMap;
14use tokio::sync::{Mutex, MutexGuard};
15
16#[derive(Debug)]
17pub struct InMemoryNetworkRepository {
18    store: Mutex<HashMap<String, NetworkRepoModel>>,
19}
20
21impl Clone for InMemoryNetworkRepository {
22    fn clone(&self) -> Self {
23        // Try to get the current data, or use empty HashMap if lock fails
24        let data = self
25            .store
26            .try_lock()
27            .map(|guard| guard.clone())
28            .unwrap_or_else(|_| HashMap::new());
29
30        Self {
31            store: Mutex::new(data),
32        }
33    }
34}
35
36impl InMemoryNetworkRepository {
37    pub fn new() -> Self {
38        Self {
39            store: Mutex::new(HashMap::new()),
40        }
41    }
42
43    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
44        Ok(lock.lock().await)
45    }
46
47    /// Gets a network by network type and name
48    pub async fn get(
49        &self,
50        network_type: NetworkType,
51        name: &str,
52    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
53        let store = Self::acquire_lock(&self.store).await?;
54        for (_, network) in store.iter() {
55            if network.network_type == network_type && network.name == name {
56                return Ok(Some(network.clone()));
57            }
58        }
59        Ok(None)
60    }
61}
62
63impl Default for InMemoryNetworkRepository {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69#[async_trait]
70impl Repository<NetworkRepoModel, String> for InMemoryNetworkRepository {
71    async fn create(&self, network: NetworkRepoModel) -> Result<NetworkRepoModel, RepositoryError> {
72        let mut store = Self::acquire_lock(&self.store).await?;
73        if store.contains_key(&network.id) {
74            return Err(RepositoryError::ConstraintViolation(format!(
75                "Network with ID {} already exists",
76                network.id
77            )));
78        }
79        store.insert(network.id.clone(), network.clone());
80        Ok(network)
81    }
82
83    async fn get_by_id(&self, id: String) -> Result<NetworkRepoModel, RepositoryError> {
84        let store = Self::acquire_lock(&self.store).await?;
85        match store.get(&id) {
86            Some(network) => Ok(network.clone()),
87            None => Err(RepositoryError::NotFound(format!(
88                "Network with ID {} not found",
89                id
90            ))),
91        }
92    }
93
94    async fn update(
95        &self,
96        _id: String,
97        _network: NetworkRepoModel,
98    ) -> Result<NetworkRepoModel, RepositoryError> {
99        Err(RepositoryError::NotSupported("Not supported".to_string()))
100    }
101
102    async fn delete_by_id(&self, _id: String) -> Result<(), RepositoryError> {
103        Err(RepositoryError::NotSupported("Not supported".to_string()))
104    }
105
106    async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
107        let store = Self::acquire_lock(&self.store).await?;
108        let networks: Vec<NetworkRepoModel> = store.values().cloned().collect();
109        Ok(networks)
110    }
111
112    async fn list_paginated(
113        &self,
114        _query: PaginationQuery,
115    ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
116        Err(RepositoryError::NotSupported("Not supported".to_string()))
117    }
118
119    async fn count(&self) -> Result<usize, RepositoryError> {
120        let store = Self::acquire_lock(&self.store).await?;
121        Ok(store.len())
122    }
123
124    async fn has_entries(&self) -> Result<bool, RepositoryError> {
125        let store = Self::acquire_lock(&self.store).await?;
126        Ok(!store.is_empty())
127    }
128
129    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
130        let mut store = Self::acquire_lock(&self.store).await?;
131        store.clear();
132        Ok(())
133    }
134}
135
136#[async_trait]
137impl NetworkRepository for InMemoryNetworkRepository {
138    async fn get_by_name(
139        &self,
140        network_type: NetworkType,
141        name: &str,
142    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
143        self.get(network_type, name).await
144    }
145
146    async fn get_by_chain_id(
147        &self,
148        network_type: NetworkType,
149        chain_id: u64,
150    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
151        // Only EVM networks have chain_id
152        if network_type != NetworkType::Evm {
153            return Ok(None);
154        }
155
156        let store = Self::acquire_lock(&self.store).await?;
157        for (_, network) in store.iter() {
158            if network.network_type == network_type {
159                if let crate::models::NetworkConfigData::Evm(evm_config) = &network.config {
160                    if evm_config.chain_id == Some(chain_id) {
161                        return Ok(Some(network.clone()));
162                    }
163                }
164            }
165        }
166        Ok(None)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use crate::config::{
173        EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
174    };
175
176    use super::*;
177
178    fn create_test_network(name: String, network_type: NetworkType) -> NetworkRepoModel {
179        let common = NetworkConfigCommon {
180            network: name.clone(),
181            from: None,
182            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
183            explorer_urls: None,
184            average_blocktime_ms: None,
185            is_testnet: Some(true),
186            tags: None,
187        };
188
189        match network_type {
190            NetworkType::Evm => {
191                let evm_config = EvmNetworkConfig {
192                    common,
193                    chain_id: Some(1),
194                    required_confirmations: Some(1),
195                    features: None,
196                    symbol: Some("ETH".to_string()),
197                    gas_price_cache: None,
198                };
199                NetworkRepoModel::new_evm(evm_config)
200            }
201            NetworkType::Solana => {
202                let solana_config = SolanaNetworkConfig { common };
203                NetworkRepoModel::new_solana(solana_config)
204            }
205            NetworkType::Stellar => {
206                let stellar_config = StellarNetworkConfig {
207                    common,
208                    passphrase: None,
209                };
210                NetworkRepoModel::new_stellar(stellar_config)
211            }
212        }
213    }
214
215    #[tokio::test]
216    async fn test_new_repository_is_empty() {
217        let repo = InMemoryNetworkRepository::new();
218        assert_eq!(repo.count().await.unwrap(), 0);
219    }
220
221    #[tokio::test]
222    async fn test_create_network() {
223        let repo = InMemoryNetworkRepository::new();
224        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
225
226        repo.create(network.clone()).await.unwrap();
227        assert_eq!(repo.count().await.unwrap(), 1);
228
229        let stored = repo.get_by_id(network.id.clone()).await.unwrap();
230        assert_eq!(stored.id, network.id);
231        assert_eq!(stored.name, network.name);
232    }
233
234    #[tokio::test]
235    async fn test_get_network_by_type_and_name() {
236        let repo = InMemoryNetworkRepository::new();
237        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
238
239        repo.create(network.clone()).await.unwrap();
240
241        let retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
242        assert!(retrieved.is_some());
243        assert_eq!(retrieved.unwrap().name, "mainnet");
244    }
245
246    #[tokio::test]
247    async fn test_get_nonexistent_network() {
248        let repo = InMemoryNetworkRepository::new();
249
250        let result = repo.get(NetworkType::Evm, "nonexistent").await.unwrap();
251        assert!(result.is_none());
252    }
253
254    #[tokio::test]
255    async fn test_create_duplicate_network() {
256        let repo = InMemoryNetworkRepository::new();
257        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
258
259        repo.create(network.clone()).await.unwrap();
260        let result = repo.create(network).await;
261
262        assert!(matches!(
263            result,
264            Err(RepositoryError::ConstraintViolation(_))
265        ));
266    }
267
268    #[tokio::test]
269    async fn test_different_network_types_same_name() {
270        let repo = InMemoryNetworkRepository::new();
271        let evm_network = create_test_network("mainnet".to_string(), NetworkType::Evm);
272        let solana_network = create_test_network("mainnet".to_string(), NetworkType::Solana);
273
274        repo.create(evm_network.clone()).await.unwrap();
275        repo.create(solana_network.clone()).await.unwrap();
276
277        assert_eq!(repo.count().await.unwrap(), 2);
278
279        let evm_retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
280        let solana_retrieved = repo.get(NetworkType::Solana, "mainnet").await.unwrap();
281
282        assert!(evm_retrieved.is_some());
283        assert!(solana_retrieved.is_some());
284        assert_eq!(evm_retrieved.unwrap().network_type, NetworkType::Evm);
285        assert_eq!(solana_retrieved.unwrap().network_type, NetworkType::Solana);
286    }
287
288    #[tokio::test]
289    async fn test_unsupported_operations() {
290        let repo = InMemoryNetworkRepository::new();
291        let network = create_test_network("test".to_string(), NetworkType::Evm);
292
293        let update_result = repo.update("test".to_string(), network.clone()).await;
294        assert!(matches!(
295            update_result,
296            Err(RepositoryError::NotSupported(_))
297        ));
298
299        let delete_result = repo.delete_by_id("test".to_string()).await;
300        assert!(matches!(
301            delete_result,
302            Err(RepositoryError::NotSupported(_))
303        ));
304
305        let pagination_result = repo
306            .list_paginated(PaginationQuery {
307                page: 1,
308                per_page: 10,
309            })
310            .await;
311        assert!(matches!(
312            pagination_result,
313            Err(RepositoryError::NotSupported(_))
314        ));
315    }
316
317    #[tokio::test]
318    async fn test_has_entries() {
319        let repo = InMemoryNetworkRepository::new();
320        assert!(!repo.has_entries().await.unwrap());
321
322        let network = create_test_network("test".to_string(), NetworkType::Evm);
323
324        repo.create(network.clone()).await.unwrap();
325        assert!(repo.has_entries().await.unwrap());
326    }
327
328    #[tokio::test]
329    async fn test_drop_all_entries() {
330        let repo = InMemoryNetworkRepository::new();
331        let network = create_test_network("test".to_string(), NetworkType::Evm);
332
333        repo.create(network.clone()).await.unwrap();
334        assert!(repo.has_entries().await.unwrap());
335
336        repo.drop_all_entries().await.unwrap();
337        assert!(!repo.has_entries().await.unwrap());
338    }
339}