openzeppelin_relayer/models/relayer/
repository.rs

1use crate::models::{
2    DisabledReason, Relayer, RelayerError, RelayerEvmPolicy, RelayerSolanaPolicy,
3    RelayerStellarPolicy,
4};
5use serde::{Deserialize, Serialize};
6
7use super::{RelayerNetworkPolicy, RelayerNetworkType, RpcConfig};
8
9// Use the domain model RelayerNetworkType directly
10pub type NetworkType = RelayerNetworkType;
11
12/// Helper for safely updating relayer repository models from domain models
13/// while preserving runtime fields like address and system_disabled
14pub struct RelayerRepoUpdater {
15    original: RelayerRepoModel,
16}
17
18impl RelayerRepoUpdater {
19    /// Create an updater from an existing repository model
20    pub fn from_existing(existing: RelayerRepoModel) -> Self {
21        Self { original: existing }
22    }
23
24    /// Apply updates from a domain model while preserving runtime fields
25    ///
26    /// This method ensures that runtime fields (address, system_disabled, disabled_reason) from the
27    /// original repository model are preserved when converting from domain model,
28    /// preventing data loss during updates.
29    pub fn apply_domain_update(self, domain: Relayer) -> RelayerRepoModel {
30        let mut updated = RelayerRepoModel::from(domain);
31        // Preserve runtime fields from original
32        updated.address = self.original.address;
33        updated.system_disabled = self.original.system_disabled;
34        updated.disabled_reason = self.original.disabled_reason;
35        updated
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RelayerRepoModel {
41    pub id: String,
42    pub name: String,
43    pub network: String,
44    pub paused: bool,
45    pub network_type: NetworkType,
46    pub signer_id: String,
47    pub policies: RelayerNetworkPolicy,
48    pub address: String,
49    pub notification_id: Option<String>,
50    pub system_disabled: bool,
51    pub disabled_reason: Option<DisabledReason>,
52    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
53}
54
55impl RelayerRepoModel {
56    pub fn validate_active_state(&self) -> Result<(), RelayerError> {
57        if self.paused {
58            return Err(RelayerError::RelayerPaused);
59        }
60
61        if self.system_disabled {
62            return Err(RelayerError::RelayerDisabled);
63        }
64
65        Ok(())
66    }
67}
68
69impl Default for RelayerRepoModel {
70    fn default() -> Self {
71        Self {
72            id: "".to_string(),
73            name: "".to_string(),
74            network: "".to_string(),
75            paused: false,
76            network_type: NetworkType::Evm,
77            signer_id: "".to_string(),
78            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
79            address: "0x".to_string(),
80            notification_id: None,
81            system_disabled: false,
82            disabled_reason: None,
83            custom_rpc_urls: None,
84        }
85    }
86}
87
88impl From<RelayerRepoModel> for Relayer {
89    fn from(repo_model: RelayerRepoModel) -> Self {
90        Self {
91            id: repo_model.id,
92            name: repo_model.name,
93            network: repo_model.network,
94            paused: repo_model.paused,
95            network_type: repo_model.network_type,
96            policies: Some(repo_model.policies),
97            signer_id: repo_model.signer_id,
98            notification_id: repo_model.notification_id,
99            custom_rpc_urls: repo_model.custom_rpc_urls,
100        }
101    }
102}
103
104impl From<Relayer> for RelayerRepoModel {
105    fn from(relayer: Relayer) -> Self {
106        Self {
107            id: relayer.id,
108            name: relayer.name,
109            network: relayer.network,
110            paused: relayer.paused,
111            network_type: relayer.network_type,
112            signer_id: relayer.signer_id,
113            policies: relayer.policies.unwrap_or_else(|| {
114                // Default policy based on network type
115                match relayer.network_type {
116                    RelayerNetworkType::Evm => {
117                        RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())
118                    }
119                    RelayerNetworkType::Solana => {
120                        RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default())
121                    }
122                    RelayerNetworkType::Stellar => {
123                        RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default())
124                    }
125                }
126            }),
127            address: "".to_string(), // Will be filled in later by process_relayers
128            notification_id: relayer.notification_id,
129            system_disabled: false,
130            disabled_reason: None,
131            custom_rpc_urls: relayer.custom_rpc_urls,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use crate::models::{
139        RelayerEvmPolicy, RelayerSolanaPolicy, RelayerStellarPolicy, SolanaAllowedTokensPolicy,
140        SolanaFeePaymentStrategy, StellarFeePaymentStrategy,
141    };
142
143    use super::*;
144
145    fn create_test_relayer(paused: bool, system_disabled: bool) -> RelayerRepoModel {
146        RelayerRepoModel {
147            id: "test_relayer".to_string(),
148            name: "Test Relayer".to_string(),
149            paused,
150            system_disabled,
151            disabled_reason: None,
152            network: "test_network".to_string(),
153            network_type: NetworkType::Evm,
154            signer_id: "test_signer".to_string(),
155            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
156            address: "0xtest".to_string(),
157            notification_id: None,
158            custom_rpc_urls: None,
159        }
160    }
161
162    fn create_test_relayer_solana(paused: bool, system_disabled: bool) -> RelayerRepoModel {
163        RelayerRepoModel {
164            id: "test_solana_relayer".to_string(),
165            name: "Test Solana Relayer".to_string(),
166            paused,
167            system_disabled,
168            network: "mainnet".to_string(),
169            network_type: NetworkType::Solana,
170            signer_id: "test_signer".to_string(),
171            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
172                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
173                min_balance: Some(1000000),
174                max_signatures: Some(5),
175                allowed_tokens: None,
176                allowed_programs: None,
177                allowed_accounts: None,
178                disallowed_accounts: None,
179                max_tx_data_size: None,
180                max_allowed_fee_lamports: None,
181                swap_config: None,
182                fee_margin_percentage: None,
183            }),
184            address: "SolanaAddress123".to_string(),
185            notification_id: None,
186            custom_rpc_urls: None,
187            ..Default::default()
188        }
189    }
190
191    fn create_test_relayer_stellar(paused: bool, system_disabled: bool) -> RelayerRepoModel {
192        RelayerRepoModel {
193            id: "test_stellar_relayer".to_string(),
194            name: "Test Stellar Relayer".to_string(),
195            paused,
196            system_disabled,
197            network: "mainnet".to_string(),
198            network_type: NetworkType::Stellar,
199            signer_id: "test_signer".to_string(),
200            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
201                min_balance: Some(20000000),
202                max_fee: Some(100000),
203                timeout_seconds: Some(30),
204                concurrent_transactions: None,
205                allowed_tokens: None,
206                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
207                slippage_percentage: None,
208                fee_margin_percentage: None,
209                swap_config: None,
210            }),
211            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
212            notification_id: None,
213            custom_rpc_urls: None,
214            ..Default::default()
215        }
216    }
217
218    #[test]
219    fn test_validate_active_state_success() {
220        let relayer = create_test_relayer(false, false);
221        assert!(relayer.validate_active_state().is_ok());
222    }
223
224    #[test]
225    fn test_validate_active_state_success_solana() {
226        let relayer = create_test_relayer_solana(false, false);
227        assert!(relayer.validate_active_state().is_ok());
228    }
229
230    #[test]
231    fn test_validate_active_state_success_stellar() {
232        let relayer = create_test_relayer_stellar(false, false);
233        assert!(relayer.validate_active_state().is_ok());
234    }
235
236    #[test]
237    fn test_validate_active_state_paused() {
238        let relayer = create_test_relayer(true, false);
239        let result = relayer.validate_active_state();
240        assert!(result.is_err());
241        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
242    }
243
244    #[test]
245    fn test_validate_active_state_paused_solana() {
246        let relayer = create_test_relayer_solana(true, false);
247        let result = relayer.validate_active_state();
248        assert!(result.is_err());
249        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
250    }
251
252    #[test]
253    fn test_validate_active_state_paused_stellar() {
254        let relayer = create_test_relayer_stellar(true, false);
255        let result = relayer.validate_active_state();
256        assert!(result.is_err());
257        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
258    }
259
260    #[test]
261    fn test_validate_active_state_disabled() {
262        let relayer = create_test_relayer(false, true);
263        let result = relayer.validate_active_state();
264        assert!(result.is_err());
265        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
266    }
267
268    #[test]
269    fn test_validate_active_state_disabled_solana() {
270        let relayer = create_test_relayer_solana(false, true);
271        let result = relayer.validate_active_state();
272        assert!(result.is_err());
273        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
274    }
275
276    #[test]
277    fn test_validate_active_state_disabled_stellar() {
278        let relayer = create_test_relayer_stellar(false, true);
279        let result = relayer.validate_active_state();
280        assert!(result.is_err());
281        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
282    }
283
284    #[test]
285    fn test_validate_active_state_both_paused_and_disabled() {
286        // When both are true, should return paused error (checked first)
287        let relayer = create_test_relayer(true, true);
288        let result = relayer.validate_active_state();
289        assert!(result.is_err());
290        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
291    }
292
293    #[test]
294    fn test_conversion_from_repo_model_to_domain_evm() {
295        let repo_model = create_test_relayer(false, false);
296        let domain_relayer = Relayer::from(repo_model.clone());
297
298        assert_eq!(domain_relayer.id, repo_model.id);
299        assert_eq!(domain_relayer.name, repo_model.name);
300        assert_eq!(domain_relayer.network, repo_model.network);
301        assert_eq!(domain_relayer.paused, repo_model.paused);
302        assert_eq!(domain_relayer.network_type, repo_model.network_type);
303        assert_eq!(domain_relayer.signer_id, repo_model.signer_id);
304        assert_eq!(domain_relayer.notification_id, repo_model.notification_id);
305        assert_eq!(domain_relayer.custom_rpc_urls, repo_model.custom_rpc_urls);
306
307        // Policies should be converted correctly
308        assert!(domain_relayer.policies.is_some());
309        if let Some(RelayerNetworkPolicy::Evm(_)) = domain_relayer.policies {
310            // Success - correct policy type
311        } else {
312            panic!("Expected EVM policy");
313        }
314    }
315
316    #[test]
317    fn test_conversion_from_repo_model_to_domain_solana() {
318        let repo_model = create_test_relayer_solana(false, false);
319        let domain_relayer = Relayer::from(repo_model.clone());
320
321        assert_eq!(domain_relayer.id, repo_model.id);
322        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Solana);
323
324        // Policies should be converted correctly
325        assert!(domain_relayer.policies.is_some());
326        if let Some(RelayerNetworkPolicy::Solana(solana_policy)) = domain_relayer.policies {
327            assert_eq!(solana_policy.min_balance, Some(1000000));
328            assert_eq!(solana_policy.max_signatures, Some(5));
329            assert_eq!(
330                solana_policy.fee_payment_strategy,
331                Some(SolanaFeePaymentStrategy::Relayer)
332            );
333        } else {
334            panic!("Expected Solana policy");
335        }
336    }
337
338    #[test]
339    fn test_conversion_from_repo_model_to_domain_stellar() {
340        let repo_model = create_test_relayer_stellar(false, false);
341        let domain_relayer = Relayer::from(repo_model.clone());
342
343        assert_eq!(domain_relayer.id, repo_model.id);
344        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Stellar);
345
346        // Policies should be converted correctly
347        assert!(domain_relayer.policies.is_some());
348        if let Some(RelayerNetworkPolicy::Stellar(stellar_policy)) = domain_relayer.policies {
349            assert_eq!(stellar_policy.min_balance, Some(20000000));
350            assert_eq!(stellar_policy.max_fee, Some(100000));
351            assert_eq!(stellar_policy.timeout_seconds, Some(30));
352        } else {
353            panic!("Expected Stellar policy");
354        }
355    }
356
357    #[test]
358    fn test_conversion_from_domain_to_repo_model_evm() {
359        let domain_relayer = Relayer {
360            id: "test_evm_relayer".to_string(),
361            name: "Test EVM Relayer".to_string(),
362            network: "mainnet".to_string(),
363            paused: false,
364            network_type: RelayerNetworkType::Evm,
365            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
366                gas_price_cap: Some(100_000_000_000),
367                eip1559_pricing: Some(true),
368                min_balance: None,
369                gas_limit_estimation: None,
370                whitelist_receivers: None,
371                private_transactions: None,
372            })),
373            signer_id: "test_signer".to_string(),
374            notification_id: Some("notification_123".to_string()),
375            custom_rpc_urls: None,
376        };
377
378        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
379
380        assert_eq!(repo_model.id, domain_relayer.id);
381        assert_eq!(repo_model.name, domain_relayer.name);
382        assert_eq!(repo_model.network, domain_relayer.network);
383        assert_eq!(repo_model.paused, domain_relayer.paused);
384        assert_eq!(repo_model.network_type, domain_relayer.network_type);
385        assert_eq!(repo_model.signer_id, domain_relayer.signer_id);
386        assert_eq!(repo_model.notification_id, domain_relayer.notification_id);
387        assert_eq!(repo_model.custom_rpc_urls, domain_relayer.custom_rpc_urls);
388
389        // Runtime fields should have default values
390        assert_eq!(repo_model.address, "");
391        assert!(!repo_model.system_disabled);
392
393        // Policies should be converted correctly
394        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
395            assert_eq!(evm_policy.gas_price_cap, Some(100_000_000_000));
396            assert_eq!(evm_policy.eip1559_pricing, Some(true));
397        } else {
398            panic!("Expected EVM policy");
399        }
400    }
401
402    #[test]
403    fn test_conversion_from_domain_to_repo_model_solana() {
404        let domain_relayer = Relayer {
405            id: "test_solana_relayer".to_string(),
406            name: "Test Solana Relayer".to_string(),
407            network: "mainnet".to_string(),
408            paused: false,
409            network_type: RelayerNetworkType::Solana,
410            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
411                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
412                min_balance: Some(5000000),
413                max_signatures: Some(8),
414                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
415                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
416                    Some(100000),
417                    None,
418                )]),
419                allowed_programs: None,
420                allowed_accounts: None,
421                disallowed_accounts: None,
422                max_tx_data_size: None,
423                max_allowed_fee_lamports: None,
424                swap_config: None,
425                fee_margin_percentage: None,
426            })),
427            signer_id: "test_signer".to_string(),
428            notification_id: None,
429            custom_rpc_urls: None,
430        };
431
432        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
433
434        assert_eq!(repo_model.network_type, RelayerNetworkType::Solana);
435
436        // Policies should be converted correctly
437        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
438            assert_eq!(
439                solana_policy.fee_payment_strategy,
440                Some(SolanaFeePaymentStrategy::User)
441            );
442            assert_eq!(solana_policy.min_balance, Some(5000000));
443            assert_eq!(solana_policy.max_signatures, Some(8));
444            assert!(solana_policy.allowed_tokens.is_some());
445        } else {
446            panic!("Expected Solana policy");
447        }
448    }
449
450    #[test]
451    fn test_conversion_from_domain_to_repo_model_stellar() {
452        let domain_relayer = Relayer {
453            id: "test_stellar_relayer".to_string(),
454            name: "Test Stellar Relayer".to_string(),
455            network: "mainnet".to_string(),
456            paused: false,
457            network_type: RelayerNetworkType::Stellar,
458            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
459                min_balance: Some(30000000),
460                max_fee: Some(150000),
461                timeout_seconds: Some(60),
462                concurrent_transactions: None,
463                allowed_tokens: None,
464                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
465                slippage_percentage: None,
466                fee_margin_percentage: None,
467                swap_config: None,
468            })),
469            signer_id: "test_signer".to_string(),
470            notification_id: None,
471            custom_rpc_urls: None,
472        };
473
474        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
475
476        assert_eq!(repo_model.network_type, RelayerNetworkType::Stellar);
477
478        // Policies should be converted correctly
479        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
480            assert_eq!(stellar_policy.min_balance, Some(30000000));
481            assert_eq!(stellar_policy.max_fee, Some(150000));
482            assert_eq!(stellar_policy.timeout_seconds, Some(60));
483        } else {
484            panic!("Expected Stellar policy");
485        }
486    }
487
488    #[test]
489    fn test_conversion_from_domain_with_no_policies_evm() {
490        let domain_relayer = Relayer {
491            id: "test_evm_relayer".to_string(),
492            name: "Test EVM Relayer".to_string(),
493            network: "mainnet".to_string(),
494            paused: false,
495            network_type: RelayerNetworkType::Evm,
496            policies: None, // No policies provided
497            signer_id: "test_signer".to_string(),
498            notification_id: None,
499            custom_rpc_urls: None,
500        };
501
502        let repo_model = RelayerRepoModel::from(domain_relayer);
503
504        // Should create default EVM policy
505        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
506            // Default EVM policy should have all None values
507            assert_eq!(evm_policy.gas_price_cap, None);
508            assert_eq!(evm_policy.eip1559_pricing, None);
509            assert_eq!(evm_policy.min_balance, None);
510            assert_eq!(evm_policy.gas_limit_estimation, None);
511            assert_eq!(evm_policy.whitelist_receivers, None);
512            assert_eq!(evm_policy.private_transactions, None);
513        } else {
514            panic!("Expected default EVM policy");
515        }
516    }
517
518    #[test]
519    fn test_conversion_from_domain_with_no_policies_solana() {
520        let domain_relayer = Relayer {
521            id: "test_solana_relayer".to_string(),
522            name: "Test Solana Relayer".to_string(),
523            network: "mainnet".to_string(),
524            paused: false,
525            network_type: RelayerNetworkType::Solana,
526            policies: None, // No policies provided
527            signer_id: "test_signer".to_string(),
528            notification_id: None,
529            custom_rpc_urls: None,
530        };
531
532        let repo_model = RelayerRepoModel::from(domain_relayer);
533
534        // Should create default Solana policy
535        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
536            // Default Solana policy should have all None values
537            assert_eq!(solana_policy.fee_payment_strategy, None);
538            assert_eq!(solana_policy.min_balance, None);
539            assert_eq!(solana_policy.max_signatures, None);
540            assert_eq!(solana_policy.allowed_tokens, None);
541            assert_eq!(solana_policy.allowed_programs, None);
542            assert_eq!(solana_policy.allowed_accounts, None);
543            assert_eq!(solana_policy.disallowed_accounts, None);
544            assert_eq!(solana_policy.max_tx_data_size, None);
545            assert_eq!(solana_policy.max_allowed_fee_lamports, None);
546            assert_eq!(solana_policy.swap_config, None);
547            assert_eq!(solana_policy.fee_margin_percentage, None);
548        } else {
549            panic!("Expected default Solana policy");
550        }
551    }
552
553    #[test]
554    fn test_conversion_from_domain_with_no_policies_stellar() {
555        let domain_relayer = Relayer {
556            id: "test_stellar_relayer".to_string(),
557            name: "Test Stellar Relayer".to_string(),
558            network: "mainnet".to_string(),
559            paused: false,
560            network_type: RelayerNetworkType::Stellar,
561            policies: None, // No policies provided
562            signer_id: "test_signer".to_string(),
563            notification_id: None,
564            custom_rpc_urls: None,
565        };
566
567        let repo_model = RelayerRepoModel::from(domain_relayer);
568
569        // Should create default Stellar policy
570        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
571            // Default Stellar policy should have all None values
572            assert_eq!(stellar_policy.min_balance, None);
573            assert_eq!(stellar_policy.max_fee, None);
574            assert_eq!(stellar_policy.timeout_seconds, None);
575        } else {
576            panic!("Expected default Stellar policy");
577        }
578    }
579
580    #[test]
581    fn test_relayer_repo_updater_preserves_runtime_fields() {
582        // Create an original relayer with runtime fields set
583        let original = RelayerRepoModel {
584            id: "test_relayer".to_string(),
585            name: "Original Name".to_string(),
586            address: "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E".to_string(), // Runtime field
587            system_disabled: true,                                             // Runtime field
588            disabled_reason: Some(DisabledReason::BalanceCheckFailed(
589                "Balance too low".to_string(),
590            )), // Runtime field
591            paused: false,
592            network: "mainnet".to_string(),
593            network_type: NetworkType::Evm,
594            signer_id: "test_signer".to_string(),
595            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
596            notification_id: None,
597            custom_rpc_urls: None,
598        };
599
600        // Create a domain model with different business fields
601        let domain_update = Relayer {
602            id: "test_relayer".to_string(),
603            name: "Updated Name".to_string(), // Changed
604            paused: true,                     // Changed
605            network: "mainnet".to_string(),
606            network_type: RelayerNetworkType::Evm,
607            signer_id: "test_signer".to_string(),
608            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
609            notification_id: Some("new_notification".to_string()), // Changed
610            custom_rpc_urls: None,
611        };
612
613        // Use updater to preserve runtime fields
614        let updated =
615            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
616
617        // Verify business fields were updated
618        assert_eq!(updated.name, "Updated Name");
619        assert!(updated.paused);
620        assert_eq!(
621            updated.notification_id,
622            Some("new_notification".to_string())
623        );
624
625        // Verify runtime fields were preserved
626        assert_eq!(
627            updated.address,
628            "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E"
629        );
630        assert!(updated.system_disabled);
631        assert_eq!(
632            updated.disabled_reason,
633            Some(DisabledReason::BalanceCheckFailed(
634                "Balance too low".to_string()
635            ))
636        );
637    }
638
639    #[test]
640    fn test_relayer_repo_updater_preserves_runtime_fields_solana() {
641        // Create an original Solana relayer with runtime fields set
642        let original = RelayerRepoModel {
643            id: "test_solana_relayer".to_string(),
644            name: "Original Solana Name".to_string(),
645            address: "SolanaOriginalAddress123".to_string(), // Runtime field
646            system_disabled: true,                           // Runtime field
647            disabled_reason: Some(DisabledReason::RpcValidationFailed(
648                "RPC check failed".to_string(),
649            )), // Runtime field
650            paused: false,
651            network: "mainnet".to_string(),
652            network_type: NetworkType::Solana,
653            signer_id: "test_signer".to_string(),
654            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
655            notification_id: None,
656            custom_rpc_urls: None,
657        };
658
659        // Create a domain model with different business fields
660        let domain_update = Relayer {
661            id: "test_solana_relayer".to_string(),
662            name: "Updated Solana Name".to_string(), // Changed
663            paused: true,                            // Changed
664            network: "mainnet".to_string(),
665            network_type: RelayerNetworkType::Solana,
666            signer_id: "test_signer".to_string(),
667            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
668                min_balance: Some(2000000), // Changed
669                ..RelayerSolanaPolicy::default()
670            })),
671            notification_id: Some("solana_notification".to_string()), // Changed
672            custom_rpc_urls: None,
673        };
674
675        // Use updater to preserve runtime fields
676        let updated =
677            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
678
679        // Verify business fields were updated
680        assert_eq!(updated.name, "Updated Solana Name");
681        assert!(updated.paused);
682        assert_eq!(
683            updated.notification_id,
684            Some("solana_notification".to_string())
685        );
686
687        // Verify runtime fields were preserved
688        assert_eq!(updated.address, "SolanaOriginalAddress123");
689        assert!(updated.system_disabled);
690        assert_eq!(
691            updated.disabled_reason,
692            Some(DisabledReason::RpcValidationFailed(
693                "RPC check failed".to_string()
694            ))
695        );
696
697        // Verify policies were updated
698        if let RelayerNetworkPolicy::Solana(solana_policy) = updated.policies {
699            assert_eq!(solana_policy.min_balance, Some(2000000));
700        } else {
701            panic!("Expected Solana policy");
702        }
703    }
704
705    #[test]
706    fn test_relayer_repo_updater_preserves_runtime_fields_stellar() {
707        // Create an original Stellar relayer with runtime fields set
708        let original = RelayerRepoModel {
709            id: "test_stellar_relayer".to_string(),
710            name: "Original Stellar Name".to_string(),
711            address: "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(), // Runtime field
712            system_disabled: false, // Runtime field
713            disabled_reason: None,  // Runtime field
714            paused: true,
715            network: "mainnet".to_string(),
716            network_type: NetworkType::Stellar,
717            signer_id: "test_signer".to_string(),
718            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
719            notification_id: Some("original_notification".to_string()),
720            custom_rpc_urls: None,
721        };
722
723        // Create a domain model with different business fields
724        let domain_update = Relayer {
725            id: "test_stellar_relayer".to_string(),
726            name: "Updated Stellar Name".to_string(), // Changed
727            paused: false,                            // Changed
728            network: "mainnet".to_string(),
729            network_type: RelayerNetworkType::Stellar,
730            signer_id: "test_signer".to_string(),
731            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
732                min_balance: Some(40000000), // Changed
733                max_fee: Some(200000),       // Changed
734                timeout_seconds: Some(120),  // Changed
735                concurrent_transactions: None,
736                allowed_tokens: None,
737                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
738                slippage_percentage: None,
739                fee_margin_percentage: None,
740                swap_config: None,
741            })),
742            notification_id: None, // Changed
743            custom_rpc_urls: None,
744        };
745
746        // Use updater to preserve runtime fields
747        let updated =
748            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
749
750        // Verify business fields were updated
751        assert_eq!(updated.name, "Updated Stellar Name");
752        assert!(!updated.paused);
753        assert_eq!(updated.notification_id, None);
754
755        // Verify runtime fields were preserved
756        assert_eq!(
757            updated.address,
758            "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
759        );
760        assert!(!updated.system_disabled);
761        assert_eq!(updated.disabled_reason, None);
762
763        // Verify policies were updated
764        if let RelayerNetworkPolicy::Stellar(stellar_policy) = updated.policies {
765            assert_eq!(stellar_policy.min_balance, Some(40000000));
766            assert_eq!(stellar_policy.max_fee, Some(200000));
767            assert_eq!(stellar_policy.timeout_seconds, Some(120));
768        } else {
769            panic!("Expected Stellar policy");
770        }
771    }
772
773    #[test]
774    fn test_repo_model_serialization_deserialization_evm() {
775        let original = create_test_relayer(false, false);
776
777        // Serialize to JSON
778        let serialized = serde_json::to_string(&original).unwrap();
779        assert!(!serialized.is_empty());
780
781        // Deserialize back
782        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
783
784        // Verify all fields match
785        assert_eq!(original.id, deserialized.id);
786        assert_eq!(original.name, deserialized.name);
787        assert_eq!(original.network, deserialized.network);
788        assert_eq!(original.paused, deserialized.paused);
789        assert_eq!(original.network_type, deserialized.network_type);
790        assert_eq!(original.signer_id, deserialized.signer_id);
791        assert_eq!(original.address, deserialized.address);
792        assert_eq!(original.notification_id, deserialized.notification_id);
793        assert_eq!(original.system_disabled, deserialized.system_disabled);
794        assert_eq!(original.custom_rpc_urls, deserialized.custom_rpc_urls);
795
796        // Verify policies match
797        match (&original.policies, &deserialized.policies) {
798            (RelayerNetworkPolicy::Evm(_), RelayerNetworkPolicy::Evm(_)) => {
799                // Success - both are EVM policies
800            }
801            _ => panic!("Policy types don't match after serialization/deserialization"),
802        }
803    }
804
805    #[test]
806    fn test_repo_model_serialization_deserialization_solana() {
807        let original = create_test_relayer_solana(true, false);
808
809        // Serialize to JSON
810        let serialized = serde_json::to_string(&original).unwrap();
811        assert!(!serialized.is_empty());
812
813        // Deserialize back
814        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
815
816        // Verify key fields match
817        assert_eq!(original.id, deserialized.id);
818        assert_eq!(original.network_type, RelayerNetworkType::Solana);
819        assert_eq!(deserialized.network_type, RelayerNetworkType::Solana);
820        assert_eq!(original.paused, deserialized.paused);
821
822        // Verify policies match
823        match (&original.policies, &deserialized.policies) {
824            (RelayerNetworkPolicy::Solana(orig), RelayerNetworkPolicy::Solana(deser)) => {
825                assert_eq!(orig.fee_payment_strategy, deser.fee_payment_strategy);
826                assert_eq!(orig.min_balance, deser.min_balance);
827                assert_eq!(orig.max_signatures, deser.max_signatures);
828            }
829            _ => panic!("Policy types don't match after serialization/deserialization"),
830        }
831    }
832
833    #[test]
834    fn test_repo_model_serialization_deserialization_stellar() {
835        let original = create_test_relayer_stellar(false, true);
836
837        // Serialize to JSON
838        let serialized = serde_json::to_string(&original).unwrap();
839        assert!(!serialized.is_empty());
840
841        // Deserialize back
842        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
843
844        // Verify key fields match
845        assert_eq!(original.id, deserialized.id);
846        assert_eq!(original.network_type, RelayerNetworkType::Stellar);
847        assert_eq!(deserialized.network_type, RelayerNetworkType::Stellar);
848        assert_eq!(original.system_disabled, deserialized.system_disabled);
849
850        // Verify policies match
851        match (&original.policies, &deserialized.policies) {
852            (RelayerNetworkPolicy::Stellar(orig), RelayerNetworkPolicy::Stellar(deser)) => {
853                assert_eq!(orig.min_balance, deser.min_balance);
854                assert_eq!(orig.max_fee, deser.max_fee);
855                assert_eq!(orig.timeout_seconds, deser.timeout_seconds);
856            }
857            _ => panic!("Policy types don't match after serialization/deserialization"),
858        }
859    }
860
861    #[test]
862    fn test_repo_model_default() {
863        let default_model = RelayerRepoModel::default();
864
865        assert_eq!(default_model.id, "");
866        assert_eq!(default_model.name, "");
867        assert_eq!(default_model.network, "");
868        assert!(!default_model.paused);
869        assert_eq!(default_model.network_type, NetworkType::Evm);
870        assert_eq!(default_model.signer_id, "");
871        assert_eq!(default_model.address, "0x");
872        assert_eq!(default_model.notification_id, None);
873        assert!(!default_model.system_disabled);
874        assert_eq!(default_model.custom_rpc_urls, None);
875
876        // Default should have EVM policy
877        if let RelayerNetworkPolicy::Evm(_) = default_model.policies {
878            // Success
879        } else {
880            panic!("Default should have EVM policy");
881        }
882    }
883
884    #[test]
885    fn test_round_trip_conversion_all_network_types() {
886        // Test round-trip conversion: Domain -> Repo -> Domain for all network types
887
888        // EVM
889        let original_evm = Relayer {
890            id: "evm_relayer".to_string(),
891            name: "EVM Relayer".to_string(),
892            network: "mainnet".to_string(),
893            paused: false,
894            network_type: RelayerNetworkType::Evm,
895            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
896                gas_price_cap: Some(50_000_000_000),
897                eip1559_pricing: Some(true),
898                min_balance: None,
899                gas_limit_estimation: None,
900                whitelist_receivers: None,
901                private_transactions: None,
902            })),
903            signer_id: "evm_signer".to_string(),
904            notification_id: Some("evm_notification".to_string()),
905            custom_rpc_urls: None,
906        };
907
908        let repo_evm = RelayerRepoModel::from(original_evm.clone());
909        let recovered_evm = Relayer::from(repo_evm);
910
911        assert_eq!(original_evm.id, recovered_evm.id);
912        assert_eq!(original_evm.network_type, recovered_evm.network_type);
913        assert_eq!(original_evm.notification_id, recovered_evm.notification_id);
914
915        // Solana
916        let original_solana = Relayer {
917            id: "solana_relayer".to_string(),
918            name: "Solana Relayer".to_string(),
919            network: "mainnet".to_string(),
920            paused: true,
921            network_type: RelayerNetworkType::Solana,
922            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
923                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
924                min_balance: Some(3000000),
925                max_signatures: None,
926                allowed_tokens: None,
927                allowed_programs: None,
928                allowed_accounts: None,
929                disallowed_accounts: None,
930                max_tx_data_size: None,
931                max_allowed_fee_lamports: None,
932                swap_config: None,
933                fee_margin_percentage: None,
934            })),
935            signer_id: "solana_signer".to_string(),
936            notification_id: None,
937            custom_rpc_urls: None,
938        };
939
940        let repo_solana = RelayerRepoModel::from(original_solana.clone());
941        let recovered_solana = Relayer::from(repo_solana);
942
943        assert_eq!(original_solana.id, recovered_solana.id);
944        assert_eq!(original_solana.network_type, recovered_solana.network_type);
945        assert_eq!(original_solana.paused, recovered_solana.paused);
946
947        // Stellar
948        let original_stellar = Relayer {
949            id: "stellar_relayer".to_string(),
950            name: "Stellar Relayer".to_string(),
951            network: "mainnet".to_string(),
952            paused: false,
953            network_type: RelayerNetworkType::Stellar,
954            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
955                min_balance: Some(50000000),
956                max_fee: Some(250000),
957                timeout_seconds: Some(180),
958                concurrent_transactions: None,
959                allowed_tokens: None,
960                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
961                slippage_percentage: None,
962                fee_margin_percentage: None,
963                swap_config: None,
964            })),
965            signer_id: "stellar_signer".to_string(),
966            notification_id: Some("stellar_notification".to_string()),
967            custom_rpc_urls: None,
968        };
969
970        let repo_stellar = RelayerRepoModel::from(original_stellar.clone());
971        let recovered_stellar = Relayer::from(repo_stellar);
972
973        assert_eq!(original_stellar.id, recovered_stellar.id);
974        assert_eq!(
975            original_stellar.network_type,
976            recovered_stellar.network_type
977        );
978        assert_eq!(
979            original_stellar.notification_id,
980            recovered_stellar.notification_id
981        );
982    }
983}