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                };
209                NetworkRepoModel::new_stellar(stellar_config)
210            }
211        }
212    }
213
214    #[tokio::test]
215    async fn test_new_repository_is_empty() {
216        let repo = InMemoryNetworkRepository::new();
217        assert_eq!(repo.count().await.unwrap(), 0);
218    }
219
220    #[tokio::test]
221    async fn test_create_network() {
222        let repo = InMemoryNetworkRepository::new();
223        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
224
225        repo.create(network.clone()).await.unwrap();
226        assert_eq!(repo.count().await.unwrap(), 1);
227
228        let stored = repo.get_by_id(network.id.clone()).await.unwrap();
229        assert_eq!(stored.id, network.id);
230        assert_eq!(stored.name, network.name);
231    }
232
233    #[tokio::test]
234    async fn test_get_network_by_type_and_name() {
235        let repo = InMemoryNetworkRepository::new();
236        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
237
238        repo.create(network.clone()).await.unwrap();
239
240        let retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
241        assert!(retrieved.is_some());
242        assert_eq!(retrieved.unwrap().name, "mainnet");
243    }
244
245    #[tokio::test]
246    async fn test_get_nonexistent_network() {
247        let repo = InMemoryNetworkRepository::new();
248
249        let result = repo.get(NetworkType::Evm, "nonexistent").await.unwrap();
250        assert!(result.is_none());
251    }
252
253    #[tokio::test]
254    async fn test_create_duplicate_network() {
255        let repo = InMemoryNetworkRepository::new();
256        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
257
258        repo.create(network.clone()).await.unwrap();
259        let result = repo.create(network).await;
260
261        assert!(matches!(
262            result,
263            Err(RepositoryError::ConstraintViolation(_))
264        ));
265    }
266
267    #[tokio::test]
268    async fn test_different_network_types_same_name() {
269        let repo = InMemoryNetworkRepository::new();
270        let evm_network = create_test_network("mainnet".to_string(), NetworkType::Evm);
271        let solana_network = create_test_network("mainnet".to_string(), NetworkType::Solana);
272
273        repo.create(evm_network.clone()).await.unwrap();
274        repo.create(solana_network.clone()).await.unwrap();
275
276        assert_eq!(repo.count().await.unwrap(), 2);
277
278        let evm_retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
279        let solana_retrieved = repo.get(NetworkType::Solana, "mainnet").await.unwrap();
280
281        assert!(evm_retrieved.is_some());
282        assert!(solana_retrieved.is_some());
283        assert_eq!(evm_retrieved.unwrap().network_type, NetworkType::Evm);
284        assert_eq!(solana_retrieved.unwrap().network_type, NetworkType::Solana);
285    }
286
287    #[tokio::test]
288    async fn test_unsupported_operations() {
289        let repo = InMemoryNetworkRepository::new();
290        let network = create_test_network("test".to_string(), NetworkType::Evm);
291
292        let update_result = repo.update("test".to_string(), network.clone()).await;
293        assert!(matches!(
294            update_result,
295            Err(RepositoryError::NotSupported(_))
296        ));
297
298        let delete_result = repo.delete_by_id("test".to_string()).await;
299        assert!(matches!(
300            delete_result,
301            Err(RepositoryError::NotSupported(_))
302        ));
303
304        let pagination_result = repo
305            .list_paginated(PaginationQuery {
306                page: 1,
307                per_page: 10,
308            })
309            .await;
310        assert!(matches!(
311            pagination_result,
312            Err(RepositoryError::NotSupported(_))
313        ));
314    }
315
316    #[tokio::test]
317    async fn test_has_entries() {
318        let repo = InMemoryNetworkRepository::new();
319        assert!(!repo.has_entries().await.unwrap());
320
321        let network = create_test_network("test".to_string(), NetworkType::Evm);
322
323        repo.create(network.clone()).await.unwrap();
324        assert!(repo.has_entries().await.unwrap());
325    }
326
327    #[tokio::test]
328    async fn test_drop_all_entries() {
329        let repo = InMemoryNetworkRepository::new();
330        let network = create_test_network("test".to_string(), NetworkType::Evm);
331
332        repo.create(network.clone()).await.unwrap();
333        assert!(repo.has_entries().await.unwrap());
334
335        repo.drop_all_entries().await.unwrap();
336        assert!(!repo.has_entries().await.unwrap());
337    }
338}