openzeppelin_relayer/repositories/
relayer.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::config::{
11    ConfigFileRelayerSolanaFeePaymentStrategy, ConfigFileRelayerSolanaSwapPolicy,
12    ConfigFileRelayerSolanaSwapStrategy,
13};
14use crate::models::{
15    JupiterSwapOptions, PaginationQuery, RelayerSolanaSwapConfig, SolanaAllowedTokensSwapConfig,
16    SolanaFeePaymentStrategy, SolanaSwapStrategy,
17};
18use crate::{
19    config::{ConfigFileNetworkType, ConfigFileRelayerNetworkPolicy, RelayerFileConfig},
20    constants::{
21        DEFAULT_EVM_MIN_BALANCE, DEFAULT_SOLANA_MIN_BALANCE, DEFAULT_STELLAR_MIN_BALANCE,
22        MAX_SOLANA_TX_DATA_SIZE,
23    },
24    domain::RelayerUpdateRequest,
25    models::{
26        NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy, RelayerRepoModel, RelayerSolanaPolicy,
27        RelayerStellarPolicy, RepositoryError, SolanaAllowedTokensPolicy,
28    },
29};
30use async_trait::async_trait;
31use eyre::Result;
32use std::collections::HashMap;
33use std::sync::Arc;
34use thiserror::Error;
35use tokio::sync::{Mutex, MutexGuard};
36
37use super::{PaginatedResult, Repository};
38
39#[async_trait]
40pub trait RelayerRepository: Repository<RelayerRepoModel, String> + Send + Sync {
41    async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError>;
42    async fn partial_update(
43        &self,
44        id: String,
45        update: RelayerUpdateRequest,
46    ) -> Result<RelayerRepoModel, RepositoryError>;
47    async fn enable_relayer(&self, relayer_id: String)
48        -> Result<RelayerRepoModel, RepositoryError>;
49    async fn disable_relayer(
50        &self,
51        relayer_id: String,
52    ) -> Result<RelayerRepoModel, RepositoryError>;
53    async fn update_policy(
54        &self,
55        id: String,
56        policy: RelayerNetworkPolicy,
57    ) -> Result<RelayerRepoModel, RepositoryError>;
58}
59
60#[derive(Debug)]
61pub struct InMemoryRelayerRepository {
62    store: Mutex<HashMap<String, RelayerRepoModel>>,
63}
64
65impl InMemoryRelayerRepository {
66    pub fn new() -> Self {
67        Self {
68            store: Mutex::new(HashMap::new()),
69        }
70    }
71    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
72        Ok(lock.lock().await)
73    }
74}
75
76impl Default for InMemoryRelayerRepository {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[async_trait]
83impl RelayerRepository for InMemoryRelayerRepository {
84    async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
85        let store = Self::acquire_lock(&self.store).await?;
86        let active_relayers: Vec<RelayerRepoModel> = store
87            .values()
88            .filter(|&relayer| !relayer.paused)
89            .cloned()
90            .collect();
91        Ok(active_relayers)
92    }
93
94    async fn partial_update(
95        &self,
96        id: String,
97        update: RelayerUpdateRequest,
98    ) -> Result<RelayerRepoModel, RepositoryError> {
99        let mut store = Self::acquire_lock(&self.store).await?;
100        if let Some(relayer) = store.get_mut(&id) {
101            if let Some(paused) = update.paused {
102                relayer.paused = paused;
103            }
104            Ok(relayer.clone())
105        } else {
106            Err(RepositoryError::NotFound(format!(
107                "Relayer with ID {} not found",
108                id
109            )))
110        }
111    }
112
113    async fn update_policy(
114        &self,
115        id: String,
116        policy: RelayerNetworkPolicy,
117    ) -> Result<RelayerRepoModel, RepositoryError> {
118        let mut store = Self::acquire_lock(&self.store).await?;
119        let relayer = store.get_mut(&id).ok_or_else(|| {
120            RepositoryError::NotFound(format!("Relayer with ID {} not found", id))
121        })?;
122        relayer.policies = policy;
123        Ok(relayer.clone())
124    }
125
126    async fn disable_relayer(
127        &self,
128        relayer_id: String,
129    ) -> Result<RelayerRepoModel, RepositoryError> {
130        let mut store = self.store.lock().await;
131        if let Some(relayer) = store.get_mut(&relayer_id) {
132            relayer.system_disabled = true;
133            Ok(relayer.clone())
134        } else {
135            Err(RepositoryError::NotFound(format!(
136                "Relayer with ID {} not found",
137                relayer_id
138            )))
139        }
140    }
141
142    async fn enable_relayer(
143        &self,
144        relayer_id: String,
145    ) -> Result<RelayerRepoModel, RepositoryError> {
146        let mut store = self.store.lock().await;
147        if let Some(relayer) = store.get_mut(&relayer_id) {
148            relayer.system_disabled = false;
149            Ok(relayer.clone())
150        } else {
151            Err(RepositoryError::NotFound(format!(
152                "Relayer with ID {} not found",
153                relayer_id
154            )))
155        }
156    }
157}
158
159#[async_trait]
160impl Repository<RelayerRepoModel, String> for InMemoryRelayerRepository {
161    async fn create(&self, relayer: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
162        let mut store = Self::acquire_lock(&self.store).await?;
163        if store.contains_key(&relayer.id) {
164            return Err(RepositoryError::ConstraintViolation(format!(
165                "Relayer with ID {} already exists",
166                relayer.id
167            )));
168        }
169        store.insert(relayer.id.clone(), relayer.clone());
170        Ok(relayer)
171    }
172
173    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
174        let store = Self::acquire_lock(&self.store).await?;
175        match store.get(&id) {
176            Some(relayer) => Ok(relayer.clone()),
177            None => Err(RepositoryError::NotFound(format!(
178                "Relayer with ID {} not found",
179                id
180            ))),
181        }
182    }
183    #[allow(clippy::map_entry)]
184    async fn update(
185        &self,
186        id: String,
187        relayer: RelayerRepoModel,
188    ) -> Result<RelayerRepoModel, RepositoryError> {
189        let mut store = Self::acquire_lock(&self.store).await?;
190        if store.contains_key(&id) {
191            // Ensure we update the existing entry
192            let mut updated_relayer = relayer;
193            updated_relayer.id = id.clone(); // Preserve original ID
194            store.insert(id, updated_relayer.clone());
195            Ok(updated_relayer)
196        } else {
197            Err(RepositoryError::NotFound(format!(
198                "Relayer with ID {} not found",
199                id
200            )))
201        }
202    }
203
204    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
205        let mut store = Self::acquire_lock(&self.store).await?;
206        if store.remove(&id).is_some() {
207            Ok(())
208        } else {
209            Err(RepositoryError::NotFound(format!(
210                "Relayer with ID {} not found",
211                id
212            )))
213        }
214    }
215
216    async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
217        let store = Self::acquire_lock(&self.store).await?;
218        Ok(store.values().cloned().collect())
219    }
220
221    async fn list_paginated(
222        &self,
223        query: PaginationQuery,
224    ) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError> {
225        let total = self.count().await?;
226        let start = ((query.page - 1) * query.per_page) as usize;
227        let items = self
228            .store
229            .lock()
230            .await
231            .values()
232            .skip(start)
233            .take(query.per_page as usize)
234            .cloned()
235            .collect();
236        Ok(PaginatedResult {
237            items,
238            total: total as u64,
239            page: query.page,
240            per_page: query.per_page,
241        })
242    }
243
244    async fn count(&self) -> Result<usize, RepositoryError> {
245        Ok(self.store.lock().await.len())
246    }
247}
248
249#[derive(Error, Debug)]
250pub enum ConversionError {
251    #[error("Invalid network type: {0}")]
252    InvalidNetworkType(String),
253    #[error("Invalid config: {0}")]
254    InvalidConfig(String),
255}
256
257impl TryFrom<RelayerFileConfig> for RelayerRepoModel {
258    type Error = ConversionError;
259
260    fn try_from(config: RelayerFileConfig) -> Result<Self, Self::Error> {
261        let network_type = match config.network_type {
262            ConfigFileNetworkType::Evm => NetworkType::Evm,
263            ConfigFileNetworkType::Stellar => NetworkType::Stellar,
264            ConfigFileNetworkType::Solana => NetworkType::Solana,
265        };
266
267        let policies = if let Some(config_policies) = &config.policies {
268            RelayerNetworkPolicy::try_from(config_policies.clone()).map_err(|_| {
269                ConversionError::InvalidNetworkType("Failed to convert network policy".to_string())
270            })?
271        } else {
272            // return default policy based on network type
273            match network_type {
274                NetworkType::Evm => RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
275                NetworkType::Stellar => {
276                    RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default())
277                }
278                NetworkType::Solana => RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
279            }
280        };
281
282        Ok(RelayerRepoModel {
283            id: config.id,
284            name: config.name,
285            network: config.network,
286            paused: config.paused,
287            network_type,
288            signer_id: config.signer_id,
289            policies,
290            address: "".to_string(), /* Default to empty address. This is later updated by the
291                                      * relayer */
292            notification_id: config.notification_id,
293            system_disabled: false,
294            custom_rpc_urls: config.custom_rpc_urls,
295        })
296    }
297}
298
299impl TryFrom<ConfigFileRelayerSolanaSwapStrategy> for SolanaSwapStrategy {
300    type Error = eyre::Error;
301
302    fn try_from(config: ConfigFileRelayerSolanaSwapStrategy) -> Result<Self, Self::Error> {
303        match config {
304            ConfigFileRelayerSolanaSwapStrategy::JupiterSwap => Ok(SolanaSwapStrategy::JupiterSwap),
305            ConfigFileRelayerSolanaSwapStrategy::JupiterUltra => {
306                Ok(SolanaSwapStrategy::JupiterUltra)
307            }
308        }
309    }
310}
311
312impl TryFrom<ConfigFileRelayerSolanaSwapPolicy> for RelayerSolanaSwapConfig {
313    type Error = eyre::Error;
314
315    fn try_from(config: ConfigFileRelayerSolanaSwapPolicy) -> Result<Self, Self::Error> {
316        Ok(RelayerSolanaSwapConfig {
317            cron_schedule: config.cron_schedule,
318            min_balance_threshold: config.min_balance_threshold,
319            strategy: config
320                .strategy
321                .map(SolanaSwapStrategy::try_from)
322                .transpose()?,
323            jupiter_swap_options: config
324                .jupiter_swap_options
325                .map(|options| JupiterSwapOptions {
326                    priority_fee_max_lamports: options.priority_fee_max_lamports,
327                    dynamic_compute_unit_limit: options.dynamic_compute_unit_limit,
328                    priority_level: options.priority_level,
329                }),
330        })
331    }
332}
333
334impl TryFrom<ConfigFileRelayerNetworkPolicy> for RelayerNetworkPolicy {
335    type Error = eyre::Error;
336
337    fn try_from(policy: ConfigFileRelayerNetworkPolicy) -> Result<Self, Self::Error> {
338        match &policy {
339            ConfigFileRelayerNetworkPolicy::Evm(evm) => {
340                Ok(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
341                    gas_price_cap: evm.gas_price_cap,
342                    whitelist_receivers: evm.whitelist_receivers.clone(),
343                    eip1559_pricing: evm.eip1559_pricing,
344                    private_transactions: evm.private_transactions.unwrap_or(false),
345                    min_balance: evm.min_balance.unwrap_or(DEFAULT_EVM_MIN_BALANCE),
346                }))
347            }
348            ConfigFileRelayerNetworkPolicy::Solana(solana) => {
349                // Create a new variable for solana.allowed_tokens.
350                // If solana.allowed_tokens is None, the resulting variable will be None;
351                // otherwise, each entry will be mapped using
352                // SolanaAllowedTokensPolicy::new_partial.
353                let mapped_allowed_tokens = solana
354                    .allowed_tokens
355                    .as_ref()
356                    .filter(|tokens| !tokens.is_empty())
357                    .map(|tokens| {
358                        tokens
359                            .iter()
360                            .map(|token| {
361                                let swap_config = token.swap_config.as_ref().map(|sc| {
362                                    SolanaAllowedTokensSwapConfig {
363                                        slippage_percentage: sc.slippage_percentage,
364                                        min_amount: sc.min_amount,
365                                        max_amount: sc.max_amount,
366                                        retain_min_amount: sc.retain_min_amount,
367                                    }
368                                });
369
370                                SolanaAllowedTokensPolicy::new_partial(
371                                    token.mint.clone(),
372                                    token.max_allowed_fee,
373                                    swap_config,
374                                )
375                            })
376                            .collect::<Vec<_>>()
377                    });
378                let fee_payment_strategy = solana.fee_payment_strategy.clone().map_or(
379                    SolanaFeePaymentStrategy::User,
380                    |fp| match fp {
381                        ConfigFileRelayerSolanaFeePaymentStrategy::User => {
382                            SolanaFeePaymentStrategy::User
383                        }
384                        ConfigFileRelayerSolanaFeePaymentStrategy::Relayer => {
385                            SolanaFeePaymentStrategy::Relayer
386                        }
387                    },
388                );
389                let swap_config = solana
390                    .swap_config
391                    .as_ref()
392                    .map(|sc| RelayerSolanaSwapConfig::try_from(sc.clone()))
393                    .transpose()?;
394
395                Ok(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
396                    fee_payment_strategy,
397                    fee_margin_percentage: solana.fee_margin_percentage,
398                    min_balance: solana.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE),
399                    allowed_accounts: solana.allowed_accounts.clone(),
400                    allowed_programs: solana.allowed_programs.clone(),
401                    allowed_tokens: mapped_allowed_tokens,
402                    disallowed_accounts: solana.disallowed_accounts.clone(),
403                    max_signatures: solana.max_signatures,
404                    max_tx_data_size: solana.max_tx_data_size.unwrap_or(MAX_SOLANA_TX_DATA_SIZE),
405                    max_allowed_fee_lamports: solana.max_allowed_fee_lamports,
406                    swap_config,
407                }))
408            }
409            ConfigFileRelayerNetworkPolicy::Stellar(stellar) => {
410                Ok(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
411                    max_fee: stellar.max_fee,
412                    timeout_seconds: stellar.timeout_seconds,
413                    min_balance: stellar.min_balance.unwrap_or(DEFAULT_STELLAR_MIN_BALANCE),
414                }))
415            }
416        }
417    }
418}
419
420/// A generic wrapper around a relayer repository implementation.
421///
422/// This structure provides a clean abstraction for relayer repositories,
423/// allowing for different concrete implementations to be used while
424/// maintaining a consistent interface.
425///
426/// # Type Parameters
427///
428/// * `T` - A repository implementation for relayer models that implements the `Repository` trait.
429///
430/// # Example
431///
432/// ```rust, ignore
433/// use std::sync::Arc;
434/// use crate::repositories::{InMemoryRelayerRepository, RelayerRepositoryStorage};
435///
436/// let repository = InMemoryRelayerRepository::new();
437/// let storage = Arc::new(RelayerRepositoryStorage::in_memory(repository));
438/// ```
439#[derive(Debug)]
440pub struct RelayerRepositoryStorage<T: Repository<RelayerRepoModel, String>> {
441    pub repository: Arc<T>,
442}
443
444impl RelayerRepositoryStorage<InMemoryRelayerRepository> {
445    /// Creates a new in-memory relayer repository storage.
446    ///
447    /// # Parameters
448    ///
449    /// * `repository` - An instance of `InMemoryRelayerRepository`.
450    ///
451    /// # Returns
452    ///
453    /// A new `RelayerRepositoryStorage` instance backed by the provided in-memory repository.
454    pub fn in_memory(repository: InMemoryRelayerRepository) -> Self {
455        Self {
456            repository: Arc::new(repository),
457        }
458    }
459}
460
461#[async_trait]
462impl<T> Repository<RelayerRepoModel, String> for RelayerRepositoryStorage<T>
463where
464    T: Repository<RelayerRepoModel, String> + Send + Sync,
465{
466    async fn create(&self, entity: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
467        self.repository.create(entity).await
468    }
469
470    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
471        self.repository.get_by_id(id).await
472    }
473
474    async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
475        self.repository.list_all().await
476    }
477
478    async fn list_paginated(
479        &self,
480        query: PaginationQuery,
481    ) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError> {
482        self.repository.list_paginated(query).await
483    }
484
485    async fn update(
486        &self,
487        id: String,
488        entity: RelayerRepoModel,
489    ) -> Result<RelayerRepoModel, RepositoryError> {
490        self.repository.update(id, entity).await
491    }
492
493    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
494        self.repository.delete_by_id(id).await
495    }
496
497    async fn count(&self) -> Result<usize, RepositoryError> {
498        self.repository.count().await
499    }
500}
501
502#[async_trait]
503impl<T> RelayerRepository for RelayerRepositoryStorage<T>
504where
505    T: RelayerRepository + Send + Sync,
506{
507    async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
508        self.repository.list_active().await
509    }
510
511    async fn partial_update(
512        &self,
513        id: String,
514        update: RelayerUpdateRequest,
515    ) -> Result<RelayerRepoModel, RepositoryError> {
516        self.repository.partial_update(id, update).await
517    }
518
519    async fn enable_relayer(
520        &self,
521        relayer_id: String,
522    ) -> Result<RelayerRepoModel, RepositoryError> {
523        self.repository.enable_relayer(relayer_id).await
524    }
525
526    async fn disable_relayer(
527        &self,
528        relayer_id: String,
529    ) -> Result<RelayerRepoModel, RepositoryError> {
530        self.repository.disable_relayer(relayer_id).await
531    }
532
533    async fn update_policy(
534        &self,
535        id: String,
536        policy: RelayerNetworkPolicy,
537    ) -> Result<RelayerRepoModel, RepositoryError> {
538        self.repository.update_policy(id, policy).await
539    }
540}
541
542#[cfg(test)]
543mockall::mock! {
544    pub RelayerRepository {}
545
546    #[async_trait]
547    impl Repository<RelayerRepoModel, String> for RelayerRepository {
548        async fn create(&self, entity: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError>;
549        async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError>;
550        async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError>;
551        async fn list_paginated(&self, query: PaginationQuery) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError>;
552        async fn update(&self, id: String, entity: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError>;
553        async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError>;
554        async fn count(&self) -> Result<usize, RepositoryError>;
555    }
556
557    #[async_trait]
558    impl RelayerRepository for RelayerRepository {
559        async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError>;
560        async fn partial_update(&self, id: String, update: RelayerUpdateRequest) -> Result<RelayerRepoModel, RepositoryError>;
561        async fn enable_relayer(&self, relayer_id: String) -> Result<RelayerRepoModel, RepositoryError>;
562        async fn disable_relayer(&self, relayer_id: String) -> Result<RelayerRepoModel, RepositoryError>;
563        async fn update_policy(&self, id: String, policy: RelayerNetworkPolicy) -> Result<RelayerRepoModel, RepositoryError>;
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    fn create_test_relayer(id: String) -> RelayerRepoModel {
572        RelayerRepoModel {
573            id: id.clone(),
574            name: format!("Relayer {}", id.clone()),
575            network: "TestNet".to_string(),
576            paused: false,
577            network_type: NetworkType::Evm,
578            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
579                gas_price_cap: None,
580                whitelist_receivers: None,
581                eip1559_pricing: Some(false),
582                private_transactions: false,
583                min_balance: 0,
584            }),
585            signer_id: "test".to_string(),
586            address: "0x".to_string(),
587            notification_id: None,
588            system_disabled: false,
589            custom_rpc_urls: None,
590        }
591    }
592
593    #[actix_web::test]
594    async fn test_new_repository_is_empty() {
595        let repo = InMemoryRelayerRepository::new();
596        assert_eq!(repo.count().await.unwrap(), 0);
597    }
598
599    #[actix_web::test]
600    async fn test_add_relayer() {
601        let repo = InMemoryRelayerRepository::new();
602        let relayer = create_test_relayer("test".to_string());
603
604        repo.create(relayer.clone()).await.unwrap();
605        assert_eq!(repo.count().await.unwrap(), 1);
606
607        let stored = repo.get_by_id("test".to_string()).await.unwrap();
608        assert_eq!(stored.id, relayer.id);
609        assert_eq!(stored.name, relayer.name);
610    }
611
612    #[actix_web::test]
613    async fn test_update_relayer() {
614        let repo = InMemoryRelayerRepository::new();
615        let mut relayer = create_test_relayer("test".to_string());
616
617        repo.create(relayer.clone()).await.unwrap();
618
619        relayer.name = "Updated Name".to_string();
620        repo.update("test".to_string(), relayer.clone())
621            .await
622            .unwrap();
623
624        let updated = repo.get_by_id("test".to_string()).await.unwrap();
625        assert_eq!(updated.name, "Updated Name");
626    }
627
628    #[actix_web::test]
629    async fn test_list_relayers() {
630        let repo = InMemoryRelayerRepository::new();
631        let relayer1 = create_test_relayer("test".to_string());
632        let relayer2 = create_test_relayer("test2".to_string());
633
634        repo.create(relayer1.clone()).await.unwrap();
635        repo.create(relayer2).await.unwrap();
636
637        let relayers = repo.list_all().await.unwrap();
638        assert_eq!(relayers.len(), 2);
639    }
640
641    #[actix_web::test]
642    async fn test_list_active_relayers() {
643        let repo = InMemoryRelayerRepository::new();
644        let relayer1 = create_test_relayer("test".to_string());
645        let mut relayer2 = create_test_relayer("test2".to_string());
646
647        relayer2.paused = true;
648
649        repo.create(relayer1.clone()).await.unwrap();
650        repo.create(relayer2).await.unwrap();
651
652        let active_relayers = repo.list_active().await.unwrap();
653        assert_eq!(active_relayers.len(), 1);
654        assert_eq!(active_relayers[0].id, "test".to_string());
655    }
656
657    #[actix_web::test]
658    async fn test_update_nonexistent_relayer() {
659        let repo = InMemoryRelayerRepository::new();
660        let relayer = create_test_relayer("test".to_string());
661
662        let result = repo.update("test".to_string(), relayer).await;
663        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
664    }
665
666    #[actix_web::test]
667    async fn test_get_nonexistent_relayer() {
668        let repo = InMemoryRelayerRepository::new();
669
670        let result = repo.get_by_id("test".to_string()).await;
671        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
672    }
673
674    #[actix_web::test]
675    async fn test_partial_update_relayer() {
676        let repo = InMemoryRelayerRepository::new();
677
678        // Add a relayer to the repository
679        let relayer_id = "test_relayer".to_string();
680        let initial_relayer = create_test_relayer(relayer_id.clone());
681
682        repo.create(initial_relayer.clone()).await.unwrap();
683
684        // Perform a partial update on the relayer
685        let update_req = RelayerUpdateRequest { paused: Some(true) };
686
687        let updated_relayer = repo
688            .partial_update(relayer_id.clone(), update_req)
689            .await
690            .unwrap();
691
692        assert_eq!(updated_relayer.id, initial_relayer.id);
693        assert!(updated_relayer.paused);
694    }
695
696    #[actix_web::test]
697    async fn test_disable_relayer() {
698        let repo = InMemoryRelayerRepository::new();
699
700        // Add a relayer to the repository
701        let relayer_id = "test_relayer".to_string();
702        let initial_relayer = create_test_relayer(relayer_id.clone());
703
704        repo.create(initial_relayer.clone()).await.unwrap();
705
706        // Disable the relayer
707        let disabled_relayer = repo.disable_relayer(relayer_id.clone()).await.unwrap();
708
709        assert_eq!(disabled_relayer.id, initial_relayer.id);
710        assert!(disabled_relayer.system_disabled);
711    }
712
713    #[actix_web::test]
714    async fn test_enable_relayer() {
715        let repo = InMemoryRelayerRepository::new();
716
717        // Add a relayer to the repository
718        let relayer_id = "test_relayer".to_string();
719        let mut initial_relayer = create_test_relayer(relayer_id.clone());
720
721        initial_relayer.system_disabled = true;
722
723        repo.create(initial_relayer.clone()).await.unwrap();
724
725        // Enable the relayer
726        let enabled_relayer = repo.enable_relayer(relayer_id.clone()).await.unwrap();
727
728        assert_eq!(enabled_relayer.id, initial_relayer.id);
729        assert!(!enabled_relayer.system_disabled);
730    }
731
732    #[actix_web::test]
733    async fn test_update_policy() {
734        let repo = InMemoryRelayerRepository::new();
735        let relayer = create_test_relayer("test".to_string());
736
737        repo.create(relayer.clone()).await.unwrap();
738
739        // Create a new policy to update
740        let new_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
741            gas_price_cap: Some(50000000000),
742            whitelist_receivers: Some(vec!["0x1234".to_string()]),
743            eip1559_pricing: Some(true),
744            private_transactions: true,
745            min_balance: 1000000,
746        });
747
748        // Update the policy
749        let updated_relayer = repo
750            .update_policy("test".to_string(), new_policy.clone())
751            .await
752            .unwrap();
753
754        // Verify the policy was updated
755        match updated_relayer.policies {
756            RelayerNetworkPolicy::Evm(policy) => {
757                assert_eq!(policy.gas_price_cap, Some(50000000000));
758                assert_eq!(policy.whitelist_receivers, Some(vec!["0x1234".to_string()]));
759                assert_eq!(policy.eip1559_pricing, Some(true));
760                assert!(policy.private_transactions);
761                assert_eq!(policy.min_balance, 1000000);
762            }
763            _ => panic!("Unexpected policy type"),
764        }
765    }
766}