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