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)]
383#[serde(rename_all = "lowercase")]
384pub enum SolanaFeePaymentStrategy {
385 #[default]
386 User,
387 Relayer,
388}
389
390#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
392#[serde(rename_all = "kebab-case")]
393pub enum SolanaSwapStrategy {
394 JupiterSwap,
395 JupiterUltra,
396 #[default]
397 Noop,
398}
399
400#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
402#[serde(deny_unknown_fields)]
403pub struct JupiterSwapOptions {
404 #[schema(nullable = false)]
406 pub priority_fee_max_lamports: Option<u64>,
407 #[schema(nullable = false)]
409 pub priority_level: Option<String>,
410 #[schema(nullable = false)]
411 pub dynamic_compute_unit_limit: Option<bool>,
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
416#[serde(deny_unknown_fields)]
417pub struct RelayerSolanaSwapConfig {
418 #[schema(nullable = false)]
420 pub strategy: Option<SolanaSwapStrategy>,
421 #[schema(nullable = false)]
423 pub cron_schedule: Option<String>,
424 #[schema(nullable = false)]
426 pub min_balance_threshold: Option<u64>,
427 #[schema(nullable = false)]
429 pub jupiter_swap_options: Option<JupiterSwapOptions>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Default)]
434#[serde(deny_unknown_fields)]
435pub struct RelayerSolanaPolicy {
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub allowed_programs: Option<Vec<String>>,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub max_signatures: Option<u8>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub max_tx_data_size: Option<u16>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub min_balance: Option<u64>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
446 #[serde(skip_serializing_if = "Option::is_none")]
447 pub fee_payment_strategy: Option<SolanaFeePaymentStrategy>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 pub fee_margin_percentage: Option<f32>,
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub allowed_accounts: Option<Vec<String>>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 pub disallowed_accounts: Option<Vec<String>>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub max_allowed_fee_lamports: Option<u64>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub swap_config: Option<RelayerSolanaSwapConfig>,
458}
459
460impl RelayerSolanaPolicy {
461 pub fn get_allowed_tokens(&self) -> Vec<SolanaAllowedTokensPolicy> {
463 self.allowed_tokens.clone().unwrap_or_default()
464 }
465
466 pub fn get_allowed_token_entry(&self, mint: &str) -> Option<SolanaAllowedTokensPolicy> {
468 self.allowed_tokens
469 .clone()
470 .unwrap_or_default()
471 .into_iter()
472 .find(|entry| entry.mint == mint)
473 }
474
475 pub fn get_swap_config(&self) -> Option<RelayerSolanaSwapConfig> {
477 self.swap_config.clone()
478 }
479
480 pub fn get_allowed_token_decimals(&self, mint: &str) -> Option<u8> {
482 self.get_allowed_token_entry(mint)
483 .and_then(|entry| entry.decimals)
484 }
485}
486#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
488#[serde(deny_unknown_fields)]
489pub struct RelayerStellarPolicy {
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub min_balance: Option<u64>,
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub max_fee: Option<u32>,
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub timeout_seconds: Option<u64>,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub concurrent_transactions: Option<bool>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
502#[serde(tag = "network_type")]
503pub enum RelayerNetworkPolicy {
504 #[serde(rename = "evm")]
505 Evm(RelayerEvmPolicy),
506 #[serde(rename = "solana")]
507 Solana(RelayerSolanaPolicy),
508 #[serde(rename = "stellar")]
509 Stellar(RelayerStellarPolicy),
510}
511
512impl RelayerNetworkPolicy {
513 pub fn get_evm_policy(&self) -> RelayerEvmPolicy {
515 match self {
516 Self::Evm(policy) => policy.clone(),
517 _ => RelayerEvmPolicy::default(),
518 }
519 }
520
521 pub fn get_solana_policy(&self) -> RelayerSolanaPolicy {
523 match self {
524 Self::Solana(policy) => policy.clone(),
525 _ => RelayerSolanaPolicy::default(),
526 }
527 }
528
529 pub fn get_stellar_policy(&self) -> RelayerStellarPolicy {
531 match self {
532 Self::Stellar(policy) => policy.clone(),
533 _ => RelayerStellarPolicy::default(),
534 }
535 }
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
540pub struct Relayer {
541 #[validate(
542 length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
543 regex(
544 path = "*ID_REGEX",
545 message = "ID must contain only letters, numbers, dashes and underscores"
546 )
547 )]
548 pub id: String,
549
550 #[validate(length(min = 1, message = "Name cannot be empty"))]
551 pub name: String,
552
553 #[validate(length(min = 1, message = "Network cannot be empty"))]
554 pub network: String,
555
556 pub paused: bool,
557 pub network_type: RelayerNetworkType,
558 pub policies: Option<RelayerNetworkPolicy>,
559
560 #[validate(length(min = 1, message = "Signer ID cannot be empty"))]
561 pub signer_id: String,
562
563 pub notification_id: Option<String>,
564 pub custom_rpc_urls: Option<Vec<RpcConfig>>,
565}
566
567impl Relayer {
568 #[allow(clippy::too_many_arguments)]
570 pub fn new(
571 id: String,
572 name: String,
573 network: String,
574 paused: bool,
575 network_type: RelayerNetworkType,
576 policies: Option<RelayerNetworkPolicy>,
577 signer_id: String,
578 notification_id: Option<String>,
579 custom_rpc_urls: Option<Vec<RpcConfig>>,
580 ) -> Self {
581 Self {
582 id,
583 name,
584 network,
585 paused,
586 network_type,
587 policies,
588 signer_id,
589 notification_id,
590 custom_rpc_urls,
591 }
592 }
593
594 pub fn validate(&self) -> Result<(), RelayerValidationError> {
596 if self.id.is_empty() {
598 return Err(RelayerValidationError::EmptyId);
599 }
600
601 if self.id.len() > 36 {
603 return Err(RelayerValidationError::IdTooLong);
604 }
605
606 Validate::validate(self).map_err(|validation_errors| {
608 for (field, errors) in validation_errors.field_errors() {
610 if let Some(error) = errors.first() {
611 let field_str = field.as_ref();
612 return match (field_str, error.code.as_ref()) {
613 ("id", "regex") => RelayerValidationError::InvalidIdFormat,
614 ("name", "length") => RelayerValidationError::EmptyName,
615 ("network", "length") => RelayerValidationError::EmptyNetwork,
616 ("signer_id", "length") => RelayerValidationError::InvalidPolicy(
617 "Signer ID cannot be empty".to_string(),
618 ),
619 _ => RelayerValidationError::InvalidIdFormat, };
621 }
622 }
623 RelayerValidationError::InvalidIdFormat
625 })?;
626
627 self.validate_policies()?;
629 self.validate_custom_rpc_urls()?;
630
631 Ok(())
632 }
633
634 fn validate_policies(&self) -> Result<(), RelayerValidationError> {
636 match (&self.network_type, &self.policies) {
637 (RelayerNetworkType::Solana, Some(RelayerNetworkPolicy::Solana(policy))) => {
638 self.validate_solana_policy(policy)?;
639 }
640 (RelayerNetworkType::Evm, Some(RelayerNetworkPolicy::Evm(_))) => {
641 }
643 (RelayerNetworkType::Stellar, Some(RelayerNetworkPolicy::Stellar(_))) => {
644 }
646 (network_type, Some(policy)) => {
648 let policy_type = match policy {
649 RelayerNetworkPolicy::Evm(_) => "EVM",
650 RelayerNetworkPolicy::Solana(_) => "Solana",
651 RelayerNetworkPolicy::Stellar(_) => "Stellar",
652 };
653 let network_type_str = format!("{:?}", network_type);
654 return Err(RelayerValidationError::InvalidPolicy(format!(
655 "Network type {} does not match policy type {}",
656 network_type_str, policy_type
657 )));
658 }
659 (_, None) => {}
661 }
662 Ok(())
663 }
664
665 fn validate_solana_policy(
667 &self,
668 policy: &RelayerSolanaPolicy,
669 ) -> Result<(), RelayerValidationError> {
670 self.validate_solana_pub_keys(&policy.allowed_accounts)?;
672 self.validate_solana_pub_keys(&policy.disallowed_accounts)?;
673 self.validate_solana_pub_keys(&policy.allowed_programs)?;
674
675 if let Some(tokens) = &policy.allowed_tokens {
677 let mint_keys: Vec<String> = tokens.iter().map(|t| t.mint.clone()).collect();
678 self.validate_solana_pub_keys(&Some(mint_keys))?;
679 }
680
681 if let Some(fee_margin) = policy.fee_margin_percentage {
683 if fee_margin < 0.0 {
684 return Err(RelayerValidationError::InvalidPolicy(
685 "Negative fee margin percentage values are not accepted".into(),
686 ));
687 }
688 }
689
690 if policy.allowed_accounts.is_some() && policy.disallowed_accounts.is_some() {
692 return Err(RelayerValidationError::InvalidPolicy(
693 "allowed_accounts and disallowed_accounts cannot be both present".into(),
694 ));
695 }
696
697 if let Some(swap_config) = &policy.swap_config {
699 self.validate_solana_swap_config(swap_config, policy)?;
700 }
701
702 Ok(())
703 }
704
705 fn validate_solana_pub_keys(
707 &self,
708 keys: &Option<Vec<String>>,
709 ) -> Result<(), RelayerValidationError> {
710 if let Some(keys) = keys {
711 let solana_pub_key_regex =
712 Regex::new(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$").map_err(|e| {
713 RelayerValidationError::InvalidPolicy(format!("Regex compilation error: {}", e))
714 })?;
715
716 for key in keys {
717 if !solana_pub_key_regex.is_match(key) {
718 return Err(RelayerValidationError::InvalidPolicy(
719 "Public key must be a valid Solana address".into(),
720 ));
721 }
722 }
723 }
724 Ok(())
725 }
726
727 fn validate_solana_swap_config(
729 &self,
730 swap_config: &RelayerSolanaSwapConfig,
731 policy: &RelayerSolanaPolicy,
732 ) -> Result<(), RelayerValidationError> {
733 if let Some(fee_payment_strategy) = &policy.fee_payment_strategy {
735 if *fee_payment_strategy == SolanaFeePaymentStrategy::Relayer {
736 return Err(RelayerValidationError::InvalidPolicy(
737 "Swap config only supported for user fee payment strategy".into(),
738 ));
739 }
740 }
741
742 if let Some(strategy) = &swap_config.strategy {
744 match strategy {
745 SolanaSwapStrategy::JupiterSwap | SolanaSwapStrategy::JupiterUltra => {
746 if self.network != "mainnet-beta" {
747 return Err(RelayerValidationError::InvalidPolicy(format!(
748 "{:?} strategy is only supported on mainnet-beta",
749 strategy
750 )));
751 }
752 }
753 SolanaSwapStrategy::Noop => {
754 }
756 }
757 }
758
759 if let Some(cron_schedule) = &swap_config.cron_schedule {
761 if cron_schedule.is_empty() {
762 return Err(RelayerValidationError::InvalidPolicy(
763 "Empty cron schedule is not accepted".into(),
764 ));
765 }
766
767 Schedule::from_str(cron_schedule).map_err(|_| {
768 RelayerValidationError::InvalidPolicy("Invalid cron schedule format".into())
769 })?;
770 }
771
772 if let Some(jupiter_options) = &swap_config.jupiter_swap_options {
774 if swap_config.strategy != Some(SolanaSwapStrategy::JupiterSwap) {
776 return Err(RelayerValidationError::InvalidPolicy(
777 "JupiterSwap options are only valid for JupiterSwap strategy".into(),
778 ));
779 }
780
781 if let Some(max_lamports) = jupiter_options.priority_fee_max_lamports {
782 if max_lamports == 0 {
783 return Err(RelayerValidationError::InvalidPolicy(
784 "Max lamports must be greater than 0".into(),
785 ));
786 }
787 }
788
789 if let Some(priority_level) = &jupiter_options.priority_level {
790 if priority_level.is_empty() {
791 return Err(RelayerValidationError::InvalidPolicy(
792 "Priority level cannot be empty".into(),
793 ));
794 }
795
796 let valid_levels = ["medium", "high", "veryHigh"];
797 if !valid_levels.contains(&priority_level.as_str()) {
798 return Err(RelayerValidationError::InvalidPolicy(
799 "Priority level must be one of: medium, high, veryHigh".into(),
800 ));
801 }
802 }
803
804 match (
806 &jupiter_options.priority_level,
807 jupiter_options.priority_fee_max_lamports,
808 ) {
809 (Some(_), None) => {
810 return Err(RelayerValidationError::InvalidPolicy(
811 "Priority Fee Max lamports must be set if priority level is set".into(),
812 ));
813 }
814 (None, Some(_)) => {
815 return Err(RelayerValidationError::InvalidPolicy(
816 "Priority level must be set if priority fee max lamports is set".into(),
817 ));
818 }
819 _ => {}
820 }
821 }
822
823 Ok(())
824 }
825
826 fn validate_custom_rpc_urls(&self) -> Result<(), RelayerValidationError> {
828 if let Some(configs) = &self.custom_rpc_urls {
829 for config in configs {
830 reqwest::Url::parse(&config.url)
831 .map_err(|_| RelayerValidationError::InvalidRpcUrl(config.url.clone()))?;
832
833 if config.weight > 100 {
834 return Err(RelayerValidationError::InvalidRpcWeight);
835 }
836 }
837 }
838 Ok(())
839 }
840
841 pub fn apply_json_patch(
851 &self,
852 patch: &serde_json::Value,
853 ) -> Result<Self, RelayerValidationError> {
854 let mut domain_json = serde_json::to_value(self).map_err(|e| {
856 RelayerValidationError::InvalidField(format!("Serialization error: {}", e))
857 })?;
858
859 json_patch::merge(&mut domain_json, patch);
861
862 let updated: Relayer = serde_json::from_value(domain_json).map_err(|e| {
864 RelayerValidationError::InvalidField(format!("Invalid result after patch: {}", e))
865 })?;
866
867 updated.validate()?;
869
870 Ok(updated)
871 }
872}
873
874#[derive(Debug, thiserror::Error)]
876pub enum RelayerValidationError {
877 #[error("Relayer ID cannot be empty")]
878 EmptyId,
879 #[error("Relayer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
880 InvalidIdFormat,
881 #[error("Relayer ID must not exceed 36 characters")]
882 IdTooLong,
883 #[error("Relayer name cannot be empty")]
884 EmptyName,
885 #[error("Network cannot be empty")]
886 EmptyNetwork,
887 #[error("Invalid relayer policy: {0}")]
888 InvalidPolicy(String),
889 #[error("Invalid RPC URL: {0}")]
890 InvalidRpcUrl(String),
891 #[error("RPC URL weight must be in range 0-100")]
892 InvalidRpcWeight,
893 #[error("Invalid field: {0}")]
894 InvalidField(String),
895}
896
897impl From<RelayerValidationError> for crate::models::ApiError {
899 fn from(error: RelayerValidationError) -> Self {
900 use crate::models::ApiError;
901
902 ApiError::BadRequest(match error {
903 RelayerValidationError::EmptyId => "ID cannot be empty".to_string(),
904 RelayerValidationError::InvalidIdFormat => {
905 "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
906 }
907 RelayerValidationError::IdTooLong => {
908 "ID must not exceed 36 characters".to_string()
909 }
910 RelayerValidationError::EmptyName => "Name cannot be empty".to_string(),
911 RelayerValidationError::EmptyNetwork => "Network cannot be empty".to_string(),
912 RelayerValidationError::InvalidPolicy(msg) => {
913 format!("Invalid relayer policy: {}", msg)
914 }
915 RelayerValidationError::InvalidRpcUrl(url) => {
916 format!("Invalid RPC URL: {}", url)
917 }
918 RelayerValidationError::InvalidRpcWeight => {
919 "RPC URL weight must be in range 0-100".to_string()
920 }
921 RelayerValidationError::InvalidField(msg) => msg.clone(),
922 })
923 }
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use serde_json::json;
930
931 #[test]
932 fn test_disabled_reason_serialization_sanitizes_details() {
933 let reason = DisabledReason::RpcValidationFailed(
935 "Connection failed to https://mainnet.infura.io/v3/SECRET_API_KEY: timeout".to_string(),
936 );
937
938 let serialized = serde_json::to_string(&reason).unwrap();
939
940 assert!(!serialized.contains("SECRET_API_KEY"));
942 assert!(!serialized.contains("infura.io"));
943
944 assert!(serialized.contains("RPC endpoint validation failed"));
946 }
947
948 #[test]
949 fn test_disabled_reason_safe_description() {
950 let reason = DisabledReason::BalanceCheckFailed(
951 "Insufficient balance: 0.001 ETH but need 0.1 ETH at address 0x123...".to_string(),
952 );
953
954 let safe = reason.safe_description();
955
956 assert!(!safe.contains("0.001"));
958 assert!(!safe.contains("0x123"));
959 assert_eq!(safe, "Insufficient balance");
960 }
961
962 #[test]
963 fn test_disabled_reason_same_variant_same_type_different_message() {
964 let reason1 = DisabledReason::RpcValidationFailed("Connection timeout".to_string());
966 let reason2 = DisabledReason::RpcValidationFailed("Connection refused".to_string());
967
968 assert!(
969 reason1.same_variant(&reason2),
970 "Same variant types with different messages should be considered the same"
971 );
972 }
973
974 #[test]
975 fn test_disabled_reason_same_variant_different_types() {
976 let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
978 let reason2 = DisabledReason::BalanceCheckFailed("Error".to_string());
979
980 assert!(
981 !reason1.same_variant(&reason2),
982 "Different variant types should not be considered the same"
983 );
984 }
985
986 #[test]
987 fn test_disabled_reason_same_variant_identical() {
988 let reason1 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
990 let reason2 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
991
992 assert!(
993 reason1.same_variant(&reason2),
994 "Identical reasons should be the same variant"
995 );
996 }
997
998 #[test]
999 fn test_disabled_reason_same_variant_multiple_same_order() {
1000 let reason1 = DisabledReason::Multiple(vec![
1002 DisabledReason::RpcValidationFailed("Error 1".to_string()),
1003 DisabledReason::BalanceCheckFailed("Error 2".to_string()),
1004 ]);
1005 let reason2 = DisabledReason::Multiple(vec![
1006 DisabledReason::RpcValidationFailed("Different error 1".to_string()),
1007 DisabledReason::BalanceCheckFailed("Different error 2".to_string()),
1008 ]);
1009
1010 assert!(
1011 reason1.same_variant(&reason2),
1012 "Multiple with same variant types in same order should be considered the same"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_disabled_reason_same_variant_multiple_different_order() {
1018 let reason1 = DisabledReason::Multiple(vec![
1020 DisabledReason::RpcValidationFailed("Error".to_string()),
1021 DisabledReason::BalanceCheckFailed("Error".to_string()),
1022 ]);
1023 let reason2 = DisabledReason::Multiple(vec![
1024 DisabledReason::BalanceCheckFailed("Error".to_string()),
1025 DisabledReason::RpcValidationFailed("Error".to_string()),
1026 ]);
1027
1028 assert!(
1029 !reason1.same_variant(&reason2),
1030 "Multiple with different order should not be considered the same"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_disabled_reason_same_variant_multiple_different_length() {
1036 let reason1 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1038 "Error".to_string(),
1039 )]);
1040 let reason2 = DisabledReason::Multiple(vec![
1041 DisabledReason::RpcValidationFailed("Error".to_string()),
1042 DisabledReason::BalanceCheckFailed("Error".to_string()),
1043 ]);
1044
1045 assert!(
1046 !reason1.same_variant(&reason2),
1047 "Multiple with different lengths should not be considered the same"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_disabled_reason_same_variant_single_vs_multiple() {
1053 let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
1055 let reason2 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1056 "Error".to_string(),
1057 )]);
1058
1059 assert!(
1060 !reason1.same_variant(&reason2),
1061 "Single variant vs Multiple should not be considered the same"
1062 );
1063 }
1064
1065 #[test]
1068 fn test_relayer_network_type_display() {
1069 assert_eq!(RelayerNetworkType::Evm.to_string(), "evm");
1070 assert_eq!(RelayerNetworkType::Solana.to_string(), "solana");
1071 assert_eq!(RelayerNetworkType::Stellar.to_string(), "stellar");
1072 }
1073
1074 #[test]
1075 fn test_relayer_network_type_from_config_file_type() {
1076 assert_eq!(
1077 RelayerNetworkType::from(ConfigFileNetworkType::Evm),
1078 RelayerNetworkType::Evm
1079 );
1080 assert_eq!(
1081 RelayerNetworkType::from(ConfigFileNetworkType::Solana),
1082 RelayerNetworkType::Solana
1083 );
1084 assert_eq!(
1085 RelayerNetworkType::from(ConfigFileNetworkType::Stellar),
1086 RelayerNetworkType::Stellar
1087 );
1088 }
1089
1090 #[test]
1091 fn test_config_file_network_type_from_relayer_type() {
1092 assert_eq!(
1093 ConfigFileNetworkType::from(RelayerNetworkType::Evm),
1094 ConfigFileNetworkType::Evm
1095 );
1096 assert_eq!(
1097 ConfigFileNetworkType::from(RelayerNetworkType::Solana),
1098 ConfigFileNetworkType::Solana
1099 );
1100 assert_eq!(
1101 ConfigFileNetworkType::from(RelayerNetworkType::Stellar),
1102 ConfigFileNetworkType::Stellar
1103 );
1104 }
1105
1106 #[test]
1107 fn test_relayer_network_type_serialization() {
1108 let evm_type = RelayerNetworkType::Evm;
1109 let serialized = serde_json::to_string(&evm_type).unwrap();
1110 assert_eq!(serialized, "\"evm\"");
1111
1112 let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1113 assert_eq!(deserialized, RelayerNetworkType::Evm);
1114
1115 let types = vec![
1117 (RelayerNetworkType::Evm, "\"evm\""),
1118 (RelayerNetworkType::Solana, "\"solana\""),
1119 (RelayerNetworkType::Stellar, "\"stellar\""),
1120 ];
1121
1122 for (network_type, expected_json) in types {
1123 let serialized = serde_json::to_string(&network_type).unwrap();
1124 assert_eq!(serialized, expected_json);
1125
1126 let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1127 assert_eq!(deserialized, network_type);
1128 }
1129 }
1130
1131 #[test]
1134 fn test_relayer_evm_policy_default() {
1135 let default_policy = RelayerEvmPolicy::default();
1136 assert_eq!(default_policy.min_balance, None);
1137 assert_eq!(default_policy.gas_limit_estimation, None);
1138 assert_eq!(default_policy.gas_price_cap, None);
1139 assert_eq!(default_policy.whitelist_receivers, None);
1140 assert_eq!(default_policy.eip1559_pricing, None);
1141 assert_eq!(default_policy.private_transactions, None);
1142 }
1143
1144 #[test]
1145 fn test_relayer_evm_policy_serialization() {
1146 let policy = RelayerEvmPolicy {
1147 min_balance: Some(1000000000000000000),
1148 gas_limit_estimation: Some(true),
1149 gas_price_cap: Some(50000000000),
1150 whitelist_receivers: Some(vec!["0x123".to_string(), "0x456".to_string()]),
1151 eip1559_pricing: Some(false),
1152 private_transactions: Some(true),
1153 };
1154
1155 let serialized = serde_json::to_string(&policy).unwrap();
1156 let deserialized: RelayerEvmPolicy = serde_json::from_str(&serialized).unwrap();
1157 assert_eq!(policy, deserialized);
1158 }
1159
1160 #[test]
1161 fn test_allowed_token_new() {
1162 let token = SolanaAllowedTokensPolicy::new(
1163 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1164 Some(100000),
1165 None,
1166 );
1167
1168 assert_eq!(token.mint, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
1169 assert_eq!(token.max_allowed_fee, Some(100000));
1170 assert_eq!(token.decimals, None);
1171 assert_eq!(token.symbol, None);
1172 assert_eq!(token.swap_config, None);
1173 }
1174
1175 #[test]
1176 fn test_allowed_token_new_partial() {
1177 let swap_config = SolanaAllowedTokensSwapConfig {
1178 slippage_percentage: Some(0.5),
1179 min_amount: Some(1000),
1180 max_amount: Some(10000000),
1181 retain_min_amount: Some(500),
1182 };
1183
1184 let token = SolanaAllowedTokensPolicy::new_partial(
1185 "TokenMint123".to_string(),
1186 Some(50000),
1187 Some(swap_config.clone()),
1188 );
1189
1190 assert_eq!(token.mint, "TokenMint123");
1191 assert_eq!(token.max_allowed_fee, Some(50000));
1192 assert_eq!(token.swap_config, Some(swap_config));
1193 }
1194
1195 #[test]
1196 fn test_allowed_token_swap_config_default() {
1197 let config = AllowedTokenSwapConfig::default();
1198 assert_eq!(config.slippage_percentage, None);
1199 assert_eq!(config.min_amount, None);
1200 assert_eq!(config.max_amount, None);
1201 assert_eq!(config.retain_min_amount, None);
1202 }
1203
1204 #[test]
1205 fn test_relayer_solana_fee_payment_strategy_default() {
1206 let default_strategy = SolanaFeePaymentStrategy::default();
1207 assert_eq!(default_strategy, SolanaFeePaymentStrategy::User);
1208 }
1209
1210 #[test]
1211 fn test_relayer_solana_swap_strategy_default() {
1212 let default_strategy = SolanaSwapStrategy::default();
1213 assert_eq!(default_strategy, SolanaSwapStrategy::Noop);
1214 }
1215
1216 #[test]
1217 fn test_jupiter_swap_options_default() {
1218 let options = JupiterSwapOptions::default();
1219 assert_eq!(options.priority_fee_max_lamports, None);
1220 assert_eq!(options.priority_level, None);
1221 assert_eq!(options.dynamic_compute_unit_limit, None);
1222 }
1223
1224 #[test]
1225 fn test_relayer_solana_swap_policy_default() {
1226 let policy = RelayerSolanaSwapConfig::default();
1227 assert_eq!(policy.strategy, None);
1228 assert_eq!(policy.cron_schedule, None);
1229 assert_eq!(policy.min_balance_threshold, None);
1230 assert_eq!(policy.jupiter_swap_options, None);
1231 }
1232
1233 #[test]
1234 fn test_relayer_solana_policy_default() {
1235 let policy = RelayerSolanaPolicy::default();
1236 assert_eq!(policy.allowed_programs, None);
1237 assert_eq!(policy.max_signatures, None);
1238 assert_eq!(policy.max_tx_data_size, None);
1239 assert_eq!(policy.min_balance, None);
1240 assert_eq!(policy.allowed_tokens, None);
1241 assert_eq!(policy.fee_payment_strategy, None);
1242 assert_eq!(policy.fee_margin_percentage, None);
1243 assert_eq!(policy.allowed_accounts, None);
1244 assert_eq!(policy.disallowed_accounts, None);
1245 assert_eq!(policy.max_allowed_fee_lamports, None);
1246 assert_eq!(policy.swap_config, None);
1247 }
1248
1249 #[test]
1250 fn test_relayer_solana_policy_get_allowed_tokens() {
1251 let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1252 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1253
1254 let policy = RelayerSolanaPolicy {
1255 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1256 ..RelayerSolanaPolicy::default()
1257 };
1258
1259 let tokens = policy.get_allowed_tokens();
1260 assert_eq!(tokens.len(), 2);
1261 assert_eq!(tokens[0], token1);
1262 assert_eq!(tokens[1], token2);
1263
1264 let empty_policy = RelayerSolanaPolicy::default();
1266 let empty_tokens = empty_policy.get_allowed_tokens();
1267 assert_eq!(empty_tokens.len(), 0);
1268 }
1269
1270 #[test]
1271 fn test_relayer_solana_policy_get_allowed_token_entry() {
1272 let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1273 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1274
1275 let policy = RelayerSolanaPolicy {
1276 allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1277 ..RelayerSolanaPolicy::default()
1278 };
1279
1280 let found_token = policy.get_allowed_token_entry("mint1").unwrap();
1281 assert_eq!(found_token, token1);
1282
1283 let not_found = policy.get_allowed_token_entry("mint3");
1284 assert!(not_found.is_none());
1285
1286 let empty_policy = RelayerSolanaPolicy::default();
1288 let empty_result = empty_policy.get_allowed_token_entry("mint1");
1289 assert!(empty_result.is_none());
1290 }
1291
1292 #[test]
1293 fn test_relayer_solana_policy_get_swap_config() {
1294 let swap_config = RelayerSolanaSwapConfig {
1295 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1296 cron_schedule: Some("0 0 * * *".to_string()),
1297 min_balance_threshold: Some(1000000),
1298 jupiter_swap_options: None,
1299 };
1300
1301 let policy = RelayerSolanaPolicy {
1302 swap_config: Some(swap_config.clone()),
1303 ..RelayerSolanaPolicy::default()
1304 };
1305
1306 let retrieved_config = policy.get_swap_config().unwrap();
1307 assert_eq!(retrieved_config, swap_config);
1308
1309 let empty_policy = RelayerSolanaPolicy::default();
1311 assert!(empty_policy.get_swap_config().is_none());
1312 }
1313
1314 #[test]
1315 fn test_relayer_solana_policy_get_allowed_token_decimals() {
1316 let mut token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1317 token1.decimals = Some(9);
1318
1319 let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1320 let policy = RelayerSolanaPolicy {
1323 allowed_tokens: Some(vec![token1, token2]),
1324 ..RelayerSolanaPolicy::default()
1325 };
1326
1327 assert_eq!(policy.get_allowed_token_decimals("mint1"), Some(9));
1328 assert_eq!(policy.get_allowed_token_decimals("mint2"), None);
1329 assert_eq!(policy.get_allowed_token_decimals("mint3"), None);
1330 }
1331
1332 #[test]
1333 fn test_relayer_stellar_policy_default() {
1334 let policy = RelayerStellarPolicy::default();
1335 assert_eq!(policy.min_balance, None);
1336 assert_eq!(policy.max_fee, None);
1337 assert_eq!(policy.timeout_seconds, None);
1338 }
1339
1340 #[test]
1343 fn test_relayer_network_policy_get_evm_policy() {
1344 let evm_policy = RelayerEvmPolicy {
1345 gas_price_cap: Some(50000000000),
1346 ..RelayerEvmPolicy::default()
1347 };
1348
1349 let network_policy = RelayerNetworkPolicy::Evm(evm_policy.clone());
1350 assert_eq!(network_policy.get_evm_policy(), evm_policy);
1351
1352 let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
1354 assert_eq!(solana_policy.get_evm_policy(), RelayerEvmPolicy::default());
1355
1356 let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
1357 assert_eq!(stellar_policy.get_evm_policy(), RelayerEvmPolicy::default());
1358 }
1359
1360 #[test]
1361 fn test_relayer_network_policy_get_solana_policy() {
1362 let solana_policy = RelayerSolanaPolicy {
1363 min_balance: Some(5000000),
1364 ..RelayerSolanaPolicy::default()
1365 };
1366
1367 let network_policy = RelayerNetworkPolicy::Solana(solana_policy.clone());
1368 assert_eq!(network_policy.get_solana_policy(), solana_policy);
1369
1370 let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
1372 assert_eq!(
1373 evm_policy.get_solana_policy(),
1374 RelayerSolanaPolicy::default()
1375 );
1376
1377 let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
1378 assert_eq!(
1379 stellar_policy.get_solana_policy(),
1380 RelayerSolanaPolicy::default()
1381 );
1382 }
1383
1384 #[test]
1385 fn test_relayer_network_policy_get_stellar_policy() {
1386 let stellar_policy = RelayerStellarPolicy {
1387 min_balance: Some(20000000),
1388 max_fee: Some(100000),
1389 timeout_seconds: Some(30),
1390 concurrent_transactions: None,
1391 };
1392
1393 let network_policy = RelayerNetworkPolicy::Stellar(stellar_policy.clone());
1394 assert_eq!(network_policy.get_stellar_policy(), stellar_policy);
1395
1396 let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
1398 assert_eq!(
1399 evm_policy.get_stellar_policy(),
1400 RelayerStellarPolicy::default()
1401 );
1402
1403 let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
1404 assert_eq!(
1405 solana_policy.get_stellar_policy(),
1406 RelayerStellarPolicy::default()
1407 );
1408 }
1409
1410 #[test]
1413 fn test_relayer_new() {
1414 let relayer = Relayer::new(
1415 "test-relayer".to_string(),
1416 "Test Relayer".to_string(),
1417 "mainnet".to_string(),
1418 false,
1419 RelayerNetworkType::Evm,
1420 Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
1421 "test-signer".to_string(),
1422 Some("test-notification".to_string()),
1423 None,
1424 );
1425
1426 assert_eq!(relayer.id, "test-relayer");
1427 assert_eq!(relayer.name, "Test Relayer");
1428 assert_eq!(relayer.network, "mainnet");
1429 assert!(!relayer.paused);
1430 assert_eq!(relayer.network_type, RelayerNetworkType::Evm);
1431 assert_eq!(relayer.signer_id, "test-signer");
1432 assert_eq!(
1433 relayer.notification_id,
1434 Some("test-notification".to_string())
1435 );
1436 assert!(relayer.policies.is_some());
1437 assert_eq!(relayer.custom_rpc_urls, None);
1438 }
1439
1440 #[test]
1443 fn test_relayer_validation_success() {
1444 let relayer = Relayer::new(
1445 "valid-relayer-id".to_string(),
1446 "Valid Relayer".to_string(),
1447 "mainnet".to_string(),
1448 false,
1449 RelayerNetworkType::Evm,
1450 None,
1451 "valid-signer".to_string(),
1452 None,
1453 None,
1454 );
1455
1456 assert!(relayer.validate().is_ok());
1457 }
1458
1459 #[test]
1460 fn test_relayer_validation_empty_id() {
1461 let relayer = Relayer::new(
1462 "".to_string(), "Valid Relayer".to_string(),
1464 "mainnet".to_string(),
1465 false,
1466 RelayerNetworkType::Evm,
1467 None,
1468 "valid-signer".to_string(),
1469 None,
1470 None,
1471 );
1472
1473 let result = relayer.validate();
1474 assert!(result.is_err());
1475 assert!(matches!(
1476 result.unwrap_err(),
1477 RelayerValidationError::EmptyId
1478 ));
1479 }
1480
1481 #[test]
1482 fn test_relayer_validation_id_too_long() {
1483 let long_id = "a".repeat(37); let relayer = Relayer::new(
1485 long_id,
1486 "Valid Relayer".to_string(),
1487 "mainnet".to_string(),
1488 false,
1489 RelayerNetworkType::Evm,
1490 None,
1491 "valid-signer".to_string(),
1492 None,
1493 None,
1494 );
1495
1496 let result = relayer.validate();
1497 assert!(result.is_err());
1498 assert!(matches!(
1499 result.unwrap_err(),
1500 RelayerValidationError::IdTooLong
1501 ));
1502 }
1503
1504 #[test]
1505 fn test_relayer_validation_invalid_id_format() {
1506 let relayer = Relayer::new(
1507 "invalid@id".to_string(), "Valid Relayer".to_string(),
1509 "mainnet".to_string(),
1510 false,
1511 RelayerNetworkType::Evm,
1512 None,
1513 "valid-signer".to_string(),
1514 None,
1515 None,
1516 );
1517
1518 let result = relayer.validate();
1519 assert!(result.is_err());
1520 assert!(matches!(
1521 result.unwrap_err(),
1522 RelayerValidationError::InvalidIdFormat
1523 ));
1524 }
1525
1526 #[test]
1527 fn test_relayer_validation_empty_name() {
1528 let relayer = Relayer::new(
1529 "valid-id".to_string(),
1530 "".to_string(), "mainnet".to_string(),
1532 false,
1533 RelayerNetworkType::Evm,
1534 None,
1535 "valid-signer".to_string(),
1536 None,
1537 None,
1538 );
1539
1540 let result = relayer.validate();
1541 assert!(result.is_err());
1542 assert!(matches!(
1543 result.unwrap_err(),
1544 RelayerValidationError::EmptyName
1545 ));
1546 }
1547
1548 #[test]
1549 fn test_relayer_validation_empty_network() {
1550 let relayer = Relayer::new(
1551 "valid-id".to_string(),
1552 "Valid Relayer".to_string(),
1553 "".to_string(), false,
1555 RelayerNetworkType::Evm,
1556 None,
1557 "valid-signer".to_string(),
1558 None,
1559 None,
1560 );
1561
1562 let result = relayer.validate();
1563 assert!(result.is_err());
1564 assert!(matches!(
1565 result.unwrap_err(),
1566 RelayerValidationError::EmptyNetwork
1567 ));
1568 }
1569
1570 #[test]
1571 fn test_relayer_validation_empty_signer_id() {
1572 let relayer = Relayer::new(
1573 "valid-id".to_string(),
1574 "Valid Relayer".to_string(),
1575 "mainnet".to_string(),
1576 false,
1577 RelayerNetworkType::Evm,
1578 None,
1579 "".to_string(), None,
1581 None,
1582 );
1583
1584 let result = relayer.validate();
1585 assert!(result.is_err());
1586 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1588 assert!(msg.contains("Signer ID cannot be empty"));
1589 } else {
1590 panic!("Expected InvalidPolicy error for empty signer ID");
1591 }
1592 }
1593
1594 #[test]
1595 fn test_relayer_validation_mismatched_network_type_and_policy() {
1596 let relayer = Relayer::new(
1597 "valid-id".to_string(),
1598 "Valid Relayer".to_string(),
1599 "mainnet".to_string(),
1600 false,
1601 RelayerNetworkType::Evm, Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default())), "valid-signer".to_string(),
1604 None,
1605 None,
1606 );
1607
1608 let result = relayer.validate();
1609 assert!(result.is_err());
1610 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1611 assert!(msg.contains("Network type") && msg.contains("does not match policy type"));
1612 } else {
1613 panic!("Expected InvalidPolicy error for mismatched network type and policy");
1614 }
1615 }
1616
1617 #[test]
1618 fn test_relayer_validation_invalid_rpc_url() {
1619 let relayer = Relayer::new(
1620 "valid-id".to_string(),
1621 "Valid Relayer".to_string(),
1622 "mainnet".to_string(),
1623 false,
1624 RelayerNetworkType::Evm,
1625 None,
1626 "valid-signer".to_string(),
1627 None,
1628 Some(vec![RpcConfig::new("invalid-url".to_string())]), );
1630
1631 let result = relayer.validate();
1632 assert!(result.is_err());
1633 assert!(matches!(
1634 result.unwrap_err(),
1635 RelayerValidationError::InvalidRpcUrl(_)
1636 ));
1637 }
1638
1639 #[test]
1640 fn test_relayer_validation_invalid_rpc_weight() {
1641 let relayer = Relayer::new(
1642 "valid-id".to_string(),
1643 "Valid Relayer".to_string(),
1644 "mainnet".to_string(),
1645 false,
1646 RelayerNetworkType::Evm,
1647 None,
1648 "valid-signer".to_string(),
1649 None,
1650 Some(vec![RpcConfig {
1651 url: "https://example.com".to_string(),
1652 weight: 150,
1653 }]), );
1655
1656 let result = relayer.validate();
1657 assert!(result.is_err());
1658 assert!(matches!(
1659 result.unwrap_err(),
1660 RelayerValidationError::InvalidRpcWeight
1661 ));
1662 }
1663
1664 #[test]
1667 fn test_relayer_validation_solana_invalid_public_key() {
1668 let policy = RelayerSolanaPolicy {
1669 allowed_programs: Some(vec!["invalid-pubkey".to_string()]), ..RelayerSolanaPolicy::default()
1671 };
1672
1673 let relayer = Relayer::new(
1674 "valid-id".to_string(),
1675 "Valid Relayer".to_string(),
1676 "mainnet".to_string(),
1677 false,
1678 RelayerNetworkType::Solana,
1679 Some(RelayerNetworkPolicy::Solana(policy)),
1680 "valid-signer".to_string(),
1681 None,
1682 None,
1683 );
1684
1685 let result = relayer.validate();
1686 assert!(result.is_err());
1687 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1688 assert!(msg.contains("Public key must be a valid Solana address"));
1689 } else {
1690 panic!("Expected InvalidPolicy error for invalid Solana public key");
1691 }
1692 }
1693
1694 #[test]
1695 fn test_relayer_validation_solana_valid_public_key() {
1696 let policy = RelayerSolanaPolicy {
1697 allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]), ..RelayerSolanaPolicy::default()
1699 };
1700
1701 let relayer = Relayer::new(
1702 "valid-id".to_string(),
1703 "Valid Relayer".to_string(),
1704 "mainnet".to_string(),
1705 false,
1706 RelayerNetworkType::Solana,
1707 Some(RelayerNetworkPolicy::Solana(policy)),
1708 "valid-signer".to_string(),
1709 None,
1710 None,
1711 );
1712
1713 assert!(relayer.validate().is_ok());
1714 }
1715
1716 #[test]
1717 fn test_relayer_validation_solana_negative_fee_margin() {
1718 let policy = RelayerSolanaPolicy {
1719 fee_margin_percentage: Some(-1.0), ..RelayerSolanaPolicy::default()
1721 };
1722
1723 let relayer = Relayer::new(
1724 "valid-id".to_string(),
1725 "Valid Relayer".to_string(),
1726 "mainnet".to_string(),
1727 false,
1728 RelayerNetworkType::Solana,
1729 Some(RelayerNetworkPolicy::Solana(policy)),
1730 "valid-signer".to_string(),
1731 None,
1732 None,
1733 );
1734
1735 let result = relayer.validate();
1736 assert!(result.is_err());
1737 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1738 assert!(msg.contains("Negative fee margin percentage values are not accepted"));
1739 } else {
1740 panic!("Expected InvalidPolicy error for negative fee margin");
1741 }
1742 }
1743
1744 #[test]
1745 fn test_relayer_validation_solana_conflicting_accounts() {
1746 let policy = RelayerSolanaPolicy {
1747 allowed_accounts: Some(vec!["11111111111111111111111111111111".to_string()]),
1748 disallowed_accounts: Some(vec!["22222222222222222222222222222222".to_string()]),
1749 ..RelayerSolanaPolicy::default()
1750 };
1751
1752 let relayer = Relayer::new(
1753 "valid-id".to_string(),
1754 "Valid Relayer".to_string(),
1755 "mainnet".to_string(),
1756 false,
1757 RelayerNetworkType::Solana,
1758 Some(RelayerNetworkPolicy::Solana(policy)),
1759 "valid-signer".to_string(),
1760 None,
1761 None,
1762 );
1763
1764 let result = relayer.validate();
1765 assert!(result.is_err());
1766 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1767 assert!(msg.contains("allowed_accounts and disallowed_accounts cannot be both present"));
1768 } else {
1769 panic!("Expected InvalidPolicy error for conflicting accounts");
1770 }
1771 }
1772
1773 #[test]
1774 fn test_relayer_validation_solana_swap_config_wrong_fee_payment_strategy() {
1775 let swap_config = RelayerSolanaSwapConfig {
1776 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1777 ..RelayerSolanaSwapConfig::default()
1778 };
1779
1780 let policy = RelayerSolanaPolicy {
1781 fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer), swap_config: Some(swap_config), ..RelayerSolanaPolicy::default()
1784 };
1785
1786 let relayer = Relayer::new(
1787 "valid-id".to_string(),
1788 "Valid Relayer".to_string(),
1789 "mainnet".to_string(),
1790 false,
1791 RelayerNetworkType::Solana,
1792 Some(RelayerNetworkPolicy::Solana(policy)),
1793 "valid-signer".to_string(),
1794 None,
1795 None,
1796 );
1797
1798 let result = relayer.validate();
1799 assert!(result.is_err());
1800 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1801 assert!(msg.contains("Swap config only supported for user fee payment strategy"));
1802 } else {
1803 panic!("Expected InvalidPolicy error for swap config with relayer fee payment");
1804 }
1805 }
1806
1807 #[test]
1808 fn test_relayer_validation_solana_jupiter_strategy_wrong_network() {
1809 let swap_config = RelayerSolanaSwapConfig {
1810 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1811 ..RelayerSolanaSwapConfig::default()
1812 };
1813
1814 let policy = RelayerSolanaPolicy {
1815 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1816 swap_config: Some(swap_config),
1817 ..RelayerSolanaPolicy::default()
1818 };
1819
1820 let relayer = Relayer::new(
1821 "valid-id".to_string(),
1822 "Valid Relayer".to_string(),
1823 "testnet".to_string(), false,
1825 RelayerNetworkType::Solana,
1826 Some(RelayerNetworkPolicy::Solana(policy)),
1827 "valid-signer".to_string(),
1828 None,
1829 None,
1830 );
1831
1832 let result = relayer.validate();
1833 assert!(result.is_err());
1834 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1835 assert!(msg.contains("strategy is only supported on mainnet-beta"));
1836 } else {
1837 panic!("Expected InvalidPolicy error for Jupiter strategy on wrong network");
1838 }
1839 }
1840
1841 #[test]
1842 fn test_relayer_validation_solana_empty_cron_schedule() {
1843 let swap_config = RelayerSolanaSwapConfig {
1844 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1845 cron_schedule: Some("".to_string()), ..RelayerSolanaSwapConfig::default()
1847 };
1848
1849 let policy = RelayerSolanaPolicy {
1850 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1851 swap_config: Some(swap_config),
1852 ..RelayerSolanaPolicy::default()
1853 };
1854
1855 let relayer = Relayer::new(
1856 "valid-id".to_string(),
1857 "Valid Relayer".to_string(),
1858 "mainnet-beta".to_string(),
1859 false,
1860 RelayerNetworkType::Solana,
1861 Some(RelayerNetworkPolicy::Solana(policy)),
1862 "valid-signer".to_string(),
1863 None,
1864 None,
1865 );
1866
1867 let result = relayer.validate();
1868 assert!(result.is_err());
1869 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1870 assert!(msg.contains("Empty cron schedule is not accepted"));
1871 } else {
1872 panic!("Expected InvalidPolicy error for empty cron schedule");
1873 }
1874 }
1875
1876 #[test]
1877 fn test_relayer_validation_solana_invalid_cron_schedule() {
1878 let swap_config = RelayerSolanaSwapConfig {
1879 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1880 cron_schedule: Some("invalid cron".to_string()), ..RelayerSolanaSwapConfig::default()
1882 };
1883
1884 let policy = RelayerSolanaPolicy {
1885 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1886 swap_config: Some(swap_config),
1887 ..RelayerSolanaPolicy::default()
1888 };
1889
1890 let relayer = Relayer::new(
1891 "valid-id".to_string(),
1892 "Valid Relayer".to_string(),
1893 "mainnet-beta".to_string(),
1894 false,
1895 RelayerNetworkType::Solana,
1896 Some(RelayerNetworkPolicy::Solana(policy)),
1897 "valid-signer".to_string(),
1898 None,
1899 None,
1900 );
1901
1902 let result = relayer.validate();
1903 assert!(result.is_err());
1904 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1905 assert!(msg.contains("Invalid cron schedule format"));
1906 } else {
1907 panic!("Expected InvalidPolicy error for invalid cron schedule");
1908 }
1909 }
1910
1911 #[test]
1912 fn test_relayer_validation_solana_jupiter_options_wrong_strategy() {
1913 let jupiter_options = JupiterSwapOptions {
1914 priority_fee_max_lamports: Some(10000),
1915 priority_level: Some("high".to_string()),
1916 dynamic_compute_unit_limit: Some(true),
1917 };
1918
1919 let swap_config = RelayerSolanaSwapConfig {
1920 strategy: Some(SolanaSwapStrategy::JupiterUltra), jupiter_swap_options: Some(jupiter_options),
1922 ..RelayerSolanaSwapConfig::default()
1923 };
1924
1925 let policy = RelayerSolanaPolicy {
1926 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1927 swap_config: Some(swap_config),
1928 ..RelayerSolanaPolicy::default()
1929 };
1930
1931 let relayer = Relayer::new(
1932 "valid-id".to_string(),
1933 "Valid Relayer".to_string(),
1934 "mainnet-beta".to_string(),
1935 false,
1936 RelayerNetworkType::Solana,
1937 Some(RelayerNetworkPolicy::Solana(policy)),
1938 "valid-signer".to_string(),
1939 None,
1940 None,
1941 );
1942
1943 let result = relayer.validate();
1944 assert!(result.is_err());
1945 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1946 assert!(msg.contains("JupiterSwap options are only valid for JupiterSwap strategy"));
1947 } else {
1948 panic!("Expected InvalidPolicy error for Jupiter options with wrong strategy");
1949 }
1950 }
1951
1952 #[test]
1953 fn test_relayer_validation_solana_jupiter_zero_max_lamports() {
1954 let jupiter_options = JupiterSwapOptions {
1955 priority_fee_max_lamports: Some(0), priority_level: Some("high".to_string()),
1957 dynamic_compute_unit_limit: Some(true),
1958 };
1959
1960 let swap_config = RelayerSolanaSwapConfig {
1961 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1962 jupiter_swap_options: Some(jupiter_options),
1963 ..RelayerSolanaSwapConfig::default()
1964 };
1965
1966 let policy = RelayerSolanaPolicy {
1967 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1968 swap_config: Some(swap_config),
1969 ..RelayerSolanaPolicy::default()
1970 };
1971
1972 let relayer = Relayer::new(
1973 "valid-id".to_string(),
1974 "Valid Relayer".to_string(),
1975 "mainnet-beta".to_string(),
1976 false,
1977 RelayerNetworkType::Solana,
1978 Some(RelayerNetworkPolicy::Solana(policy)),
1979 "valid-signer".to_string(),
1980 None,
1981 None,
1982 );
1983
1984 let result = relayer.validate();
1985 assert!(result.is_err());
1986 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1987 assert!(msg.contains("Max lamports must be greater than 0"));
1988 } else {
1989 panic!("Expected InvalidPolicy error for zero max lamports");
1990 }
1991 }
1992
1993 #[test]
1994 fn test_relayer_validation_solana_jupiter_empty_priority_level() {
1995 let jupiter_options = JupiterSwapOptions {
1996 priority_fee_max_lamports: Some(10000),
1997 priority_level: Some("".to_string()), dynamic_compute_unit_limit: Some(true),
1999 };
2000
2001 let swap_config = RelayerSolanaSwapConfig {
2002 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2003 jupiter_swap_options: Some(jupiter_options),
2004 ..RelayerSolanaSwapConfig::default()
2005 };
2006
2007 let policy = RelayerSolanaPolicy {
2008 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2009 swap_config: Some(swap_config),
2010 ..RelayerSolanaPolicy::default()
2011 };
2012
2013 let relayer = Relayer::new(
2014 "valid-id".to_string(),
2015 "Valid Relayer".to_string(),
2016 "mainnet-beta".to_string(),
2017 false,
2018 RelayerNetworkType::Solana,
2019 Some(RelayerNetworkPolicy::Solana(policy)),
2020 "valid-signer".to_string(),
2021 None,
2022 None,
2023 );
2024
2025 let result = relayer.validate();
2026 assert!(result.is_err());
2027 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2028 assert!(msg.contains("Priority level cannot be empty"));
2029 } else {
2030 panic!("Expected InvalidPolicy error for empty priority level");
2031 }
2032 }
2033
2034 #[test]
2035 fn test_relayer_validation_solana_jupiter_invalid_priority_level() {
2036 let jupiter_options = JupiterSwapOptions {
2037 priority_fee_max_lamports: Some(10000),
2038 priority_level: Some("invalid".to_string()), dynamic_compute_unit_limit: Some(true),
2040 };
2041
2042 let swap_config = RelayerSolanaSwapConfig {
2043 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2044 jupiter_swap_options: Some(jupiter_options),
2045 ..RelayerSolanaSwapConfig::default()
2046 };
2047
2048 let policy = RelayerSolanaPolicy {
2049 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2050 swap_config: Some(swap_config),
2051 ..RelayerSolanaPolicy::default()
2052 };
2053
2054 let relayer = Relayer::new(
2055 "valid-id".to_string(),
2056 "Valid Relayer".to_string(),
2057 "mainnet-beta".to_string(),
2058 false,
2059 RelayerNetworkType::Solana,
2060 Some(RelayerNetworkPolicy::Solana(policy)),
2061 "valid-signer".to_string(),
2062 None,
2063 None,
2064 );
2065
2066 let result = relayer.validate();
2067 assert!(result.is_err());
2068 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2069 assert!(msg.contains("Priority level must be one of: medium, high, veryHigh"));
2070 } else {
2071 panic!("Expected InvalidPolicy error for invalid priority level");
2072 }
2073 }
2074
2075 #[test]
2076 fn test_relayer_validation_solana_jupiter_missing_priority_fee() {
2077 let jupiter_options = JupiterSwapOptions {
2078 priority_fee_max_lamports: None, priority_level: Some("high".to_string()),
2080 dynamic_compute_unit_limit: Some(true),
2081 };
2082
2083 let swap_config = RelayerSolanaSwapConfig {
2084 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2085 jupiter_swap_options: Some(jupiter_options),
2086 ..RelayerSolanaSwapConfig::default()
2087 };
2088
2089 let policy = RelayerSolanaPolicy {
2090 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2091 swap_config: Some(swap_config),
2092 ..RelayerSolanaPolicy::default()
2093 };
2094
2095 let relayer = Relayer::new(
2096 "valid-id".to_string(),
2097 "Valid Relayer".to_string(),
2098 "mainnet-beta".to_string(),
2099 false,
2100 RelayerNetworkType::Solana,
2101 Some(RelayerNetworkPolicy::Solana(policy)),
2102 "valid-signer".to_string(),
2103 None,
2104 None,
2105 );
2106
2107 let result = relayer.validate();
2108 assert!(result.is_err());
2109 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2110 assert!(msg.contains("Priority Fee Max lamports must be set if priority level is set"));
2111 } else {
2112 panic!("Expected InvalidPolicy error for missing priority fee");
2113 }
2114 }
2115
2116 #[test]
2117 fn test_relayer_validation_solana_jupiter_missing_priority_level() {
2118 let jupiter_options = JupiterSwapOptions {
2119 priority_fee_max_lamports: Some(10000),
2120 priority_level: None, dynamic_compute_unit_limit: Some(true),
2122 };
2123
2124 let swap_config = RelayerSolanaSwapConfig {
2125 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2126 jupiter_swap_options: Some(jupiter_options),
2127 ..RelayerSolanaSwapConfig::default()
2128 };
2129
2130 let policy = RelayerSolanaPolicy {
2131 fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2132 swap_config: Some(swap_config),
2133 ..RelayerSolanaPolicy::default()
2134 };
2135
2136 let relayer = Relayer::new(
2137 "valid-id".to_string(),
2138 "Valid Relayer".to_string(),
2139 "mainnet-beta".to_string(),
2140 false,
2141 RelayerNetworkType::Solana,
2142 Some(RelayerNetworkPolicy::Solana(policy)),
2143 "valid-signer".to_string(),
2144 None,
2145 None,
2146 );
2147
2148 let result = relayer.validate();
2149 assert!(result.is_err());
2150 if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2151 assert!(msg.contains("Priority level must be set if priority fee max lamports is set"));
2152 } else {
2153 panic!("Expected InvalidPolicy error for missing priority level");
2154 }
2155 }
2156
2157 #[test]
2160 fn test_relayer_validation_error_to_api_error() {
2161 use crate::models::ApiError;
2162
2163 let errors = vec![
2165 (RelayerValidationError::EmptyId, "ID cannot be empty"),
2166 (RelayerValidationError::InvalidIdFormat, "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long"),
2167 (RelayerValidationError::IdTooLong, "ID must not exceed 36 characters"),
2168 (RelayerValidationError::EmptyName, "Name cannot be empty"),
2169 (RelayerValidationError::EmptyNetwork, "Network cannot be empty"),
2170 (RelayerValidationError::InvalidPolicy("test error".to_string()), "Invalid relayer policy: test error"),
2171 (RelayerValidationError::InvalidRpcUrl("http://invalid".to_string()), "Invalid RPC URL: http://invalid"),
2172 (RelayerValidationError::InvalidRpcWeight, "RPC URL weight must be in range 0-100"),
2173 (RelayerValidationError::InvalidField("test field error".to_string()), "test field error"),
2174 ];
2175
2176 for (validation_error, expected_message) in errors {
2177 let api_error: ApiError = validation_error.into();
2178 if let ApiError::BadRequest(message) = api_error {
2179 assert_eq!(message, expected_message);
2180 } else {
2181 panic!("Expected BadRequest variant");
2182 }
2183 }
2184 }
2185
2186 #[test]
2189 fn test_apply_json_patch_comprehensive() {
2190 let relayer = Relayer {
2192 id: "test-relayer".to_string(),
2193 name: "Original Name".to_string(),
2194 network: "mainnet".to_string(),
2195 paused: false,
2196 network_type: RelayerNetworkType::Evm,
2197 policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
2198 min_balance: Some(1000000000000000000),
2199 gas_limit_estimation: Some(true),
2200 gas_price_cap: Some(50000000000),
2201 whitelist_receivers: None,
2202 eip1559_pricing: Some(false),
2203 private_transactions: None,
2204 })),
2205 signer_id: "test-signer".to_string(),
2206 notification_id: Some("old-notification".to_string()),
2207 custom_rpc_urls: None,
2208 };
2209
2210 let patch = json!({
2212 "name": "Updated Name via JSON Patch",
2213 "paused": true,
2214 "policies": {
2215 "min_balance": "2000000000000000000",
2216 "gas_price_cap": null, "eip1559_pricing": true, "whitelist_receivers": ["0x123", "0x456"] },
2221 "notification_id": null, "custom_rpc_urls": [{"url": "https://example.com", "weight": 100}]
2223 });
2224
2225 let updated_relayer = relayer.apply_json_patch(&patch).unwrap();
2227
2228 assert_eq!(updated_relayer.name, "Updated Name via JSON Patch");
2230 assert!(updated_relayer.paused);
2231 assert_eq!(updated_relayer.notification_id, None); assert!(updated_relayer.custom_rpc_urls.is_some());
2233
2234 if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = updated_relayer.policies {
2236 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!(
2241 evm_policy.whitelist_receivers,
2242 Some(vec!["0x123".to_string(), "0x456".to_string()])
2243 ); assert_eq!(evm_policy.private_transactions, None); } else {
2246 panic!("Expected EVM policy");
2247 }
2248 }
2249
2250 #[test]
2251 fn test_apply_json_patch_validation_failure() {
2252 let relayer = Relayer {
2253 id: "test-relayer".to_string(),
2254 name: "Original Name".to_string(),
2255 network: "mainnet".to_string(),
2256 paused: false,
2257 network_type: RelayerNetworkType::Evm,
2258 policies: None,
2259 signer_id: "test-signer".to_string(),
2260 notification_id: None,
2261 custom_rpc_urls: None,
2262 };
2263
2264 let invalid_patch = json!({
2266 "name": "" });
2268
2269 let result = relayer.apply_json_patch(&invalid_patch);
2271 assert!(result.is_err());
2272 assert!(result
2273 .unwrap_err()
2274 .to_string()
2275 .contains("Relayer name cannot be empty"));
2276 }
2277
2278 #[test]
2279 fn test_apply_json_patch_invalid_result() {
2280 let relayer = Relayer {
2281 id: "test-relayer".to_string(),
2282 name: "Original Name".to_string(),
2283 network: "mainnet".to_string(),
2284 paused: false,
2285 network_type: RelayerNetworkType::Evm,
2286 policies: None,
2287 signer_id: "test-signer".to_string(),
2288 notification_id: None,
2289 custom_rpc_urls: None,
2290 };
2291
2292 let invalid_patch = json!({
2294 "network_type": "invalid_type" });
2296
2297 let result = relayer.apply_json_patch(&invalid_patch);
2299 assert!(result.is_err());
2300 let error_msg = result.unwrap_err().to_string();
2302 assert!(
2303 error_msg.contains("Invalid patch format")
2304 || error_msg.contains("Invalid result after patch")
2305 );
2306 }
2307}