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        let mut store = Self::acquire_lock(&self.store).await?;
99
100        if !store.contains_key(&id) {
101            return Err(RepositoryError::NotFound(format!(
102                "Network with id {id} not found"
103            )));
104        }
105
106        store.insert(id, network.clone());
107        Ok(network)
108    }
109
110    async fn delete_by_id(&self, _id: String) -> Result<(), RepositoryError> {
111        Err(RepositoryError::NotSupported("Not supported".to_string()))
112    }
113
114    async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
115        let store = Self::acquire_lock(&self.store).await?;
116        let networks: Vec<NetworkRepoModel> = store.values().cloned().collect();
117        Ok(networks)
118    }
119
120    async fn list_paginated(
121        &self,
122        query: PaginationQuery,
123    ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
124        let total = self.count().await?;
125        let start = ((query.page - 1) * query.per_page) as usize;
126
127        let items = Self::acquire_lock(&self.store)
128            .await?
129            .values()
130            .skip(start)
131            .take(query.per_page as usize)
132            .cloned()
133            .collect();
134
135        Ok(PaginatedResult {
136            items,
137            total: total as u64,
138            page: query.page,
139            per_page: query.per_page,
140        })
141    }
142
143    async fn count(&self) -> Result<usize, RepositoryError> {
144        let store = Self::acquire_lock(&self.store).await?;
145        Ok(store.len())
146    }
147
148    async fn has_entries(&self) -> Result<bool, RepositoryError> {
149        let store = Self::acquire_lock(&self.store).await?;
150        Ok(!store.is_empty())
151    }
152
153    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
154        let mut store = Self::acquire_lock(&self.store).await?;
155        store.clear();
156        Ok(())
157    }
158}
159
160#[async_trait]
161impl NetworkRepository for InMemoryNetworkRepository {
162    async fn get_by_name(
163        &self,
164        network_type: NetworkType,
165        name: &str,
166    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
167        self.get(network_type, name).await
168    }
169
170    async fn get_by_chain_id(
171        &self,
172        network_type: NetworkType,
173        chain_id: u64,
174    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
175        // Only EVM networks have chain_id
176        if network_type != NetworkType::Evm {
177            return Ok(None);
178        }
179
180        let store = Self::acquire_lock(&self.store).await?;
181        for (_, network) in store.iter() {
182            if network.network_type == network_type {
183                if let crate::models::NetworkConfigData::Evm(evm_config) = &network.config {
184                    if evm_config.chain_id == Some(chain_id) {
185                        return Ok(Some(network.clone()));
186                    }
187                }
188            }
189        }
190        Ok(None)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use crate::config::{
197        EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
198    };
199
200    use super::*;
201
202    fn create_test_network(name: String, network_type: NetworkType) -> NetworkRepoModel {
203        let common = NetworkConfigCommon {
204            network: name.clone(),
205            from: None,
206            rpc_urls: Some(vec![crate::models::RpcConfig::new(
207                "https://rpc.example.com".to_string(),
208            )]),
209            explorer_urls: None,
210            average_blocktime_ms: None,
211            is_testnet: Some(true),
212            tags: None,
213        };
214
215        match network_type {
216            NetworkType::Evm => {
217                let evm_config = EvmNetworkConfig {
218                    common,
219                    chain_id: Some(1),
220                    required_confirmations: Some(1),
221                    features: None,
222                    symbol: Some("ETH".to_string()),
223                    gas_price_cache: None,
224                };
225                NetworkRepoModel::new_evm(evm_config)
226            }
227            NetworkType::Solana => {
228                let solana_config = SolanaNetworkConfig { common };
229                NetworkRepoModel::new_solana(solana_config)
230            }
231            NetworkType::Stellar => {
232                let stellar_config = StellarNetworkConfig {
233                    common,
234                    passphrase: None,
235                    horizon_url: None,
236                };
237                NetworkRepoModel::new_stellar(stellar_config)
238            }
239        }
240    }
241
242    #[tokio::test]
243    async fn test_new_repository_is_empty() {
244        let repo = InMemoryNetworkRepository::new();
245        assert_eq!(repo.count().await.unwrap(), 0);
246    }
247
248    #[tokio::test]
249    async fn test_create_network() {
250        let repo = InMemoryNetworkRepository::new();
251        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
252
253        repo.create(network.clone()).await.unwrap();
254        assert_eq!(repo.count().await.unwrap(), 1);
255
256        let stored = repo.get_by_id(network.id.clone()).await.unwrap();
257        assert_eq!(stored.id, network.id);
258        assert_eq!(stored.name, network.name);
259    }
260
261    #[tokio::test]
262    async fn test_get_network_by_type_and_name() {
263        let repo = InMemoryNetworkRepository::new();
264        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
265
266        repo.create(network.clone()).await.unwrap();
267
268        let retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
269        assert!(retrieved.is_some());
270        assert_eq!(retrieved.unwrap().name, "mainnet");
271    }
272
273    #[tokio::test]
274    async fn test_get_nonexistent_network() {
275        let repo = InMemoryNetworkRepository::new();
276
277        let result = repo.get(NetworkType::Evm, "nonexistent").await.unwrap();
278        assert!(result.is_none());
279    }
280
281    #[tokio::test]
282    async fn test_create_duplicate_network() {
283        let repo = InMemoryNetworkRepository::new();
284        let network = create_test_network("mainnet".to_string(), NetworkType::Evm);
285
286        repo.create(network.clone()).await.unwrap();
287        let result = repo.create(network).await;
288
289        assert!(matches!(
290            result,
291            Err(RepositoryError::ConstraintViolation(_))
292        ));
293    }
294
295    #[tokio::test]
296    async fn test_different_network_types_same_name() {
297        let repo = InMemoryNetworkRepository::new();
298        let evm_network = create_test_network("mainnet".to_string(), NetworkType::Evm);
299        let solana_network = create_test_network("mainnet".to_string(), NetworkType::Solana);
300
301        repo.create(evm_network.clone()).await.unwrap();
302        repo.create(solana_network.clone()).await.unwrap();
303
304        assert_eq!(repo.count().await.unwrap(), 2);
305
306        let evm_retrieved = repo.get(NetworkType::Evm, "mainnet").await.unwrap();
307        let solana_retrieved = repo.get(NetworkType::Solana, "mainnet").await.unwrap();
308
309        assert!(evm_retrieved.is_some());
310        assert!(solana_retrieved.is_some());
311        assert_eq!(evm_retrieved.unwrap().network_type, NetworkType::Evm);
312        assert_eq!(solana_retrieved.unwrap().network_type, NetworkType::Solana);
313    }
314
315    #[tokio::test]
316    async fn test_unsupported_operations() {
317        let repo = InMemoryNetworkRepository::new();
318
319        // Delete is still unsupported
320        let delete_result = repo.delete_by_id("test".to_string()).await;
321        assert!(matches!(
322            delete_result,
323            Err(RepositoryError::NotSupported(_))
324        ));
325    }
326
327    #[tokio::test]
328    async fn test_update_network() {
329        let repo = InMemoryNetworkRepository::new();
330        let network = create_test_network("test".to_string(), NetworkType::Evm);
331        // Note: new_evm generates ID as "evm:{name}" -> "evm:test"
332        let network_id = network.id.clone();
333
334        // First create the network
335        repo.create(network.clone()).await.unwrap();
336
337        // Update should work now
338        let mut updated_network = network.clone();
339        updated_network.name = "Updated Name".to_string();
340
341        let update_result = repo
342            .update(network_id.clone(), updated_network.clone())
343            .await;
344        assert!(update_result.is_ok());
345        let updated = update_result.unwrap();
346        assert_eq!(updated.name, "Updated Name");
347
348        // Verify it was persisted
349        let retrieved = repo.get_by_id(network_id).await.unwrap();
350        assert_eq!(retrieved.name, "Updated Name");
351    }
352
353    #[tokio::test]
354    async fn test_update_network_not_found() {
355        let repo = InMemoryNetworkRepository::new();
356        let network = create_test_network("test".to_string(), NetworkType::Evm);
357
358        // Update on non-existent network should fail
359        let update_result = repo.update("nonexistent".to_string(), network).await;
360        assert!(matches!(update_result, Err(RepositoryError::NotFound(_))));
361    }
362
363    #[tokio::test]
364    async fn test_list_paginated() {
365        let repo = InMemoryNetworkRepository::new();
366
367        // Create multiple networks
368        for i in 0..5 {
369            let network = create_test_network(format!("network-{i}"), NetworkType::Evm);
370            repo.create(network).await.unwrap();
371        }
372
373        // Test pagination
374        let result = repo
375            .list_paginated(PaginationQuery {
376                page: 1,
377                per_page: 2,
378            })
379            .await;
380        assert!(result.is_ok());
381        let paginated = result.unwrap();
382        assert_eq!(paginated.items.len(), 2);
383        assert_eq!(paginated.total, 5);
384        assert_eq!(paginated.page, 1);
385        assert_eq!(paginated.per_page, 2);
386
387        // Test second page
388        let result2 = repo
389            .list_paginated(PaginationQuery {
390                page: 2,
391                per_page: 2,
392            })
393            .await;
394        assert!(result2.is_ok());
395        let paginated2 = result2.unwrap();
396        assert_eq!(paginated2.items.len(), 2);
397        assert_eq!(paginated2.page, 2);
398    }
399
400    #[tokio::test]
401    async fn test_has_entries() {
402        let repo = InMemoryNetworkRepository::new();
403        assert!(!repo.has_entries().await.unwrap());
404
405        let network = create_test_network("test".to_string(), NetworkType::Evm);
406
407        repo.create(network.clone()).await.unwrap();
408        assert!(repo.has_entries().await.unwrap());
409    }
410
411    #[tokio::test]
412    async fn test_drop_all_entries() {
413        let repo = InMemoryNetworkRepository::new();
414        let network = create_test_network("test".to_string(), NetworkType::Evm);
415
416        repo.create(network.clone()).await.unwrap();
417        assert!(repo.has_entries().await.unwrap());
418
419        repo.drop_all_entries().await.unwrap();
420        assert!(!repo.has_entries().await.unwrap());
421    }
422}