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