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                };
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}