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