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