openzeppelin_relayer/bootstrap/
initialize_relayers.rs

1//! Relayer initialization
2//!
3//! This module contains functions for initializing relayers, ensuring they are
4//! properly configured and ready for operation.
5use crate::{
6    domain::{get_network_relayer, Relayer},
7    jobs::JobProducerTrait,
8    models::{
9        NetworkRepoModel, NotificationRepoModel, RelayerRepoModel, SignerRepoModel,
10        ThinDataAppState, TransactionRepoModel,
11    },
12    repositories::{
13        ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
14        Repository, TransactionCounterTrait, TransactionRepository,
15    },
16};
17use color_eyre::{eyre::WrapErr, Result};
18use futures::future::try_join_all;
19use tracing::debug;
20
21/// Internal function for initializing a relayer using a provided relayer service.
22/// This allows for easier testing with mocked relayers.
23/// Uses generics for static dispatch instead of dynamic dispatch.
24async fn initialize_relayer_with_service<R>(relayer_id: &str, relayer_service: &R) -> Result<()>
25where
26    R: Relayer,
27{
28    debug!(relayer_id = %relayer_id, "initializing relayer");
29
30    relayer_service
31        .initialize_relayer()
32        .await
33        .wrap_err_with(|| format!("Failed to initialize relayer: {relayer_id}"))?;
34
35    Ok(())
36}
37
38pub async fn initialize_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
39    relayer_id: String,
40    app_state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
41) -> Result<()>
42where
43    J: JobProducerTrait + Send + Sync + 'static,
44    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
45    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
46    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
47    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
48    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
49    TCR: TransactionCounterTrait + Send + Sync + 'static,
50    PR: PluginRepositoryTrait + Send + Sync + 'static,
51    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
52{
53    let relayer_service = get_network_relayer(relayer_id.clone(), &app_state).await?;
54
55    initialize_relayer_with_service(&relayer_id, &relayer_service).await
56}
57
58/// Collects relayer IDs that need initialization
59pub fn get_relayer_ids_to_initialize(relayers: &[RelayerRepoModel]) -> Vec<String> {
60    relayers.iter().map(|r| r.id.clone()).collect()
61}
62
63pub async fn initialize_relayers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
64    app_state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
65) -> Result<()>
66where
67    J: JobProducerTrait + Send + Sync + 'static,
68    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
69    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
70    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
71    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
72    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
73    TCR: TransactionCounterTrait + Send + Sync + 'static,
74    PR: PluginRepositoryTrait + Send + Sync + 'static,
75    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
76{
77    let relayers = app_state.relayer_repository.list_all().await?;
78
79    // Early return for empty list - no work to do
80    if relayers.is_empty() {
81        debug!("No relayers to initialize");
82        return Ok(());
83    }
84
85    debug!(count = relayers.len(), "Initializing relayers concurrently");
86
87    let relayer_futures = relayers.iter().map(|relayer| {
88        let app_state = app_state.clone();
89        async move { initialize_relayer(relayer.id.clone(), app_state).await }
90    });
91
92    try_join_all(relayer_futures)
93        .await
94        .wrap_err("Failed to initialize relayers")?;
95
96    debug!(
97        count = relayers.len(),
98        "All relayers initialized successfully"
99    );
100    Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::utils::mocks::mockutils::create_mock_relayer;
107
108    #[test]
109    fn test_get_relayer_ids_with_empty_list() {
110        let relayers: Vec<RelayerRepoModel> = vec![];
111        let ids = get_relayer_ids_to_initialize(&relayers);
112
113        assert_eq!(ids.len(), 0, "Should return empty list for no relayers");
114    }
115
116    #[test]
117    fn test_get_relayer_ids_with_single_relayer() {
118        let relayers = vec![create_mock_relayer("relayer-1".to_string(), false)];
119
120        let ids = get_relayer_ids_to_initialize(&relayers);
121
122        assert_eq!(ids.len(), 1, "Should return one ID");
123        assert_eq!(ids[0], "relayer-1");
124    }
125
126    #[test]
127    fn test_get_relayer_ids_with_multiple_relayers() {
128        let relayers = vec![
129            create_mock_relayer("evm-relayer".to_string(), false),
130            create_mock_relayer("solana-relayer".to_string(), false),
131            create_mock_relayer("stellar-relayer".to_string(), false),
132        ];
133
134        let ids = get_relayer_ids_to_initialize(&relayers);
135
136        assert_eq!(ids.len(), 3, "Should return three IDs");
137        assert_eq!(ids[0], "evm-relayer");
138        assert_eq!(ids[1], "solana-relayer");
139        assert_eq!(ids[2], "stellar-relayer");
140    }
141
142    #[test]
143    fn test_get_relayer_ids_with_mixed_states() {
144        let mut relayers = vec![
145            create_mock_relayer("active-relayer".to_string(), false),
146            create_mock_relayer("paused-relayer".to_string(), false),
147            create_mock_relayer("disabled-relayer".to_string(), false),
148        ];
149
150        // Modify states
151        relayers[1].paused = true;
152        relayers[2].system_disabled = true;
153
154        let ids = get_relayer_ids_to_initialize(&relayers);
155
156        // Should include ALL relayers regardless of state (initialization handles state)
157        assert_eq!(
158            ids.len(),
159            3,
160            "Should include all relayers regardless of state"
161        );
162        assert!(ids.contains(&"active-relayer".to_string()));
163        assert!(ids.contains(&"paused-relayer".to_string()));
164        assert!(ids.contains(&"disabled-relayer".to_string()));
165    }
166
167    #[test]
168    fn test_get_relayer_ids_with_different_network_types() {
169        let relayers = vec![
170            create_mock_relayer("evm-1".to_string(), false),
171            create_mock_relayer("evm-2".to_string(), false),
172            create_mock_relayer("solana-1".to_string(), false),
173            create_mock_relayer("stellar-1".to_string(), false),
174        ];
175
176        let ids = get_relayer_ids_to_initialize(&relayers);
177
178        assert_eq!(ids.len(), 4, "Should include all network types");
179
180        // Verify all network types are included
181        assert!(ids.iter().any(|id| id.starts_with("evm-")));
182        assert!(ids.iter().any(|id| id.starts_with("solana-")));
183        assert!(ids.iter().any(|id| id.starts_with("stellar-")));
184    }
185
186    #[test]
187    fn test_concurrent_initialization_count() {
188        // This test verifies the number of concurrent initializations
189        // that would be triggered for different relayer counts
190
191        let test_cases = vec![
192            (0, 0),   // No relayers = no initializations
193            (1, 1),   // One relayer = one initialization
194            (5, 5),   // Five relayers = five concurrent initializations
195            (10, 10), // Ten relayers = ten concurrent initializations
196        ];
197
198        for (relayer_count, expected_init_count) in test_cases {
199            let relayers: Vec<RelayerRepoModel> = (0..relayer_count)
200                .map(|i| create_mock_relayer(format!("relayer-{}", i), false))
201                .collect();
202
203            let ids = get_relayer_ids_to_initialize(&relayers);
204
205            assert_eq!(
206                ids.len(),
207                expected_init_count,
208                "Should create {} initializations for {} relayers",
209                expected_init_count,
210                relayer_count
211            );
212        }
213    }
214
215    #[tokio::test]
216    async fn test_initialize_relayer_with_service_success() {
217        use crate::domain::MockRelayer;
218
219        let mut mock_relayer = MockRelayer::new();
220        mock_relayer
221            .expect_initialize_relayer()
222            .times(1)
223            .returning(|| Box::pin(async { Ok(()) }));
224
225        let result = initialize_relayer_with_service("test-relayer", &mock_relayer).await;
226
227        assert!(result.is_ok(), "Should successfully initialize relayer");
228    }
229
230    #[tokio::test]
231    async fn test_initialize_relayer_with_service_failure() {
232        use crate::domain::MockRelayer;
233        use crate::models::RelayerError;
234
235        let mut mock_relayer = MockRelayer::new();
236        mock_relayer
237            .expect_initialize_relayer()
238            .times(1)
239            .returning(|| {
240                Box::pin(async {
241                    Err(RelayerError::ProviderError(
242                        "RPC connection failed".to_string(),
243                    ))
244                })
245            });
246
247        let result = initialize_relayer_with_service("test-relayer", &mock_relayer).await;
248
249        assert!(
250            result.is_err(),
251            "Should fail when initialize_relayer returns error"
252        );
253        let err = result.unwrap_err();
254        assert!(err
255            .to_string()
256            .contains("Failed to initialize relayer: test-relayer"));
257    }
258
259    #[tokio::test]
260    async fn test_initialize_relayer_with_service_called_once() {
261        use crate::domain::MockRelayer;
262
263        let mut mock_relayer = MockRelayer::new();
264        // Verify that initialize_relayer is called exactly once
265        mock_relayer
266            .expect_initialize_relayer()
267            .times(1)
268            .returning(|| Box::pin(async { Ok(()) }));
269
270        let _ = initialize_relayer_with_service("relayer-123", &mock_relayer).await;
271
272        // Mock will panic if expectations aren't met (called more/less than once)
273    }
274
275    #[tokio::test]
276    async fn test_initialize_relayer_with_service_multiple_relayers() {
277        use crate::domain::MockRelayer;
278
279        // Test that we can call initialize_relayer_with_service multiple times
280        let mut mock_relayer_1 = MockRelayer::new();
281        mock_relayer_1
282            .expect_initialize_relayer()
283            .times(1)
284            .returning(|| Box::pin(async { Ok(()) }));
285
286        let mut mock_relayer_2 = MockRelayer::new();
287        mock_relayer_2
288            .expect_initialize_relayer()
289            .times(1)
290            .returning(|| Box::pin(async { Ok(()) }));
291
292        let result1 = initialize_relayer_with_service("relayer-1", &mock_relayer_1).await;
293        let result2 = initialize_relayer_with_service("relayer-2", &mock_relayer_2).await;
294
295        assert!(
296            result1.is_ok(),
297            "First relayer should initialize successfully"
298        );
299        assert!(
300            result2.is_ok(),
301            "Second relayer should initialize successfully"
302        );
303    }
304}