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