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                include_revert_data: Some(false),
367                gas_price_cap: Some(100_000_000_000),
368                eip1559_pricing: Some(true),
369                min_balance: None,
370                gas_limit_estimation: None,
371                whitelist_receivers: None,
372                private_transactions: None,
373            })),
374            signer_id: "test_signer".to_string(),
375            notification_id: Some("notification_123".to_string()),
376            custom_rpc_urls: None,
377        };
378
379        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
380
381        assert_eq!(repo_model.id, domain_relayer.id);
382        assert_eq!(repo_model.name, domain_relayer.name);
383        assert_eq!(repo_model.network, domain_relayer.network);
384        assert_eq!(repo_model.paused, domain_relayer.paused);
385        assert_eq!(repo_model.network_type, domain_relayer.network_type);
386        assert_eq!(repo_model.signer_id, domain_relayer.signer_id);
387        assert_eq!(repo_model.notification_id, domain_relayer.notification_id);
388        assert_eq!(repo_model.custom_rpc_urls, domain_relayer.custom_rpc_urls);
389
390        // Runtime fields should have default values
391        assert_eq!(repo_model.address, "");
392        assert!(!repo_model.system_disabled);
393
394        // Policies should be converted correctly
395        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
396            assert_eq!(evm_policy.include_revert_data, Some(false));
397            assert_eq!(evm_policy.gas_price_cap, Some(100_000_000_000));
398            assert_eq!(evm_policy.eip1559_pricing, Some(true));
399        } else {
400            panic!("Expected EVM policy");
401        }
402    }
403
404    #[test]
405    fn test_conversion_from_domain_to_repo_model_solana() {
406        let domain_relayer = Relayer {
407            id: "test_solana_relayer".to_string(),
408            name: "Test Solana Relayer".to_string(),
409            network: "mainnet".to_string(),
410            paused: false,
411            network_type: RelayerNetworkType::Solana,
412            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
413                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
414                min_balance: Some(5000000),
415                max_signatures: Some(8),
416                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
417                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
418                    Some(100000),
419                    None,
420                )]),
421                allowed_programs: None,
422                allowed_accounts: None,
423                disallowed_accounts: None,
424                max_tx_data_size: None,
425                max_allowed_fee_lamports: None,
426                swap_config: None,
427                fee_margin_percentage: None,
428            })),
429            signer_id: "test_signer".to_string(),
430            notification_id: None,
431            custom_rpc_urls: None,
432        };
433
434        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
435
436        assert_eq!(repo_model.network_type, RelayerNetworkType::Solana);
437
438        // Policies should be converted correctly
439        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
440            assert_eq!(
441                solana_policy.fee_payment_strategy,
442                Some(SolanaFeePaymentStrategy::User)
443            );
444            assert_eq!(solana_policy.min_balance, Some(5000000));
445            assert_eq!(solana_policy.max_signatures, Some(8));
446            assert!(solana_policy.allowed_tokens.is_some());
447        } else {
448            panic!("Expected Solana policy");
449        }
450    }
451
452    #[test]
453    fn test_conversion_from_domain_to_repo_model_stellar() {
454        let domain_relayer = Relayer {
455            id: "test_stellar_relayer".to_string(),
456            name: "Test Stellar Relayer".to_string(),
457            network: "mainnet".to_string(),
458            paused: false,
459            network_type: RelayerNetworkType::Stellar,
460            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
461                min_balance: Some(30000000),
462                max_fee: Some(150000),
463                timeout_seconds: Some(60),
464                concurrent_transactions: None,
465                allowed_tokens: None,
466                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
467                slippage_percentage: None,
468                fee_margin_percentage: None,
469                swap_config: None,
470            })),
471            signer_id: "test_signer".to_string(),
472            notification_id: None,
473            custom_rpc_urls: None,
474        };
475
476        let repo_model = RelayerRepoModel::from(domain_relayer.clone());
477
478        assert_eq!(repo_model.network_type, RelayerNetworkType::Stellar);
479
480        // Policies should be converted correctly
481        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
482            assert_eq!(stellar_policy.min_balance, Some(30000000));
483            assert_eq!(stellar_policy.max_fee, Some(150000));
484            assert_eq!(stellar_policy.timeout_seconds, Some(60));
485        } else {
486            panic!("Expected Stellar policy");
487        }
488    }
489
490    #[test]
491    fn test_conversion_from_domain_with_no_policies_evm() {
492        let domain_relayer = Relayer {
493            id: "test_evm_relayer".to_string(),
494            name: "Test EVM Relayer".to_string(),
495            network: "mainnet".to_string(),
496            paused: false,
497            network_type: RelayerNetworkType::Evm,
498            policies: None, // No policies provided
499            signer_id: "test_signer".to_string(),
500            notification_id: None,
501            custom_rpc_urls: None,
502        };
503
504        let repo_model = RelayerRepoModel::from(domain_relayer);
505
506        // Should create default EVM policy
507        if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
508            // Default EVM policy should have all None values
509            assert_eq!(evm_policy.gas_price_cap, None);
510            assert_eq!(evm_policy.eip1559_pricing, None);
511            assert_eq!(evm_policy.min_balance, None);
512            assert_eq!(evm_policy.gas_limit_estimation, None);
513            assert_eq!(evm_policy.whitelist_receivers, None);
514            assert_eq!(evm_policy.private_transactions, None);
515        } else {
516            panic!("Expected default EVM policy");
517        }
518    }
519
520    #[test]
521    fn test_conversion_from_domain_with_no_policies_solana() {
522        let domain_relayer = Relayer {
523            id: "test_solana_relayer".to_string(),
524            name: "Test Solana Relayer".to_string(),
525            network: "mainnet".to_string(),
526            paused: false,
527            network_type: RelayerNetworkType::Solana,
528            policies: None, // No policies provided
529            signer_id: "test_signer".to_string(),
530            notification_id: None,
531            custom_rpc_urls: None,
532        };
533
534        let repo_model = RelayerRepoModel::from(domain_relayer);
535
536        // Should create default Solana policy
537        if let RelayerNetworkPolicy::Solana(solana_policy) = repo_model.policies {
538            // Default Solana policy should have all None values
539            assert_eq!(solana_policy.fee_payment_strategy, None);
540            assert_eq!(solana_policy.min_balance, None);
541            assert_eq!(solana_policy.max_signatures, None);
542            assert_eq!(solana_policy.allowed_tokens, None);
543            assert_eq!(solana_policy.allowed_programs, None);
544            assert_eq!(solana_policy.allowed_accounts, None);
545            assert_eq!(solana_policy.disallowed_accounts, None);
546            assert_eq!(solana_policy.max_tx_data_size, None);
547            assert_eq!(solana_policy.max_allowed_fee_lamports, None);
548            assert_eq!(solana_policy.swap_config, None);
549            assert_eq!(solana_policy.fee_margin_percentage, None);
550        } else {
551            panic!("Expected default Solana policy");
552        }
553    }
554
555    #[test]
556    fn test_conversion_from_domain_with_no_policies_stellar() {
557        let domain_relayer = Relayer {
558            id: "test_stellar_relayer".to_string(),
559            name: "Test Stellar Relayer".to_string(),
560            network: "mainnet".to_string(),
561            paused: false,
562            network_type: RelayerNetworkType::Stellar,
563            policies: None, // No policies provided
564            signer_id: "test_signer".to_string(),
565            notification_id: None,
566            custom_rpc_urls: None,
567        };
568
569        let repo_model = RelayerRepoModel::from(domain_relayer);
570
571        // Should create default Stellar policy
572        if let RelayerNetworkPolicy::Stellar(stellar_policy) = repo_model.policies {
573            // Default Stellar policy should have all None values
574            assert_eq!(stellar_policy.min_balance, None);
575            assert_eq!(stellar_policy.max_fee, None);
576            assert_eq!(stellar_policy.timeout_seconds, None);
577        } else {
578            panic!("Expected default Stellar policy");
579        }
580    }
581
582    #[test]
583    fn test_relayer_repo_updater_preserves_runtime_fields() {
584        // Create an original relayer with runtime fields set
585        let original = RelayerRepoModel {
586            id: "test_relayer".to_string(),
587            name: "Original Name".to_string(),
588            address: "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E".to_string(), // Runtime field
589            system_disabled: true,                                             // Runtime field
590            disabled_reason: Some(DisabledReason::BalanceCheckFailed(
591                "Balance too low".to_string(),
592            )), // Runtime field
593            paused: false,
594            network: "mainnet".to_string(),
595            network_type: NetworkType::Evm,
596            signer_id: "test_signer".to_string(),
597            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
598            notification_id: None,
599            custom_rpc_urls: None,
600        };
601
602        // Create a domain model with different business fields
603        let domain_update = Relayer {
604            id: "test_relayer".to_string(),
605            name: "Updated Name".to_string(), // Changed
606            paused: true,                     // Changed
607            network: "mainnet".to_string(),
608            network_type: RelayerNetworkType::Evm,
609            signer_id: "test_signer".to_string(),
610            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
611            notification_id: Some("new_notification".to_string()), // Changed
612            custom_rpc_urls: None,
613        };
614
615        // Use updater to preserve runtime fields
616        let updated =
617            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
618
619        // Verify business fields were updated
620        assert_eq!(updated.name, "Updated Name");
621        assert!(updated.paused);
622        assert_eq!(
623            updated.notification_id,
624            Some("new_notification".to_string())
625        );
626
627        // Verify runtime fields were preserved
628        assert_eq!(
629            updated.address,
630            "0x742d35Cc6634C0532925a3b8D8C2e48a73F6ba2E"
631        );
632        assert!(updated.system_disabled);
633        assert_eq!(
634            updated.disabled_reason,
635            Some(DisabledReason::BalanceCheckFailed(
636                "Balance too low".to_string()
637            ))
638        );
639    }
640
641    #[test]
642    fn test_relayer_repo_updater_preserves_runtime_fields_solana() {
643        // Create an original Solana relayer with runtime fields set
644        let original = RelayerRepoModel {
645            id: "test_solana_relayer".to_string(),
646            name: "Original Solana Name".to_string(),
647            address: "SolanaOriginalAddress123".to_string(), // Runtime field
648            system_disabled: true,                           // Runtime field
649            disabled_reason: Some(DisabledReason::RpcValidationFailed(
650                "RPC check failed".to_string(),
651            )), // Runtime field
652            paused: false,
653            network: "mainnet".to_string(),
654            network_type: NetworkType::Solana,
655            signer_id: "test_signer".to_string(),
656            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
657            notification_id: None,
658            custom_rpc_urls: None,
659        };
660
661        // Create a domain model with different business fields
662        let domain_update = Relayer {
663            id: "test_solana_relayer".to_string(),
664            name: "Updated Solana Name".to_string(), // Changed
665            paused: true,                            // Changed
666            network: "mainnet".to_string(),
667            network_type: RelayerNetworkType::Solana,
668            signer_id: "test_signer".to_string(),
669            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
670                min_balance: Some(2000000), // Changed
671                ..RelayerSolanaPolicy::default()
672            })),
673            notification_id: Some("solana_notification".to_string()), // Changed
674            custom_rpc_urls: None,
675        };
676
677        // Use updater to preserve runtime fields
678        let updated =
679            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
680
681        // Verify business fields were updated
682        assert_eq!(updated.name, "Updated Solana Name");
683        assert!(updated.paused);
684        assert_eq!(
685            updated.notification_id,
686            Some("solana_notification".to_string())
687        );
688
689        // Verify runtime fields were preserved
690        assert_eq!(updated.address, "SolanaOriginalAddress123");
691        assert!(updated.system_disabled);
692        assert_eq!(
693            updated.disabled_reason,
694            Some(DisabledReason::RpcValidationFailed(
695                "RPC check failed".to_string()
696            ))
697        );
698
699        // Verify policies were updated
700        if let RelayerNetworkPolicy::Solana(solana_policy) = updated.policies {
701            assert_eq!(solana_policy.min_balance, Some(2000000));
702        } else {
703            panic!("Expected Solana policy");
704        }
705    }
706
707    #[test]
708    fn test_relayer_repo_updater_preserves_runtime_fields_stellar() {
709        // Create an original Stellar relayer with runtime fields set
710        let original = RelayerRepoModel {
711            id: "test_stellar_relayer".to_string(),
712            name: "Original Stellar Name".to_string(),
713            address: "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(), // Runtime field
714            system_disabled: false, // Runtime field
715            disabled_reason: None,  // Runtime field
716            paused: true,
717            network: "mainnet".to_string(),
718            network_type: NetworkType::Stellar,
719            signer_id: "test_signer".to_string(),
720            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
721            notification_id: Some("original_notification".to_string()),
722            custom_rpc_urls: None,
723        };
724
725        // Create a domain model with different business fields
726        let domain_update = Relayer {
727            id: "test_stellar_relayer".to_string(),
728            name: "Updated Stellar Name".to_string(), // Changed
729            paused: false,                            // Changed
730            network: "mainnet".to_string(),
731            network_type: RelayerNetworkType::Stellar,
732            signer_id: "test_signer".to_string(),
733            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
734                min_balance: Some(40000000), // Changed
735                max_fee: Some(200000),       // Changed
736                timeout_seconds: Some(120),  // Changed
737                concurrent_transactions: None,
738                allowed_tokens: None,
739                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
740                slippage_percentage: None,
741                fee_margin_percentage: None,
742                swap_config: None,
743            })),
744            notification_id: None, // Changed
745            custom_rpc_urls: None,
746        };
747
748        // Use updater to preserve runtime fields
749        let updated =
750            RelayerRepoUpdater::from_existing(original.clone()).apply_domain_update(domain_update);
751
752        // Verify business fields were updated
753        assert_eq!(updated.name, "Updated Stellar Name");
754        assert!(!updated.paused);
755        assert_eq!(updated.notification_id, None);
756
757        // Verify runtime fields were preserved
758        assert_eq!(
759            updated.address,
760            "GORIGINALXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
761        );
762        assert!(!updated.system_disabled);
763        assert_eq!(updated.disabled_reason, None);
764
765        // Verify policies were updated
766        if let RelayerNetworkPolicy::Stellar(stellar_policy) = updated.policies {
767            assert_eq!(stellar_policy.min_balance, Some(40000000));
768            assert_eq!(stellar_policy.max_fee, Some(200000));
769            assert_eq!(stellar_policy.timeout_seconds, Some(120));
770        } else {
771            panic!("Expected Stellar policy");
772        }
773    }
774
775    #[test]
776    fn test_repo_model_serialization_deserialization_evm() {
777        let original = create_test_relayer(false, false);
778
779        // Serialize to JSON
780        let serialized = serde_json::to_string(&original).unwrap();
781        assert!(!serialized.is_empty());
782
783        // Deserialize back
784        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
785
786        // Verify all fields match
787        assert_eq!(original.id, deserialized.id);
788        assert_eq!(original.name, deserialized.name);
789        assert_eq!(original.network, deserialized.network);
790        assert_eq!(original.paused, deserialized.paused);
791        assert_eq!(original.network_type, deserialized.network_type);
792        assert_eq!(original.signer_id, deserialized.signer_id);
793        assert_eq!(original.address, deserialized.address);
794        assert_eq!(original.notification_id, deserialized.notification_id);
795        assert_eq!(original.system_disabled, deserialized.system_disabled);
796        assert_eq!(original.custom_rpc_urls, deserialized.custom_rpc_urls);
797
798        // Verify policies match
799        match (&original.policies, &deserialized.policies) {
800            (RelayerNetworkPolicy::Evm(_), RelayerNetworkPolicy::Evm(_)) => {
801                // Success - both are EVM policies
802            }
803            _ => panic!("Policy types don't match after serialization/deserialization"),
804        }
805    }
806
807    #[test]
808    fn test_repo_model_serialization_deserialization_solana() {
809        let original = create_test_relayer_solana(true, false);
810
811        // Serialize to JSON
812        let serialized = serde_json::to_string(&original).unwrap();
813        assert!(!serialized.is_empty());
814
815        // Deserialize back
816        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
817
818        // Verify key fields match
819        assert_eq!(original.id, deserialized.id);
820        assert_eq!(original.network_type, RelayerNetworkType::Solana);
821        assert_eq!(deserialized.network_type, RelayerNetworkType::Solana);
822        assert_eq!(original.paused, deserialized.paused);
823
824        // Verify policies match
825        match (&original.policies, &deserialized.policies) {
826            (RelayerNetworkPolicy::Solana(orig), RelayerNetworkPolicy::Solana(deser)) => {
827                assert_eq!(orig.fee_payment_strategy, deser.fee_payment_strategy);
828                assert_eq!(orig.min_balance, deser.min_balance);
829                assert_eq!(orig.max_signatures, deser.max_signatures);
830            }
831            _ => panic!("Policy types don't match after serialization/deserialization"),
832        }
833    }
834
835    #[test]
836    fn test_repo_model_serialization_deserialization_stellar() {
837        let original = create_test_relayer_stellar(false, true);
838
839        // Serialize to JSON
840        let serialized = serde_json::to_string(&original).unwrap();
841        assert!(!serialized.is_empty());
842
843        // Deserialize back
844        let deserialized: RelayerRepoModel = serde_json::from_str(&serialized).unwrap();
845
846        // Verify key fields match
847        assert_eq!(original.id, deserialized.id);
848        assert_eq!(original.network_type, RelayerNetworkType::Stellar);
849        assert_eq!(deserialized.network_type, RelayerNetworkType::Stellar);
850        assert_eq!(original.system_disabled, deserialized.system_disabled);
851
852        // Verify policies match
853        match (&original.policies, &deserialized.policies) {
854            (RelayerNetworkPolicy::Stellar(orig), RelayerNetworkPolicy::Stellar(deser)) => {
855                assert_eq!(orig.min_balance, deser.min_balance);
856                assert_eq!(orig.max_fee, deser.max_fee);
857                assert_eq!(orig.timeout_seconds, deser.timeout_seconds);
858            }
859            _ => panic!("Policy types don't match after serialization/deserialization"),
860        }
861    }
862
863    #[test]
864    fn test_repo_model_default() {
865        let default_model = RelayerRepoModel::default();
866
867        assert_eq!(default_model.id, "");
868        assert_eq!(default_model.name, "");
869        assert_eq!(default_model.network, "");
870        assert!(!default_model.paused);
871        assert_eq!(default_model.network_type, NetworkType::Evm);
872        assert_eq!(default_model.signer_id, "");
873        assert_eq!(default_model.address, "0x");
874        assert_eq!(default_model.notification_id, None);
875        assert!(!default_model.system_disabled);
876        assert_eq!(default_model.custom_rpc_urls, None);
877
878        // Default should have EVM policy
879        if let RelayerNetworkPolicy::Evm(_) = default_model.policies {
880            // Success
881        } else {
882            panic!("Default should have EVM policy");
883        }
884    }
885
886    #[test]
887    fn test_round_trip_conversion_all_network_types() {
888        // Test round-trip conversion: Domain -> Repo -> Domain for all network types
889
890        // EVM
891        let original_evm = Relayer {
892            id: "evm_relayer".to_string(),
893            name: "EVM Relayer".to_string(),
894            network: "mainnet".to_string(),
895            paused: false,
896            network_type: RelayerNetworkType::Evm,
897            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
898                include_revert_data: Some(false),
899                gas_price_cap: Some(50_000_000_000),
900                eip1559_pricing: Some(true),
901                min_balance: None,
902                gas_limit_estimation: None,
903                whitelist_receivers: None,
904                private_transactions: None,
905            })),
906            signer_id: "evm_signer".to_string(),
907            notification_id: Some("evm_notification".to_string()),
908            custom_rpc_urls: None,
909        };
910
911        let repo_evm = RelayerRepoModel::from(original_evm.clone());
912        let recovered_evm = Relayer::from(repo_evm);
913
914        assert_eq!(original_evm.id, recovered_evm.id);
915        assert_eq!(original_evm.network_type, recovered_evm.network_type);
916        assert_eq!(original_evm.notification_id, recovered_evm.notification_id);
917        if let Some(RelayerNetworkPolicy::Evm(policy)) = recovered_evm.policies {
918            assert_eq!(policy.include_revert_data, Some(false));
919        } else {
920            panic!("Expected EVM policy");
921        }
922
923        // Solana
924        let original_solana = Relayer {
925            id: "solana_relayer".to_string(),
926            name: "Solana Relayer".to_string(),
927            network: "mainnet".to_string(),
928            paused: true,
929            network_type: RelayerNetworkType::Solana,
930            policies: Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
931                fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
932                min_balance: Some(3000000),
933                max_signatures: None,
934                allowed_tokens: None,
935                allowed_programs: None,
936                allowed_accounts: None,
937                disallowed_accounts: None,
938                max_tx_data_size: None,
939                max_allowed_fee_lamports: None,
940                swap_config: None,
941                fee_margin_percentage: None,
942            })),
943            signer_id: "solana_signer".to_string(),
944            notification_id: None,
945            custom_rpc_urls: None,
946        };
947
948        let repo_solana = RelayerRepoModel::from(original_solana.clone());
949        let recovered_solana = Relayer::from(repo_solana);
950
951        assert_eq!(original_solana.id, recovered_solana.id);
952        assert_eq!(original_solana.network_type, recovered_solana.network_type);
953        assert_eq!(original_solana.paused, recovered_solana.paused);
954
955        // Stellar
956        let original_stellar = Relayer {
957            id: "stellar_relayer".to_string(),
958            name: "Stellar Relayer".to_string(),
959            network: "mainnet".to_string(),
960            paused: false,
961            network_type: RelayerNetworkType::Stellar,
962            policies: Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
963                min_balance: Some(50000000),
964                max_fee: Some(250000),
965                timeout_seconds: Some(180),
966                concurrent_transactions: None,
967                allowed_tokens: None,
968                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
969                slippage_percentage: None,
970                fee_margin_percentage: None,
971                swap_config: None,
972            })),
973            signer_id: "stellar_signer".to_string(),
974            notification_id: Some("stellar_notification".to_string()),
975            custom_rpc_urls: None,
976        };
977
978        let repo_stellar = RelayerRepoModel::from(original_stellar.clone());
979        let recovered_stellar = Relayer::from(repo_stellar);
980
981        assert_eq!(original_stellar.id, recovered_stellar.id);
982        assert_eq!(
983            original_stellar.network_type,
984            recovered_stellar.network_type
985        );
986        assert_eq!(
987            original_stellar.notification_id,
988            recovered_stellar.notification_id
989        );
990    }
991}