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 {id} not found"
116            )))
117        }
118    }
119
120    async fn update_policy(
121        &self,
122        id: String,
123        policy: RelayerNetworkPolicy,
124    ) -> Result<RelayerRepoModel, RepositoryError> {
125        let mut store = Self::acquire_lock(&self.store).await?;
126        let relayer = store
127            .get_mut(&id)
128            .ok_or_else(|| RepositoryError::NotFound(format!("Relayer with ID {id} not found")))?;
129        relayer.policies = policy;
130        Ok(relayer.clone())
131    }
132
133    async fn disable_relayer(
134        &self,
135        relayer_id: String,
136        reason: DisabledReason,
137    ) -> Result<RelayerRepoModel, RepositoryError> {
138        let mut store = self.store.lock().await;
139        if let Some(relayer) = store.get_mut(&relayer_id) {
140            relayer.system_disabled = true;
141            relayer.disabled_reason = Some(reason);
142            Ok(relayer.clone())
143        } else {
144            Err(RepositoryError::NotFound(format!(
145                "Relayer with ID {relayer_id} not found"
146            )))
147        }
148    }
149
150    async fn enable_relayer(
151        &self,
152        relayer_id: String,
153    ) -> Result<RelayerRepoModel, RepositoryError> {
154        let mut store = self.store.lock().await;
155        if let Some(relayer) = store.get_mut(&relayer_id) {
156            relayer.system_disabled = false;
157            relayer.disabled_reason = None;
158            Ok(relayer.clone())
159        } else {
160            Err(RepositoryError::NotFound(format!(
161                "Relayer with ID {relayer_id} not found"
162            )))
163        }
164    }
165
166    fn is_persistent_storage(&self) -> bool {
167        false
168    }
169
170    // Uses the trait default implementation which returns None
171}
172
173#[async_trait]
174impl Repository<RelayerRepoModel, String> for InMemoryRelayerRepository {
175    async fn create(&self, relayer: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
176        let mut store = Self::acquire_lock(&self.store).await?;
177        if store.contains_key(&relayer.id) {
178            return Err(RepositoryError::ConstraintViolation(format!(
179                "Relayer with ID {} already exists",
180                relayer.id
181            )));
182        }
183        store.insert(relayer.id.clone(), relayer.clone());
184        Ok(relayer)
185    }
186
187    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
188        let store = Self::acquire_lock(&self.store).await?;
189        match store.get(&id) {
190            Some(relayer) => Ok(relayer.clone()),
191            None => Err(RepositoryError::NotFound(format!(
192                "Relayer with ID {id} not found"
193            ))),
194        }
195    }
196    #[allow(clippy::map_entry)]
197    async fn update(
198        &self,
199        id: String,
200        relayer: RelayerRepoModel,
201    ) -> Result<RelayerRepoModel, RepositoryError> {
202        let mut store = Self::acquire_lock(&self.store).await?;
203        if store.contains_key(&id) {
204            // Ensure we update the existing entry
205            let mut updated_relayer = relayer;
206            updated_relayer.id = id.clone(); // Preserve original ID
207            store.insert(id, updated_relayer.clone());
208            Ok(updated_relayer)
209        } else {
210            Err(RepositoryError::NotFound(format!(
211                "Relayer with ID {id} not found"
212            )))
213        }
214    }
215
216    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
217        let mut store = Self::acquire_lock(&self.store).await?;
218        if store.remove(&id).is_some() {
219            Ok(())
220        } else {
221            Err(RepositoryError::NotFound(format!(
222                "Relayer with ID {id} not found"
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                include_revert_data: None,
286                gas_price_cap: None,
287                whitelist_receivers: None,
288                eip1559_pricing: Some(false),
289                private_transactions: Some(false),
290                min_balance: Some(0),
291                gas_limit_estimation: Some(true),
292            }),
293            signer_id: "test".to_string(),
294            address: "0x".to_string(),
295            notification_id: None,
296            system_disabled: false,
297            custom_rpc_urls: None,
298            ..Default::default()
299        }
300    }
301
302    #[actix_web::test]
303    async fn test_new_repository_is_empty() {
304        let repo = InMemoryRelayerRepository::new();
305        assert_eq!(repo.count().await.unwrap(), 0);
306    }
307
308    #[actix_web::test]
309    async fn test_add_relayer() {
310        let repo = InMemoryRelayerRepository::new();
311        let relayer = create_test_relayer("test".to_string());
312
313        repo.create(relayer.clone()).await.unwrap();
314        assert_eq!(repo.count().await.unwrap(), 1);
315
316        let stored = repo.get_by_id("test".to_string()).await.unwrap();
317        assert_eq!(stored.id, relayer.id);
318        assert_eq!(stored.name, relayer.name);
319    }
320
321    #[actix_web::test]
322    async fn test_update_relayer() {
323        let repo = InMemoryRelayerRepository::new();
324        let mut relayer = create_test_relayer("test".to_string());
325
326        repo.create(relayer.clone()).await.unwrap();
327
328        relayer.name = "Updated Name".to_string();
329        repo.update("test".to_string(), relayer.clone())
330            .await
331            .unwrap();
332
333        let updated = repo.get_by_id("test".to_string()).await.unwrap();
334        assert_eq!(updated.name, "Updated Name");
335    }
336
337    #[actix_web::test]
338    async fn test_list_relayers() {
339        let repo = InMemoryRelayerRepository::new();
340        let relayer1 = create_test_relayer("test".to_string());
341        let relayer2 = create_test_relayer("test2".to_string());
342
343        repo.create(relayer1.clone()).await.unwrap();
344        repo.create(relayer2).await.unwrap();
345
346        let relayers = repo.list_all().await.unwrap();
347        assert_eq!(relayers.len(), 2);
348    }
349
350    #[actix_web::test]
351    async fn test_list_active_relayers() {
352        let repo = InMemoryRelayerRepository::new();
353        let relayer1 = create_test_relayer("test".to_string());
354        let mut relayer2 = create_test_relayer("test2".to_string());
355
356        relayer2.paused = true;
357
358        repo.create(relayer1.clone()).await.unwrap();
359        repo.create(relayer2).await.unwrap();
360
361        let active_relayers = repo.list_active().await.unwrap();
362        assert_eq!(active_relayers.len(), 1);
363        assert_eq!(active_relayers[0].id, "test".to_string());
364    }
365
366    #[actix_web::test]
367    async fn test_update_nonexistent_relayer() {
368        let repo = InMemoryRelayerRepository::new();
369        let relayer = create_test_relayer("test".to_string());
370
371        let result = repo.update("test".to_string(), relayer).await;
372        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
373    }
374
375    #[actix_web::test]
376    async fn test_get_nonexistent_relayer() {
377        let repo = InMemoryRelayerRepository::new();
378
379        let result = repo.get_by_id("test".to_string()).await;
380        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
381    }
382
383    #[actix_web::test]
384    async fn test_partial_update_relayer() {
385        let repo = InMemoryRelayerRepository::new();
386
387        // Add a relayer to the repository
388        let relayer_id = "test_relayer".to_string();
389        let initial_relayer = create_test_relayer(relayer_id.clone());
390
391        repo.create(initial_relayer.clone()).await.unwrap();
392
393        // Perform a partial update on the relayer
394        let update_req = UpdateRelayerRequest {
395            name: None,
396            paused: Some(true),
397            policies: None,
398            notification_id: None,
399            custom_rpc_urls: None,
400        };
401
402        let updated_relayer = repo
403            .partial_update(relayer_id.clone(), update_req)
404            .await
405            .unwrap();
406
407        assert_eq!(updated_relayer.id, initial_relayer.id);
408        assert!(updated_relayer.paused);
409    }
410
411    #[actix_web::test]
412    async fn test_disable_relayer() {
413        let repo = InMemoryRelayerRepository::new();
414
415        // Add a relayer to the repository
416        let relayer_id = "test_relayer".to_string();
417        let initial_relayer = create_test_relayer(relayer_id.clone());
418
419        repo.create(initial_relayer.clone()).await.unwrap();
420
421        // Disable the relayer
422        let disabled_relayer = repo
423            .disable_relayer(
424                relayer_id.clone(),
425                DisabledReason::BalanceCheckFailed("test reason".to_string()),
426            )
427            .await
428            .unwrap();
429
430        assert_eq!(disabled_relayer.id, initial_relayer.id);
431        assert!(disabled_relayer.system_disabled);
432        assert_eq!(
433            disabled_relayer.disabled_reason,
434            Some(DisabledReason::BalanceCheckFailed(
435                "test reason".to_string()
436            ))
437        );
438    }
439
440    #[actix_web::test]
441    async fn test_enable_relayer() {
442        let repo = InMemoryRelayerRepository::new();
443
444        // Add a relayer to the repository
445        let relayer_id = "test_relayer".to_string();
446        let mut initial_relayer = create_test_relayer(relayer_id.clone());
447
448        initial_relayer.system_disabled = true;
449
450        repo.create(initial_relayer.clone()).await.unwrap();
451
452        // Enable the relayer
453        let enabled_relayer = repo.enable_relayer(relayer_id.clone()).await.unwrap();
454
455        assert_eq!(enabled_relayer.id, initial_relayer.id);
456        assert!(!enabled_relayer.system_disabled);
457    }
458
459    #[actix_web::test]
460    async fn test_update_policy() {
461        let repo = InMemoryRelayerRepository::new();
462        let relayer = create_test_relayer("test".to_string());
463
464        repo.create(relayer.clone()).await.unwrap();
465
466        // Create a new policy to update
467        let new_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
468            include_revert_data: None,
469            gas_price_cap: Some(50000000000),
470            whitelist_receivers: Some(vec!["0x1234".to_string()]),
471            eip1559_pricing: Some(true),
472            private_transactions: Some(true),
473            min_balance: Some(1000000),
474            gas_limit_estimation: Some(true),
475        });
476
477        // Update the policy
478        let updated_relayer = repo
479            .update_policy("test".to_string(), new_policy.clone())
480            .await
481            .unwrap();
482
483        // Verify the policy was updated
484        match updated_relayer.policies {
485            RelayerNetworkPolicy::Evm(policy) => {
486                assert_eq!(policy.gas_price_cap, Some(50000000000));
487                assert_eq!(policy.whitelist_receivers, Some(vec!["0x1234".to_string()]));
488                assert_eq!(policy.eip1559_pricing, Some(true));
489                assert!(policy.private_transactions.unwrap_or(false));
490                assert_eq!(policy.min_balance, Some(1000000));
491            }
492            _ => panic!("Unexpected policy type"),
493        }
494    }
495
496    // test has_entries
497    #[actix_web::test]
498    async fn test_has_entries() {
499        let repo = InMemoryRelayerRepository::new();
500        assert!(!repo.has_entries().await.unwrap());
501
502        let relayer = create_test_relayer("test".to_string());
503
504        repo.create(relayer.clone()).await.unwrap();
505        assert!(repo.has_entries().await.unwrap());
506    }
507
508    #[actix_web::test]
509    async fn test_drop_all_entries() {
510        let repo = InMemoryRelayerRepository::new();
511        let relayer = create_test_relayer("test".to_string());
512
513        repo.create(relayer.clone()).await.unwrap();
514
515        assert!(repo.has_entries().await.unwrap());
516
517        repo.drop_all_entries().await.unwrap();
518        assert!(!repo.has_entries().await.unwrap());
519    }
520
521    #[actix_web::test]
522    async fn test_list_by_signer_id() {
523        let repo = InMemoryRelayerRepository::new();
524
525        // Create test relayers with different signers
526        let relayer1 = RelayerRepoModel {
527            id: "relayer-1".to_string(),
528            name: "Relayer 1".to_string(),
529            network: "ethereum".to_string(),
530            paused: false,
531            network_type: NetworkType::Evm,
532            signer_id: "signer-alpha".to_string(),
533            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
534            address: "0x1111".to_string(),
535            notification_id: None,
536            system_disabled: false,
537            custom_rpc_urls: None,
538            ..Default::default()
539        };
540
541        let relayer2 = RelayerRepoModel {
542            id: "relayer-2".to_string(),
543            name: "Relayer 2".to_string(),
544            network: "polygon".to_string(),
545            paused: true,
546            network_type: NetworkType::Evm,
547            signer_id: "signer-alpha".to_string(), // Same signer as relayer1
548            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
549            address: "0x2222".to_string(),
550            notification_id: None,
551            system_disabled: false,
552            custom_rpc_urls: None,
553            ..Default::default()
554        };
555
556        let relayer3 = RelayerRepoModel {
557            id: "relayer-3".to_string(),
558            name: "Relayer 3".to_string(),
559            network: "solana".to_string(),
560            paused: false,
561            network_type: NetworkType::Solana,
562            signer_id: "signer-beta".to_string(), // Different signer
563            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
564            address: "solana-addr".to_string(),
565            notification_id: None,
566            system_disabled: false,
567            custom_rpc_urls: None,
568            ..Default::default()
569        };
570
571        let relayer4 = RelayerRepoModel {
572            id: "relayer-4".to_string(),
573            name: "Relayer 4".to_string(),
574            network: "stellar".to_string(),
575            paused: false,
576            network_type: NetworkType::Stellar,
577            signer_id: "signer-alpha".to_string(), // Same signer as relayer1 and relayer2
578            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
579            address: "stellar-addr".to_string(),
580            notification_id: Some("notification-1".to_string()),
581            system_disabled: true,
582            custom_rpc_urls: None,
583            ..Default::default()
584        };
585
586        // Add all relayers to the repository
587        repo.create(relayer1).await.unwrap();
588        repo.create(relayer2).await.unwrap();
589        repo.create(relayer3).await.unwrap();
590        repo.create(relayer4).await.unwrap();
591
592        // Test: Find relayers with signer-alpha (should return 3: relayer-1, relayer-2, relayer-4)
593        let relayers_with_alpha = repo.list_by_signer_id("signer-alpha").await.unwrap();
594        assert_eq!(relayers_with_alpha.len(), 3);
595
596        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
597        assert!(alpha_ids.contains(&"relayer-1".to_string()));
598        assert!(alpha_ids.contains(&"relayer-2".to_string()));
599        assert!(alpha_ids.contains(&"relayer-4".to_string()));
600        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
601
602        // Verify the relayers have different states (paused, system_disabled)
603        let relayer2_found = relayers_with_alpha
604            .iter()
605            .find(|r| r.id == "relayer-2")
606            .unwrap();
607        let relayer4_found = relayers_with_alpha
608            .iter()
609            .find(|r| r.id == "relayer-4")
610            .unwrap();
611        assert!(relayer2_found.paused); // Should be paused
612        assert!(relayer4_found.system_disabled); // Should be disabled
613
614        // Test: Find relayers with signer-beta (should return 1: relayer-3)
615        let relayers_with_beta = repo.list_by_signer_id("signer-beta").await.unwrap();
616        assert_eq!(relayers_with_beta.len(), 1);
617        assert_eq!(relayers_with_beta[0].id, "relayer-3");
618        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
619
620        // Test: Find relayers with non-existent signer (should return empty)
621        let relayers_with_gamma = repo.list_by_signer_id("signer-gamma").await.unwrap();
622        assert_eq!(relayers_with_gamma.len(), 0);
623
624        // Test: Find relayers with empty signer ID (should return empty)
625        let relayers_with_empty = repo.list_by_signer_id("").await.unwrap();
626        assert_eq!(relayers_with_empty.len(), 0);
627
628        // Test: Verify total count hasn't changed
629        assert_eq!(repo.count().await.unwrap(), 4);
630
631        // Test: Remove one relayer and verify list_by_signer_id updates correctly
632        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
633
634        let relayers_with_alpha_after_delete =
635            repo.list_by_signer_id("signer-alpha").await.unwrap();
636        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
637
638        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
639            .iter()
640            .map(|r| r.id.clone())
641            .collect();
642        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
643        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
644        assert!(alpha_ids_after.contains(&"relayer-4".to_string()));
645    }
646
647    #[actix_web::test]
648    async fn test_list_by_notification_id() {
649        let repo = InMemoryRelayerRepository::new();
650
651        // Create test relayers with different notifications
652        let relayer1 = RelayerRepoModel {
653            id: "relayer-1".to_string(),
654            name: "Relayer 1".to_string(),
655            network: "ethereum".to_string(),
656            paused: false,
657            network_type: NetworkType::Evm,
658            signer_id: "test-signer".to_string(),
659            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
660            address: "0x1111".to_string(),
661            notification_id: Some("notification-alpha".to_string()),
662            system_disabled: false,
663            custom_rpc_urls: None,
664            ..Default::default()
665        };
666
667        let relayer2 = RelayerRepoModel {
668            id: "relayer-2".to_string(),
669            name: "Relayer 2".to_string(),
670            network: "polygon".to_string(),
671            paused: true,
672            network_type: NetworkType::Evm,
673            signer_id: "test-signer".to_string(),
674            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
675            address: "0x2222".to_string(),
676            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1
677            system_disabled: false,
678            custom_rpc_urls: None,
679            ..Default::default()
680        };
681
682        let relayer3 = RelayerRepoModel {
683            id: "relayer-3".to_string(),
684            name: "Relayer 3".to_string(),
685            network: "solana".to_string(),
686            paused: false,
687            network_type: NetworkType::Solana,
688            signer_id: "test-signer".to_string(),
689            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
690            address: "solana-addr".to_string(),
691            notification_id: Some("notification-beta".to_string()), // Different notification
692            system_disabled: false,
693            custom_rpc_urls: None,
694            ..Default::default()
695        };
696
697        let relayer4 = RelayerRepoModel {
698            id: "relayer-4".to_string(),
699            name: "Relayer 4".to_string(),
700            network: "stellar".to_string(),
701            paused: false,
702            network_type: NetworkType::Stellar,
703            signer_id: "test-signer".to_string(),
704            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
705            address: "stellar-addr".to_string(),
706            notification_id: None, // No notification
707            system_disabled: true,
708            custom_rpc_urls: None,
709            ..Default::default()
710        };
711
712        let relayer5 = RelayerRepoModel {
713            id: "relayer-5".to_string(),
714            name: "Relayer 5".to_string(),
715            network: "bsc".to_string(),
716            paused: false,
717            network_type: NetworkType::Evm,
718            signer_id: "test-signer".to_string(),
719            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
720            address: "0x5555".to_string(),
721            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1 and relayer2
722            system_disabled: false,
723            custom_rpc_urls: None,
724            ..Default::default()
725        };
726
727        // Add all relayers to the repository
728        repo.create(relayer1).await.unwrap();
729        repo.create(relayer2).await.unwrap();
730        repo.create(relayer3).await.unwrap();
731        repo.create(relayer4).await.unwrap();
732        repo.create(relayer5).await.unwrap();
733
734        // Test: Find relayers with notification-alpha (should return 3: relayer-1, relayer-2, relayer-5)
735        let relayers_with_alpha = repo
736            .list_by_notification_id("notification-alpha")
737            .await
738            .unwrap();
739        assert_eq!(relayers_with_alpha.len(), 3);
740
741        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
742        assert!(alpha_ids.contains(&"relayer-1".to_string()));
743        assert!(alpha_ids.contains(&"relayer-2".to_string()));
744        assert!(alpha_ids.contains(&"relayer-5".to_string()));
745        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
746        assert!(!alpha_ids.contains(&"relayer-4".to_string()));
747
748        // Verify the relayers have different states (paused, different networks)
749        let relayer2_found = relayers_with_alpha
750            .iter()
751            .find(|r| r.id == "relayer-2")
752            .unwrap();
753        let relayer5_found = relayers_with_alpha
754            .iter()
755            .find(|r| r.id == "relayer-5")
756            .unwrap();
757        assert!(relayer2_found.paused); // Should be paused
758        assert_eq!(relayer5_found.network, "bsc"); // Should be on BSC network
759
760        // Test: Find relayers with notification-beta (should return 1: relayer-3)
761        let relayers_with_beta = repo
762            .list_by_notification_id("notification-beta")
763            .await
764            .unwrap();
765        assert_eq!(relayers_with_beta.len(), 1);
766        assert_eq!(relayers_with_beta[0].id, "relayer-3");
767        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
768
769        // Test: Find relayers with non-existent notification (should return empty)
770        let relayers_with_gamma = repo
771            .list_by_notification_id("notification-gamma")
772            .await
773            .unwrap();
774        assert_eq!(relayers_with_gamma.len(), 0);
775
776        // Test: Find relayers with empty string notification (should return empty)
777        let relayers_with_empty = repo.list_by_notification_id("").await.unwrap();
778        assert_eq!(relayers_with_empty.len(), 0);
779
780        // Test: Verify total count hasn't changed
781        assert_eq!(repo.count().await.unwrap(), 5);
782
783        // Test: Remove one relayer and verify list_by_notification_id updates correctly
784        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
785
786        let relayers_with_alpha_after_delete = repo
787            .list_by_notification_id("notification-alpha")
788            .await
789            .unwrap();
790        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
791
792        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
793            .iter()
794            .map(|r| r.id.clone())
795            .collect();
796        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
797        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
798        assert!(alpha_ids_after.contains(&"relayer-5".to_string()));
799
800        // Test: Update a relayer's notification and verify the lists update correctly
801        let mut updated_relayer = repo.get_by_id("relayer-5".to_string()).await.unwrap();
802        updated_relayer.notification_id = Some("notification-beta".to_string());
803        repo.update("relayer-5".to_string(), updated_relayer)
804            .await
805            .unwrap();
806
807        // Check notification-alpha list again (should now have only relayer-1)
808        let relayers_with_alpha_final = repo
809            .list_by_notification_id("notification-alpha")
810            .await
811            .unwrap();
812        assert_eq!(relayers_with_alpha_final.len(), 1);
813        assert_eq!(relayers_with_alpha_final[0].id, "relayer-1");
814
815        // Check notification-beta list (should now have relayer-3 and relayer-5)
816        let relayers_with_beta_final = repo
817            .list_by_notification_id("notification-beta")
818            .await
819            .unwrap();
820        assert_eq!(relayers_with_beta_final.len(), 2);
821        let beta_ids_final: Vec<String> = relayers_with_beta_final
822            .iter()
823            .map(|r| r.id.clone())
824            .collect();
825        assert!(beta_ids_final.contains(&"relayer-3".to_string()));
826        assert!(beta_ids_final.contains(&"relayer-5".to_string()));
827    }
828}