openzeppelin_relayer/repositories/relayer/
relayer_in_memory.rs

1//! This module defines the `RelayerRepository` trait and its in-memory implementation,
2//! `InMemoryRelayerRepository`. It provides functionality for managing relayers, including
3//! creating, updating, enabling, disabling, and listing relayers. The module also includes
4//! conversion logic for transforming configuration file data into repository models and
5//! implements pagination for listing relayers.
6//!
7//! The `RelayerRepository` trait is designed to be implemented by any storage backend,
8//! allowing for flexibility in how relayers are stored and managed. The in-memory
9//! implementation is useful for testing and development purposes.
10use crate::models::PaginationQuery;
11use crate::{
12    models::UpdateRelayerRequest,
13    models::{DisabledReason, RelayerNetworkPolicy, RelayerRepoModel, RepositoryError},
14};
15use async_trait::async_trait;
16use eyre::Result;
17use std::collections::HashMap;
18use tokio::sync::{Mutex, MutexGuard};
19
20use crate::repositories::{PaginatedResult, RelayerRepository, Repository};
21
22#[derive(Debug)]
23pub struct InMemoryRelayerRepository {
24    store: Mutex<HashMap<String, RelayerRepoModel>>,
25}
26
27impl InMemoryRelayerRepository {
28    pub fn new() -> Self {
29        Self {
30            store: Mutex::new(HashMap::new()),
31        }
32    }
33    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
34        Ok(lock.lock().await)
35    }
36}
37
38impl Default for InMemoryRelayerRepository {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl Clone for InMemoryRelayerRepository {
45    fn clone(&self) -> Self {
46        // Try to get the current data, or use empty HashMap if lock fails
47        let data = self
48            .store
49            .try_lock()
50            .map(|guard| guard.clone())
51            .unwrap_or_else(|_| HashMap::new());
52
53        Self {
54            store: Mutex::new(data),
55        }
56    }
57}
58
59#[async_trait]
60impl RelayerRepository for InMemoryRelayerRepository {
61    async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
62        let store = Self::acquire_lock(&self.store).await?;
63        let active_relayers: Vec<RelayerRepoModel> = store
64            .values()
65            .filter(|&relayer| !relayer.paused)
66            .cloned()
67            .collect();
68        Ok(active_relayers)
69    }
70
71    async fn list_by_signer_id(
72        &self,
73        signer_id: &str,
74    ) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
75        let store = Self::acquire_lock(&self.store).await?;
76        let relayers_with_signer: Vec<RelayerRepoModel> = store
77            .values()
78            .filter(|&relayer| relayer.signer_id == signer_id)
79            .cloned()
80            .collect();
81        Ok(relayers_with_signer)
82    }
83
84    async fn list_by_notification_id(
85        &self,
86        notification_id: &str,
87    ) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
88        let store = Self::acquire_lock(&self.store).await?;
89        let relayers_with_notification: Vec<RelayerRepoModel> = store
90            .values()
91            .filter(|&relayer| {
92                relayer
93                    .notification_id
94                    .as_ref()
95                    .is_some_and(|id| id == notification_id)
96            })
97            .cloned()
98            .collect();
99        Ok(relayers_with_notification)
100    }
101
102    async fn partial_update(
103        &self,
104        id: String,
105        update: UpdateRelayerRequest,
106    ) -> Result<RelayerRepoModel, RepositoryError> {
107        let mut store = Self::acquire_lock(&self.store).await?;
108        if let Some(relayer) = store.get_mut(&id) {
109            if let Some(paused) = update.paused {
110                relayer.paused = paused;
111            }
112            Ok(relayer.clone())
113        } else {
114            Err(RepositoryError::NotFound(format!(
115                "Relayer with ID {} not found",
116                id
117            )))
118        }
119    }
120
121    async fn update_policy(
122        &self,
123        id: String,
124        policy: RelayerNetworkPolicy,
125    ) -> Result<RelayerRepoModel, RepositoryError> {
126        let mut store = Self::acquire_lock(&self.store).await?;
127        let relayer = store.get_mut(&id).ok_or_else(|| {
128            RepositoryError::NotFound(format!("Relayer with ID {} not found", id))
129        })?;
130        relayer.policies = policy;
131        Ok(relayer.clone())
132    }
133
134    async fn disable_relayer(
135        &self,
136        relayer_id: String,
137        reason: DisabledReason,
138    ) -> Result<RelayerRepoModel, RepositoryError> {
139        let mut store = self.store.lock().await;
140        if let Some(relayer) = store.get_mut(&relayer_id) {
141            relayer.system_disabled = true;
142            relayer.disabled_reason = Some(reason);
143            Ok(relayer.clone())
144        } else {
145            Err(RepositoryError::NotFound(format!(
146                "Relayer with ID {} not found",
147                relayer_id
148            )))
149        }
150    }
151
152    async fn enable_relayer(
153        &self,
154        relayer_id: String,
155    ) -> Result<RelayerRepoModel, RepositoryError> {
156        let mut store = self.store.lock().await;
157        if let Some(relayer) = store.get_mut(&relayer_id) {
158            relayer.system_disabled = false;
159            relayer.disabled_reason = None;
160            Ok(relayer.clone())
161        } else {
162            Err(RepositoryError::NotFound(format!(
163                "Relayer with ID {} not found",
164                relayer_id
165            )))
166        }
167    }
168}
169
170#[async_trait]
171impl Repository<RelayerRepoModel, String> for InMemoryRelayerRepository {
172    async fn create(&self, relayer: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
173        let mut store = Self::acquire_lock(&self.store).await?;
174        if store.contains_key(&relayer.id) {
175            return Err(RepositoryError::ConstraintViolation(format!(
176                "Relayer with ID {} already exists",
177                relayer.id
178            )));
179        }
180        store.insert(relayer.id.clone(), relayer.clone());
181        Ok(relayer)
182    }
183
184    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
185        let store = Self::acquire_lock(&self.store).await?;
186        match store.get(&id) {
187            Some(relayer) => Ok(relayer.clone()),
188            None => Err(RepositoryError::NotFound(format!(
189                "Relayer with ID {} not found",
190                id
191            ))),
192        }
193    }
194    #[allow(clippy::map_entry)]
195    async fn update(
196        &self,
197        id: String,
198        relayer: RelayerRepoModel,
199    ) -> Result<RelayerRepoModel, RepositoryError> {
200        let mut store = Self::acquire_lock(&self.store).await?;
201        if store.contains_key(&id) {
202            // Ensure we update the existing entry
203            let mut updated_relayer = relayer;
204            updated_relayer.id = id.clone(); // Preserve original ID
205            store.insert(id, updated_relayer.clone());
206            Ok(updated_relayer)
207        } else {
208            Err(RepositoryError::NotFound(format!(
209                "Relayer with ID {} not found",
210                id
211            )))
212        }
213    }
214
215    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
216        let mut store = Self::acquire_lock(&self.store).await?;
217        if store.remove(&id).is_some() {
218            Ok(())
219        } else {
220            Err(RepositoryError::NotFound(format!(
221                "Relayer with ID {} not found",
222                id
223            )))
224        }
225    }
226
227    async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
228        let store = Self::acquire_lock(&self.store).await?;
229        Ok(store.values().cloned().collect())
230    }
231
232    async fn list_paginated(
233        &self,
234        query: PaginationQuery,
235    ) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError> {
236        let total = self.count().await?;
237        let start = ((query.page - 1) * query.per_page) as usize;
238        let items = self
239            .store
240            .lock()
241            .await
242            .values()
243            .skip(start)
244            .take(query.per_page as usize)
245            .cloned()
246            .collect();
247        Ok(PaginatedResult {
248            items,
249            total: total as u64,
250            page: query.page,
251            per_page: query.per_page,
252        })
253    }
254
255    async fn count(&self) -> Result<usize, RepositoryError> {
256        Ok(self.store.lock().await.len())
257    }
258
259    async fn has_entries(&self) -> Result<bool, RepositoryError> {
260        let store = Self::acquire_lock(&self.store).await?;
261        Ok(!store.is_empty())
262    }
263
264    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
265        let mut store = Self::acquire_lock(&self.store).await?;
266        store.clear();
267        Ok(())
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use crate::models::{NetworkType, RelayerEvmPolicy};
274
275    use super::*;
276
277    fn create_test_relayer(id: String) -> RelayerRepoModel {
278        RelayerRepoModel {
279            id: id.clone(),
280            name: format!("Relayer {}", id.clone()),
281            network: "TestNet".to_string(),
282            paused: false,
283            network_type: NetworkType::Evm,
284            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
285                gas_price_cap: None,
286                whitelist_receivers: None,
287                eip1559_pricing: Some(false),
288                private_transactions: Some(false),
289                min_balance: Some(0),
290                gas_limit_estimation: Some(true),
291            }),
292            signer_id: "test".to_string(),
293            address: "0x".to_string(),
294            notification_id: None,
295            system_disabled: false,
296            custom_rpc_urls: None,
297            ..Default::default()
298        }
299    }
300
301    #[actix_web::test]
302    async fn test_new_repository_is_empty() {
303        let repo = InMemoryRelayerRepository::new();
304        assert_eq!(repo.count().await.unwrap(), 0);
305    }
306
307    #[actix_web::test]
308    async fn test_add_relayer() {
309        let repo = InMemoryRelayerRepository::new();
310        let relayer = create_test_relayer("test".to_string());
311
312        repo.create(relayer.clone()).await.unwrap();
313        assert_eq!(repo.count().await.unwrap(), 1);
314
315        let stored = repo.get_by_id("test".to_string()).await.unwrap();
316        assert_eq!(stored.id, relayer.id);
317        assert_eq!(stored.name, relayer.name);
318    }
319
320    #[actix_web::test]
321    async fn test_update_relayer() {
322        let repo = InMemoryRelayerRepository::new();
323        let mut relayer = create_test_relayer("test".to_string());
324
325        repo.create(relayer.clone()).await.unwrap();
326
327        relayer.name = "Updated Name".to_string();
328        repo.update("test".to_string(), relayer.clone())
329            .await
330            .unwrap();
331
332        let updated = repo.get_by_id("test".to_string()).await.unwrap();
333        assert_eq!(updated.name, "Updated Name");
334    }
335
336    #[actix_web::test]
337    async fn test_list_relayers() {
338        let repo = InMemoryRelayerRepository::new();
339        let relayer1 = create_test_relayer("test".to_string());
340        let relayer2 = create_test_relayer("test2".to_string());
341
342        repo.create(relayer1.clone()).await.unwrap();
343        repo.create(relayer2).await.unwrap();
344
345        let relayers = repo.list_all().await.unwrap();
346        assert_eq!(relayers.len(), 2);
347    }
348
349    #[actix_web::test]
350    async fn test_list_active_relayers() {
351        let repo = InMemoryRelayerRepository::new();
352        let relayer1 = create_test_relayer("test".to_string());
353        let mut relayer2 = create_test_relayer("test2".to_string());
354
355        relayer2.paused = true;
356
357        repo.create(relayer1.clone()).await.unwrap();
358        repo.create(relayer2).await.unwrap();
359
360        let active_relayers = repo.list_active().await.unwrap();
361        assert_eq!(active_relayers.len(), 1);
362        assert_eq!(active_relayers[0].id, "test".to_string());
363    }
364
365    #[actix_web::test]
366    async fn test_update_nonexistent_relayer() {
367        let repo = InMemoryRelayerRepository::new();
368        let relayer = create_test_relayer("test".to_string());
369
370        let result = repo.update("test".to_string(), relayer).await;
371        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
372    }
373
374    #[actix_web::test]
375    async fn test_get_nonexistent_relayer() {
376        let repo = InMemoryRelayerRepository::new();
377
378        let result = repo.get_by_id("test".to_string()).await;
379        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
380    }
381
382    #[actix_web::test]
383    async fn test_partial_update_relayer() {
384        let repo = InMemoryRelayerRepository::new();
385
386        // Add a relayer to the repository
387        let relayer_id = "test_relayer".to_string();
388        let initial_relayer = create_test_relayer(relayer_id.clone());
389
390        repo.create(initial_relayer.clone()).await.unwrap();
391
392        // Perform a partial update on the relayer
393        let update_req = UpdateRelayerRequest {
394            name: None,
395            paused: Some(true),
396            policies: None,
397            notification_id: None,
398            custom_rpc_urls: None,
399        };
400
401        let updated_relayer = repo
402            .partial_update(relayer_id.clone(), update_req)
403            .await
404            .unwrap();
405
406        assert_eq!(updated_relayer.id, initial_relayer.id);
407        assert!(updated_relayer.paused);
408    }
409
410    #[actix_web::test]
411    async fn test_disable_relayer() {
412        let repo = InMemoryRelayerRepository::new();
413
414        // Add a relayer to the repository
415        let relayer_id = "test_relayer".to_string();
416        let initial_relayer = create_test_relayer(relayer_id.clone());
417
418        repo.create(initial_relayer.clone()).await.unwrap();
419
420        // Disable the relayer
421        let disabled_relayer = repo
422            .disable_relayer(
423                relayer_id.clone(),
424                DisabledReason::BalanceCheckFailed("test reason".to_string()),
425            )
426            .await
427            .unwrap();
428
429        assert_eq!(disabled_relayer.id, initial_relayer.id);
430        assert!(disabled_relayer.system_disabled);
431        assert_eq!(
432            disabled_relayer.disabled_reason,
433            Some(DisabledReason::BalanceCheckFailed(
434                "test reason".to_string()
435            ))
436        );
437    }
438
439    #[actix_web::test]
440    async fn test_enable_relayer() {
441        let repo = InMemoryRelayerRepository::new();
442
443        // Add a relayer to the repository
444        let relayer_id = "test_relayer".to_string();
445        let mut initial_relayer = create_test_relayer(relayer_id.clone());
446
447        initial_relayer.system_disabled = true;
448
449        repo.create(initial_relayer.clone()).await.unwrap();
450
451        // Enable the relayer
452        let enabled_relayer = repo.enable_relayer(relayer_id.clone()).await.unwrap();
453
454        assert_eq!(enabled_relayer.id, initial_relayer.id);
455        assert!(!enabled_relayer.system_disabled);
456    }
457
458    #[actix_web::test]
459    async fn test_update_policy() {
460        let repo = InMemoryRelayerRepository::new();
461        let relayer = create_test_relayer("test".to_string());
462
463        repo.create(relayer.clone()).await.unwrap();
464
465        // Create a new policy to update
466        let new_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
467            gas_price_cap: Some(50000000000),
468            whitelist_receivers: Some(vec!["0x1234".to_string()]),
469            eip1559_pricing: Some(true),
470            private_transactions: Some(true),
471            min_balance: Some(1000000),
472            gas_limit_estimation: Some(true),
473        });
474
475        // Update the policy
476        let updated_relayer = repo
477            .update_policy("test".to_string(), new_policy.clone())
478            .await
479            .unwrap();
480
481        // Verify the policy was updated
482        match updated_relayer.policies {
483            RelayerNetworkPolicy::Evm(policy) => {
484                assert_eq!(policy.gas_price_cap, Some(50000000000));
485                assert_eq!(policy.whitelist_receivers, Some(vec!["0x1234".to_string()]));
486                assert_eq!(policy.eip1559_pricing, Some(true));
487                assert!(policy.private_transactions.unwrap_or(false));
488                assert_eq!(policy.min_balance, Some(1000000));
489            }
490            _ => panic!("Unexpected policy type"),
491        }
492    }
493
494    // test has_entries
495    #[actix_web::test]
496    async fn test_has_entries() {
497        let repo = InMemoryRelayerRepository::new();
498        assert!(!repo.has_entries().await.unwrap());
499
500        let relayer = create_test_relayer("test".to_string());
501
502        repo.create(relayer.clone()).await.unwrap();
503        assert!(repo.has_entries().await.unwrap());
504    }
505
506    #[actix_web::test]
507    async fn test_drop_all_entries() {
508        let repo = InMemoryRelayerRepository::new();
509        let relayer = create_test_relayer("test".to_string());
510
511        repo.create(relayer.clone()).await.unwrap();
512
513        assert!(repo.has_entries().await.unwrap());
514
515        repo.drop_all_entries().await.unwrap();
516        assert!(!repo.has_entries().await.unwrap());
517    }
518
519    #[actix_web::test]
520    async fn test_list_by_signer_id() {
521        let repo = InMemoryRelayerRepository::new();
522
523        // Create test relayers with different signers
524        let relayer1 = RelayerRepoModel {
525            id: "relayer-1".to_string(),
526            name: "Relayer 1".to_string(),
527            network: "ethereum".to_string(),
528            paused: false,
529            network_type: NetworkType::Evm,
530            signer_id: "signer-alpha".to_string(),
531            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
532            address: "0x1111".to_string(),
533            notification_id: None,
534            system_disabled: false,
535            custom_rpc_urls: None,
536            ..Default::default()
537        };
538
539        let relayer2 = RelayerRepoModel {
540            id: "relayer-2".to_string(),
541            name: "Relayer 2".to_string(),
542            network: "polygon".to_string(),
543            paused: true,
544            network_type: NetworkType::Evm,
545            signer_id: "signer-alpha".to_string(), // Same signer as relayer1
546            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
547            address: "0x2222".to_string(),
548            notification_id: None,
549            system_disabled: false,
550            custom_rpc_urls: None,
551            ..Default::default()
552        };
553
554        let relayer3 = RelayerRepoModel {
555            id: "relayer-3".to_string(),
556            name: "Relayer 3".to_string(),
557            network: "solana".to_string(),
558            paused: false,
559            network_type: NetworkType::Solana,
560            signer_id: "signer-beta".to_string(), // Different signer
561            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
562            address: "solana-addr".to_string(),
563            notification_id: None,
564            system_disabled: false,
565            custom_rpc_urls: None,
566            ..Default::default()
567        };
568
569        let relayer4 = RelayerRepoModel {
570            id: "relayer-4".to_string(),
571            name: "Relayer 4".to_string(),
572            network: "stellar".to_string(),
573            paused: false,
574            network_type: NetworkType::Stellar,
575            signer_id: "signer-alpha".to_string(), // Same signer as relayer1 and relayer2
576            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
577            address: "stellar-addr".to_string(),
578            notification_id: Some("notification-1".to_string()),
579            system_disabled: true,
580            custom_rpc_urls: None,
581            ..Default::default()
582        };
583
584        // Add all relayers to the repository
585        repo.create(relayer1).await.unwrap();
586        repo.create(relayer2).await.unwrap();
587        repo.create(relayer3).await.unwrap();
588        repo.create(relayer4).await.unwrap();
589
590        // Test: Find relayers with signer-alpha (should return 3: relayer-1, relayer-2, relayer-4)
591        let relayers_with_alpha = repo.list_by_signer_id("signer-alpha").await.unwrap();
592        assert_eq!(relayers_with_alpha.len(), 3);
593
594        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
595        assert!(alpha_ids.contains(&"relayer-1".to_string()));
596        assert!(alpha_ids.contains(&"relayer-2".to_string()));
597        assert!(alpha_ids.contains(&"relayer-4".to_string()));
598        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
599
600        // Verify the relayers have different states (paused, system_disabled)
601        let relayer2_found = relayers_with_alpha
602            .iter()
603            .find(|r| r.id == "relayer-2")
604            .unwrap();
605        let relayer4_found = relayers_with_alpha
606            .iter()
607            .find(|r| r.id == "relayer-4")
608            .unwrap();
609        assert!(relayer2_found.paused); // Should be paused
610        assert!(relayer4_found.system_disabled); // Should be disabled
611
612        // Test: Find relayers with signer-beta (should return 1: relayer-3)
613        let relayers_with_beta = repo.list_by_signer_id("signer-beta").await.unwrap();
614        assert_eq!(relayers_with_beta.len(), 1);
615        assert_eq!(relayers_with_beta[0].id, "relayer-3");
616        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
617
618        // Test: Find relayers with non-existent signer (should return empty)
619        let relayers_with_gamma = repo.list_by_signer_id("signer-gamma").await.unwrap();
620        assert_eq!(relayers_with_gamma.len(), 0);
621
622        // Test: Find relayers with empty signer ID (should return empty)
623        let relayers_with_empty = repo.list_by_signer_id("").await.unwrap();
624        assert_eq!(relayers_with_empty.len(), 0);
625
626        // Test: Verify total count hasn't changed
627        assert_eq!(repo.count().await.unwrap(), 4);
628
629        // Test: Remove one relayer and verify list_by_signer_id updates correctly
630        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
631
632        let relayers_with_alpha_after_delete =
633            repo.list_by_signer_id("signer-alpha").await.unwrap();
634        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
635
636        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
637            .iter()
638            .map(|r| r.id.clone())
639            .collect();
640        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
641        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
642        assert!(alpha_ids_after.contains(&"relayer-4".to_string()));
643    }
644
645    #[actix_web::test]
646    async fn test_list_by_notification_id() {
647        let repo = InMemoryRelayerRepository::new();
648
649        // Create test relayers with different notifications
650        let relayer1 = RelayerRepoModel {
651            id: "relayer-1".to_string(),
652            name: "Relayer 1".to_string(),
653            network: "ethereum".to_string(),
654            paused: false,
655            network_type: NetworkType::Evm,
656            signer_id: "test-signer".to_string(),
657            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
658            address: "0x1111".to_string(),
659            notification_id: Some("notification-alpha".to_string()),
660            system_disabled: false,
661            custom_rpc_urls: None,
662            ..Default::default()
663        };
664
665        let relayer2 = RelayerRepoModel {
666            id: "relayer-2".to_string(),
667            name: "Relayer 2".to_string(),
668            network: "polygon".to_string(),
669            paused: true,
670            network_type: NetworkType::Evm,
671            signer_id: "test-signer".to_string(),
672            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
673            address: "0x2222".to_string(),
674            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1
675            system_disabled: false,
676            custom_rpc_urls: None,
677            ..Default::default()
678        };
679
680        let relayer3 = RelayerRepoModel {
681            id: "relayer-3".to_string(),
682            name: "Relayer 3".to_string(),
683            network: "solana".to_string(),
684            paused: false,
685            network_type: NetworkType::Solana,
686            signer_id: "test-signer".to_string(),
687            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
688            address: "solana-addr".to_string(),
689            notification_id: Some("notification-beta".to_string()), // Different notification
690            system_disabled: false,
691            custom_rpc_urls: None,
692            ..Default::default()
693        };
694
695        let relayer4 = RelayerRepoModel {
696            id: "relayer-4".to_string(),
697            name: "Relayer 4".to_string(),
698            network: "stellar".to_string(),
699            paused: false,
700            network_type: NetworkType::Stellar,
701            signer_id: "test-signer".to_string(),
702            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
703            address: "stellar-addr".to_string(),
704            notification_id: None, // No notification
705            system_disabled: true,
706            custom_rpc_urls: None,
707            ..Default::default()
708        };
709
710        let relayer5 = RelayerRepoModel {
711            id: "relayer-5".to_string(),
712            name: "Relayer 5".to_string(),
713            network: "bsc".to_string(),
714            paused: false,
715            network_type: NetworkType::Evm,
716            signer_id: "test-signer".to_string(),
717            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
718            address: "0x5555".to_string(),
719            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1 and relayer2
720            system_disabled: false,
721            custom_rpc_urls: None,
722            ..Default::default()
723        };
724
725        // Add all relayers to the repository
726        repo.create(relayer1).await.unwrap();
727        repo.create(relayer2).await.unwrap();
728        repo.create(relayer3).await.unwrap();
729        repo.create(relayer4).await.unwrap();
730        repo.create(relayer5).await.unwrap();
731
732        // Test: Find relayers with notification-alpha (should return 3: relayer-1, relayer-2, relayer-5)
733        let relayers_with_alpha = repo
734            .list_by_notification_id("notification-alpha")
735            .await
736            .unwrap();
737        assert_eq!(relayers_with_alpha.len(), 3);
738
739        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
740        assert!(alpha_ids.contains(&"relayer-1".to_string()));
741        assert!(alpha_ids.contains(&"relayer-2".to_string()));
742        assert!(alpha_ids.contains(&"relayer-5".to_string()));
743        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
744        assert!(!alpha_ids.contains(&"relayer-4".to_string()));
745
746        // Verify the relayers have different states (paused, different networks)
747        let relayer2_found = relayers_with_alpha
748            .iter()
749            .find(|r| r.id == "relayer-2")
750            .unwrap();
751        let relayer5_found = relayers_with_alpha
752            .iter()
753            .find(|r| r.id == "relayer-5")
754            .unwrap();
755        assert!(relayer2_found.paused); // Should be paused
756        assert_eq!(relayer5_found.network, "bsc"); // Should be on BSC network
757
758        // Test: Find relayers with notification-beta (should return 1: relayer-3)
759        let relayers_with_beta = repo
760            .list_by_notification_id("notification-beta")
761            .await
762            .unwrap();
763        assert_eq!(relayers_with_beta.len(), 1);
764        assert_eq!(relayers_with_beta[0].id, "relayer-3");
765        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
766
767        // Test: Find relayers with non-existent notification (should return empty)
768        let relayers_with_gamma = repo
769            .list_by_notification_id("notification-gamma")
770            .await
771            .unwrap();
772        assert_eq!(relayers_with_gamma.len(), 0);
773
774        // Test: Find relayers with empty string notification (should return empty)
775        let relayers_with_empty = repo.list_by_notification_id("").await.unwrap();
776        assert_eq!(relayers_with_empty.len(), 0);
777
778        // Test: Verify total count hasn't changed
779        assert_eq!(repo.count().await.unwrap(), 5);
780
781        // Test: Remove one relayer and verify list_by_notification_id updates correctly
782        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
783
784        let relayers_with_alpha_after_delete = repo
785            .list_by_notification_id("notification-alpha")
786            .await
787            .unwrap();
788        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
789
790        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
791            .iter()
792            .map(|r| r.id.clone())
793            .collect();
794        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
795        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
796        assert!(alpha_ids_after.contains(&"relayer-5".to_string()));
797
798        // Test: Update a relayer's notification and verify the lists update correctly
799        let mut updated_relayer = repo.get_by_id("relayer-5".to_string()).await.unwrap();
800        updated_relayer.notification_id = Some("notification-beta".to_string());
801        repo.update("relayer-5".to_string(), updated_relayer)
802            .await
803            .unwrap();
804
805        // Check notification-alpha list again (should now have only relayer-1)
806        let relayers_with_alpha_final = repo
807            .list_by_notification_id("notification-alpha")
808            .await
809            .unwrap();
810        assert_eq!(relayers_with_alpha_final.len(), 1);
811        assert_eq!(relayers_with_alpha_final[0].id, "relayer-1");
812
813        // Check notification-beta list (should now have relayer-3 and relayer-5)
814        let relayers_with_beta_final = repo
815            .list_by_notification_id("notification-beta")
816            .await
817            .unwrap();
818        assert_eq!(relayers_with_beta_final.len(), 2);
819        let beta_ids_final: Vec<String> = relayers_with_beta_final
820            .iter()
821            .map(|r| r.id.clone())
822            .collect();
823        assert!(beta_ids_final.contains(&"relayer-3".to_string()));
824        assert!(beta_ids_final.contains(&"relayer-5".to_string()));
825    }
826}