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,
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            }),
206            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
207            notification_id: None,
208            custom_rpc_urls: None,
209            ..Default::default()
210        }
211    }
212
213    #[test]
214    fn test_validate_active_state_success() {
215        let relayer = create_test_relayer(false, false);
216        assert!(relayer.validate_active_state().is_ok());
217    }
218
219    #[test]
220    fn test_validate_active_state_success_solana() {
221        let relayer = create_test_relayer_solana(false, false);
222        assert!(relayer.validate_active_state().is_ok());
223    }
224
225    #[test]
226    fn test_validate_active_state_success_stellar() {
227        let relayer = create_test_relayer_stellar(false, false);
228        assert!(relayer.validate_active_state().is_ok());
229    }
230
231    #[test]
232    fn test_validate_active_state_paused() {
233        let relayer = create_test_relayer(true, false);
234        let result = relayer.validate_active_state();
235        assert!(result.is_err());
236        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
237    }
238
239    #[test]
240    fn test_validate_active_state_paused_solana() {
241        let relayer = create_test_relayer_solana(true, false);
242        let result = relayer.validate_active_state();
243        assert!(result.is_err());
244        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
245    }
246
247    #[test]
248    fn test_validate_active_state_paused_stellar() {
249        let relayer = create_test_relayer_stellar(true, false);
250        let result = relayer.validate_active_state();
251        assert!(result.is_err());
252        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
253    }
254
255    #[test]
256    fn test_validate_active_state_disabled() {
257        let relayer = create_test_relayer(false, true);
258        let result = relayer.validate_active_state();
259        assert!(result.is_err());
260        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
261    }
262
263    #[test]
264    fn test_validate_active_state_disabled_solana() {
265        let relayer = create_test_relayer_solana(false, true);
266        let result = relayer.validate_active_state();
267        assert!(result.is_err());
268        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
269    }
270
271    #[test]
272    fn test_validate_active_state_disabled_stellar() {
273        let relayer = create_test_relayer_stellar(false, true);
274        let result = relayer.validate_active_state();
275        assert!(result.is_err());
276        assert!(matches!(result.unwrap_err(), RelayerError::RelayerDisabled));
277    }
278
279    #[test]
280    fn test_validate_active_state_both_paused_and_disabled() {
281        // When both are true, should return paused error (checked first)
282        let relayer = create_test_relayer(true, true);
283        let result = relayer.validate_active_state();
284        assert!(result.is_err());
285        assert!(matches!(result.unwrap_err(), RelayerError::RelayerPaused));
286    }
287
288    #[test]
289    fn test_conversion_from_repo_model_to_domain_evm() {
290        let repo_model = create_test_relayer(false, false);
291        let domain_relayer = Relayer::from(repo_model.clone());
292
293        assert_eq!(domain_relayer.id, repo_model.id);
294        assert_eq!(domain_relayer.name, repo_model.name);
295        assert_eq!(domain_relayer.network, repo_model.network);
296        assert_eq!(domain_relayer.paused, repo_model.paused);
297        assert_eq!(domain_relayer.network_type, repo_model.network_type);
298        assert_eq!(domain_relayer.signer_id, repo_model.signer_id);
299        assert_eq!(domain_relayer.notification_id, repo_model.notification_id);
300        assert_eq!(domain_relayer.custom_rpc_urls, repo_model.custom_rpc_urls);
301
302        // Policies should be converted correctly
303        assert!(domain_relayer.policies.is_some());
304        if let Some(RelayerNetworkPolicy::Evm(_)) = domain_relayer.policies {
305            // Success - correct policy type
306        } else {
307            panic!("Expected EVM policy");
308        }
309    }
310
311    #[test]
312    fn test_conversion_from_repo_model_to_domain_solana() {
313        let repo_model = create_test_relayer_solana(false, false);
314        let domain_relayer = Relayer::from(repo_model.clone());
315
316        assert_eq!(domain_relayer.id, repo_model.id);
317        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Solana);
318
319        // Policies should be converted correctly
320        assert!(domain_relayer.policies.is_some());
321        if let Some(RelayerNetworkPolicy::Solana(solana_policy)) = domain_relayer.policies {
322            assert_eq!(solana_policy.min_balance, Some(1000000));
323            assert_eq!(solana_policy.max_signatures, Some(5));
324            assert_eq!(
325                solana_policy.fee_payment_strategy,
326                Some(SolanaFeePaymentStrategy::Relayer)
327            );
328        } else {
329            panic!("Expected Solana policy");
330        }
331    }
332
333    #[test]
334    fn test_conversion_from_repo_model_to_domain_stellar() {
335        let repo_model = create_test_relayer_stellar(false, false);
336        let domain_relayer = Relayer::from(repo_model.clone());
337
338        assert_eq!(domain_relayer.id, repo_model.id);
339        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Stellar);
340
341        // Policies should be converted correctly
342        assert!(domain_relayer.policies.is_some());
343        if let Some(RelayerNetworkPolicy::Stellar(stellar_policy)) = domain_relayer.policies {
344            assert_eq!(stellar_policy.min_balance, Some(20000000));
345            assert_eq!(stellar_policy.max_fee, Some(100000));
346            assert_eq!(stellar_policy.timeout_seconds, Some(30));
347        } else {
348            panic!("Expected Stellar policy");
349        }
350    }
351
352    #[test]
353    fn test_conversion_from_domain_to_repo_model_evm() {
354        let domain_relayer = Relayer {
355            id: "test_evm_relayer".to_string(),
356            name: "Test EVM Relayer".to_string(),
357            network: "mainnet".to_string(),
358            paused: false,
359            network_type: RelayerNetworkType::Evm,
360            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
361                gas_price_cap: Some(100_000_000_000),
362                eip1559_pricing: Some(true),
363                min_balance: None,
364                gas_limit_estimation: None,
365                whitelist_receivers: None,
366                private_transactions: None,
367            })),
368            signer_id: "test_signer".to_string(),
369            notification_id: Some("notification_123".to_string()),
370            custom_rpc_urls: None,
371        };
372
373        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
374
375        assert_eq!(repo_model.id, domain_relayer.id);
376        assert_eq!(repo_model.name, domain_relayer.name);
377        assert_eq!(repo_model.network, domain_relayer.network);
378        assert_eq!(repo_model.paused, domain_relayer.paused);
379        assert_eq!(repo_model.network_type, domain_relayer.network_type);
380        assert_eq!(repo_model.signer_id, domain_relayer.signer_id);
381        assert_eq!(repo_model.notification_id, domain_relayer.notification_id);
382        assert_eq!(repo_model.custom_rpc_urls, domain_relayer.custom_rpc_urls);
383
384        // Runtime fields should have default values
385        assert_eq!(repo_model.address, "");
386        assert!(!repo_model.system_disabled);
387
388        // Policies should be converted correctly
389        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
390            assert_eq!(evm_policy.gas_price_cap, Some(100_000_000_000));
391            assert_eq!(evm_policy.eip1559_pricing, Some(true));
392        } else {
393            panic!("Expected EVM policy");
394        }
395    }
396
397    #[test]
398    fn test_conversion_from_domain_to_repo_model_solana() {
399        let domain_relayer = Relayer {
400            id: "test_solana_relayer".to_string(),
401            name: "Test Solana Relayer".to_string(),
402            network: "mainnet".to_string(),
403            paused: false,
404            network_type: RelayerNetworkType::Solana,
405            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
406                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
407                min_balance: Some(5000000),
408                max_signatures: Some(8),
409                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
410                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
411                    Some(100000),
412                    None,
413                )]),
414                allowed_programs: None,
415                allowed_accounts: None,
416                disallowed_accounts: None,
417                max_tx_data_size: None,
418                max_allowed_fee_lamports: None,
419                swap_config: None,
420                fee_margin_percentage: None,
421            })),
422            signer_id: "test_signer".to_string(),
423            notification_id: None,
424            custom_rpc_urls: None,
425        };
426
427        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
428
429        assert_eq!(repo_model.network_type, RelayerNetworkType::Solana);
430
431        // Policies should be converted correctly
432        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
433            assert_eq!(
434                solana_policy.fee_payment_strategy,
435                Some(SolanaFeePaymentStrategy::User)
436            );
437            assert_eq!(solana_policy.min_balance, Some(5000000));
438            assert_eq!(solana_policy.max_signatures, Some(8));
439            assert!(solana_policy.allowed_tokens.is_some());
440        } else {
441            panic!("Expected Solana policy");
442        }
443    }
444
445    #[test]
446    fn test_conversion_from_domain_to_repo_model_stellar() {
447        let domain_relayer = Relayer {
448            id: "test_stellar_relayer".to_string(),
449            name: "Test Stellar Relayer".to_string(),
450            network: "mainnet".to_string(),
451            paused: false,
452            network_type: RelayerNetworkType::Stellar,
453            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
454                min_balance: Some(30000000),
455                max_fee: Some(150000),
456                timeout_seconds: Some(60),
457                concurrent_transactions: None,
458            })),
459            signer_id: "test_signer".to_string(),
460            notification_id: None,
461            custom_rpc_urls: None,
462        };
463
464        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
465
466        assert_eq!(repo_model.network_type, RelayerNetworkType::Stellar);
467
468        // Policies should be converted correctly
469        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
470            assert_eq!(stellar_policy.min_balance, Some(30000000));
471            assert_eq!(stellar_policy.max_fee, Some(150000));
472            assert_eq!(stellar_policy.timeout_seconds, Some(60));
473        } else {
474            panic!("Expected Stellar policy");
475        }
476    }
477
478    #[test]
479    fn test_conversion_from_domain_with_no_policies_evm() {
480        let domain_relayer = Relayer {
481            id: "test_evm_relayer".to_string(),
482            name: "Test EVM Relayer".to_string(),
483            network: "mainnet".to_string(),
484            paused: false,
485            network_type: RelayerNetworkType::Evm,
486            policies: None, // No policies provided
487            signer_id: "test_signer".to_string(),
488            notification_id: None,
489            custom_rpc_urls: None,
490        };
491
492        let repo_model = RelayerRepoModel::from(domain_relayer);
493
494        // Should create default EVM policy
495        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
496            // Default EVM policy should have all None values
497            assert_eq!(evm_policy.gas_price_cap, None);
498            assert_eq!(evm_policy.eip1559_pricing, None);
499            assert_eq!(evm_policy.min_balance, None);
500            assert_eq!(evm_policy.gas_limit_estimation, None);
501            assert_eq!(evm_policy.whitelist_receivers, None);
502            assert_eq!(evm_policy.private_transactions, None);
503        } else {
504            panic!("Expected default EVM policy");
505        }
506    }
507
508    #[test]
509    fn test_conversion_from_domain_with_no_policies_solana() {
510        let domain_relayer = Relayer {
511            id: "test_solana_relayer".to_string(),
512            name: "Test Solana Relayer".to_string(),
513            network: "mainnet".to_string(),
514            paused: false,
515            network_type: RelayerNetworkType::Solana,
516            policies: None, // No policies provided
517            signer_id: "test_signer".to_string(),
518            notification_id: None,
519            custom_rpc_urls: None,
520        };
521
522        let repo_model = RelayerRepoModel::from(domain_relayer);
523
524        // Should create default Solana policy
525        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
526            // Default Solana policy should have all None values
527            assert_eq!(solana_policy.fee_payment_strategy, None);
528            assert_eq!(solana_policy.min_balance, None);
529            assert_eq!(solana_policy.max_signatures, None);
530            assert_eq!(solana_policy.allowed_tokens, None);
531            assert_eq!(solana_policy.allowed_programs, None);
532            assert_eq!(solana_policy.allowed_accounts, None);
533            assert_eq!(solana_policy.disallowed_accounts, None);
534            assert_eq!(solana_policy.max_tx_data_size, None);
535            assert_eq!(solana_policy.max_allowed_fee_lamports, None);
536            assert_eq!(solana_policy.swap_config, None);
537            assert_eq!(solana_policy.fee_margin_percentage, None);
538        } else {
539            panic!("Expected default Solana policy");
540        }
541    }
542
543    #[test]
544    fn test_conversion_from_domain_with_no_policies_stellar() {
545        let domain_relayer = Relayer {
546            id: "test_stellar_relayer".to_string(),
547            name: "Test Stellar Relayer".to_string(),
548            network: "mainnet".to_string(),
549            paused: false,
550            network_type: RelayerNetworkType::Stellar,
551            policies: None, // No policies provided
552            signer_id: "test_signer".to_string(),
553            notification_id: None,
554            custom_rpc_urls: None,
555        };
556
557        let repo_model = RelayerRepoModel::from(domain_relayer);
558
559        // Should create default Stellar policy
560        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
561            // Default Stellar policy should have all None values
562            assert_eq!(stellar_policy.min_balance, None);
563            assert_eq!(stellar_policy.max_fee, None);
564            assert_eq!(stellar_policy.timeout_seconds, None);
565        } else {
566            panic!("Expected default Stellar policy");
567        }
568    }
569
570    #[test]
571    fn test_relayer_repo_updater_preserves_runtime_fields() {
572        // Create an original relayer with runtime fields set
573        let original = RelayerRepoModel {
574            id: "test_relayer".to_string(),
575            name: "Original Name".to_string(),
576            address: "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E".to_string(), // Runtime field
577            system_disabled: true,                                             // Runtime field
578            disabled_reason: Some(DisabledReason::BalanceCheckFailed(
579                "Balance too low".to_string(),
580            )), // Runtime field
581            paused: false,
582            network: "mainnet".to_string(),
583            network_type: NetworkType::Evm,
584            signer_id: "test_signer".to_string(),
585            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
586            notification_id: None,
587            custom_rpc_urls: None,
588        };
589
590        // Create a domain model with different business fields
591        let domain_update = Relayer {
592            id: "test_relayer".to_string(),
593            name: "Updated Name".to_string(), // Changed
594            paused: true,                     // Changed
595            network: "mainnet".to_string(),
596            network_type: RelayerNetworkType::Evm,
597            signer_id: "test_signer".to_string(),
598            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
599            notification_id: Some("new_notification".to_string()), // Changed
600            custom_rpc_urls: None,
601        };
602
603        // Use updater to preserve runtime fields
604        let updated =
605            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
606
607        // Verify business fields were updated
608        assert_eq!(updated.name, "Updated Name");
609        assert!(updated.paused);
610        assert_eq!(
611            updated.notification_id,
612            Some("new_notification".to_string())
613        );
614
615        // Verify runtime fields were preserved
616        assert_eq!(
617            updated.address,
618            "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E"
619        );
620        assert!(updated.system_disabled);
621        assert_eq!(
622            updated.disabled_reason,
623            Some(DisabledReason::BalanceCheckFailed(
624                "Balance too low".to_string()
625            ))
626        );
627    }
628
629    #[test]
630    fn test_relayer_repo_updater_preserves_runtime_fields_solana() {
631        // Create an original Solana relayer with runtime fields set
632        let original = RelayerRepoModel {
633            id: "test_solana_relayer".to_string(),
634            name: "Original Solana Name".to_string(),
635            address: "SolanaOriginalAddress123".to_string(), // Runtime field
636            system_disabled: true,                           // Runtime field
637            disabled_reason: Some(DisabledReason::RpcValidationFailed(
638                "RPC check failed".to_string(),
639            )), // Runtime field
640            paused: false,
641            network: "mainnet".to_string(),
642            network_type: NetworkType::Solana,
643            signer_id: "test_signer".to_string(),
644            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
645            notification_id: None,
646            custom_rpc_urls: None,
647        };
648
649        // Create a domain model with different business fields
650        let domain_update = Relayer {
651            id: "test_solana_relayer".to_string(),
652            name: "Updated Solana Name".to_string(), // Changed
653            paused: true,                            // Changed
654            network: "mainnet".to_string(),
655            network_type: RelayerNetworkType::Solana,
656            signer_id: "test_signer".to_string(),
657            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
658                min_balance: Some(2000000), // Changed
659                ..RelayerSolanaPolicy::default()
660            })),
661            notification_id: Some("solana_notification".to_string()), // Changed
662            custom_rpc_urls: None,
663        };
664
665        // Use updater to preserve runtime fields
666        let updated =
667            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
668
669        // Verify business fields were updated
670        assert_eq!(updated.name, "Updated Solana Name");
671        assert!(updated.paused);
672        assert_eq!(
673            updated.notification_id,
674            Some("solana_notification".to_string())
675        );
676
677        // Verify runtime fields were preserved
678        assert_eq!(updated.address, "SolanaOriginalAddress123");
679        assert!(updated.system_disabled);
680        assert_eq!(
681            updated.disabled_reason,
682            Some(DisabledReason::RpcValidationFailed(
683                "RPC check failed".to_string()
684            ))
685        );
686
687        // Verify policies were updated
688        if let RelayerNetworkPolicy::Solana(solana_policy) = updated.policies {
689            assert_eq!(solana_policy.min_balance, Some(2000000));
690        } else {
691            panic!("Expected Solana policy");
692        }
693    }
694
695    #[test]
696    fn test_relayer_repo_updater_preserves_runtime_fields_stellar() {
697        // Create an original Stellar relayer with runtime fields set
698        let original = RelayerRepoModel {
699            id: "test_stellar_relayer".to_string(),
700            name: "Original Stellar Name".to_string(),
701            address: "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(), // Runtime field
702            system_disabled: false, // Runtime field
703            disabled_reason: None,  // Runtime field
704            paused: true,
705            network: "mainnet".to_string(),
706            network_type: NetworkType::Stellar,
707            signer_id: "test_signer".to_string(),
708            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
709            notification_id: Some("original_notification".to_string()),
710            custom_rpc_urls: None,
711        };
712
713        // Create a domain model with different business fields
714        let domain_update = Relayer {
715            id: "test_stellar_relayer".to_string(),
716            name: "Updated Stellar Name".to_string(), // Changed
717            paused: false,                            // Changed
718            network: "mainnet".to_string(),
719            network_type: RelayerNetworkType::Stellar,
720            signer_id: "test_signer".to_string(),
721            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
722                min_balance: Some(40000000), // Changed
723                max_fee: Some(200000),       // Changed
724                timeout_seconds: Some(120),  // Changed
725                concurrent_transactions: None,
726            })),
727            notification_id: None, // Changed
728            custom_rpc_urls: None,
729        };
730
731        // Use updater to preserve runtime fields
732        let updated =
733            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
734
735        // Verify business fields were updated
736        assert_eq!(updated.name, "Updated Stellar Name");
737        assert!(!updated.paused);
738        assert_eq!(updated.notification_id, None);
739
740        // Verify runtime fields were preserved
741        assert_eq!(
742            updated.address,
743            "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
744        );
745        assert!(!updated.system_disabled);
746        assert_eq!(updated.disabled_reason, None);
747
748        // Verify policies were updated
749        if let RelayerNetworkPolicy::Stellar(stellar_policy) = updated.policies {
750            assert_eq!(stellar_policy.min_balance, Some(40000000));
751            assert_eq!(stellar_policy.max_fee, Some(200000));
752            assert_eq!(stellar_policy.timeout_seconds, Some(120));
753        } else {
754            panic!("Expected Stellar policy");
755        }
756    }
757
758    #[test]
759    fn test_repo_model_serialization_deserialization_evm() {
760        let original = create_test_relayer(false, false);
761
762        // Serialize to JSON
763        let serialized = serde_json::to_string(&original).unwrap();
764        assert!(!serialized.is_empty());
765
766        // Deserialize back
767        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
768
769        // Verify all fields match
770        assert_eq!(original.id, deserialized.id);
771        assert_eq!(original.name, deserialized.name);
772        assert_eq!(original.network, deserialized.network);
773        assert_eq!(original.paused, deserialized.paused);
774        assert_eq!(original.network_type, deserialized.network_type);
775        assert_eq!(original.signer_id, deserialized.signer_id);
776        assert_eq!(original.address, deserialized.address);
777        assert_eq!(original.notification_id, deserialized.notification_id);
778        assert_eq!(original.system_disabled, deserialized.system_disabled);
779        assert_eq!(original.custom_rpc_urls, deserialized.custom_rpc_urls);
780
781        // Verify policies match
782        match (&original.policies, &deserialized.policies) {
783            (RelayerNetworkPolicy::Evm(_), RelayerNetworkPolicy::Evm(_)) => {
784                // Success - both are EVM policies
785            }
786            _ => panic!("Policy types don't match after serialization/deserialization"),
787        }
788    }
789
790    #[test]
791    fn test_repo_model_serialization_deserialization_solana() {
792        let original = create_test_relayer_solana(true, false);
793
794        // Serialize to JSON
795        let serialized = serde_json::to_string(&original).unwrap();
796        assert!(!serialized.is_empty());
797
798        // Deserialize back
799        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
800
801        // Verify key fields match
802        assert_eq!(original.id, deserialized.id);
803        assert_eq!(original.network_type, RelayerNetworkType::Solana);
804        assert_eq!(deserialized.network_type, RelayerNetworkType::Solana);
805        assert_eq!(original.paused, deserialized.paused);
806
807        // Verify policies match
808        match (&original.policies, &deserialized.policies) {
809            (RelayerNetworkPolicy::Solana(orig), RelayerNetworkPolicy::Solana(deser)) => {
810                assert_eq!(orig.fee_payment_strategy, deser.fee_payment_strategy);
811                assert_eq!(orig.min_balance, deser.min_balance);
812                assert_eq!(orig.max_signatures, deser.max_signatures);
813            }
814            _ => panic!("Policy types don't match after serialization/deserialization"),
815        }
816    }
817
818    #[test]
819    fn test_repo_model_serialization_deserialization_stellar() {
820        let original = create_test_relayer_stellar(false, true);
821
822        // Serialize to JSON
823        let serialized = serde_json::to_string(&original).unwrap();
824        assert!(!serialized.is_empty());
825
826        // Deserialize back
827        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
828
829        // Verify key fields match
830        assert_eq!(original.id, deserialized.id);
831        assert_eq!(original.network_type, RelayerNetworkType::Stellar);
832        assert_eq!(deserialized.network_type, RelayerNetworkType::Stellar);
833        assert_eq!(original.system_disabled, deserialized.system_disabled);
834
835        // Verify policies match
836        match (&original.policies, &deserialized.policies) {
837            (RelayerNetworkPolicy::Stellar(orig), RelayerNetworkPolicy::Stellar(deser)) => {
838                assert_eq!(orig.min_balance, deser.min_balance);
839                assert_eq!(orig.max_fee, deser.max_fee);
840                assert_eq!(orig.timeout_seconds, deser.timeout_seconds);
841            }
842            _ => panic!("Policy types don't match after serialization/deserialization"),
843        }
844    }
845
846    #[test]
847    fn test_repo_model_default() {
848        let default_model = RelayerRepoModel::default();
849
850        assert_eq!(default_model.id, "");
851        assert_eq!(default_model.name, "");
852        assert_eq!(default_model.network, "");
853        assert!(!default_model.paused);
854        assert_eq!(default_model.network_type, NetworkType::Evm);
855        assert_eq!(default_model.signer_id, "");
856        assert_eq!(default_model.address, "0x");
857        assert_eq!(default_model.notification_id, None);
858        assert!(!default_model.system_disabled);
859        assert_eq!(default_model.custom_rpc_urls, None);
860
861        // Default should have EVM policy
862        if let RelayerNetworkPolicy::Evm(_) = default_model.policies {
863            // Success
864        } else {
865            panic!("Default should have EVM policy");
866        }
867    }
868
869    #[test]
870    fn test_round_trip_conversion_all_network_types() {
871        // Test round-trip conversion: Domain -> Repo -> Domain for all network types
872
873        // EVM
874        let original_evm = Relayer {
875            id: "evm_relayer".to_string(),
876            name: "EVM Relayer".to_string(),
877            network: "mainnet".to_string(),
878            paused: false,
879            network_type: RelayerNetworkType::Evm,
880            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
881                gas_price_cap: Some(50_000_000_000),
882                eip1559_pricing: Some(true),
883                min_balance: None,
884                gas_limit_estimation: None,
885                whitelist_receivers: None,
886                private_transactions: None,
887            })),
888            signer_id: "evm_signer".to_string(),
889            notification_id: Some("evm_notification".to_string()),
890            custom_rpc_urls: None,
891        };
892
893        let repo_evm = RelayerRepoModel::from(original_evm.clone());
894        let recovered_evm = Relayer::from(repo_evm);
895
896        assert_eq!(original_evm.id, recovered_evm.id);
897        assert_eq!(original_evm.network_type, recovered_evm.network_type);
898        assert_eq!(original_evm.notification_id, recovered_evm.notification_id);
899
900        // Solana
901        let original_solana = Relayer {
902            id: "solana_relayer".to_string(),
903            name: "Solana Relayer".to_string(),
904            network: "mainnet".to_string(),
905            paused: true,
906            network_type: RelayerNetworkType::Solana,
907            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
908                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
909                min_balance: Some(3000000),
910                max_signatures: None,
911                allowed_tokens: None,
912                allowed_programs: None,
913                allowed_accounts: None,
914                disallowed_accounts: None,
915                max_tx_data_size: None,
916                max_allowed_fee_lamports: None,
917                swap_config: None,
918                fee_margin_percentage: None,
919            })),
920            signer_id: "solana_signer".to_string(),
921            notification_id: None,
922            custom_rpc_urls: None,
923        };
924
925        let repo_solana = RelayerRepoModel::from(original_solana.clone());
926        let recovered_solana = Relayer::from(repo_solana);
927
928        assert_eq!(original_solana.id, recovered_solana.id);
929        assert_eq!(original_solana.network_type, recovered_solana.network_type);
930        assert_eq!(original_solana.paused, recovered_solana.paused);
931
932        // Stellar
933        let original_stellar = Relayer {
934            id: "stellar_relayer".to_string(),
935            name: "Stellar Relayer".to_string(),
936            network: "mainnet".to_string(),
937            paused: false,
938            network_type: RelayerNetworkType::Stellar,
939            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
940                min_balance: Some(50000000),
941                max_fee: Some(250000),
942                timeout_seconds: Some(180),
943                concurrent_transactions: None,
944            })),
945            signer_id: "stellar_signer".to_string(),
946            notification_id: Some("stellar_notification".to_string()),
947            custom_rpc_urls: None,
948        };
949
950        let repo_stellar = RelayerRepoModel::from(original_stellar.clone());
951        let recovered_stellar = Relayer::from(repo_stellar);
952
953        assert_eq!(original_stellar.id, recovered_stellar.id);
954        assert_eq!(
955            original_stellar.network_type,
956            recovered_stellar.network_type
957        );
958        assert_eq!(
959            original_stellar.notification_id,
960            recovered_stellar.notification_id
961        );
962    }
963}