1mod config;
15pub use config::*;
16
17pub mod request;
18pub use request::*;
19
20mod response;
21pub use response::*;
22
23pub mod repository;
24pub use repository::*;
25
26mod rpc_config;
27pub use rpc_config::*;
28
29use crate::utils::{sanitize_url_for_error, validate_safe_url};
30use crate::{
31 config::ConfigFileNetworkType,
32 constants::ID_REGEX,
33 utils::{deserialize_optional_u128, serialize_optional_u128},
34};
35use apalis_cron::Schedule;
36use regex::Regex;
37use serde::{Deserialize, Serialize};
38use std::{
39 fmt::{Display, Formatter},
40 str::FromStr,
41};
42use utoipa::ToSchema;
43use validator::Validate;
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, ToSchema)]
47#[serde(rename_all = "lowercase")]
48pub enum RelayerNetworkType {
49 Evm,
50 Solana,
51 Stellar,
52}
53
54impl Display for RelayerNetworkType {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 match self {
57 RelayerNetworkType::Evm => write!(f, "evm"),
58 RelayerNetworkType::Solana => write!(f, "solana"),
59 RelayerNetworkType::Stellar => write!(f, "stellar"),
60 }
61 }
62}
63
64impl From<ConfigFileNetworkType> for RelayerNetworkType {
65 fn from(config_type: ConfigFileNetworkType) -> Self {
66 match config_type {
67 ConfigFileNetworkType::Evm => RelayerNetworkType::Evm,
68 ConfigFileNetworkType::Solana => RelayerNetworkType::Solana,
69 ConfigFileNetworkType::Stellar => RelayerNetworkType::Stellar,
70 }
71 }
72}
73
74impl From<RelayerNetworkType> for ConfigFileNetworkType {
75 fn from(domain_type: RelayerNetworkType) -> Self {
76 match domain_type {
77 RelayerNetworkType::Evm => ConfigFileNetworkType::Evm,
78 RelayerNetworkType::Solana => ConfigFileNetworkType::Solana,
79 RelayerNetworkType::Stellar => ConfigFileNetworkType::Stellar,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
87#[serde(tag = "type", content = "details")]
88pub enum HealthCheckFailure {
89 NonceSyncFailed(String),
91 RpcValidationFailed(String),
93 BalanceCheckFailed(String),
95 SequenceSyncFailed(String),
97}
98
99impl Display for HealthCheckFailure {
100 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101 match self {
102 HealthCheckFailure::NonceSyncFailed(msg) => write!(f, "Nonce sync failed: {msg}"),
103 HealthCheckFailure::RpcValidationFailed(msg) => {
104 write!(f, "RPC validation failed: {msg}")
105 }
106 HealthCheckFailure::BalanceCheckFailed(msg) => {
107 write!(f, "Balance check failed: {msg}")
108 }
109 HealthCheckFailure::SequenceSyncFailed(msg) => {
110 write!(f, "Sequence sync failed: {msg}")
111 }
112 }
113 }
114}
115
116#[derive(Debug, Clone, Deserialize, PartialEq, ToSchema)]
119#[serde(tag = "type", content = "details")]
120pub enum DisabledReason {
121 NonceSyncFailed(String),
123 RpcValidationFailed(String),
125 BalanceCheckFailed(String),
127 SequenceSyncFailed(String),
129 #[schema(value_type = Vec<String>)]
131 Multiple(Vec<DisabledReason>),
132}
133
134impl Serialize for DisabledReason {
136 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
137 where
138 S: serde::Serializer,
139 {
140 use serde::ser::SerializeStruct;
141
142 let mut state = serializer.serialize_struct("DisabledReason", 2)?;
143
144 match self {
145 DisabledReason::NonceSyncFailed(_) => {
146 state.serialize_field("type", "NonceSyncFailed")?;
147 state.serialize_field("details", "Nonce synchronization failed")?;
148 }
149 DisabledReason::RpcValidationFailed(_) => {
150 state.serialize_field("type", "RpcValidationFailed")?;
151 state.serialize_field("details", "RPC endpoint validation failed")?;
152 }
153 DisabledReason::BalanceCheckFailed(_) => {
154 state.serialize_field("type", "BalanceCheckFailed")?;
155 state.serialize_field("details", "Insufficient balance")?;
156 }
157 DisabledReason::SequenceSyncFailed(_) => {
158 state.serialize_field("type", "SequenceSyncFailed")?;
159 state.serialize_field("details", "Sequence synchronization failed")?;
160 }
161 DisabledReason::Multiple(reasons) => {
162 state.serialize_field("type", "Multiple")?;
163 state.serialize_field("details", reasons)?;
164 }
165 }
166
167 state.end()
168 }
169}
170
171impl DisabledReason {
172 pub fn from_health_failure(failure: HealthCheckFailure) -> Self {
174 match failure {
175 HealthCheckFailure::NonceSyncFailed(msg) => DisabledReason::NonceSyncFailed(msg),
176 HealthCheckFailure::RpcValidationFailed(msg) => {
177 DisabledReason::RpcValidationFailed(msg)
178 }
179 HealthCheckFailure::BalanceCheckFailed(msg) => DisabledReason::BalanceCheckFailed(msg),
180 HealthCheckFailure::SequenceSyncFailed(msg) => DisabledReason::SequenceSyncFailed(msg),
181 }
182 }
183
184 pub fn from_health_failures(failures: Vec<HealthCheckFailure>) -> Option<Self> {
191 match failures.len() {
192 0 => None,
193 1 => Some(Self::from_health_failure(
194 failures.into_iter().next().unwrap(),
195 )),
196 _ => Some(DisabledReason::Multiple(
197 failures
198 .into_iter()
199 .map(Self::from_health_failure)
200 .collect(),
201 )),
202 }
203 }
204
205 pub fn from_failures(failures: Vec<DisabledReason>) -> Option<Self> {
212 match failures.len() {
213 0 => None,
214 1 => Some(failures.into_iter().next().unwrap()),
215 _ => Some(DisabledReason::Multiple(failures)),
216 }
217 }
218
219 pub fn description(&self) -> String {
221 match self {
222 DisabledReason::NonceSyncFailed(e) => format!("Nonce sync failed: {e}"),
223 DisabledReason::RpcValidationFailed(e) => format!("RPC validation failed: {e}"),
224 DisabledReason::BalanceCheckFailed(e) => format!("Balance check failed: {e}"),
225 DisabledReason::SequenceSyncFailed(e) => format!("Sequence sync failed: {e}"),
226 DisabledReason::Multiple(reasons) => reasons
227 .iter()
228 .map(|r| r.description())
229 .collect::<Vec<_>>()
230 .join(", "),
231 }
232 }
233
234 pub fn safe_description(&self) -> String {
237 match self {
238 DisabledReason::NonceSyncFailed(_) => "Nonce synchronization failed".to_string(),
239 DisabledReason::RpcValidationFailed(_) => "RPC endpoint validation failed".to_string(),
240 DisabledReason::BalanceCheckFailed(_) => "Insufficient balance".to_string(),
241 DisabledReason::SequenceSyncFailed(_) => "Sequence synchronization failed".to_string(),
242 DisabledReason::Multiple(reasons) => reasons
243 .iter()
244 .map(|r| r.safe_description())
245 .collect::<Vec<_>>()
246 .join(", "),
247 }
248 }
249
250 pub fn same_variant(&self, other: &Self) -> bool {
253 use std::mem::discriminant;
254
255 match (self, other) {
256 (DisabledReason::Multiple(a), DisabledReason::Multiple(b)) => {
257 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.same_variant(y))
259 }
260 _ => discriminant(self) == discriminant(other),
261 }
262 }
263
264 pub fn from_error_string(error: String) -> Self {
268 let error_lower = error.to_lowercase();
269
270 if error_lower.contains("nonce") {
271 DisabledReason::NonceSyncFailed(error)
272 } else if error_lower.contains("rpc") {
273 DisabledReason::RpcValidationFailed(error)
274 } else if error_lower.contains("balance") {
275 DisabledReason::BalanceCheckFailed(error)
276 } else if error_lower.contains("sequence") {
277 DisabledReason::SequenceSyncFailed(error)
278 } else {
279 DisabledReason::RpcValidationFailed(error)
281 }
282 }
283}
284
285impl std::fmt::Display for DisabledReason {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 write!(f, "{}", self.description())
288 }
289}
290
291#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
293#[serde(deny_unknown_fields)]
294pub struct RelayerEvmPolicy {
295 #[serde(skip_serializing_if = "Option::is_none")]
296 #[serde(
297 serialize_with = "serialize_optional_u128",
298 deserialize_with = "deserialize_optional_u128",
299 default
300 )]
301 pub min_balance: Option<u128>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub gas_limit_estimation: Option<bool>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 #[serde(
306 serialize_with = "serialize_optional_u128",
307 deserialize_with = "deserialize_optional_u128",
308 default
309 )]
310 pub gas_price_cap: Option<u128>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub whitelist_receivers: Option<Vec<String>>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub eip1559_pricing: Option<bool>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub private_transactions: Option<bool>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub include_revert_data: Option<bool>,
319}
320
321#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
323#[serde(deny_unknown_fields)]
324pub struct SolanaAllowedTokensSwapConfig {
325 #[schema(nullable = false)]
327 pub slippage_percentage: Option<f32>,
328 #[schema(nullable = false)]
330 pub min_amount: Option<u64>,
331 #[schema(nullable = false)]
333 pub max_amount: Option<u64>,
334 #[schema(nullable = false)]
336 pub retain_min_amount: Option<u64>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
341#[serde(deny_unknown_fields)]
342pub struct SolanaAllowedTokensPolicy {
343 pub mint: String,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 #[schema(nullable = false)]
346 pub decimals: Option<u8>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 #[schema(nullable = false)]
349 pub symbol: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 #[schema(nullable = false)]
352 pub max_allowed_fee: Option<u64>,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 #[schema(nullable = false)]
355 pub swap_config: Option<SolanaAllowedTokensSwapConfig>,
356}
357
358impl SolanaAllowedTokensPolicy {
359 pub fn new(
361 mint: String,
362 max_allowed_fee: Option<u64>,
363 swap_config: Option<SolanaAllowedTokensSwapConfig>,
364 ) -> Self {
365 Self {
366 mint,
367 decimals: None,
368 symbol: None,
369 max_allowed_fee,
370 swap_config,
371 }
372 }
373
374 pub fn new_partial(
376 mint: String,
377 max_allowed_fee: Option<u64>,
378 swap_config: Option<SolanaAllowedTokensSwapConfig>,
379 ) -> Self {
380 Self::new(mint, max_allowed_fee, swap_config)
381 }
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
392#[serde(rename_all = "lowercase")]
393pub enum SolanaFeePaymentStrategy {
394 #[default]
395 User,
396 Relayer,
397}
398
399#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
401#[serde(rename_all = "kebab-case")]
402pub enum SolanaSwapStrategy {
403 JupiterSwap,
404 JupiterUltra,
405 #[default]
406 Noop,
407}
408
409#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
411#[serde(deny_unknown_fields)]
412pub struct JupiterSwapOptions {
413 #[schema(nullable = false)]
415 pub priority_fee_max_lamports: Option<u64>,
416 #[schema(nullable = false)]
418 pub priority_level: Option<String>,
419 #[schema(nullable = false)]
420 pub dynamic_compute_unit_limit: Option<bool>,
421}
422
423#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
425#[serde(deny_unknown_fields)]
426pub struct RelayerSolanaSwapConfig {
427 #[schema(nullable = false)]
429 pub strategy: Option<SolanaSwapStrategy>,
430 #[schema(nullable = false)]
432 pub cron_schedule: Option<String>,
433 #[schema(nullable = false)]
435 pub min_balance_threshold: Option<u64>,
436 #[schema(nullable = false)]
438 pub jupiter_swap_options: Option<JupiterSwapOptions>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Default)]
443#[serde(deny_unknown_fields)]
444pub struct RelayerSolanaPolicy {
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub allowed_programs: Option<Vec<String>>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub max_signatures: Option<u8>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub max_tx_data_size: Option<u16>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub min_balance: Option<u64>,
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 #[schema(nullable = false)]
457 pub fee_payment_strategy: Option<SolanaFeePaymentStrategy>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub fee_margin_percentage: Option<f32>,
460 #[serde(skip_serializing_if = "Option::is_none")]
461 pub allowed_accounts: Option<Vec<String>>,
462 #[serde(skip_serializing_if = "Option::is_none")]
463 pub disallowed_accounts: Option<Vec<String>>,
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub max_allowed_fee_lamports: Option<u64>,
466 #[serde(skip_serializing_if = "Option::is_none")]
467 #[schema(nullable = false)]
468 pub swap_config: Option<RelayerSolanaSwapConfig>,
469}
470
471impl RelayerSolanaPolicy {
472 pub fn get_allowed_tokens(&self) -> Vec<SolanaAllowedTokensPolicy> {
474 self.allowed_tokens.clone().unwrap_or_default()
475 }
476
477 pub fn get_allowed_token_entry(&self, mint: &str) -> Option<SolanaAllowedTokensPolicy> {
479 self.allowed_tokens
480 .clone()
481 .unwrap_or_default()
482 .into_iter()
483 .find(|entry| entry.mint == mint)
484 }
485
486 pub fn get_swap_config(&self) -> Option<RelayerSolanaSwapConfig> {
488 self.swap_config.clone()
489 }
490
491 pub fn get_allowed_token_decimals(&self, mint: &str) -> Option<u8> {
493 self.get_allowed_token_entry(mint)
494 .and_then(|entry| entry.decimals)
495 }
496}
497
498#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
500#[serde(deny_unknown_fields)]
501pub struct StellarAllowedTokensSwapConfig {
502 #[schema(nullable = false)]
504 pub slippage_percentage: Option<f32>,
505 #[schema(nullable = false)]
507 pub min_amount: Option<u64>,
508 #[schema(nullable = false)]
510 pub max_amount: Option<u64>,
511 #[schema(nullable = false)]
513 pub retain_min_amount: Option<u64>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
517#[serde(rename_all = "kebab-case")]
518pub enum StellarTokenKind {
519 Native,
520 Classic { code: String, issuer: String },
521 Contract { contract_id: String },
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
525#[serde(deny_unknown_fields)]
526pub struct StellarTokenMetadata {
527 pub kind: StellarTokenKind,
528 pub decimals: u32,
529 pub canonical_asset_id: String,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
534#[serde(deny_unknown_fields)]
535pub struct StellarAllowedTokensPolicy {
536 pub asset: String,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 #[schema(nullable = false)]
539 pub metadata: Option<StellarTokenMetadata>,
540 #[serde(skip_serializing_if = "Option::is_none")]
541 #[schema(nullable = false)]
542 pub max_allowed_fee: Option<u64>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 #[schema(nullable = false)]
545 pub swap_config: Option<StellarAllowedTokensSwapConfig>,
546}
547
548impl StellarAllowedTokensPolicy {
549 pub fn new(
551 asset: String,
552 metadata: Option<StellarTokenMetadata>,
553 max_allowed_fee: Option<u64>,
554 swap_config: Option<StellarAllowedTokensSwapConfig>,
555 ) -> Self {
556 Self {
557 asset,
558 metadata,
559 max_allowed_fee,
560 swap_config,
561 }
562 }
563}
564
565#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
571#[serde(rename_all = "lowercase")]
572pub enum StellarFeePaymentStrategy {
573 User,
574 Relayer,
575}
576
577#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq)]
579#[serde(rename_all = "kebab-case")]
580pub enum StellarSwapStrategy {
581 OrderBook,
583 Soroswap,
585}
586
587#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
589#[serde(deny_unknown_fields)]
590pub struct RelayerStellarSwapConfig {
591 #[schema(nullable = false)]
594 #[serde(default)]
595 pub strategies: Vec<StellarSwapStrategy>,
596 #[schema(nullable = false)]
598 pub cron_schedule: Option<String>,
599 #[schema(nullable = false)]
601 pub min_balance_threshold: Option<u64>,
602}
603
604#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
606#[serde(deny_unknown_fields)]
607pub struct RelayerStellarPolicy {
608 #[serde(skip_serializing_if = "Option::is_none")]
609 pub min_balance: Option<u64>,
610 #[serde(skip_serializing_if = "Option::is_none")]
611 pub max_fee: Option<u32>,
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub timeout_seconds: Option<u64>,
614 #[serde(skip_serializing_if = "Option::is_none")]
615 pub concurrent_transactions: Option<bool>,
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub allowed_tokens: Option<Vec<StellarAllowedTokensPolicy>>,
618 #[serde(skip_serializing_if = "Option::is_none")]
620 #[schema(nullable = false)]
621 pub fee_payment_strategy: Option<StellarFeePaymentStrategy>,
622 #[serde(skip_serializing_if = "Option::is_none")]
623 pub slippage_percentage: Option<f32>,
624 #[serde(skip_serializing_if = "Option::is_none")]
625 pub fee_margin_percentage: Option<f32>,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 #[schema(nullable = false)]
628 pub swap_config: Option<RelayerStellarSwapConfig>,
629}
630
631impl RelayerStellarPolicy {
632 pub fn get_allowed_tokens(&self) -> Vec<StellarAllowedTokensPolicy> {
634 self.allowed_tokens.clone().unwrap_or_default()
635 }
636
637 pub fn get_allowed_token_entry(&self, asset: &str) -> Option<StellarAllowedTokensPolicy> {
639 self.allowed_tokens
640 .clone()
641 .unwrap_or_default()
642 .into_iter()
643 .find(|entry| entry.asset == asset)
644 }
645
646 pub fn get_allowed_token_decimals(&self, asset: &str) -> Option<u8> {
648 self.get_allowed_token_entry(asset).and_then(|entry| {
649 entry
650 .metadata
651 .and_then(|metadata| u8::try_from(metadata.decimals).ok())
652 })
653 }
654
655 pub fn get_swap_config(&self) -> Option<RelayerStellarSwapConfig> {
657 self.swap_config.clone()
658 }
659
660 pub fn is_user_fee_payment(&self) -> bool {
662 self.fee_payment_strategy == Some(StellarFeePaymentStrategy::User)
663 }
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
668#[serde(tag = "network_type")]
669pub enum RelayerNetworkPolicy {
670 #[serde(rename = "evm")]
671 Evm(RelayerEvmPolicy),
672 #[serde(rename = "solana")]
673 Solana(RelayerSolanaPolicy),
674 #[serde(rename = "stellar")]
675 Stellar(RelayerStellarPolicy),
676}
677
678impl RelayerNetworkPolicy {
679 pub fn get_evm_policy(&self) -> RelayerEvmPolicy {
681 match self {
682 Self::Evm(policy) => policy.clone(),
683 _ => RelayerEvmPolicy::default(),
684 }
685 }
686
687 pub fn get_solana_policy(&self) -> RelayerSolanaPolicy {
689 match self {
690 Self::Solana(policy) => policy.clone(),
691 _ => RelayerSolanaPolicy::default(),
692 }
693 }
694
695 pub fn get_stellar_policy(&self) -> RelayerStellarPolicy {
697 match self {
698 Self::Stellar(policy) => policy.clone(),
699 _ => RelayerStellarPolicy::default(),
700 }
701 }
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
706pub struct Relayer {
707 #[validate(
708 length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
709 regex(
710 path = "*ID_REGEX",
711 message = "ID must contain only letters, numbers, dashes and underscores"
712 )
713 )]
714 pub id: String,
715
716 #[validate(length(min = 1, message = "Name cannot be empty"))]
717 pub name: String,
718
719 #[validate(length(min = 1, message = "Network cannot be empty"))]
720 pub network: String,
721
722 pub paused: bool,
723 pub network_type: RelayerNetworkType,
724 pub policies: Option<RelayerNetworkPolicy>,
725
726 #[validate(length(min = 1, message = "Signer ID cannot be empty"))]
727 pub signer_id: String,
728
729 pub notification_id: Option<String>,
730 pub custom_rpc_urls: Option<Vec<RpcConfig>>,
731}
732
733impl Relayer {
734 #[allow(clippy::too_many_arguments)]
736 pub fn new(
737 id: String,
738 name: String,
739 network: String,
740 paused: bool,
741 network_type: RelayerNetworkType,
742 policies: Option<RelayerNetworkPolicy>,
743 signer_id: String,
744 notification_id: Option<String>,
745 custom_rpc_urls: Option<Vec<RpcConfig>>,
746 ) -> Self {
747 Self {
748 id,
749 name,
750 network,
751 paused,
752 network_type,
753 policies,
754 signer_id,
755 notification_id,
756 custom_rpc_urls,
757 }
758 }
759
760 pub fn validate(&self) -> Result<(), RelayerValidationError> {
762 if self.id.is_empty() {
764 return Err(RelayerValidationError::EmptyId);
765 }
766
767 if self.id.len() > 36 {
769 return Err(RelayerValidationError::IdTooLong);
770 }
771
772 Validate::validate(self).map_err(|validation_errors| {
774 for (field, errors) in validation_errors.field_errors() {
776 if let Some(error) = errors.first() {
777 let field_str = field.as_ref();
778 return match (field_str, error.code.as_ref()) {
779 ("id", "regex") => RelayerValidationError::InvalidIdFormat,
780 ("name", "length") => RelayerValidationError::EmptyName,
781 ("network", "length") => RelayerValidationError::EmptyNetwork,
782 ("signer_id", "length") => RelayerValidationError::InvalidPolicy(
783 "Signer ID cannot be empty".to_string(),
784 ),
785 _ => RelayerValidationError::InvalidIdFormat, };
787 }
788 }
789 RelayerValidationError::InvalidIdFormat
791 })?;
792
793 self.validate_policies()?;
795 self.validate_custom_rpc_urls()?;
796
797 Ok(())
798 }
799
800 fn validate_policies(&self) -> Result<(), RelayerValidationError> {
802 match (&self.network_type, &self.policies) {
803 (RelayerNetworkType::Solana, Some(RelayerNetworkPolicy::Solana(policy))) => {
804 self.validate_solana_policy(policy)?;
805 }
806 (RelayerNetworkType::Evm, Some(RelayerNetworkPolicy::Evm(_))) => {
807 }
809 (RelayerNetworkType::Stellar, Some(RelayerNetworkPolicy::Stellar(policy))) => {
810 self.validate_stellar_policy(policy)?;
811 }
812 (RelayerNetworkType::Stellar, None) => {
813 return Err(RelayerValidationError::InvalidPolicy(
814 "Stellar policy is required. fee_payment_strategy is required".into(),
815 ));
816 }
817 (network_type, Some(policy)) => {
819 let policy_type = match policy {
820 RelayerNetworkPolicy::Evm(_) => "EVM",
821 RelayerNetworkPolicy::Solana(_) => "Solana",
822 RelayerNetworkPolicy::Stellar(_) => "Stellar",
823 };
824 let network_type_str = format!("{network_type:?}");
825 return Err(RelayerValidationError::InvalidPolicy(format!(
826 "Network type {network_type_str} does not match policy type {policy_type}"
827 )));
828 }
829 (_, None) => {}
831 }
832 Ok(())
833 }
834
835 fn validate_solana_policy(
837 &self,
838 policy: &RelayerSolanaPolicy,
839 ) -> Result<(), RelayerValidationError> {
840 self.validate_solana_pub_keys(&policy.allowed_accounts)?;
842 self.validate_solana_pub_keys(&policy.disallowed_accounts)?;
843 self.validate_solana_pub_keys(&policy.allowed_programs)?;
844
845 if let Some(tokens) = &policy.allowed_tokens {
847 let mint_keys: Vec<String> = tokens.iter().map(|t| t.mint.clone()).collect();
848 self.validate_solana_pub_keys(&Some(mint_keys))?;
849 }
850
851 if let Some(fee_margin) = policy.fee_margin_percentage {
853 if fee_margin < 0.0 {
854 return Err(RelayerValidationError::InvalidPolicy(
855 "Negative fee margin percentage values are not accepted".into(),
856 ));
857 }
858 }
859
860 if policy.allowed_accounts.is_some() && policy.disallowed_accounts.is_some() {
862 return Err(RelayerValidationError::InvalidPolicy(
863 "allowed_accounts and disallowed_accounts cannot be both present".into(),
864 ));
865 }
866
867 if let Some(swap_config) = &policy.swap_config {
869 self.validate_solana_swap_config(swap_config, policy)?;
870 }
871
872 Ok(())
873 }
874
875 fn validate_solana_pub_keys(
877 &self,
878 keys: &Option<Vec<String>>,
879 ) -> Result<(), RelayerValidationError> {
880 if let Some(keys) = keys {
881 let solana_pub_key_regex =
882 Regex::new(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$").map_err(|e| {
883 RelayerValidationError::InvalidPolicy(format!("Regex compilation error: {e}"))
884 })?;
885
886 for key in keys {
887 if !solana_pub_key_regex.is_match(key) {
888 return Err(RelayerValidationError::InvalidPolicy(
889 "Public key must be a valid Solana address".into(),
890 ));
891 }
892 }
893 }
894 Ok(())
895 }
896
897 fn validate_solana_swap_config(
899 &self,
900 swap_config: &RelayerSolanaSwapConfig,
901 policy: &RelayerSolanaPolicy,
902 ) -> Result<(), RelayerValidationError> {
903 if let Some(fee_payment_strategy) = &policy.fee_payment_strategy {
905 if *fee_payment_strategy == SolanaFeePaymentStrategy::Relayer {
906 return Err(RelayerValidationError::InvalidPolicy(
907 "Swap config only supported for user fee payment strategy".into(),
908 ));
909 }
910 }
911
912 if let Some(strategy) = &swap_config.strategy {
914 match strategy {
915 SolanaSwapStrategy::JupiterSwap | SolanaSwapStrategy::JupiterUltra => {
916 if self.network != "mainnet-beta" {
917 return Err(RelayerValidationError::InvalidPolicy(format!(
918 "{strategy:?} strategy is only supported on mainnet-beta"
919 )));
920 }
921 }
922 SolanaSwapStrategy::Noop => {
923 }
925 }
926 }
927
928 if let Some(cron_schedule) = &swap_config.cron_schedule {
930 if cron_schedule.is_empty() {
931 return Err(RelayerValidationError::InvalidPolicy(
932 "Empty cron schedule is not accepted".into(),
933 ));
934 }
935
936 Schedule::from_str(cron_schedule).map_err(|_| {
937 RelayerValidationError::InvalidPolicy("Invalid cron schedule format".into())
938 })?;
939 }
940
941 if let Some(jupiter_options) = &swap_config.jupiter_swap_options {
943 if swap_config.strategy != Some(SolanaSwapStrategy::JupiterSwap) {
945 return Err(RelayerValidationError::InvalidPolicy(
946 "JupiterSwap options are only valid for JupiterSwap strategy".into(),
947 ));
948 }
949
950 if let Some(max_lamports) = jupiter_options.priority_fee_max_lamports {
951 if max_lamports == 0 {
952 return Err(RelayerValidationError::InvalidPolicy(
953 "Max lamports must be greater than 0".into(),
954 ));
955 }
956 }
957
958 if let Some(priority_level) = &jupiter_options.priority_level {
959 if priority_level.is_empty() {
960 return Err(RelayerValidationError::InvalidPolicy(
961 "Priority level cannot be empty".into(),
962 ));
963 }
964
965 let valid_levels = ["medium", "high", "veryHigh"];
966 if !valid_levels.contains(&priority_level.as_str()) {
967 return Err(RelayerValidationError::InvalidPolicy(
968 "Priority level must be one of: medium, high, veryHigh".into(),
969 ));
970 }
971 }
972
973 match (
975 &jupiter_options.priority_level,
976 jupiter_options.priority_fee_max_lamports,
977 ) {
978 (Some(_), None) => {
979 return Err(RelayerValidationError::InvalidPolicy(
980 "Priority Fee Max lamports must be set if priority level is set".into(),
981 ));
982 }
983 (None, Some(_)) => {
984 return Err(RelayerValidationError::InvalidPolicy(
985 "Priority level must be set if priority fee max lamports is set".into(),
986 ));
987 }
988 _ => {}
989 }
990 }
991
992 Ok(())
993 }
994
995 fn validate_stellar_policy(
997 &self,
998 policy: &RelayerStellarPolicy,
999 ) -> Result<(), RelayerValidationError> {
1000 if policy.fee_payment_strategy.is_none() {
1001 return Err(RelayerValidationError::InvalidPolicy(
1002 "Fee payment strategy is required".into(),
1003 ));
1004 }
1005 if let Some(fee_margin) = policy.fee_margin_percentage {
1007 if fee_margin < 0.0 {
1008 return Err(RelayerValidationError::InvalidPolicy(
1009 "Negative fee margin percentage values are not accepted".into(),
1010 ));
1011 }
1012 }
1013
1014 if let Some(slippage) = policy.slippage_percentage {
1016 if !(0.0..=100.0).contains(&slippage) {
1017 return Err(RelayerValidationError::InvalidPolicy(
1018 "Slippage percentage must be between 0 and 100".into(),
1019 ));
1020 }
1021 }
1022
1023 if let Some(tokens) = &policy.allowed_tokens {
1025 for token in tokens {
1026 self.validate_stellar_asset_identifier(&token.asset)?;
1027 }
1028 }
1029
1030 if let Some(swap_config) = &policy.swap_config {
1032 self.validate_stellar_swap_config(swap_config, policy)?;
1033 }
1034
1035 Ok(())
1036 }
1037
1038 fn validate_stellar_asset_identifier(&self, asset: &str) -> Result<(), RelayerValidationError> {
1045 if asset == "native" || asset == "XLM" || asset.is_empty() {
1047 return Ok(());
1048 }
1049
1050 if asset.starts_with('C') && asset.len() == 56 && !asset.contains(':') {
1052 return Ok(());
1055 }
1056
1057 if let Some(colon_pos) = asset.find(':') {
1059 let code = &asset[..colon_pos];
1060 let issuer = &asset[colon_pos + 1..];
1061
1062 if code.is_empty() || code.len() > 12 {
1064 return Err(RelayerValidationError::InvalidPolicy(
1065 "Asset code must be between 1 and 12 characters".into(),
1066 ));
1067 }
1068
1069 if !code.chars().all(|c| c.is_alphanumeric()) {
1070 return Err(RelayerValidationError::InvalidPolicy(
1071 "Asset code must contain only alphanumeric characters".into(),
1072 ));
1073 }
1074
1075 if issuer.len() != 56 {
1077 return Err(RelayerValidationError::InvalidPolicy(
1078 "Issuer address must be 56 characters long".into(),
1079 ));
1080 }
1081
1082 if !issuer.starts_with('G') {
1083 return Err(RelayerValidationError::InvalidPolicy(
1084 "Issuer address must start with 'G'".into(),
1085 ));
1086 }
1087
1088 let stellar_address_regex = Regex::new(r"^G[0-9A-Z]{55}$").map_err(|e| {
1090 RelayerValidationError::InvalidPolicy(format!("Regex compilation error: {e}"))
1091 })?;
1092
1093 if !stellar_address_regex.is_match(issuer) {
1094 return Err(RelayerValidationError::InvalidPolicy(
1095 "Issuer address must be a valid Stellar address".into(),
1096 ));
1097 }
1098
1099 return Ok(());
1100 }
1101
1102 Err(RelayerValidationError::InvalidPolicy(
1104 "Asset identifier must be 'native', 'XLM', 'CODE:ISSUER', or a contract address".into(),
1105 ))
1106 }
1107
1108 fn validate_stellar_swap_config(
1110 &self,
1111 swap_config: &RelayerStellarSwapConfig,
1112 policy: &RelayerStellarPolicy,
1113 ) -> Result<(), RelayerValidationError> {
1114 if let Some(fee_payment_strategy) = &policy.fee_payment_strategy {
1116 if *fee_payment_strategy == StellarFeePaymentStrategy::Relayer {
1117 return Err(RelayerValidationError::InvalidPolicy(
1118 "Swap config only supported for user fee payment strategy".into(),
1119 ));
1120 }
1121 }
1122
1123 if let Some(cron_schedule) = &swap_config.cron_schedule {
1125 if cron_schedule.is_empty() {
1126 return Err(RelayerValidationError::InvalidPolicy(
1127 "Empty cron schedule is not accepted".into(),
1128 ));
1129 }
1130
1131 Schedule::from_str(cron_schedule).map_err(|_| {
1132 RelayerValidationError::InvalidPolicy("Invalid cron schedule format".into())
1133 })?;
1134 }
1135
1136 if swap_config.strategies.is_empty() {
1138 return Err(RelayerValidationError::InvalidPolicy(
1139 "Swap config must include at least one strategy".into(),
1140 ));
1141 }
1142
1143 Ok(())
1144 }
1145
1146 fn validate_custom_rpc_urls(&self) -> Result<(), RelayerValidationError> {
1148 if let Some(configs) = &self.custom_rpc_urls {
1149 let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
1151 let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
1152
1153 for config in configs {
1154 reqwest::Url::parse(&config.url).map_err(|_| {
1156 RelayerValidationError::InvalidRpcUrl(sanitize_url_for_error(&config.url))
1157 })?;
1158
1159 validate_safe_url(&config.url, &allowed_hosts, block_private_ips).map_err(
1161 |err| {
1162 RelayerValidationError::InvalidRpcUrl(format!(
1163 "{}: {}",
1164 sanitize_url_for_error(&config.url),
1165 err
1166 ))
1167 },
1168 )?;
1169
1170 if config.weight > 100 {
1172 return Err(RelayerValidationError::InvalidRpcWeight);
1173 }
1174 }
1175 }
1176 Ok(())
1177 }
1178
1179 pub fn apply_json_patch(
1189 &self,
1190 patch: &serde_json::Value,
1191 ) -> Result<Self, RelayerValidationError> {
1192 let mut domain_json = serde_json::to_value(self).map_err(|e| {
1194 RelayerValidationError::InvalidField(format!("Serialization error: {e}"))
1195 })?;
1196
1197 json_patch::merge(&mut domain_json, patch);
1199
1200 let updated: Relayer = serde_json::from_value(domain_json).map_err(|e| {
1202 RelayerValidationError::InvalidField(format!("Invalid result after patch: {e}"))
1203 })?;
1204
1205 updated.validate()?;
1207
1208 Ok(updated)
1209 }
1210}
1211
1212#[derive(Debug, thiserror::Error)]
1214pub enum RelayerValidationError {
1215 #[error("Relayer ID cannot be empty")]
1216 EmptyId,
1217 #[error("Relayer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
1218 InvalidIdFormat,
1219 #[error("Relayer ID must not exceed 36 characters")]
1220 IdTooLong,
1221 #[error("Relayer name cannot be empty")]
1222 EmptyName,
1223 #[error("Network cannot be empty")]
1224 EmptyNetwork,
1225 #[error("Invalid relayer policy: {0}")]
1226 InvalidPolicy(String),
1227 #[error("Invalid RPC URL: {0}")]
1228 InvalidRpcUrl(String),
1229 #[error("RPC URL weight must be in range 0-100")]
1230 InvalidRpcWeight,
1231 #[error("Invalid field: {0}")]
1232 InvalidField(String),
1233}
1234
1235impl From<RelayerValidationError> for crate::models::ApiError {
1237 fn from(error: RelayerValidationError) -> Self {
1238 use crate::models::ApiError;
1239
1240 ApiError::BadRequest(match error {
1241 RelayerValidationError::EmptyId => "ID cannot be empty".to_string(),
1242 RelayerValidationError::InvalidIdFormat => {
1243 "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
1244 }
1245 RelayerValidationError::IdTooLong => {
1246 "ID must not exceed 36 characters".to_string()
1247 }
1248 RelayerValidationError::EmptyName => "Name cannot be empty".to_string(),
1249 RelayerValidationError::EmptyNetwork => "Network cannot be empty".to_string(),
1250 RelayerValidationError::InvalidPolicy(msg) => {
1251 format!("Invalid relayer policy: {msg}")
1252 }
1253 RelayerValidationError::InvalidRpcUrl(url) => {
1254 format!("Invalid RPC URL: {url}")
1255 }
1256 RelayerValidationError::InvalidRpcWeight => {
1257 "RPC URL weight must be in range 0-100".to_string()
1258 }
1259 RelayerValidationError::InvalidField(msg) => msg.clone(),
1260 })
1261 }
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266 use super::*;
1267 use serde_json::json;
1268
1269 #[test]
1270 fn test_disabled_reason_serialization_sanitizes_details() {
1271 let reason = DisabledReason::RpcValidationFailed(
1273 "Connection failed to https://mainnet.infura.io/v3/SECRET_API_KEY: timeout".to_string(),
1274 );
1275
1276 let serialized = serde_json::to_string(&reason).unwrap();
1277
1278 assert!(!serialized.contains("SECRET_API_KEY"));
1280 assert!(!serialized.contains("infura.io"));
1281
1282 assert!(serialized.contains("RPC endpoint validation failed"));
1284 }
1285
1286 #[test]
1287 fn test_disabled_reason_safe_description() {
1288 let reason = DisabledReason::BalanceCheckFailed(
1289 "Insufficient balance: 0.001 ETH but need 0.1 ETH at address 0x123...".to_string(),
1290 );
1291
1292 let safe = reason.safe_description();
1293
1294 assert!(!safe.contains("0.001"));
1296 assert!(!safe.contains("0x123"));
1297 assert_eq!(safe, "Insufficient balance");
1298 }
1299
1300 #[test]
1301 fn test_disabled_reason_same_variant_same_type_different_message() {
1302 let reason1 = DisabledReason::RpcValidationFailed("Connection timeout".to_string());
1304 let reason2 = DisabledReason::RpcValidationFailed("Connection refused".to_string());
1305
1306 assert!(
1307 reason1.same_variant(&reason2),
1308 "Same variant types with different messages should be considered the same"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_disabled_reason_same_variant_different_types() {
1314 let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
1316 let reason2 = DisabledReason::BalanceCheckFailed("Error".to_string());
1317
1318 assert!(
1319 !reason1.same_variant(&reason2),
1320 "Different variant types should not be considered the same"
1321 );
1322 }
1323
1324 #[test]
1325 fn test_disabled_reason_same_variant_identical() {
1326 let reason1 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
1328 let reason2 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
1329
1330 assert!(
1331 reason1.same_variant(&reason2),
1332 "Identical reasons should be the same variant"
1333 );
1334 }
1335
1336 #[test]
1337 fn test_disabled_reason_same_variant_multiple_same_order() {
1338 let reason1 = DisabledReason::Multiple(vec![
1340 DisabledReason::RpcValidationFailed("Error 1".to_string()),
1341 DisabledReason::BalanceCheckFailed("Error 2".to_string()),
1342 ]);
1343 let reason2 = DisabledReason::Multiple(vec![
1344 DisabledReason::RpcValidationFailed("Different error 1".to_string()),
1345 DisabledReason::BalanceCheckFailed("Different error 2".to_string()),
1346 ]);
1347
1348 assert!(
1349 reason1.same_variant(&reason2),
1350 "Multiple with same variant types in same order should be considered the same"
1351 );
1352 }
1353
1354 #[test]
1355 fn test_disabled_reason_same_variant_multiple_different_order() {
1356 let reason1 = DisabledReason::Multiple(vec![
1358 DisabledReason::RpcValidationFailed("Error".to_string()),
1359 DisabledReason::BalanceCheckFailed("Error".to_string()),
1360 ]);
1361 let reason2 = DisabledReason::Multiple(vec![
1362 DisabledReason::BalanceCheckFailed("Error".to_string()),
1363 DisabledReason::RpcValidationFailed("Error".to_string()),
1364 ]);
1365
1366 assert!(
1367 !reason1.same_variant(&reason2),
1368 "Multiple with different order should not be considered the same"
1369 );
1370 }
1371
1372 #[test]
1373 fn test_disabled_reason_same_variant_multiple_different_length() {
1374 let reason1 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1376 "Error".to_string(),
1377 )]);
1378 let reason2 = DisabledReason::Multiple(vec![
1379 DisabledReason::RpcValidationFailed("Error".to_string()),
1380 DisabledReason::BalanceCheckFailed("Error".to_string()),
1381 ]);
1382
1383 assert!(
1384 !reason1.same_variant(&reason2),
1385 "Multiple with different lengths should not be considered the same"
1386 );
1387 }
1388
1389 #[test]
1390 fn test_disabled_reason_same_variant_single_vs_multiple() {
1391 let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
1393 let reason2 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1394 "Error".to_string(),
1395 )]);
1396
1397 assert!(
1398 !reason1.same_variant(&reason2),
1399 "Single variant vs Multiple should not be considered the same"
1400 );
1401 }
1402
1403 #[test]
1406 fn test_health_check_failure_display() {
1407 let failure1 = HealthCheckFailure::NonceSyncFailed("nonce mismatch".to_string());
1408 assert_eq!(failure1.to_string(), "Nonce sync failed: nonce mismatch");
1409
1410 let failure2 = HealthCheckFailure::RpcValidationFailed("connection timeout".to_string());
1411 assert_eq!(
1412 failure2.to_string(),
1413 "RPC validation failed: connection timeout"
1414 );
1415
1416 let failure3 = HealthCheckFailure::BalanceCheckFailed("insufficient funds".to_string());
1417 assert_eq!(
1418 failure3.to_string(),
1419 "Balance check failed: insufficient funds"
1420 );
1421
1422 let failure4 = HealthCheckFailure::SequenceSyncFailed("sequence error".to_string());
1423 assert_eq!(failure4.to_string(), "Sequence sync failed: sequence error");
1424 }
1425
1426 #[test]
1427 fn test_health_check_failure_serialization() {
1428 let failure = HealthCheckFailure::RpcValidationFailed("test error".to_string());
1429 let serialized = serde_json::to_string(&failure).unwrap();
1430 let deserialized: HealthCheckFailure = serde_json::from_str(&serialized).unwrap();
1431 assert_eq!(failure, deserialized);
1432 }
1433
1434 #[test]
1437 fn test_disabled_reason_from_health_failure() {
1438 let health_failure = HealthCheckFailure::NonceSyncFailed("nonce error".to_string());
1439 let disabled_reason = DisabledReason::from_health_failure(health_failure);
1440 assert!(matches!(
1441 disabled_reason,
1442 DisabledReason::NonceSyncFailed(_)
1443 ));
1444
1445 let health_failure2 = HealthCheckFailure::RpcValidationFailed("rpc error".to_string());
1446 let disabled_reason2 = DisabledReason::from_health_failure(health_failure2);
1447 assert!(matches!(
1448 disabled_reason2,
1449 DisabledReason::RpcValidationFailed(_)
1450 ));
1451
1452 let health_failure3 = HealthCheckFailure::BalanceCheckFailed("balance error".to_string());
1453 let disabled_reason3 = DisabledReason::from_health_failure(health_failure3);
1454 assert!(matches!(
1455 disabled_reason3,
1456 DisabledReason::BalanceCheckFailed(_)
1457 ));
1458
1459 let health_failure4 = HealthCheckFailure::SequenceSyncFailed("sequence error".to_string());
1460 let disabled_reason4 = DisabledReason::from_health_failure(health_failure4);
1461 assert!(matches!(
1462 disabled_reason4,
1463 DisabledReason::SequenceSyncFailed(_)
1464 ));
1465 }
1466
1467 #[test]
1468 fn test_disabled_reason_from_health_failures_empty() {
1469 let failures: Vec<HealthCheckFailure> = vec![];
1470 let result = DisabledReason::from_health_failures(failures);
1471 assert!(result.is_none());
1472 }
1473
1474 #[test]
1475 fn test_disabled_reason_from_health_failures_single() {
1476 let failures = vec![HealthCheckFailure::NonceSyncFailed("error".to_string())];
1477 let result = DisabledReason::from_health_failures(failures).unwrap();
1478 assert!(matches!(result, DisabledReason::NonceSyncFailed(_)));
1479 }
1480
1481 #[test]
1482 fn test_disabled_reason_from_health_failures_multiple() {
1483 let failures = vec![
1484 HealthCheckFailure::NonceSyncFailed("error1".to_string()),
1485 HealthCheckFailure::RpcValidationFailed("error2".to_string()),
1486 ];
1487 let result = DisabledReason::from_health_failures(failures).unwrap();
1488 if let DisabledReason::Multiple(reasons) = result {
1489 assert_eq!(reasons.len(), 2);
1490 assert!(matches!(reasons[0], DisabledReason::NonceSyncFailed(_)));
1491 assert!(matches!(reasons[1], DisabledReason::RpcValidationFailed(_)));
1492 } else {
1493 panic!("Expected Multiple variant");
1494 }
1495 }
1496
1497 #[test]
1498 fn test_disabled_reason_from_failures_empty() {
1499 let failures: Vec<DisabledReason> = vec![];
1500 let result = DisabledReason::from_failures(failures);
1501 assert!(result.is_none());
1502 }
1503
1504 #[test]
1505 fn test_disabled_reason_from_failures_single() {
1506 let failures = vec![DisabledReason::NonceSyncFailed("error".to_string())];
1507 let result = DisabledReason::from_failures(failures).unwrap();
1508 assert!(matches!(result, DisabledReason::NonceSyncFailed(_)));
1509 }
1510
1511 #[test]
1512 fn test_disabled_reason_from_failures_multiple() {
1513 let failures = vec![
1514 DisabledReason::NonceSyncFailed("error1".to_string()),
1515 DisabledReason::RpcValidationFailed("error2".to_string()),
1516 ];
1517 let result = DisabledReason::from_failures(failures).unwrap();
1518 if let DisabledReason::Multiple(reasons) = result {
1519 assert_eq!(reasons.len(), 2);
1520 } else {
1521 panic!("Expected Multiple variant");
1522 }
1523 }
1524
1525 #[test]
1526 fn test_disabled_reason_description() {
1527 let reason1 = DisabledReason::NonceSyncFailed("nonce error".to_string());
1528 assert_eq!(reason1.description(), "Nonce sync failed: nonce error");
1529
1530 let reason2 = DisabledReason::RpcValidationFailed("rpc error".to_string());
1531 assert_eq!(reason2.description(), "RPC validation failed: rpc error");
1532
1533 let reason3 = DisabledReason::BalanceCheckFailed("balance error".to_string());
1534 assert_eq!(reason3.description(), "Balance check failed: balance error");
1535
1536 let reason4 = DisabledReason::SequenceSyncFailed("sequence error".to_string());
1537 assert_eq!(
1538 reason4.description(),
1539 "Sequence sync failed: sequence error"
1540 );
1541
1542 let reason5 = DisabledReason::Multiple(vec![
1543 DisabledReason::NonceSyncFailed("error1".to_string()),
1544 DisabledReason::RpcValidationFailed("error2".to_string()),
1545 ]);
1546 assert_eq!(
1547 reason5.description(),
1548 "Nonce sync failed: error1, RPC validation failed: error2"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_disabled_reason_display() {
1554 let reason = DisabledReason::NonceSyncFailed("test error".to_string());
1555 assert_eq!(reason.to_string(), "Nonce sync failed: test error");
1556 }
1557
1558 #[test]
1559 fn test_disabled_reason_from_error_string_nonce() {
1560 let reason = DisabledReason::from_error_string("Failed to sync nonce".to_string());
1561 assert!(matches!(reason, DisabledReason::NonceSyncFailed(_)));
1562 }
1563
1564 #[test]
1565 fn test_disabled_reason_from_error_string_rpc() {
1566 let reason = DisabledReason::from_error_string("RPC endpoint unreachable".to_string());
1567 assert!(matches!(reason, DisabledReason::RpcValidationFailed(_)));
1568 }
1569
1570 #[test]
1571 fn test_disabled_reason_from_error_string_balance() {
1572 let reason = DisabledReason::from_error_string("Insufficient balance detected".to_string());
1573 assert!(matches!(reason, DisabledReason::BalanceCheckFailed(_)));
1574 }
1575
1576 #[test]
1577 fn test_disabled_reason_from_error_string_sequence() {
1578 let reason = DisabledReason::from_error_string("Sequence number mismatch".to_string());
1579 assert!(matches!(reason, DisabledReason::SequenceSyncFailed(_)));
1580 }
1581
1582 #[test]
1583 fn test_disabled_reason_from_error_string_unknown() {
1584 let reason = DisabledReason::from_error_string("Unknown error occurred".to_string());
1585 assert!(matches!(reason, DisabledReason::RpcValidationFailed(_)));
1587 }
1588
1589 #[test]
1592 fn test_relayer_network_type_display() {
1593 assert_eq!(RelayerNetworkType::Evm.to_string(), "evm");
1594 assert_eq!(RelayerNetworkType::Solana.to_string(), "solana");
1595 assert_eq!(RelayerNetworkType::Stellar.to_string(), "stellar");
1596 }
1597
1598 #[test]
1599 fn test_relayer_network_type_from_config_file_type() {
1600 assert_eq!(
1601 RelayerNetworkType::from(ConfigFileNetworkType::Evm),
1602 RelayerNetworkType::Evm
1603 );
1604 assert_eq!(
1605 RelayerNetworkType::from(ConfigFileNetworkType::Solana),
1606 RelayerNetworkType::Solana
1607 );
1608 assert_eq!(
1609 RelayerNetworkType::from(ConfigFileNetworkType::Stellar),
1610 RelayerNetworkType::Stellar
1611 );
1612 }
1613
1614 #[test]
1615 fn test_config_file_network_type_from_relayer_type() {
1616 assert_eq!(
1617 ConfigFileNetworkType::from(RelayerNetworkType::Evm),
1618 ConfigFileNetworkType::Evm
1619 );
1620 assert_eq!(
1621 ConfigFileNetworkType::from(RelayerNetworkType::Solana),
1622 ConfigFileNetworkType::Solana
1623 );
1624 assert_eq!(
1625 ConfigFileNetworkType::from(RelayerNetworkType::Stellar),
1626 ConfigFileNetworkType::Stellar
1627 );
1628 }
1629
1630 #[test]
1631 fn test_relayer_network_type_serialization() {
1632 let evm_type = RelayerNetworkType::Evm;
1633 let serialized = serde_json::to_string(&evm_type).unwrap();
1634 assert_eq!(serialized, "\"evm\"");
1635
1636 let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1637 assert_eq!(deserialized, RelayerNetworkType::Evm);
1638
1639 let types = vec![
1641 (RelayerNetworkType::Evm, "\"evm\""),
1642 (RelayerNetworkType::Solana, "\"solana\""),
1643 (RelayerNetworkType::Stellar, "\"stellar\""),
1644 ];
1645
1646 for (network_type, expected_json) in types {
1647 let serialized = serde_json::to_string(&network_type).unwrap();
1648 assert_eq!(serialized, expected_json);
1649
1650 let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1651 assert_eq!(deserialized, network_type);
1652 }
1653 }
1654
1655 #[test]
1658 fn test_relayer_evm_policy_default() {
1659 let default_policy = RelayerEvmPolicy::default();
1660 assert_eq!(default_policy.min_balance, None);
1661 assert_eq!(default_policy.gas_limit_estimation, None);
1662 assert_eq!(default_policy.gas_price_cap, None);
1663 assert_eq!(default_policy.whitelist_receivers, None);
1664 assert_eq!(default_policy.eip1559_pricing, None);
1665 assert_eq!(default_policy.private_transactions, None);
1666 }
1667
1668 #[test]
1669 fn test_relayer_evm_policy_serialization() {
1670 let policy = RelayerEvmPolicy {
1671 include_revert_data: None,
1672 min_balance: Some(1000000000000000000),
1673 gas_limit_estimation: Some(true),
1674 gas_price_cap: Some(50000000000),
1675 whitelist_receivers: Some(vec!["0x123".to_string(), "0x456".to_string()]),
1676 eip1559_pricing: Some(false),
1677 private_transactions: Some(true),
1678 };
1679
1680 let serialized = serde_json::to_string(&policy).unwrap();
1681 let deserialized: RelayerEvmPolicy = serde_json::from_str(&serialized).unwrap();
1682 assert_eq!(policy, deserialized);
1683 }
1684
1685 #[test]
1686 fn test_allowed_token_new() {
1687 let token = SolanaAllowedTokensPolicy::new(
1688 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1689 Some(100000),
1690 None,
1691 );
1692
1693 assert_eq!(token.mint, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
1694 assert_eq!(token.max_allowed_fee, Some(100000));
1695 assert_eq!(token.decimals, None);
1696 assert_eq!(token.symbol, None);
1697 assert_eq!(token.swap_config, None);
1698 }
1699
1700 #[test]
1701 fn test_allowed_token_new_partial() {
1702 let swap_config = SolanaAllowedTokensSwapConfig {
1703 slippage_percentage: Some(0.5),
1704 min_amount: Some(1000),
1705 max_amount: Some(10000000),
1706 retain_min_amount: Some(500),
1707 };
1708
1709 let token = SolanaAllowedTokensPolicy::new_partial(
1710 "TokenMint123".to_string(),
1711 Some(50000),
1712 Some(swap_config.clone()),
1713 );
1714
1715 assert_eq!(token.mint, "TokenMint123");
1716 assert_eq!(token.max_allowed_fee, Some(50000));
1717 assert_eq!(token.swap_config, Some(swap_config));
1718 }
1719
1720 #[test]
1721 fn test_allowed_token_swap_config_default() {
1722 let config = AllowedTokenSwapConfig::default();
1723 assert_eq!(config.slippage_percentage, None);
1724 assert_eq!(config.min_amount, None);
1725 assert_eq!(config.max_amount, None);
1726 assert_eq!(config.retain_min_amount, None);
1727 }
1728
1729 #[test]
1730 fn test_relayer_solana_fee_payment_strategy_default() {
1731 let default_strategy = SolanaFeePaymentStrategy::default();
1732 assert_eq!(default_strategy, SolanaFeePaymentStrategy::User);
1733 }
1734
1735 #[test]
1736 fn test_relayer_solana_swap_strategy_default() {
1737 let default_strategy = SolanaSwapStrategy::default();
1738 assert_eq!(default_strategy, SolanaSwapStrategy::Noop);
1739 }
1740
1741 #[test]
1742 fn test_jupiter_swap_options_default() {
1743 let options = JupiterSwapOptions::default();
1744 assert_eq!(options.priority_fee_max_lamports, None);
1745 assert_eq!(options.priority_level, None);
1746 assert_eq!(options.dynamic_compute_unit_limit, None);
1747 }
1748
1749 #[test]
1750 fn test_relayer_solana_swap_policy_default() {
1751 let policy = RelayerSolanaSwapConfig::default();
1752 assert_eq!(policy.strategy, None);
1753 assert_eq!(policy.cron_schedule, None);
1754 assert_eq!(policy.min_balance_threshold, None);
1755 assert_eq!(policy.jupiter_swap_options, None);
1756 }
1757
1758 #[test]
1759 fn test_relayer_solana_policy_default() {
1760 let policy = RelayerSolanaPolicy::default();
1761 assert_eq!(policy.allowed_programs, None);
1762 assert_eq!(policy.max_signatures, None);
1763 assert_eq!(policy.max_tx_data_size, None);
1764 assert_eq!(policy.min_balance, None);
1765 assert_eq!(policy.allowed_tokens, None);
1766 assert_eq!(policy.fee_payment_strategy, None);
1767 assert_eq!(policy.fee_margin_percentage, None);
1768 assert_eq!(policy.allowed_accounts, None);
1769 assert_eq!(policy.disallowed_accounts, None);
1770 assert_eq!(policy.max_allowed_fee_lamports, None);
1771 assert_eq!(policy.swap_config, None);
1772 }
1773
1774 #[test]
1775 fn test_relayer_solana_policy_get_allowed_tokens() {
1776 let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1777 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1778
1779 let policy = RelayerSolanaPolicy {
1780 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1781 ..RelayerSolanaPolicy::default()
1782 };
1783
1784 let tokens = policy.get_allowed_tokens();
1785 assert_eq!(tokens.len(), 2);
1786 assert_eq!(tokens[0], token1);
1787 assert_eq!(tokens[1], token2);
1788
1789 let empty_policy = RelayerSolanaPolicy::default();
1791 let empty_tokens = empty_policy.get_allowed_tokens();
1792 assert_eq!(empty_tokens.len(), 0);
1793 }
1794
1795 #[test]
1796 fn test_relayer_solana_policy_get_allowed_token_entry() {
1797 let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1798 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1799
1800 let policy = RelayerSolanaPolicy {
1801 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1802 ..RelayerSolanaPolicy::default()
1803 };
1804
1805 let found_token = policy.get_allowed_token_entry("mint1").unwrap();
1806 assert_eq!(found_token, token1);
1807
1808 let not_found = policy.get_allowed_token_entry("mint3");
1809 assert!(not_found.is_none());
1810
1811 let empty_policy = RelayerSolanaPolicy::default();
1813 let empty_result = empty_policy.get_allowed_token_entry("mint1");
1814 assert!(empty_result.is_none());
1815 }
1816
1817 #[test]
1818 fn test_relayer_solana_policy_get_swap_config() {
1819 let swap_config = RelayerSolanaSwapConfig {
1820 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1821 cron_schedule: Some("0 0 * * *".to_string()),
1822 min_balance_threshold: Some(1000000),
1823 jupiter_swap_options: None,
1824 };
1825
1826 let policy = RelayerSolanaPolicy {
1827 swap_config: Some(swap_config.clone()),
1828 ..RelayerSolanaPolicy::default()
1829 };
1830
1831 let retrieved_config = policy.get_swap_config().unwrap();
1832 assert_eq!(retrieved_config, swap_config);
1833
1834 let empty_policy = RelayerSolanaPolicy::default();
1836 assert!(empty_policy.get_swap_config().is_none());
1837 }
1838
1839 #[test]
1840 fn test_relayer_solana_policy_get_allowed_token_decimals() {
1841 let mut token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1842 token1.decimals = Some(9);
1843
1844 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1845 let policy = RelayerSolanaPolicy {
1848 allowed_tokens: Some(vec![token1, token2]),
1849 ..RelayerSolanaPolicy::default()
1850 };
1851
1852 assert_eq!(policy.get_allowed_token_decimals("mint1"), Some(9));
1853 assert_eq!(policy.get_allowed_token_decimals("mint2"), None);
1854 assert_eq!(policy.get_allowed_token_decimals("mint3"), None);
1855 }
1856
1857 #[test]
1858 fn test_relayer_stellar_policy_default() {
1859 let policy = RelayerStellarPolicy::default();
1860 assert_eq!(policy.min_balance, None);
1861 assert_eq!(policy.max_fee, None);
1862 assert_eq!(policy.timeout_seconds, None);
1863 assert_eq!(policy.concurrent_transactions, None);
1864 assert_eq!(policy.allowed_tokens, None);
1865 assert_eq!(policy.fee_payment_strategy, None);
1866 assert_eq!(policy.slippage_percentage, None);
1867 assert_eq!(policy.fee_margin_percentage, None);
1868 assert_eq!(policy.swap_config, None);
1869 }
1870
1871 #[test]
1872 fn test_stellar_allowed_tokens_policy_new() {
1873 let metadata = StellarTokenMetadata {
1874 kind: StellarTokenKind::Native,
1875 decimals: 7,
1876 canonical_asset_id: "native".to_string(),
1877 };
1878
1879 let swap_config = StellarAllowedTokensSwapConfig {
1880 slippage_percentage: Some(0.5),
1881 min_amount: Some(1000),
1882 max_amount: Some(10000000),
1883 retain_min_amount: Some(500),
1884 };
1885
1886 let token = StellarAllowedTokensPolicy::new(
1887 "native".to_string(),
1888 Some(metadata.clone()),
1889 Some(100000),
1890 Some(swap_config.clone()),
1891 );
1892
1893 assert_eq!(token.asset, "native");
1894 assert_eq!(token.metadata, Some(metadata));
1895 assert_eq!(token.max_allowed_fee, Some(100000));
1896 assert_eq!(token.swap_config, Some(swap_config));
1897 }
1898
1899 #[test]
1900 fn test_relayer_stellar_policy_get_allowed_tokens() {
1901 let token1 = StellarAllowedTokensPolicy::new("native".to_string(), None, Some(1000), None);
1902 let token2 = StellarAllowedTokensPolicy::new(
1903 "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN".to_string(),
1904 None,
1905 Some(2000),
1906 None,
1907 );
1908
1909 let policy = RelayerStellarPolicy {
1910 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1911 ..RelayerStellarPolicy::default()
1912 };
1913
1914 let tokens = policy.get_allowed_tokens();
1915 assert_eq!(tokens.len(), 2);
1916 assert_eq!(tokens[0], token1);
1917 assert_eq!(tokens[1], token2);
1918
1919 let empty_policy = RelayerStellarPolicy::default();
1921 let empty_tokens = empty_policy.get_allowed_tokens();
1922 assert_eq!(empty_tokens.len(), 0);
1923 }
1924
1925 #[test]
1926 fn test_relayer_stellar_policy_get_allowed_token_entry() {
1927 let token1 = StellarAllowedTokensPolicy::new("native".to_string(), None, Some(1000), None);
1928 let token2 = StellarAllowedTokensPolicy::new(
1929 "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN".to_string(),
1930 None,
1931 Some(2000),
1932 None,
1933 );
1934
1935 let policy = RelayerStellarPolicy {
1936 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1937 ..RelayerStellarPolicy::default()
1938 };
1939
1940 let found_token = policy.get_allowed_token_entry("native").unwrap();
1941 assert_eq!(found_token, token1);
1942
1943 let not_found = policy.get_allowed_token_entry(
1944 "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2",
1945 );
1946 assert!(not_found.is_none());
1947
1948 let empty_policy = RelayerStellarPolicy::default();
1950 let empty_result = empty_policy.get_allowed_token_entry("native");
1951 assert!(empty_result.is_none());
1952 }
1953
1954 #[test]
1955 fn test_relayer_stellar_policy_get_allowed_token_decimals() {
1956 let metadata1 = StellarTokenMetadata {
1957 kind: StellarTokenKind::Native,
1958 decimals: 7,
1959 canonical_asset_id: "native".to_string(),
1960 };
1961
1962 let metadata2 = StellarTokenMetadata {
1963 kind: StellarTokenKind::Classic {
1964 code: "USDC".to_string(),
1965 issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN".to_string(),
1966 },
1967 decimals: 6,
1968 canonical_asset_id: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
1969 .to_string(),
1970 };
1971
1972 let token1 = StellarAllowedTokensPolicy::new(
1973 "native".to_string(),
1974 Some(metadata1),
1975 Some(1000),
1976 None,
1977 );
1978 let token2 = StellarAllowedTokensPolicy::new(
1979 "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN".to_string(),
1980 Some(metadata2),
1981 Some(2000),
1982 None,
1983 );
1984 let token3 = StellarAllowedTokensPolicy::new(
1985 "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2".to_string(),
1986 None,
1987 Some(3000),
1988 None,
1989 );
1990
1991 let policy = RelayerStellarPolicy {
1992 allowed_tokens: Some(vec![token1, token2, token3]),
1993 ..RelayerStellarPolicy::default()
1994 };
1995
1996 assert_eq!(policy.get_allowed_token_decimals("native"), Some(7));
1997 assert_eq!(
1998 policy.get_allowed_token_decimals(
1999 "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
2000 ),
2001 Some(6)
2002 );
2003 assert_eq!(
2004 policy.get_allowed_token_decimals(
2005 "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2"
2006 ),
2007 None
2008 );
2009 assert_eq!(policy.get_allowed_token_decimals("unknown"), None);
2010 }
2011
2012 #[test]
2013 fn test_relayer_stellar_policy_get_swap_config() {
2014 let swap_config = RelayerStellarSwapConfig {
2015 strategies: vec![StellarSwapStrategy::OrderBook],
2016 cron_schedule: Some("0 0 * * *".to_string()),
2017 min_balance_threshold: Some(1000000),
2018 };
2019
2020 let policy = RelayerStellarPolicy {
2021 swap_config: Some(swap_config.clone()),
2022 ..RelayerStellarPolicy::default()
2023 };
2024
2025 let retrieved_config = policy.get_swap_config().unwrap();
2026 assert_eq!(retrieved_config, swap_config);
2027
2028 let empty_policy = RelayerStellarPolicy::default();
2030 assert!(empty_policy.get_swap_config().is_none());
2031 }
2032
2033 #[test]
2036 fn test_relayer_network_policy_get_evm_policy() {
2037 let evm_policy = RelayerEvmPolicy {
2038 gas_price_cap: Some(50000000000),
2039 ..RelayerEvmPolicy::default()
2040 };
2041
2042 let network_policy = RelayerNetworkPolicy::Evm(evm_policy.clone());
2043 assert_eq!(network_policy.get_evm_policy(), evm_policy);
2044
2045 let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
2047 assert_eq!(solana_policy.get_evm_policy(), RelayerEvmPolicy::default());
2048
2049 let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
2050 assert_eq!(stellar_policy.get_evm_policy(), RelayerEvmPolicy::default());
2051 }
2052
2053 #[test]
2054 fn test_relayer_network_policy_get_solana_policy() {
2055 let solana_policy = RelayerSolanaPolicy {
2056 min_balance: Some(5000000),
2057 ..RelayerSolanaPolicy::default()
2058 };
2059
2060 let network_policy = RelayerNetworkPolicy::Solana(solana_policy.clone());
2061 assert_eq!(network_policy.get_solana_policy(), solana_policy);
2062
2063 let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
2065 assert_eq!(
2066 evm_policy.get_solana_policy(),
2067 RelayerSolanaPolicy::default()
2068 );
2069
2070 let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
2071 assert_eq!(
2072 stellar_policy.get_solana_policy(),
2073 RelayerSolanaPolicy::default()
2074 );
2075 }
2076
2077 #[test]
2078 fn test_relayer_network_policy_get_stellar_policy() {
2079 let stellar_policy = RelayerStellarPolicy {
2080 min_balance: Some(20000000),
2081 max_fee: Some(100000),
2082 timeout_seconds: Some(30),
2083 concurrent_transactions: None,
2084 allowed_tokens: None,
2085 fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
2086 slippage_percentage: None,
2087 fee_margin_percentage: None,
2088 swap_config: None,
2089 };
2090
2091 let network_policy = RelayerNetworkPolicy::Stellar(stellar_policy.clone());
2092 assert_eq!(network_policy.get_stellar_policy(), stellar_policy);
2093
2094 let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
2096 assert_eq!(
2097 evm_policy.get_stellar_policy(),
2098 RelayerStellarPolicy::default()
2099 );
2100
2101 let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
2102 assert_eq!(
2103 solana_policy.get_stellar_policy(),
2104 RelayerStellarPolicy::default()
2105 );
2106 }
2107
2108 #[test]
2111 fn test_relayer_new() {
2112 let relayer = Relayer::new(
2113 "test-relayer".to_string(),
2114 "Test Relayer".to_string(),
2115 "mainnet".to_string(),
2116 false,
2117 RelayerNetworkType::Evm,
2118 Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
2119 "test-signer".to_string(),
2120 Some("test-notification".to_string()),
2121 None,
2122 );
2123
2124 assert_eq!(relayer.id, "test-relayer");
2125 assert_eq!(relayer.name, "Test Relayer");
2126 assert_eq!(relayer.network, "mainnet");
2127 assert!(!relayer.paused);
2128 assert_eq!(relayer.network_type, RelayerNetworkType::Evm);
2129 assert_eq!(relayer.signer_id, "test-signer");
2130 assert_eq!(
2131 relayer.notification_id,
2132 Some("test-notification".to_string())
2133 );
2134 assert!(relayer.policies.is_some());
2135 assert_eq!(relayer.custom_rpc_urls, None);
2136 }
2137
2138 #[test]
2141 fn test_relayer_validation_success() {
2142 let relayer = Relayer::new(
2143 "valid-relayer-id".to_string(),
2144 "Valid Relayer".to_string(),
2145 "mainnet".to_string(),
2146 false,
2147 RelayerNetworkType::Evm,
2148 None,
2149 "valid-signer".to_string(),
2150 None,
2151 None,
2152 );
2153
2154 assert!(relayer.validate().is_ok());
2155 }
2156
2157 #[test]
2158 fn test_relayer_validation_empty_id() {
2159 let relayer = Relayer::new(
2160 "".to_string(), "Valid Relayer".to_string(),
2162 "mainnet".to_string(),
2163 false,
2164 RelayerNetworkType::Evm,
2165 None,
2166 "valid-signer".to_string(),
2167 None,
2168 None,
2169 );
2170
2171 let result = relayer.validate();
2172 assert!(result.is_err());
2173 assert!(matches!(
2174 result.unwrap_err(),
2175 RelayerValidationError::EmptyId
2176 ));
2177 }
2178
2179 #[test]
2180 fn test_relayer_validation_id_too_long() {
2181 let long_id = "a".repeat(37); let relayer = Relayer::new(
2183 long_id,
2184 "Valid Relayer".to_string(),
2185 "mainnet".to_string(),
2186 false,
2187 RelayerNetworkType::Evm,
2188 None,
2189 "valid-signer".to_string(),
2190 None,
2191 None,
2192 );
2193
2194 let result = relayer.validate();
2195 assert!(result.is_err());
2196 assert!(matches!(
2197 result.unwrap_err(),
2198 RelayerValidationError::IdTooLong
2199 ));
2200 }
2201
2202 #[test]
2203 fn test_relayer_validation_invalid_id_format() {
2204 let relayer = Relayer::new(
2205 "invalid@id".to_string(), "Valid Relayer".to_string(),
2207 "mainnet".to_string(),
2208 false,
2209 RelayerNetworkType::Evm,
2210 None,
2211 "valid-signer".to_string(),
2212 None,
2213 None,
2214 );
2215
2216 let result = relayer.validate();
2217 assert!(result.is_err());
2218 assert!(matches!(
2219 result.unwrap_err(),
2220 RelayerValidationError::InvalidIdFormat
2221 ));
2222 }
2223
2224 #[test]
2225 fn test_relayer_validation_empty_name() {
2226 let relayer = Relayer::new(
2227 "valid-id".to_string(),
2228 "".to_string(), "mainnet".to_string(),
2230 false,
2231 RelayerNetworkType::Evm,
2232 None,
2233 "valid-signer".to_string(),
2234 None,
2235 None,
2236 );
2237
2238 let result = relayer.validate();
2239 assert!(result.is_err());
2240 assert!(matches!(
2241 result.unwrap_err(),
2242 RelayerValidationError::EmptyName
2243 ));
2244 }
2245
2246 #[test]
2247 fn test_relayer_validation_empty_network() {
2248 let relayer = Relayer::new(
2249 "valid-id".to_string(),
2250 "Valid Relayer".to_string(),
2251 "".to_string(), false,
2253 RelayerNetworkType::Evm,
2254 None,
2255 "valid-signer".to_string(),
2256 None,
2257 None,
2258 );
2259
2260 let result = relayer.validate();
2261 assert!(result.is_err());
2262 assert!(matches!(
2263 result.unwrap_err(),
2264 RelayerValidationError::EmptyNetwork
2265 ));
2266 }
2267
2268 #[test]
2269 fn test_relayer_validation_empty_signer_id() {
2270 let relayer = Relayer::new(
2271 "valid-id".to_string(),
2272 "Valid Relayer".to_string(),
2273 "mainnet".to_string(),
2274 false,
2275 RelayerNetworkType::Evm,
2276 None,
2277 "".to_string(), None,
2279 None,
2280 );
2281
2282 let result = relayer.validate();
2283 assert!(result.is_err());
2284 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2286 assert!(msg.contains("Signer ID cannot be empty"));
2287 } else {
2288 panic!("Expected InvalidPolicy error for empty signer ID");
2289 }
2290 }
2291
2292 #[test]
2293 fn test_relayer_validation_mismatched_network_type_and_policy() {
2294 let relayer = Relayer::new(
2295 "valid-id".to_string(),
2296 "Valid Relayer".to_string(),
2297 "mainnet".to_string(),
2298 false,
2299 RelayerNetworkType::Evm, Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default())), "valid-signer".to_string(),
2302 None,
2303 None,
2304 );
2305
2306 let result = relayer.validate();
2307 assert!(result.is_err());
2308 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2309 assert!(msg.contains("Network type") && msg.contains("does not match policy type"));
2310 } else {
2311 panic!("Expected InvalidPolicy error for mismatched network type and policy");
2312 }
2313 }
2314
2315 #[test]
2316 fn test_relayer_validation_invalid_rpc_url() {
2317 let relayer = Relayer::new(
2318 "valid-id".to_string(),
2319 "Valid Relayer".to_string(),
2320 "mainnet".to_string(),
2321 false,
2322 RelayerNetworkType::Evm,
2323 None,
2324 "valid-signer".to_string(),
2325 None,
2326 Some(vec![RpcConfig::new("invalid-url".to_string())]), );
2328
2329 let result = relayer.validate();
2330 assert!(result.is_err());
2331 assert!(matches!(
2332 result.unwrap_err(),
2333 RelayerValidationError::InvalidRpcUrl(_)
2334 ));
2335 }
2336
2337 #[test]
2338 fn test_relayer_validation_invalid_rpc_weight() {
2339 let relayer = Relayer::new(
2340 "valid-id".to_string(),
2341 "Valid Relayer".to_string(),
2342 "mainnet".to_string(),
2343 false,
2344 RelayerNetworkType::Evm,
2345 None,
2346 "valid-signer".to_string(),
2347 None,
2348 Some(vec![RpcConfig {
2349 url: "https://example.com".to_string(),
2350 weight: 150,
2351 ..Default::default()
2352 }]), );
2354
2355 let result = relayer.validate();
2356 assert!(result.is_err());
2357 assert!(matches!(
2358 result.unwrap_err(),
2359 RelayerValidationError::InvalidRpcWeight
2360 ));
2361 }
2362
2363 #[test]
2366 fn test_relayer_validation_solana_invalid_public_key() {
2367 let policy = RelayerSolanaPolicy {
2368 allowed_programs: Some(vec!["invalid-pubkey".to_string()]), ..RelayerSolanaPolicy::default()
2370 };
2371
2372 let relayer = Relayer::new(
2373 "valid-id".to_string(),
2374 "Valid Relayer".to_string(),
2375 "mainnet".to_string(),
2376 false,
2377 RelayerNetworkType::Solana,
2378 Some(RelayerNetworkPolicy::Solana(policy)),
2379 "valid-signer".to_string(),
2380 None,
2381 None,
2382 );
2383
2384 let result = relayer.validate();
2385 assert!(result.is_err());
2386 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2387 assert!(msg.contains("Public key must be a valid Solana address"));
2388 } else {
2389 panic!("Expected InvalidPolicy error for invalid Solana public key");
2390 }
2391 }
2392
2393 #[test]
2394 fn test_relayer_validation_solana_valid_public_key() {
2395 let policy = RelayerSolanaPolicy {
2396 allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]), ..RelayerSolanaPolicy::default()
2398 };
2399
2400 let relayer = Relayer::new(
2401 "valid-id".to_string(),
2402 "Valid Relayer".to_string(),
2403 "mainnet".to_string(),
2404 false,
2405 RelayerNetworkType::Solana,
2406 Some(RelayerNetworkPolicy::Solana(policy)),
2407 "valid-signer".to_string(),
2408 None,
2409 None,
2410 );
2411
2412 assert!(relayer.validate().is_ok());
2413 }
2414
2415 #[test]
2416 fn test_relayer_validation_solana_negative_fee_margin() {
2417 let policy = RelayerSolanaPolicy {
2418 fee_margin_percentage: Some(-1.0), ..RelayerSolanaPolicy::default()
2420 };
2421
2422 let relayer = Relayer::new(
2423 "valid-id".to_string(),
2424 "Valid Relayer".to_string(),
2425 "mainnet".to_string(),
2426 false,
2427 RelayerNetworkType::Solana,
2428 Some(RelayerNetworkPolicy::Solana(policy)),
2429 "valid-signer".to_string(),
2430 None,
2431 None,
2432 );
2433
2434 let result = relayer.validate();
2435 assert!(result.is_err());
2436 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2437 assert!(msg.contains("Negative fee margin percentage values are not accepted"));
2438 } else {
2439 panic!("Expected InvalidPolicy error for negative fee margin");
2440 }
2441 }
2442
2443 #[test]
2444 fn test_relayer_validation_solana_conflicting_accounts() {
2445 let policy = RelayerSolanaPolicy {
2446 allowed_accounts: Some(vec!["11111111111111111111111111111111".to_string()]),
2447 disallowed_accounts: Some(vec!["22222222222222222222222222222222".to_string()]),
2448 ..RelayerSolanaPolicy::default()
2449 };
2450
2451 let relayer = Relayer::new(
2452 "valid-id".to_string(),
2453 "Valid Relayer".to_string(),
2454 "mainnet".to_string(),
2455 false,
2456 RelayerNetworkType::Solana,
2457 Some(RelayerNetworkPolicy::Solana(policy)),
2458 "valid-signer".to_string(),
2459 None,
2460 None,
2461 );
2462
2463 let result = relayer.validate();
2464 assert!(result.is_err());
2465 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2466 assert!(msg.contains("allowed_accounts and disallowed_accounts cannot be both present"));
2467 } else {
2468 panic!("Expected InvalidPolicy error for conflicting accounts");
2469 }
2470 }
2471
2472 #[test]
2473 fn test_relayer_validation_solana_swap_config_wrong_fee_payment_strategy() {
2474 let swap_config = RelayerSolanaSwapConfig {
2475 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2476 ..RelayerSolanaSwapConfig::default()
2477 };
2478
2479 let policy = RelayerSolanaPolicy {
2480 fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer), swap_config: Some(swap_config), ..RelayerSolanaPolicy::default()
2483 };
2484
2485 let relayer = Relayer::new(
2486 "valid-id".to_string(),
2487 "Valid Relayer".to_string(),
2488 "mainnet".to_string(),
2489 false,
2490 RelayerNetworkType::Solana,
2491 Some(RelayerNetworkPolicy::Solana(policy)),
2492 "valid-signer".to_string(),
2493 None,
2494 None,
2495 );
2496
2497 let result = relayer.validate();
2498 assert!(result.is_err());
2499 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2500 assert!(msg.contains("Swap config only supported for user fee payment strategy"));
2501 } else {
2502 panic!("Expected InvalidPolicy error for swap config with relayer fee payment");
2503 }
2504 }
2505
2506 #[test]
2507 fn test_relayer_validation_solana_jupiter_strategy_wrong_network() {
2508 let swap_config = RelayerSolanaSwapConfig {
2509 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2510 ..RelayerSolanaSwapConfig::default()
2511 };
2512
2513 let policy = RelayerSolanaPolicy {
2514 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2515 swap_config: Some(swap_config),
2516 ..RelayerSolanaPolicy::default()
2517 };
2518
2519 let relayer = Relayer::new(
2520 "valid-id".to_string(),
2521 "Valid Relayer".to_string(),
2522 "testnet".to_string(), false,
2524 RelayerNetworkType::Solana,
2525 Some(RelayerNetworkPolicy::Solana(policy)),
2526 "valid-signer".to_string(),
2527 None,
2528 None,
2529 );
2530
2531 let result = relayer.validate();
2532 assert!(result.is_err());
2533 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2534 assert!(msg.contains("strategy is only supported on mainnet-beta"));
2535 } else {
2536 panic!("Expected InvalidPolicy error for Jupiter strategy on wrong network");
2537 }
2538 }
2539
2540 #[test]
2541 fn test_relayer_validation_solana_empty_cron_schedule() {
2542 let swap_config = RelayerSolanaSwapConfig {
2543 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2544 cron_schedule: Some("".to_string()), ..RelayerSolanaSwapConfig::default()
2546 };
2547
2548 let policy = RelayerSolanaPolicy {
2549 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2550 swap_config: Some(swap_config),
2551 ..RelayerSolanaPolicy::default()
2552 };
2553
2554 let relayer = Relayer::new(
2555 "valid-id".to_string(),
2556 "Valid Relayer".to_string(),
2557 "mainnet-beta".to_string(),
2558 false,
2559 RelayerNetworkType::Solana,
2560 Some(RelayerNetworkPolicy::Solana(policy)),
2561 "valid-signer".to_string(),
2562 None,
2563 None,
2564 );
2565
2566 let result = relayer.validate();
2567 assert!(result.is_err());
2568 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2569 assert!(msg.contains("Empty cron schedule is not accepted"));
2570 } else {
2571 panic!("Expected InvalidPolicy error for empty cron schedule");
2572 }
2573 }
2574
2575 #[test]
2576 fn test_relayer_validation_solana_invalid_cron_schedule() {
2577 let swap_config = RelayerSolanaSwapConfig {
2578 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2579 cron_schedule: Some("invalid cron".to_string()), ..RelayerSolanaSwapConfig::default()
2581 };
2582
2583 let policy = RelayerSolanaPolicy {
2584 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2585 swap_config: Some(swap_config),
2586 ..RelayerSolanaPolicy::default()
2587 };
2588
2589 let relayer = Relayer::new(
2590 "valid-id".to_string(),
2591 "Valid Relayer".to_string(),
2592 "mainnet-beta".to_string(),
2593 false,
2594 RelayerNetworkType::Solana,
2595 Some(RelayerNetworkPolicy::Solana(policy)),
2596 "valid-signer".to_string(),
2597 None,
2598 None,
2599 );
2600
2601 let result = relayer.validate();
2602 assert!(result.is_err());
2603 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2604 assert!(msg.contains("Invalid cron schedule format"));
2605 } else {
2606 panic!("Expected InvalidPolicy error for invalid cron schedule");
2607 }
2608 }
2609
2610 #[test]
2611 fn test_relayer_validation_solana_jupiter_options_wrong_strategy() {
2612 let jupiter_options = JupiterSwapOptions {
2613 priority_fee_max_lamports: Some(10000),
2614 priority_level: Some("high".to_string()),
2615 dynamic_compute_unit_limit: Some(true),
2616 };
2617
2618 let swap_config = RelayerSolanaSwapConfig {
2619 strategy: Some(SolanaSwapStrategy::JupiterUltra), jupiter_swap_options: Some(jupiter_options),
2621 ..RelayerSolanaSwapConfig::default()
2622 };
2623
2624 let policy = RelayerSolanaPolicy {
2625 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2626 swap_config: Some(swap_config),
2627 ..RelayerSolanaPolicy::default()
2628 };
2629
2630 let relayer = Relayer::new(
2631 "valid-id".to_string(),
2632 "Valid Relayer".to_string(),
2633 "mainnet-beta".to_string(),
2634 false,
2635 RelayerNetworkType::Solana,
2636 Some(RelayerNetworkPolicy::Solana(policy)),
2637 "valid-signer".to_string(),
2638 None,
2639 None,
2640 );
2641
2642 let result = relayer.validate();
2643 assert!(result.is_err());
2644 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2645 assert!(msg.contains("JupiterSwap options are only valid for JupiterSwap strategy"));
2646 } else {
2647 panic!("Expected InvalidPolicy error for Jupiter options with wrong strategy");
2648 }
2649 }
2650
2651 #[test]
2652 fn test_relayer_validation_solana_jupiter_zero_max_lamports() {
2653 let jupiter_options = JupiterSwapOptions {
2654 priority_fee_max_lamports: Some(0), priority_level: Some("high".to_string()),
2656 dynamic_compute_unit_limit: Some(true),
2657 };
2658
2659 let swap_config = RelayerSolanaSwapConfig {
2660 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2661 jupiter_swap_options: Some(jupiter_options),
2662 ..RelayerSolanaSwapConfig::default()
2663 };
2664
2665 let policy = RelayerSolanaPolicy {
2666 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2667 swap_config: Some(swap_config),
2668 ..RelayerSolanaPolicy::default()
2669 };
2670
2671 let relayer = Relayer::new(
2672 "valid-id".to_string(),
2673 "Valid Relayer".to_string(),
2674 "mainnet-beta".to_string(),
2675 false,
2676 RelayerNetworkType::Solana,
2677 Some(RelayerNetworkPolicy::Solana(policy)),
2678 "valid-signer".to_string(),
2679 None,
2680 None,
2681 );
2682
2683 let result = relayer.validate();
2684 assert!(result.is_err());
2685 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2686 assert!(msg.contains("Max lamports must be greater than 0"));
2687 } else {
2688 panic!("Expected InvalidPolicy error for zero max lamports");
2689 }
2690 }
2691
2692 #[test]
2693 fn test_relayer_validation_solana_jupiter_empty_priority_level() {
2694 let jupiter_options = JupiterSwapOptions {
2695 priority_fee_max_lamports: Some(10000),
2696 priority_level: Some("".to_string()), dynamic_compute_unit_limit: Some(true),
2698 };
2699
2700 let swap_config = RelayerSolanaSwapConfig {
2701 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2702 jupiter_swap_options: Some(jupiter_options),
2703 ..RelayerSolanaSwapConfig::default()
2704 };
2705
2706 let policy = RelayerSolanaPolicy {
2707 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2708 swap_config: Some(swap_config),
2709 ..RelayerSolanaPolicy::default()
2710 };
2711
2712 let relayer = Relayer::new(
2713 "valid-id".to_string(),
2714 "Valid Relayer".to_string(),
2715 "mainnet-beta".to_string(),
2716 false,
2717 RelayerNetworkType::Solana,
2718 Some(RelayerNetworkPolicy::Solana(policy)),
2719 "valid-signer".to_string(),
2720 None,
2721 None,
2722 );
2723
2724 let result = relayer.validate();
2725 assert!(result.is_err());
2726 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2727 assert!(msg.contains("Priority level cannot be empty"));
2728 } else {
2729 panic!("Expected InvalidPolicy error for empty priority level");
2730 }
2731 }
2732
2733 #[test]
2734 fn test_relayer_validation_solana_jupiter_invalid_priority_level() {
2735 let jupiter_options = JupiterSwapOptions {
2736 priority_fee_max_lamports: Some(10000),
2737 priority_level: Some("invalid".to_string()), dynamic_compute_unit_limit: Some(true),
2739 };
2740
2741 let swap_config = RelayerSolanaSwapConfig {
2742 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2743 jupiter_swap_options: Some(jupiter_options),
2744 ..RelayerSolanaSwapConfig::default()
2745 };
2746
2747 let policy = RelayerSolanaPolicy {
2748 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2749 swap_config: Some(swap_config),
2750 ..RelayerSolanaPolicy::default()
2751 };
2752
2753 let relayer = Relayer::new(
2754 "valid-id".to_string(),
2755 "Valid Relayer".to_string(),
2756 "mainnet-beta".to_string(),
2757 false,
2758 RelayerNetworkType::Solana,
2759 Some(RelayerNetworkPolicy::Solana(policy)),
2760 "valid-signer".to_string(),
2761 None,
2762 None,
2763 );
2764
2765 let result = relayer.validate();
2766 assert!(result.is_err());
2767 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2768 assert!(msg.contains("Priority level must be one of: medium, high, veryHigh"));
2769 } else {
2770 panic!("Expected InvalidPolicy error for invalid priority level");
2771 }
2772 }
2773
2774 #[test]
2775 fn test_relayer_validation_solana_jupiter_missing_priority_fee() {
2776 let jupiter_options = JupiterSwapOptions {
2777 priority_fee_max_lamports: None, priority_level: Some("high".to_string()),
2779 dynamic_compute_unit_limit: Some(true),
2780 };
2781
2782 let swap_config = RelayerSolanaSwapConfig {
2783 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2784 jupiter_swap_options: Some(jupiter_options),
2785 ..RelayerSolanaSwapConfig::default()
2786 };
2787
2788 let policy = RelayerSolanaPolicy {
2789 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2790 swap_config: Some(swap_config),
2791 ..RelayerSolanaPolicy::default()
2792 };
2793
2794 let relayer = Relayer::new(
2795 "valid-id".to_string(),
2796 "Valid Relayer".to_string(),
2797 "mainnet-beta".to_string(),
2798 false,
2799 RelayerNetworkType::Solana,
2800 Some(RelayerNetworkPolicy::Solana(policy)),
2801 "valid-signer".to_string(),
2802 None,
2803 None,
2804 );
2805
2806 let result = relayer.validate();
2807 assert!(result.is_err());
2808 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2809 assert!(msg.contains("Priority Fee Max lamports must be set if priority level is set"));
2810 } else {
2811 panic!("Expected InvalidPolicy error for missing priority fee");
2812 }
2813 }
2814
2815 #[test]
2816 fn test_relayer_validation_solana_jupiter_missing_priority_level() {
2817 let jupiter_options = JupiterSwapOptions {
2818 priority_fee_max_lamports: Some(10000),
2819 priority_level: None, dynamic_compute_unit_limit: Some(true),
2821 };
2822
2823 let swap_config = RelayerSolanaSwapConfig {
2824 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2825 jupiter_swap_options: Some(jupiter_options),
2826 ..RelayerSolanaSwapConfig::default()
2827 };
2828
2829 let policy = RelayerSolanaPolicy {
2830 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2831 swap_config: Some(swap_config),
2832 ..RelayerSolanaPolicy::default()
2833 };
2834
2835 let relayer = Relayer::new(
2836 "valid-id".to_string(),
2837 "Valid Relayer".to_string(),
2838 "mainnet-beta".to_string(),
2839 false,
2840 RelayerNetworkType::Solana,
2841 Some(RelayerNetworkPolicy::Solana(policy)),
2842 "valid-signer".to_string(),
2843 None,
2844 None,
2845 );
2846
2847 let result = relayer.validate();
2848 assert!(result.is_err());
2849 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2850 assert!(msg.contains("Priority level must be set if priority fee max lamports is set"));
2851 } else {
2852 panic!("Expected InvalidPolicy error for missing priority level");
2853 }
2854 }
2855
2856 #[test]
2859 fn test_relayer_validation_error_to_api_error() {
2860 use crate::models::ApiError;
2861
2862 let errors = vec![
2864 (RelayerValidationError::EmptyId, "ID cannot be empty"),
2865 (RelayerValidationError::InvalidIdFormat, "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long"),
2866 (RelayerValidationError::IdTooLong, "ID must not exceed 36 characters"),
2867 (RelayerValidationError::EmptyName, "Name cannot be empty"),
2868 (RelayerValidationError::EmptyNetwork, "Network cannot be empty"),
2869 (RelayerValidationError::InvalidPolicy("test error".to_string()), "Invalid relayer policy: test error"),
2870 (RelayerValidationError::InvalidRpcUrl("http://invalid".to_string()), "Invalid RPC URL: http://invalid"),
2871 (RelayerValidationError::InvalidRpcWeight, "RPC URL weight must be in range 0-100"),
2872 (RelayerValidationError::InvalidField("test field error".to_string()), "test field error"),
2873 ];
2874
2875 for (validation_error, expected_message) in errors {
2876 let api_error: ApiError = validation_error.into();
2877 if let ApiError::BadRequest(message) = api_error {
2878 assert_eq!(message, expected_message);
2879 } else {
2880 panic!("Expected BadRequest variant");
2881 }
2882 }
2883 }
2884
2885 #[test]
2888 fn test_apply_json_patch_comprehensive() {
2889 let relayer = Relayer {
2891 id: "test-relayer".to_string(),
2892 name: "Original Name".to_string(),
2893 network: "mainnet".to_string(),
2894 paused: false,
2895 network_type: RelayerNetworkType::Evm,
2896 policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
2897 include_revert_data: None,
2898 min_balance: Some(1000000000000000000),
2899 gas_limit_estimation: Some(true),
2900 gas_price_cap: Some(50000000000),
2901 whitelist_receivers: None,
2902 eip1559_pricing: Some(false),
2903 private_transactions: None,
2904 })),
2905 signer_id: "test-signer".to_string(),
2906 notification_id: Some("old-notification".to_string()),
2907 custom_rpc_urls: None,
2908 };
2909
2910 let patch = json!({
2912 "name": "Updated Name via JSON Patch",
2913 "paused": true,
2914 "policies": {
2915 "min_balance": "2000000000000000000",
2916 "gas_price_cap": null, "eip1559_pricing": true, "whitelist_receivers": ["0x123", "0x456"] },
2921 "notification_id": null, "custom_rpc_urls": [{"url": "https://example.com", "weight": 100}]
2923 });
2924
2925 let updated_relayer = relayer.apply_json_patch(&patch).unwrap();
2927
2928 assert_eq!(updated_relayer.name, "Updated Name via JSON Patch");
2930 assert!(updated_relayer.paused);
2931 assert_eq!(updated_relayer.notification_id, None); assert!(updated_relayer.custom_rpc_urls.is_some());
2933
2934 if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = updated_relayer.policies {
2936 assert_eq!(evm_policy.min_balance, Some(2000000000000000000)); assert_eq!(evm_policy.gas_price_cap, None); assert_eq!(evm_policy.eip1559_pricing, Some(true)); assert_eq!(evm_policy.gas_limit_estimation, Some(true)); assert_eq!(
2941 evm_policy.whitelist_receivers,
2942 Some(vec!["0x123".to_string(), "0x456".to_string()])
2943 ); assert_eq!(evm_policy.private_transactions, None); } else {
2946 panic!("Expected EVM policy");
2947 }
2948 }
2949
2950 #[test]
2951 fn test_apply_json_patch_validation_failure() {
2952 let relayer = Relayer {
2953 id: "test-relayer".to_string(),
2954 name: "Original Name".to_string(),
2955 network: "mainnet".to_string(),
2956 paused: false,
2957 network_type: RelayerNetworkType::Evm,
2958 policies: None,
2959 signer_id: "test-signer".to_string(),
2960 notification_id: None,
2961 custom_rpc_urls: None,
2962 };
2963
2964 let invalid_patch = json!({
2966 "name": "" });
2968
2969 let result = relayer.apply_json_patch(&invalid_patch);
2971 assert!(result.is_err());
2972 assert!(result
2973 .unwrap_err()
2974 .to_string()
2975 .contains("Relayer name cannot be empty"));
2976 }
2977
2978 #[test]
2979 fn test_apply_json_patch_invalid_result() {
2980 let relayer = Relayer {
2981 id: "test-relayer".to_string(),
2982 name: "Original Name".to_string(),
2983 network: "mainnet".to_string(),
2984 paused: false,
2985 network_type: RelayerNetworkType::Evm,
2986 policies: None,
2987 signer_id: "test-signer".to_string(),
2988 notification_id: None,
2989 custom_rpc_urls: None,
2990 };
2991
2992 let invalid_patch = json!({
2994 "network_type": "invalid_type" });
2996
2997 let result = relayer.apply_json_patch(&invalid_patch);
2999 assert!(result.is_err());
3000 let error_msg = result.unwrap_err().to_string();
3002 assert!(
3003 error_msg.contains("Invalid patch format")
3004 || error_msg.contains("Invalid result after patch")
3005 );
3006 }
3007}