1use super::{Relayer, RelayerNetworkPolicy, RelayerValidationError, RpcConfig};
14use crate::config::{ConfigFileError, ConfigFileNetworkType, NetworksFileConfig};
15use serde::{Deserialize, Serialize};
16use std::collections::HashSet;
17
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
19#[serde(rename_all = "lowercase")]
20pub enum ConfigFileRelayerNetworkPolicy {
21 Evm(ConfigFileRelayerEvmPolicy),
22 Solana(ConfigFileRelayerSolanaPolicy),
23 Stellar(ConfigFileRelayerStellarPolicy),
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
27#[serde(deny_unknown_fields)]
28pub struct ConfigFileRelayerEvmPolicy {
29 pub gas_price_cap: Option<u128>,
30 pub whitelist_receivers: Option<Vec<String>>,
31 pub eip1559_pricing: Option<bool>,
32 pub private_transactions: Option<bool>,
33 pub min_balance: Option<u128>,
34 pub gas_limit_estimation: Option<bool>,
35 pub include_revert_data: Option<bool>,
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
39pub struct AllowedTokenSwapConfig {
40 pub slippage_percentage: Option<f32>,
42 pub min_amount: Option<u64>,
44 pub max_amount: Option<u64>,
46 pub retain_min_amount: Option<u64>,
48}
49
50#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
51pub struct AllowedToken {
52 pub mint: String,
53 pub decimals: Option<u8>,
55 pub symbol: Option<String>,
57 pub max_allowed_fee: Option<u64>,
59 pub swap_config: Option<AllowedTokenSwapConfig>,
61}
62
63#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
64#[serde(rename_all = "lowercase")]
65pub enum ConfigFileSolanaFeePaymentStrategy {
66 User,
67 Relayer,
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
71#[serde(rename_all = "kebab-case")]
72pub enum ConfigFileRelayerSolanaSwapStrategy {
73 JupiterSwap,
74 JupiterUltra,
75}
76
77#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
78pub struct JupiterSwapOptions {
79 pub priority_fee_max_lamports: Option<u64>,
81 pub priority_level: Option<String>,
83
84 pub dynamic_compute_unit_limit: Option<bool>,
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
88#[serde(deny_unknown_fields)]
89pub struct ConfigFileRelayerSolanaSwapConfig {
90 pub strategy: Option<ConfigFileRelayerSolanaSwapStrategy>,
92
93 pub cron_schedule: Option<String>,
95
96 pub min_balance_threshold: Option<u64>,
98
99 pub jupiter_swap_options: Option<JupiterSwapOptions>,
101}
102
103#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
104#[serde(deny_unknown_fields)]
105pub struct ConfigFileRelayerSolanaPolicy {
106 pub fee_payment_strategy: Option<ConfigFileSolanaFeePaymentStrategy>,
108
109 pub fee_margin_percentage: Option<f32>,
111
112 pub min_balance: Option<u64>,
114
115 pub allowed_tokens: Option<Vec<AllowedToken>>,
117
118 pub allowed_programs: Option<Vec<String>>,
121
122 pub allowed_accounts: Option<Vec<String>>,
125
126 pub disallowed_accounts: Option<Vec<String>>,
129
130 pub max_tx_data_size: Option<u16>,
132
133 pub max_signatures: Option<u8>,
135
136 pub max_allowed_fee_lamports: Option<u64>,
138
139 pub swap_config: Option<ConfigFileRelayerSolanaSwapConfig>,
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
144#[serde(rename_all = "lowercase")]
145pub enum ConfigFileStellarFeePaymentStrategy {
146 User,
147 Relayer,
148}
149
150#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
151pub struct StellarAllowedTokenSwapConfig {
152 pub slippage_percentage: Option<f32>,
154 pub min_amount: Option<u64>,
156 pub max_amount: Option<u64>,
158 pub retain_min_amount: Option<u64>,
160}
161
162#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
163pub struct StellarAllowedToken {
164 pub asset: String,
165 pub max_allowed_fee: Option<u64>,
167 pub swap_config: Option<StellarAllowedTokenSwapConfig>,
169}
170
171#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
172#[serde(rename_all = "kebab-case")]
173pub enum ConfigFileRelayerStellarSwapStrategy {
174 OrderBook,
175 Soroswap,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
179#[serde(deny_unknown_fields)]
180pub struct ConfigFileRelayerStellarSwapConfig {
181 #[serde(default)]
184 pub strategies: Vec<ConfigFileRelayerStellarSwapStrategy>,
185 pub cron_schedule: Option<String>,
187 pub min_balance_threshold: Option<u64>,
189}
190
191#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
192#[serde(deny_unknown_fields)]
193pub struct ConfigFileRelayerStellarPolicy {
194 pub max_fee: Option<u32>,
195 pub timeout_seconds: Option<u64>,
196 pub min_balance: Option<u64>,
197 pub concurrent_transactions: Option<bool>,
198 pub fee_payment_strategy: Option<ConfigFileStellarFeePaymentStrategy>,
201 pub slippage_percentage: Option<f32>,
203 pub fee_margin_percentage: Option<f32>,
205 pub allowed_tokens: Option<Vec<StellarAllowedToken>>,
207 pub swap_config: Option<ConfigFileRelayerStellarSwapConfig>,
209}
210
211#[derive(Debug, Serialize, Clone)]
212pub struct RelayerFileConfig {
213 pub id: String,
214 pub name: String,
215 pub network: String,
216 pub paused: bool,
217 #[serde(flatten)]
218 pub network_type: ConfigFileNetworkType,
219 #[serde(default)]
220 pub policies: Option<ConfigFileRelayerNetworkPolicy>,
221 pub signer_id: String,
222 #[serde(default)]
223 pub notification_id: Option<String>,
224 #[serde(default)]
225 pub custom_rpc_urls: Option<Vec<RpcConfig>>,
226}
227
228use serde::{de, Deserializer};
229use serde_json::Value;
230
231impl<'de> Deserialize<'de> for RelayerFileConfig {
232 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233 where
234 D: Deserializer<'de>,
235 {
236 let mut value: Value = Value::deserialize(deserializer)?;
238
239 let id = value
241 .get("id")
242 .and_then(Value::as_str)
243 .ok_or_else(|| de::Error::missing_field("id"))?
244 .to_string();
245
246 let name = value
247 .get("name")
248 .and_then(Value::as_str)
249 .ok_or_else(|| de::Error::missing_field("name"))?
250 .to_string();
251
252 let network = value
253 .get("network")
254 .and_then(Value::as_str)
255 .ok_or_else(|| de::Error::missing_field("network"))?
256 .to_string();
257
258 let paused = value
259 .get("paused")
260 .and_then(Value::as_bool)
261 .ok_or_else(|| de::Error::missing_field("paused"))?;
262
263 let network_type: ConfigFileNetworkType = serde_json::from_value(
265 value
266 .get("network_type")
267 .cloned()
268 .ok_or_else(|| de::Error::missing_field("network_type"))?,
269 )
270 .map_err(de::Error::custom)?;
271
272 let signer_id = value
273 .get("signer_id")
274 .and_then(Value::as_str)
275 .ok_or_else(|| de::Error::missing_field("signer_id"))?
276 .to_string();
277
278 let notification_id = value
279 .get("notification_id")
280 .and_then(Value::as_str)
281 .map(|s| s.to_string());
282
283 let policies = if let Some(policy_value) = value.get_mut("policies") {
285 match network_type {
286 ConfigFileNetworkType::Evm => {
287 serde_json::from_value::<ConfigFileRelayerEvmPolicy>(policy_value.clone())
288 .map(ConfigFileRelayerNetworkPolicy::Evm)
289 .map(Some)
290 .map_err(de::Error::custom)
291 }
292 ConfigFileNetworkType::Solana => {
293 serde_json::from_value::<ConfigFileRelayerSolanaPolicy>(policy_value.clone())
294 .map(ConfigFileRelayerNetworkPolicy::Solana)
295 .map(Some)
296 .map_err(de::Error::custom)
297 }
298 ConfigFileNetworkType::Stellar => {
299 serde_json::from_value::<ConfigFileRelayerStellarPolicy>(policy_value.clone())
300 .map(ConfigFileRelayerNetworkPolicy::Stellar)
301 .map(Some)
302 .map_err(de::Error::custom)
303 }
304 }
305 } else {
306 Ok(None) }?;
308
309 let custom_rpc_urls = value
310 .get("custom_rpc_urls")
311 .and_then(|v| v.as_array())
312 .map(|arr| {
313 arr.iter()
314 .filter_map(|v| {
315 if let Some(url_str) = v.as_str() {
317 Some(RpcConfig::new(url_str.to_string()))
319 } else {
320 serde_json::from_value::<RpcConfig>(v.clone()).ok()
322 }
323 })
324 .collect()
325 });
326
327 Ok(RelayerFileConfig {
328 id,
329 name,
330 network,
331 paused,
332 network_type,
333 policies,
334 signer_id,
335 notification_id,
336 custom_rpc_urls,
337 })
338 }
339}
340
341impl TryFrom<RelayerFileConfig> for Relayer {
342 type Error = ConfigFileError;
343
344 fn try_from(config: RelayerFileConfig) -> Result<Self, Self::Error> {
345 let policies = if let Some(config_policies) = config.policies {
347 Some(convert_config_policies_to_domain(config_policies)?)
348 } else {
349 None
350 };
351
352 let relayer = Relayer::new(
354 config.id,
355 config.name,
356 config.network,
357 config.paused,
358 config.network_type.into(),
359 policies,
360 config.signer_id,
361 config.notification_id,
362 config.custom_rpc_urls,
363 );
364
365 relayer.validate().map_err(|e| match e {
367 RelayerValidationError::EmptyId => ConfigFileError::MissingField("relayer id".into()),
368 RelayerValidationError::InvalidIdFormat => ConfigFileError::InvalidIdFormat(
369 "ID must contain only letters, numbers, dashes and underscores".into(),
370 ),
371 RelayerValidationError::IdTooLong => {
372 ConfigFileError::InvalidIdLength("ID length must not exceed 36 characters".into())
373 }
374 RelayerValidationError::EmptyName => {
375 ConfigFileError::MissingField("relayer name".into())
376 }
377 RelayerValidationError::EmptyNetwork => ConfigFileError::MissingField("network".into()),
378 RelayerValidationError::InvalidPolicy(msg) => ConfigFileError::InvalidPolicy(msg),
379 RelayerValidationError::InvalidRpcUrl(msg) => {
380 ConfigFileError::InvalidFormat(format!("Invalid RPC URL: {msg}"))
381 }
382 RelayerValidationError::InvalidRpcWeight => {
383 ConfigFileError::InvalidFormat("RPC URL weight must be in range 0-100".to_string())
384 }
385 RelayerValidationError::InvalidField(msg) => ConfigFileError::InvalidFormat(msg),
386 })?;
387
388 Ok(relayer)
389 }
390}
391
392fn convert_config_policies_to_domain(
393 config_policies: ConfigFileRelayerNetworkPolicy,
394) -> Result<RelayerNetworkPolicy, ConfigFileError> {
395 match config_policies {
396 ConfigFileRelayerNetworkPolicy::Evm(evm_policy) => {
397 Ok(RelayerNetworkPolicy::Evm(super::RelayerEvmPolicy {
398 min_balance: evm_policy.min_balance,
399 gas_limit_estimation: evm_policy.gas_limit_estimation,
400 gas_price_cap: evm_policy.gas_price_cap,
401 whitelist_receivers: evm_policy.whitelist_receivers,
402 eip1559_pricing: evm_policy.eip1559_pricing,
403 private_transactions: evm_policy.private_transactions,
404 include_revert_data: evm_policy.include_revert_data,
405 }))
406 }
407 ConfigFileRelayerNetworkPolicy::Solana(solana_policy) => {
408 let swap_config = if let Some(config_swap) = solana_policy.swap_config {
409 Some(super::RelayerSolanaSwapConfig {
410 strategy: config_swap.strategy.map(|s| match s {
411 ConfigFileRelayerSolanaSwapStrategy::JupiterSwap => {
412 super::SolanaSwapStrategy::JupiterSwap
413 }
414 ConfigFileRelayerSolanaSwapStrategy::JupiterUltra => {
415 super::SolanaSwapStrategy::JupiterUltra
416 }
417 }),
418 cron_schedule: config_swap.cron_schedule,
419 min_balance_threshold: config_swap.min_balance_threshold,
420 jupiter_swap_options: config_swap.jupiter_swap_options.map(|opts| {
421 super::JupiterSwapOptions {
422 priority_fee_max_lamports: opts.priority_fee_max_lamports,
423 priority_level: opts.priority_level,
424 dynamic_compute_unit_limit: opts.dynamic_compute_unit_limit,
425 }
426 }),
427 })
428 } else {
429 None
430 };
431
432 Ok(RelayerNetworkPolicy::Solana(super::RelayerSolanaPolicy {
433 allowed_programs: solana_policy.allowed_programs,
434 max_signatures: solana_policy.max_signatures,
435 max_tx_data_size: solana_policy.max_tx_data_size,
436 min_balance: solana_policy.min_balance,
437 allowed_tokens: solana_policy.allowed_tokens.map(|tokens| {
438 tokens
439 .into_iter()
440 .map(|t| super::SolanaAllowedTokensPolicy {
441 mint: t.mint,
442 decimals: t.decimals,
443 symbol: t.symbol,
444 max_allowed_fee: t.max_allowed_fee,
445 swap_config: t.swap_config.map(|sc| {
446 super::SolanaAllowedTokensSwapConfig {
447 slippage_percentage: sc.slippage_percentage,
448 min_amount: sc.min_amount,
449 max_amount: sc.max_amount,
450 retain_min_amount: sc.retain_min_amount,
451 }
452 }),
453 })
454 .collect()
455 }),
456 fee_payment_strategy: solana_policy.fee_payment_strategy.map(|s| match s {
457 ConfigFileSolanaFeePaymentStrategy::User => {
458 super::SolanaFeePaymentStrategy::User
459 }
460 ConfigFileSolanaFeePaymentStrategy::Relayer => {
461 super::SolanaFeePaymentStrategy::Relayer
462 }
463 }),
464 fee_margin_percentage: solana_policy.fee_margin_percentage,
465 allowed_accounts: solana_policy.allowed_accounts,
466 disallowed_accounts: solana_policy.disallowed_accounts,
467 max_allowed_fee_lamports: solana_policy.max_allowed_fee_lamports,
468 swap_config,
469 }))
470 }
471 ConfigFileRelayerNetworkPolicy::Stellar(stellar_policy) => {
472 let swap_config = if let Some(config_swap) = stellar_policy.swap_config {
473 Some(super::RelayerStellarSwapConfig {
474 strategies: config_swap
475 .strategies
476 .into_iter()
477 .map(|s| match s {
478 ConfigFileRelayerStellarSwapStrategy::OrderBook => {
479 super::StellarSwapStrategy::OrderBook
480 }
481 ConfigFileRelayerStellarSwapStrategy::Soroswap => {
482 super::StellarSwapStrategy::Soroswap
483 }
484 })
485 .collect(),
486 cron_schedule: config_swap.cron_schedule,
487 min_balance_threshold: config_swap.min_balance_threshold,
488 })
489 } else {
490 None
491 };
492
493 Ok(RelayerNetworkPolicy::Stellar(super::RelayerStellarPolicy {
494 min_balance: stellar_policy.min_balance,
495 max_fee: stellar_policy.max_fee,
496 timeout_seconds: stellar_policy.timeout_seconds,
497 concurrent_transactions: stellar_policy.concurrent_transactions,
498 allowed_tokens: stellar_policy.allowed_tokens.map(|tokens| {
499 tokens
500 .into_iter()
501 .map(|t| super::StellarAllowedTokensPolicy {
502 asset: t.asset,
503 metadata: None,
504 max_allowed_fee: t.max_allowed_fee,
505 swap_config: t.swap_config.map(|sc| {
506 super::StellarAllowedTokensSwapConfig {
507 slippage_percentage: sc.slippage_percentage,
508 min_amount: sc.min_amount,
509 max_amount: sc.max_amount,
510 retain_min_amount: sc.retain_min_amount,
511 }
512 }),
513 })
514 .collect()
515 }),
516 fee_payment_strategy: stellar_policy.fee_payment_strategy.map(|s| match s {
517 ConfigFileStellarFeePaymentStrategy::User => {
518 super::StellarFeePaymentStrategy::User
519 }
520 ConfigFileStellarFeePaymentStrategy::Relayer => {
521 super::StellarFeePaymentStrategy::Relayer
522 }
523 }),
524 slippage_percentage: stellar_policy.slippage_percentage,
525 fee_margin_percentage: stellar_policy.fee_margin_percentage,
526 swap_config,
527 }))
528 }
529 }
530}
531
532#[derive(Debug, Serialize, Deserialize, Clone)]
533#[serde(deny_unknown_fields)]
534pub struct RelayersFileConfig {
535 pub relayers: Vec<RelayerFileConfig>,
536}
537
538impl RelayersFileConfig {
539 pub fn new(relayers: Vec<RelayerFileConfig>) -> Self {
540 Self { relayers }
541 }
542
543 pub fn validate(&self, networks: &NetworksFileConfig) -> Result<(), ConfigFileError> {
544 if self.relayers.is_empty() {
545 return Ok(());
546 }
547
548 let mut ids = HashSet::new();
549 for relayer_config in &self.relayers {
550 if relayer_config.network.is_empty() {
551 return Err(ConfigFileError::InvalidFormat(
552 "relayer.network cannot be empty".into(),
553 ));
554 }
555
556 if networks
557 .get_network(relayer_config.network_type, &relayer_config.network)
558 .is_none()
559 {
560 return Err(ConfigFileError::InvalidReference(format!(
561 "Relayer '{}' references non-existent network '{}' for type '{:?}'",
562 relayer_config.id, relayer_config.network, relayer_config.network_type
563 )));
564 }
565
566 let relayer = Relayer::try_from(relayer_config.clone())?;
568 relayer.validate().map_err(|e| match e {
569 RelayerValidationError::EmptyId => {
570 ConfigFileError::MissingField("relayer id".into())
571 }
572 RelayerValidationError::InvalidIdFormat => ConfigFileError::InvalidIdFormat(
573 "ID must contain only letters, numbers, dashes and underscores".into(),
574 ),
575 RelayerValidationError::IdTooLong => ConfigFileError::InvalidIdLength(
576 "ID length must not exceed 36 characters".into(),
577 ),
578 RelayerValidationError::EmptyName => {
579 ConfigFileError::MissingField("relayer name".into())
580 }
581 RelayerValidationError::EmptyNetwork => {
582 ConfigFileError::MissingField("network".into())
583 }
584 RelayerValidationError::InvalidPolicy(msg) => ConfigFileError::InvalidPolicy(msg),
585 RelayerValidationError::InvalidRpcUrl(msg) => {
586 ConfigFileError::InvalidFormat(format!("Invalid RPC URL: {msg}"))
587 }
588 RelayerValidationError::InvalidRpcWeight => ConfigFileError::InvalidFormat(
589 "RPC URL weight must be in range 0-100".to_string(),
590 ),
591 RelayerValidationError::InvalidField(msg) => ConfigFileError::InvalidFormat(msg),
592 })?;
593
594 if !ids.insert(relayer_config.id.clone()) {
595 return Err(ConfigFileError::DuplicateId(relayer_config.id.clone()));
596 }
597 }
598 Ok(())
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::config::ConfigFileNetworkType;
606 use crate::models::relayer::{SolanaFeePaymentStrategy, SolanaSwapStrategy};
607 use serde_json;
608
609 fn create_test_networks_config() -> NetworksFileConfig {
610 NetworksFileConfig::new(vec![]).unwrap()
612 }
613
614 #[test]
615 fn test_relayer_file_config_deserialization_evm() {
616 let json_input = r#"{
617 "id": "test-evm-relayer",
618 "name": "Test EVM Relayer",
619 "network": "mainnet",
620 "paused": false,
621 "network_type": "evm",
622 "signer_id": "test-signer",
623 "policies": {
624 "gas_price_cap": 100000000000,
625 "eip1559_pricing": true,
626 "min_balance": 1000000000000000000,
627 "gas_limit_estimation": false,
628 "private_transactions": null
629 },
630 "notification_id": "test-notification",
631 "custom_rpc_urls": [
632 "https://mainnet.infura.io/v3/test",
633 {"url": "https://eth.llamarpc.com", "weight": 80}
634 ]
635 }"#;
636
637 let config: RelayerFileConfig = serde_json::from_str(json_input).unwrap();
638
639 assert_eq!(config.id, "test-evm-relayer");
640 assert_eq!(config.name, "Test EVM Relayer");
641 assert_eq!(config.network, "mainnet");
642 assert!(!config.paused);
643 assert_eq!(config.network_type, ConfigFileNetworkType::Evm);
644 assert_eq!(config.signer_id, "test-signer");
645 assert_eq!(
646 config.notification_id,
647 Some("test-notification".to_string())
648 );
649
650 assert!(config.policies.is_some());
652 if let Some(ConfigFileRelayerNetworkPolicy::Evm(evm_policy)) = config.policies {
653 assert_eq!(evm_policy.gas_price_cap, Some(100000000000));
654 assert_eq!(evm_policy.eip1559_pricing, Some(true));
655 assert_eq!(evm_policy.min_balance, Some(1000000000000000000));
656 assert_eq!(evm_policy.gas_limit_estimation, Some(false));
657 assert_eq!(evm_policy.private_transactions, None);
658 } else {
659 panic!("Expected EVM policy");
660 }
661
662 assert!(config.custom_rpc_urls.is_some());
664 let rpc_urls = config.custom_rpc_urls.unwrap();
665 assert_eq!(rpc_urls.len(), 2);
666 assert_eq!(rpc_urls[0].url, "https://mainnet.infura.io/v3/test");
667 assert_eq!(rpc_urls[0].weight, 100); assert_eq!(rpc_urls[1].url, "https://eth.llamarpc.com");
669 assert_eq!(rpc_urls[1].weight, 80);
670 }
671
672 #[test]
673 fn test_evm_policy_include_revert_data_serde_roundtrip() {
674 let omitted: ConfigFileRelayerEvmPolicy =
675 serde_json::from_str(r#"{ "gas_limit_estimation": true }"#).unwrap();
676 assert_eq!(omitted.include_revert_data, None);
677
678 let explicit: ConfigFileRelayerEvmPolicy =
679 serde_json::from_str(r#"{ "include_revert_data": false }"#).unwrap();
680 assert_eq!(explicit.include_revert_data, Some(false));
681
682 let reparsed: ConfigFileRelayerEvmPolicy =
683 serde_json::from_str(&serde_json::to_string(&explicit).unwrap()).unwrap();
684 assert_eq!(reparsed.include_revert_data, Some(false));
685 }
686
687 #[test]
688 fn test_relayer_file_config_deserialization_solana() {
689 let json_input = r#"{
690 "id": "test-solana-relayer",
691 "name": "Test Solana Relayer",
692 "network": "mainnet",
693 "paused": true,
694 "network_type": "solana",
695 "signer_id": "test-signer",
696 "policies": {
697 "fee_payment_strategy": "relayer",
698 "min_balance": 5000000,
699 "max_signatures": 8,
700 "max_tx_data_size": 1024,
701 "fee_margin_percentage": 2.5,
702 "allowed_tokens": [
703 {
704 "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
705 "decimals": 6,
706 "symbol": "USDC",
707 "max_allowed_fee": 100000,
708 "swap_config": {
709 "slippage_percentage": 0.5,
710 "min_amount": 1000,
711 "max_amount": 10000000
712 }
713 }
714 ],
715 "allowed_programs": ["11111111111111111111111111111111"],
716 "swap_config": {
717 "strategy": "jupiter-swap",
718 "cron_schedule": "0 0 * * *",
719 "min_balance_threshold": 1000000,
720 "jupiter_swap_options": {
721 "priority_fee_max_lamports": 10000,
722 "priority_level": "high",
723 "dynamic_compute_unit_limit": true
724 }
725 }
726 }
727 }"#;
728
729 let config: RelayerFileConfig = serde_json::from_str(json_input).unwrap();
730
731 assert_eq!(config.id, "test-solana-relayer");
732 assert_eq!(config.network_type, ConfigFileNetworkType::Solana);
733 assert!(config.paused);
734
735 assert!(config.policies.is_some());
737 if let Some(ConfigFileRelayerNetworkPolicy::Solana(solana_policy)) = config.policies {
738 assert_eq!(
739 solana_policy.fee_payment_strategy,
740 Some(ConfigFileSolanaFeePaymentStrategy::Relayer)
741 );
742 assert_eq!(solana_policy.min_balance, Some(5000000));
743 assert_eq!(solana_policy.max_signatures, Some(8));
744 assert_eq!(solana_policy.max_tx_data_size, Some(1024));
745 assert_eq!(solana_policy.fee_margin_percentage, Some(2.5));
746
747 assert!(solana_policy.allowed_tokens.is_some());
749 let tokens = solana_policy.allowed_tokens.as_ref().unwrap();
750 assert_eq!(tokens.len(), 1);
751 assert_eq!(
752 tokens[0].mint,
753 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
754 );
755 assert_eq!(tokens[0].decimals, Some(6));
756 assert_eq!(tokens[0].symbol, Some("USDC".to_string()));
757 assert_eq!(tokens[0].max_allowed_fee, Some(100000));
758
759 assert!(tokens[0].swap_config.is_some());
761 let token_swap = tokens[0].swap_config.as_ref().unwrap();
762 assert_eq!(token_swap.slippage_percentage, Some(0.5));
763 assert_eq!(token_swap.min_amount, Some(1000));
764 assert_eq!(token_swap.max_amount, Some(10000000));
765
766 assert!(solana_policy.swap_config.is_some());
768 let swap_config = solana_policy.swap_config.as_ref().unwrap();
769 assert_eq!(
770 swap_config.strategy,
771 Some(ConfigFileRelayerSolanaSwapStrategy::JupiterSwap)
772 );
773 assert_eq!(swap_config.cron_schedule, Some("0 0 * * *".to_string()));
774 assert_eq!(swap_config.min_balance_threshold, Some(1000000));
775
776 assert!(swap_config.jupiter_swap_options.is_some());
778 let jupiter_opts = swap_config.jupiter_swap_options.as_ref().unwrap();
779 assert_eq!(jupiter_opts.priority_fee_max_lamports, Some(10000));
780 assert_eq!(jupiter_opts.priority_level, Some("high".to_string()));
781 assert_eq!(jupiter_opts.dynamic_compute_unit_limit, Some(true));
782 } else {
783 panic!("Expected Solana policy");
784 }
785 }
786
787 #[test]
788 fn test_relayer_file_config_deserialization_stellar() {
789 let json_input = r#"{
790 "id": "test-stellar-relayer",
791 "name": "Test Stellar Relayer",
792 "network": "mainnet",
793 "paused": false,
794 "network_type": "stellar",
795 "signer_id": "test-signer",
796 "policies": {
797 "min_balance": 20000000,
798 "max_fee": 100000,
799 "timeout_seconds": 30
800 },
801 "custom_rpc_urls": [
802 {"url": "https://stellar-node.example.com", "weight": 100}
803 ]
804 }"#;
805
806 let config: RelayerFileConfig = serde_json::from_str(json_input).unwrap();
807
808 assert_eq!(config.id, "test-stellar-relayer");
809 assert_eq!(config.network_type, ConfigFileNetworkType::Stellar);
810 assert!(!config.paused);
811
812 assert!(config.policies.is_some());
814 if let Some(ConfigFileRelayerNetworkPolicy::Stellar(stellar_policy)) = config.policies {
815 assert_eq!(stellar_policy.min_balance, Some(20000000));
816 assert_eq!(stellar_policy.max_fee, Some(100000));
817 assert_eq!(stellar_policy.timeout_seconds, Some(30));
818 } else {
819 panic!("Expected Stellar policy");
820 }
821 }
822
823 #[test]
824 fn test_relayer_file_config_deserialization_minimal() {
825 let json_input = r#"{
827 "id": "minimal-relayer",
828 "name": "Minimal Relayer",
829 "network": "testnet",
830 "paused": false,
831 "network_type": "evm",
832 "signer_id": "minimal-signer"
833 }"#;
834
835 let config: RelayerFileConfig = serde_json::from_str(json_input).unwrap();
836
837 assert_eq!(config.id, "minimal-relayer");
838 assert_eq!(config.name, "Minimal Relayer");
839 assert_eq!(config.network, "testnet");
840 assert!(!config.paused);
841 assert_eq!(config.network_type, ConfigFileNetworkType::Evm);
842 assert_eq!(config.signer_id, "minimal-signer");
843 assert_eq!(config.notification_id, None);
844 assert_eq!(config.policies, None);
845 assert_eq!(config.custom_rpc_urls, None);
846 }
847
848 #[test]
849 fn test_relayer_file_config_deserialization_missing_required_field() {
850 let json_input = r#"{
852 "name": "Test Relayer",
853 "network": "mainnet",
854 "paused": false,
855 "network_type": "evm",
856 "signer_id": "test-signer"
857 }"#;
858
859 let result = serde_json::from_str::<RelayerFileConfig>(json_input);
860 assert!(result.is_err());
861 assert!(result
862 .unwrap_err()
863 .to_string()
864 .contains("missing field `id`"));
865 }
866
867 #[test]
868 fn test_relayer_file_config_deserialization_invalid_network_type() {
869 let json_input = r#"{
870 "id": "test-relayer",
871 "name": "Test Relayer",
872 "network": "mainnet",
873 "paused": false,
874 "network_type": "invalid",
875 "signer_id": "test-signer"
876 }"#;
877
878 let result = serde_json::from_str::<RelayerFileConfig>(json_input);
879 assert!(result.is_err());
880 }
881
882 #[test]
883 fn test_relayer_file_config_deserialization_wrong_policy_for_network_type() {
884 let json_input = r#"{
886 "id": "test-relayer",
887 "name": "Test Relayer",
888 "network": "mainnet",
889 "paused": false,
890 "network_type": "evm",
891 "signer_id": "test-signer",
892 "policies": {
893 "fee_payment_strategy": "relayer"
894 }
895 }"#;
896
897 let result = serde_json::from_str::<RelayerFileConfig>(json_input);
898 assert!(result.is_err());
899 }
900
901 #[test]
902 fn test_convert_config_policies_to_domain_evm() {
903 let config_policy = ConfigFileRelayerNetworkPolicy::Evm(ConfigFileRelayerEvmPolicy {
904 include_revert_data: None,
905 gas_price_cap: Some(50000000000),
906 whitelist_receivers: Some(vec!["0x123".to_string(), "0x456".to_string()]),
907 eip1559_pricing: Some(true),
908 private_transactions: Some(false),
909 min_balance: Some(2000000000000000000),
910 gas_limit_estimation: Some(true),
911 });
912
913 let domain_policy = convert_config_policies_to_domain(config_policy).unwrap();
914
915 if let RelayerNetworkPolicy::Evm(evm_policy) = domain_policy {
916 assert_eq!(evm_policy.gas_price_cap, Some(50000000000));
917 assert_eq!(
918 evm_policy.whitelist_receivers,
919 Some(vec!["0x123".to_string(), "0x456".to_string()])
920 );
921 assert_eq!(evm_policy.eip1559_pricing, Some(true));
922 assert_eq!(evm_policy.private_transactions, Some(false));
923 assert_eq!(evm_policy.min_balance, Some(2000000000000000000));
924 assert_eq!(evm_policy.gas_limit_estimation, Some(true));
925 } else {
926 panic!("Expected EVM domain policy");
927 }
928 }
929
930 #[test]
931 fn test_convert_config_policies_to_domain_solana() {
932 let config_policy = ConfigFileRelayerNetworkPolicy::Solana(ConfigFileRelayerSolanaPolicy {
933 fee_payment_strategy: Some(ConfigFileSolanaFeePaymentStrategy::User),
934 fee_margin_percentage: Some(1.5),
935 min_balance: Some(3000000),
936 allowed_tokens: Some(vec![AllowedToken {
937 mint: "TokenMint123".to_string(),
938 decimals: Some(9),
939 symbol: Some("TOKEN".to_string()),
940 max_allowed_fee: Some(50000),
941 swap_config: Some(AllowedTokenSwapConfig {
942 slippage_percentage: Some(1.0),
943 min_amount: Some(100),
944 max_amount: Some(1000000),
945 retain_min_amount: Some(500),
946 }),
947 }]),
948 allowed_programs: Some(vec!["Program123".to_string()]),
949 allowed_accounts: Some(vec!["Account123".to_string()]),
950 disallowed_accounts: None,
951 max_tx_data_size: Some(2048),
952 max_signatures: Some(10),
953 max_allowed_fee_lamports: Some(100000),
954 swap_config: Some(ConfigFileRelayerSolanaSwapConfig {
955 strategy: Some(ConfigFileRelayerSolanaSwapStrategy::JupiterUltra),
956 cron_schedule: Some("0 */6 * * *".to_string()),
957 min_balance_threshold: Some(2000000),
958 jupiter_swap_options: Some(JupiterSwapOptions {
959 priority_fee_max_lamports: Some(5000),
960 priority_level: Some("medium".to_string()),
961 dynamic_compute_unit_limit: Some(false),
962 }),
963 }),
964 });
965
966 let domain_policy = convert_config_policies_to_domain(config_policy).unwrap();
967
968 if let RelayerNetworkPolicy::Solana(solana_policy) = domain_policy {
969 assert_eq!(
970 solana_policy.fee_payment_strategy,
971 Some(SolanaFeePaymentStrategy::User)
972 );
973 assert_eq!(solana_policy.fee_margin_percentage, Some(1.5));
974 assert_eq!(solana_policy.min_balance, Some(3000000));
975 assert_eq!(solana_policy.max_tx_data_size, Some(2048));
976 assert_eq!(solana_policy.max_signatures, Some(10));
977
978 assert!(solana_policy.allowed_tokens.is_some());
980 let tokens = solana_policy.allowed_tokens.unwrap();
981 assert_eq!(tokens.len(), 1);
982 assert_eq!(tokens[0].mint, "TokenMint123");
983 assert_eq!(tokens[0].decimals, Some(9));
984 assert_eq!(tokens[0].symbol, Some("TOKEN".to_string()));
985 assert_eq!(tokens[0].max_allowed_fee, Some(50000));
986
987 assert!(solana_policy.swap_config.is_some());
989 let swap_config = solana_policy.swap_config.unwrap();
990 assert_eq!(swap_config.strategy, Some(SolanaSwapStrategy::JupiterUltra));
991 assert_eq!(swap_config.cron_schedule, Some("0 */6 * * *".to_string()));
992 assert_eq!(swap_config.min_balance_threshold, Some(2000000));
993 } else {
994 panic!("Expected Solana domain policy");
995 }
996 }
997
998 #[test]
999 fn test_convert_config_policies_to_domain_stellar() {
1000 let config_policy =
1001 ConfigFileRelayerNetworkPolicy::Stellar(ConfigFileRelayerStellarPolicy {
1002 min_balance: Some(25000000),
1003 max_fee: Some(150000),
1004 timeout_seconds: Some(60),
1005 concurrent_transactions: None,
1006 allowed_tokens: None,
1007 fee_payment_strategy: Some(ConfigFileStellarFeePaymentStrategy::User),
1008 slippage_percentage: None,
1009 fee_margin_percentage: None,
1010 swap_config: None,
1011 });
1012
1013 let domain_policy = convert_config_policies_to_domain(config_policy).unwrap();
1014
1015 if let RelayerNetworkPolicy::Stellar(stellar_policy) = domain_policy {
1016 assert_eq!(stellar_policy.min_balance, Some(25000000));
1017 assert_eq!(stellar_policy.max_fee, Some(150000));
1018 assert_eq!(stellar_policy.timeout_seconds, Some(60));
1019 } else {
1020 panic!("Expected Stellar domain policy");
1021 }
1022 }
1023
1024 #[test]
1025 fn test_try_from_relayer_file_config_to_domain_evm() {
1026 let config = RelayerFileConfig {
1027 id: "test-evm".to_string(),
1028 name: "Test EVM Relayer".to_string(),
1029 network: "mainnet".to_string(),
1030 paused: false,
1031 network_type: ConfigFileNetworkType::Evm,
1032 policies: Some(ConfigFileRelayerNetworkPolicy::Evm(
1033 ConfigFileRelayerEvmPolicy {
1034 include_revert_data: None,
1035 gas_price_cap: Some(75000000000),
1036 whitelist_receivers: None,
1037 eip1559_pricing: Some(true),
1038 private_transactions: None,
1039 min_balance: None,
1040 gas_limit_estimation: None,
1041 },
1042 )),
1043 signer_id: "test-signer".to_string(),
1044 notification_id: Some("test-notification".to_string()),
1045 custom_rpc_urls: None,
1046 };
1047
1048 let domain_relayer = Relayer::try_from(config).unwrap();
1049
1050 assert_eq!(domain_relayer.id, "test-evm");
1051 assert_eq!(domain_relayer.name, "Test EVM Relayer");
1052 assert_eq!(domain_relayer.network, "mainnet");
1053 assert!(!domain_relayer.paused);
1054 assert_eq!(
1055 domain_relayer.network_type,
1056 crate::models::relayer::RelayerNetworkType::Evm
1057 );
1058 assert_eq!(domain_relayer.signer_id, "test-signer");
1059 assert_eq!(
1060 domain_relayer.notification_id,
1061 Some("test-notification".to_string())
1062 );
1063
1064 assert!(domain_relayer.policies.is_some());
1066 if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = domain_relayer.policies {
1067 assert_eq!(evm_policy.gas_price_cap, Some(75000000000));
1068 assert_eq!(evm_policy.eip1559_pricing, Some(true));
1069 } else {
1070 panic!("Expected EVM domain policy");
1071 }
1072 }
1073
1074 #[test]
1075 fn test_try_from_relayer_file_config_to_domain_solana() {
1076 let config = RelayerFileConfig {
1077 id: "test-solana".to_string(),
1078 name: "Test Solana Relayer".to_string(),
1079 network: "mainnet".to_string(),
1080 paused: true,
1081 network_type: ConfigFileNetworkType::Solana,
1082 policies: Some(ConfigFileRelayerNetworkPolicy::Solana(
1083 ConfigFileRelayerSolanaPolicy {
1084 fee_payment_strategy: Some(ConfigFileSolanaFeePaymentStrategy::Relayer),
1085 fee_margin_percentage: None,
1086 min_balance: Some(4000000),
1087 allowed_tokens: None,
1088 allowed_programs: None,
1089 allowed_accounts: None,
1090 disallowed_accounts: None,
1091 max_tx_data_size: None,
1092 max_signatures: Some(7),
1093 max_allowed_fee_lamports: None,
1094 swap_config: None,
1095 },
1096 )),
1097 signer_id: "test-signer".to_string(),
1098 notification_id: None,
1099 custom_rpc_urls: None,
1100 };
1101
1102 let domain_relayer = Relayer::try_from(config).unwrap();
1103
1104 assert_eq!(
1105 domain_relayer.network_type,
1106 crate::models::relayer::RelayerNetworkType::Solana
1107 );
1108 assert!(domain_relayer.paused);
1109
1110 assert!(domain_relayer.policies.is_some());
1112 if let Some(RelayerNetworkPolicy::Solana(solana_policy)) = domain_relayer.policies {
1113 assert_eq!(
1114 solana_policy.fee_payment_strategy,
1115 Some(SolanaFeePaymentStrategy::Relayer)
1116 );
1117 assert_eq!(solana_policy.min_balance, Some(4000000));
1118 assert_eq!(solana_policy.max_signatures, Some(7));
1119 } else {
1120 panic!("Expected Solana domain policy");
1121 }
1122 }
1123
1124 #[test]
1125 fn test_try_from_relayer_file_config_to_domain_stellar() {
1126 let config = RelayerFileConfig {
1127 id: "test-stellar".to_string(),
1128 name: "Test Stellar Relayer".to_string(),
1129 network: "mainnet".to_string(),
1130 paused: false,
1131 network_type: ConfigFileNetworkType::Stellar,
1132 policies: Some(ConfigFileRelayerNetworkPolicy::Stellar(
1133 ConfigFileRelayerStellarPolicy {
1134 min_balance: Some(35000000),
1135 max_fee: Some(200000),
1136 timeout_seconds: Some(90),
1137 concurrent_transactions: None,
1138 allowed_tokens: None,
1139 fee_payment_strategy: Some(ConfigFileStellarFeePaymentStrategy::User),
1140 slippage_percentage: None,
1141 fee_margin_percentage: None,
1142 swap_config: None,
1143 },
1144 )),
1145 signer_id: "test-signer".to_string(),
1146 notification_id: None,
1147 custom_rpc_urls: None,
1148 };
1149
1150 let domain_relayer = Relayer::try_from(config).unwrap();
1151
1152 assert_eq!(
1153 domain_relayer.network_type,
1154 crate::models::relayer::RelayerNetworkType::Stellar
1155 );
1156
1157 assert!(domain_relayer.policies.is_some());
1159 if let Some(RelayerNetworkPolicy::Stellar(stellar_policy)) = domain_relayer.policies {
1160 assert_eq!(stellar_policy.min_balance, Some(35000000));
1161 assert_eq!(stellar_policy.max_fee, Some(200000));
1162 assert_eq!(stellar_policy.timeout_seconds, Some(90));
1163 } else {
1164 panic!("Expected Stellar domain policy");
1165 }
1166 }
1167
1168 #[test]
1169 fn test_try_from_relayer_file_config_validation_error() {
1170 let config = RelayerFileConfig {
1171 id: "".to_string(), name: "Test Relayer".to_string(),
1173 network: "mainnet".to_string(),
1174 paused: false,
1175 network_type: ConfigFileNetworkType::Evm,
1176 policies: None,
1177 signer_id: "test-signer".to_string(),
1178 notification_id: None,
1179 custom_rpc_urls: None,
1180 };
1181
1182 let result = Relayer::try_from(config);
1183 assert!(result.is_err());
1184
1185 if let Err(ConfigFileError::MissingField(field)) = result {
1186 assert_eq!(field, "relayer id");
1187 } else {
1188 panic!("Expected MissingField error for empty ID");
1189 }
1190 }
1191
1192 #[test]
1193 fn test_try_from_relayer_file_config_invalid_id_format() {
1194 let config = RelayerFileConfig {
1195 id: "invalid@id".to_string(), name: "Test Relayer".to_string(),
1197 network: "mainnet".to_string(),
1198 paused: false,
1199 network_type: ConfigFileNetworkType::Evm,
1200 policies: None,
1201 signer_id: "test-signer".to_string(),
1202 notification_id: None,
1203 custom_rpc_urls: None,
1204 };
1205
1206 let result = Relayer::try_from(config);
1207 assert!(result.is_err());
1208
1209 if let Err(ConfigFileError::InvalidIdFormat(_)) = result {
1210 } else {
1212 panic!("Expected InvalidIdFormat error");
1213 }
1214 }
1215
1216 #[test]
1217 fn test_relayers_file_config_validation_success() {
1218 let relayer_config = RelayerFileConfig {
1219 id: "test-relayer".to_string(),
1220 name: "Test Relayer".to_string(),
1221 network: "mainnet".to_string(),
1222 paused: false,
1223 network_type: ConfigFileNetworkType::Evm,
1224 policies: None,
1225 signer_id: "test-signer".to_string(),
1226 notification_id: None,
1227 custom_rpc_urls: None,
1228 };
1229
1230 let relayers_config = RelayersFileConfig::new(vec![relayer_config]);
1231 let networks_config = create_test_networks_config();
1232
1233 let result = relayers_config.validate(&networks_config);
1236
1237 assert!(result.is_err());
1239 if let Err(ConfigFileError::InvalidReference(_)) = result {
1240 } else {
1242 panic!("Expected InvalidReference error");
1243 }
1244 }
1245
1246 #[test]
1247 fn test_relayers_file_config_validation_duplicate_ids() {
1248 let relayer_config1 = RelayerFileConfig {
1249 id: "duplicate-id".to_string(),
1250 name: "Test Relayer 1".to_string(),
1251 network: "mainnet".to_string(),
1252 paused: false,
1253 network_type: ConfigFileNetworkType::Evm,
1254 policies: None,
1255 signer_id: "test-signer1".to_string(),
1256 notification_id: None,
1257 custom_rpc_urls: None,
1258 };
1259
1260 let relayer_config2 = RelayerFileConfig {
1261 id: "duplicate-id".to_string(), name: "Test Relayer 2".to_string(),
1263 network: "testnet".to_string(),
1264 paused: false,
1265 network_type: ConfigFileNetworkType::Solana,
1266 policies: None,
1267 signer_id: "test-signer2".to_string(),
1268 notification_id: None,
1269 custom_rpc_urls: None,
1270 };
1271
1272 let relayers_config = RelayersFileConfig::new(vec![relayer_config1, relayer_config2]);
1273 let networks_config = create_test_networks_config();
1274
1275 let result = relayers_config.validate(&networks_config);
1276 assert!(result.is_err());
1277
1278 match result {
1281 Err(ConfigFileError::DuplicateId(id)) => {
1282 assert_eq!(id, "duplicate-id");
1283 }
1284 Err(ConfigFileError::InvalidReference(_)) => {
1285 }
1287 Err(other) => {
1288 panic!("Expected DuplicateId or InvalidReference error, got: {other:?}");
1289 }
1290 Ok(_) => {
1291 panic!("Expected validation to fail but it succeeded");
1292 }
1293 }
1294 }
1295
1296 #[test]
1297 fn test_relayers_file_config_validation_empty_network() {
1298 let relayer_config = RelayerFileConfig {
1299 id: "test-relayer".to_string(),
1300 name: "Test Relayer".to_string(),
1301 network: "".to_string(), paused: false,
1303 network_type: ConfigFileNetworkType::Evm,
1304 policies: None,
1305 signer_id: "test-signer".to_string(),
1306 notification_id: None,
1307 custom_rpc_urls: None,
1308 };
1309
1310 let relayers_config = RelayersFileConfig::new(vec![relayer_config]);
1311 let networks_config = create_test_networks_config();
1312
1313 let result = relayers_config.validate(&networks_config);
1314 assert!(result.is_err());
1315
1316 if let Err(ConfigFileError::InvalidFormat(msg)) = result {
1317 assert!(msg.contains("relayer.network cannot be empty"));
1318 } else {
1319 panic!("Expected InvalidFormat error for empty network");
1320 }
1321 }
1322
1323 #[test]
1324 fn test_config_file_policy_serialization() {
1325 let evm_policy = ConfigFileRelayerEvmPolicy {
1327 include_revert_data: None,
1328 gas_price_cap: Some(80000000000),
1329 whitelist_receivers: Some(vec!["0xabc".to_string()]),
1330 eip1559_pricing: Some(false),
1331 private_transactions: Some(true),
1332 min_balance: Some(500000000000000000),
1333 gas_limit_estimation: Some(true),
1334 };
1335
1336 let serialized = serde_json::to_string(&evm_policy).unwrap();
1337 let deserialized: ConfigFileRelayerEvmPolicy = serde_json::from_str(&serialized).unwrap();
1338 assert_eq!(evm_policy, deserialized);
1339
1340 let solana_policy = ConfigFileRelayerSolanaPolicy {
1341 fee_payment_strategy: Some(ConfigFileSolanaFeePaymentStrategy::User),
1342 fee_margin_percentage: Some(3.0),
1343 min_balance: Some(6000000),
1344 allowed_tokens: None,
1345 allowed_programs: Some(vec!["Program456".to_string()]),
1346 allowed_accounts: None,
1347 disallowed_accounts: Some(vec!["DisallowedAccount".to_string()]),
1348 max_tx_data_size: Some(1536),
1349 max_signatures: Some(12),
1350 max_allowed_fee_lamports: Some(200000),
1351 swap_config: None,
1352 };
1353
1354 let serialized = serde_json::to_string(&solana_policy).unwrap();
1355 let deserialized: ConfigFileRelayerSolanaPolicy =
1356 serde_json::from_str(&serialized).unwrap();
1357 assert_eq!(solana_policy, deserialized);
1358
1359 let stellar_policy = ConfigFileRelayerStellarPolicy {
1360 min_balance: Some(45000000),
1361 max_fee: Some(250000),
1362 timeout_seconds: Some(120),
1363 concurrent_transactions: None,
1364 allowed_tokens: None,
1365 fee_payment_strategy: Some(ConfigFileStellarFeePaymentStrategy::Relayer),
1366 slippage_percentage: None,
1367 fee_margin_percentage: None,
1368 swap_config: None,
1369 };
1370
1371 let serialized = serde_json::to_string(&stellar_policy).unwrap();
1372 let deserialized: ConfigFileRelayerStellarPolicy =
1373 serde_json::from_str(&serialized).unwrap();
1374 assert_eq!(stellar_policy, deserialized);
1375 }
1376}