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 {id} not found"
89            ))),
90        }
91    }
92
93    async fn update(
94        &self,
95        _id: String,
96        _network: NetworkRepoModel,
97    ) -> Result<NetworkRepoModel, RepositoryError> {
98        Err(RepositoryError::NotSupported("Not supported".to_string()))
99    }
100
101    async fn delete_by_id(&self, _id: String) -> Result<(), RepositoryError> {
102        Err(RepositoryError::NotSupported("Not supported".to_string()))
103    }
104
105    async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
106        let store = Self::acquire_lock(&self.store).await?;
107        let networks: Vec<NetworkRepoModel> = store.values().cloned().collect();
108        Ok(networks)
109    }
110
111    async fn list_paginated(
112        &self,
113        _query: PaginationQuery,
114    ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
115        Err(RepositoryError::NotSupported("Not supported".to_string()))
116    }
117
118    async fn count(&self) -> Result<usize, RepositoryError> {
119        let store = Self::acquire_lock(&self.store).await?;
120        Ok(store.len())
121    }
122
123    async fn has_entries(&self) -> Result<bool, RepositoryError> {
124        let store = Self::acquire_lock(&self.store).await?;
125        Ok(!store.is_empty())
126    }
127
128    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
129        let mut store = Self::acquire_lock(&self.store).await?;
130        store.clear();
131        Ok(())
132    }
133}
134
135#[async_trait]
136impl NetworkRepository for InMemoryNetworkRepository {
137    async fn get_by_name(
138        &self,
139        network_type: NetworkType,
140        name: &str,
141    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
142        self.get(network_type, name).await
143    }
144
145    async fn get_by_chain_id(
146        &self,
147        network_type: NetworkType,
148        chain_id: u64,
149    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
150        // Only EVM networks have chain_id
151        if network_type != NetworkType::Evm {
152            return Ok(None);
153        }
154
155        let store = Self::acquire_lock(&self.store).await?;
156        for (_, network) in store.iter() {
157            if network.network_type == network_type {
158                if let crate::models::NetworkConfigData::Evm(evm_config) = &network.config {
159                    if evm_config.chain_id == Some(chain_id) {
160                        return Ok(Some(network.clone()));
161                    }
162                }
163            }
164        }
165        Ok(None)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::config::{
172        EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
173    };
174
175    use super::*;
176
177    fn create_test_network(name: String, network_type: NetworkType) -> NetworkRepoModel {
178        let common = NetworkConfigCommon {
179            network: name.clone(),
180            from: None,
181            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
182            explorer_urls: None,
183            average_blocktime_ms: None,
184            is_testnet: Some(true),
185            tags: None,
186        };
187
188        match network_type {
189            NetworkType::Evm => {
190                let evm_config = EvmNetworkConfig {
191                    common,
192                    chain_id: Some(1),
193                    required_confirmations: Some(1),
194                    features: None,
195                    symbol: Some("ETH".to_string()),
196                    gas_price_cache: None,
197                };
198                NetworkRepoModel::new_evm(evm_config)
199            }
200            NetworkType::Solana => {
201                let solana_config = SolanaNetworkConfig { common };
202                NetworkRepoModel::new_solana(solana_config)
203            }
204            NetworkType::Stellar => {
205                let stellar_config = StellarNetworkConfig {
206                    common,
207                    passphrase: None,
208                    horizon_url: 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}