openzeppelin_relayer/models/relayer/
mod.rs

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