openzeppelin_relayer/repositories/network/
network_in_memory.rs1use 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 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 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 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 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 let network_id = network.id.clone();
333
334 repo.create(network.clone()).await.unwrap();
336
337 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 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 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 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 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 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}