openzeppelin_relayer/models/relayer/
repository.rs

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