openzeppelin_relayer/models/relayer/
mod.rs

1//! Relayer domain model and business logic.
2//!
3//! This module provides the central `Relayer` type that represents relayers
4//! throughout the relayer system, including:
5//!
6//! - **Domain Model**: Core `Relayer` struct with validation and configuration
7//! - **Business Logic**: Update operations and validation rules
8//! - **Error Handling**: Comprehensive validation error types
9//! - **Interoperability**: Conversions between API, config, and repository representations
10//!
11//! The relayer model supports multiple network types (EVM, Solana, Stellar) with
12//! network-specific policies and configurations.
13
14mod config;
15pub use config::*;
16
17pub mod request;
18pub use request::*;
19
20mod response;
21pub use response::*;
22
23pub mod repository;
24pub use repository::*;
25
26mod rpc_config;
27pub use rpc_config::*;
28
29use crate::{
30    config::ConfigFileNetworkType,
31    constants::ID_REGEX,
32    utils::{deserialize_optional_u128, serialize_optional_u128},
33};
34use apalis_cron::Schedule;
35use regex::Regex;
36use serde::{Deserialize, Serialize};
37use std::{
38    fmt::{Display, Formatter},
39    str::FromStr,
40};
41use utoipa::ToSchema;
42use validator::Validate;
43
44/// Network type enum for relayers
45#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, ToSchema)]
46#[serde(rename_all = "lowercase")]
47pub enum RelayerNetworkType {
48    Evm,
49    Solana,
50    Stellar,
51}
52
53impl Display for RelayerNetworkType {
54    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55        match self {
56            RelayerNetworkType::Evm => write!(f, "evm"),
57            RelayerNetworkType::Solana => write!(f, "solana"),
58            RelayerNetworkType::Stellar => write!(f, "stellar"),
59        }
60    }
61}
62
63impl From<ConfigFileNetworkType> for RelayerNetworkType {
64    fn from(config_type: ConfigFileNetworkType) -> Self {
65        match config_type {
66            ConfigFileNetworkType::Evm => RelayerNetworkType::Evm,
67            ConfigFileNetworkType::Solana => RelayerNetworkType::Solana,
68            ConfigFileNetworkType::Stellar => RelayerNetworkType::Stellar,
69        }
70    }
71}
72
73impl From<RelayerNetworkType> for ConfigFileNetworkType {
74    fn from(domain_type: RelayerNetworkType) -> Self {
75        match domain_type {
76            RelayerNetworkType::Evm => ConfigFileNetworkType::Evm,
77            RelayerNetworkType::Solana => ConfigFileNetworkType::Solana,
78            RelayerNetworkType::Stellar => ConfigFileNetworkType::Stellar,
79        }
80    }
81}
82
83/// Health check failure type
84/// Represents transient validation failures during health checks
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
86#[serde(tag = "type", content = "details")]
87pub enum HealthCheckFailure {
88    /// Nonce synchronization failed during health check
89    NonceSyncFailed(String),
90    /// RPC endpoint validation failed
91    RpcValidationFailed(String),
92    /// Balance check failed (below minimum threshold)
93    BalanceCheckFailed(String),
94    /// Sequence number synchronization failed (Stellar)
95    SequenceSyncFailed(String),
96}
97
98impl Display for HealthCheckFailure {
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        match self {
101            HealthCheckFailure::NonceSyncFailed(msg) => write!(f, "Nonce sync failed: {}", msg),
102            HealthCheckFailure::RpcValidationFailed(msg) => {
103                write!(f, "RPC validation failed: {}", msg)
104            }
105            HealthCheckFailure::BalanceCheckFailed(msg) => {
106                write!(f, "Balance check failed: {}", msg)
107            }
108            HealthCheckFailure::SequenceSyncFailed(msg) => {
109                write!(f, "Sequence sync failed: {}", msg)
110            }
111        }
112    }
113}
114
115/// Reason for a relayer being disabled by the system
116/// This represents persistent state, converted from HealthCheckFailure when disabling
117#[derive(Debug, Clone, Deserialize, PartialEq, ToSchema)]
118#[serde(tag = "type", content = "details")]
119pub enum DisabledReason {
120    /// Nonce synchronization failed during initialization
121    NonceSyncFailed(String),
122    /// RPC endpoint validation failed
123    RpcValidationFailed(String),
124    /// Balance check failed (below minimum threshold)
125    BalanceCheckFailed(String),
126    /// Sequence number synchronization failed (Stellar)
127    SequenceSyncFailed(String),
128    /// Multiple failures occurred simultaneously
129    #[schema(value_type = Vec<String>)]
130    Multiple(Vec<DisabledReason>),
131}
132
133// Custom serialization that sanitizes error details for external exposure
134impl Serialize for DisabledReason {
135    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136    where
137        S: serde::Serializer,
138    {
139        use serde::ser::SerializeStruct;
140
141        let mut state = serializer.serialize_struct("DisabledReason", 2)?;
142
143        match self {
144            DisabledReason::NonceSyncFailed(_) => {
145                state.serialize_field("type", "NonceSyncFailed")?;
146                state.serialize_field("details", "Nonce synchronization failed")?;
147            }
148            DisabledReason::RpcValidationFailed(_) => {
149                state.serialize_field("type", "RpcValidationFailed")?;
150                state.serialize_field("details", "RPC endpoint validation failed")?;
151            }
152            DisabledReason::BalanceCheckFailed(_) => {
153                state.serialize_field("type", "BalanceCheckFailed")?;
154                state.serialize_field("details", "Insufficient balance")?;
155            }
156            DisabledReason::SequenceSyncFailed(_) => {
157                state.serialize_field("type", "SequenceSyncFailed")?;
158                state.serialize_field("details", "Sequence synchronization failed")?;
159            }
160            DisabledReason::Multiple(reasons) => {
161                state.serialize_field("type", "Multiple")?;
162                state.serialize_field("details", reasons)?;
163            }
164        }
165
166        state.end()
167    }
168}
169
170impl DisabledReason {
171    /// Convert from HealthCheckFailure to DisabledReason
172    pub fn from_health_failure(failure: HealthCheckFailure) -> Self {
173        match failure {
174            HealthCheckFailure::NonceSyncFailed(msg) => DisabledReason::NonceSyncFailed(msg),
175            HealthCheckFailure::RpcValidationFailed(msg) => {
176                DisabledReason::RpcValidationFailed(msg)
177            }
178            HealthCheckFailure::BalanceCheckFailed(msg) => DisabledReason::BalanceCheckFailed(msg),
179            HealthCheckFailure::SequenceSyncFailed(msg) => DisabledReason::SequenceSyncFailed(msg),
180        }
181    }
182
183    /// Create a DisabledReason from multiple health check failures
184    ///
185    /// Returns:
186    /// - None if the failures vector is empty
187    /// - Single variant if only one failure
188    /// - Multiple variant if there are multiple failures
189    pub fn from_health_failures(failures: Vec<HealthCheckFailure>) -> Option<Self> {
190        match failures.len() {
191            0 => None,
192            1 => Some(Self::from_health_failure(
193                failures.into_iter().next().unwrap(),
194            )),
195            _ => Some(DisabledReason::Multiple(
196                failures
197                    .into_iter()
198                    .map(Self::from_health_failure)
199                    .collect(),
200            )),
201        }
202    }
203
204    /// Create a reason from multiple DisabledReasons (for internal use)
205    ///
206    /// Returns:
207    /// - None if the failures vector is empty
208    /// - Single variant if only one failure
209    /// - Multiple variant if there are multiple failures
210    pub fn from_failures(failures: Vec<DisabledReason>) -> Option<Self> {
211        match failures.len() {
212            0 => None,
213            1 => Some(failures.into_iter().next().unwrap()),
214            _ => Some(DisabledReason::Multiple(failures)),
215        }
216    }
217
218    /// Get a human-readable description of the disabled reason
219    pub fn description(&self) -> String {
220        match self {
221            DisabledReason::NonceSyncFailed(e) => format!("Nonce sync failed: {}", e),
222            DisabledReason::RpcValidationFailed(e) => format!("RPC validation failed: {}", e),
223            DisabledReason::BalanceCheckFailed(e) => format!("Balance check failed: {}", e),
224            DisabledReason::SequenceSyncFailed(e) => format!("Sequence sync failed: {}", e),
225            DisabledReason::Multiple(reasons) => reasons
226                .iter()
227                .map(|r| r.description())
228                .collect::<Vec<_>>()
229                .join(", "),
230        }
231    }
232
233    /// Get a sanitized description safe for external exposure (API/webhooks)
234    /// Removes potentially sensitive information like URLs, keys, and detailed error messages
235    pub fn safe_description(&self) -> String {
236        match self {
237            DisabledReason::NonceSyncFailed(_) => "Nonce synchronization failed".to_string(),
238            DisabledReason::RpcValidationFailed(_) => "RPC endpoint validation failed".to_string(),
239            DisabledReason::BalanceCheckFailed(_) => "Insufficient balance".to_string(),
240            DisabledReason::SequenceSyncFailed(_) => "Sequence synchronization failed".to_string(),
241            DisabledReason::Multiple(reasons) => reasons
242                .iter()
243                .map(|r| r.safe_description())
244                .collect::<Vec<_>>()
245                .join(", "),
246        }
247    }
248
249    /// Check if two DisabledReason instances are the same variant type,
250    /// ignoring the error message details.
251    pub fn same_variant(&self, other: &Self) -> bool {
252        use std::mem::discriminant;
253
254        match (self, other) {
255            (DisabledReason::Multiple(a), DisabledReason::Multiple(b)) => {
256                // For Multiple, check if they have the same variant types in the same order
257                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.same_variant(y))
258            }
259            _ => discriminant(self) == discriminant(other),
260        }
261    }
262
263    /// Create a DisabledReason from an error string, attempting to categorize it
264    ///
265    /// This provides backward compatibility when converting from plain strings
266    pub fn from_error_string(error: String) -> Self {
267        let error_lower = error.to_lowercase();
268
269        if error_lower.contains("nonce") {
270            DisabledReason::NonceSyncFailed(error)
271        } else if error_lower.contains("rpc") {
272            DisabledReason::RpcValidationFailed(error)
273        } else if error_lower.contains("balance") {
274            DisabledReason::BalanceCheckFailed(error)
275        } else if error_lower.contains("sequence") {
276            DisabledReason::SequenceSyncFailed(error)
277        } else {
278            // Default to RPC validation for unrecognized errors
279            DisabledReason::RpcValidationFailed(error)
280        }
281    }
282}
283
284impl std::fmt::Display for DisabledReason {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "{}", self.description())
287    }
288}
289
290/// EVM-specific relayer policy configuration
291#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
292#[serde(deny_unknown_fields)]
293pub struct RelayerEvmPolicy {
294    #[serde(skip_serializing_if = "Option::is_none")]
295    #[serde(
296        serialize_with = "serialize_optional_u128",
297        deserialize_with = "deserialize_optional_u128",
298        default
299    )]
300    pub min_balance: Option<u128>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub gas_limit_estimation: Option<bool>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    #[serde(
305        serialize_with = "serialize_optional_u128",
306        deserialize_with = "deserialize_optional_u128",
307        default
308    )]
309    pub gas_price_cap: Option<u128>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub whitelist_receivers: Option<Vec<String>>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub eip1559_pricing: Option<bool>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub private_transactions: Option<bool>,
316}
317
318/// Solana token swap configuration
319#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
320#[serde(deny_unknown_fields)]
321pub struct SolanaAllowedTokensSwapConfig {
322    /// Conversion slippage percentage for token. Optional.
323    #[schema(nullable = false)]
324    pub slippage_percentage: Option<f32>,
325    /// Minimum amount of tokens to swap. Optional.
326    #[schema(nullable = false)]
327    pub min_amount: Option<u64>,
328    /// Maximum amount of tokens to swap. Optional.
329    #[schema(nullable = false)]
330    pub max_amount: Option<u64>,
331    /// Minimum amount of tokens to retain after swap. Optional.
332    #[schema(nullable = false)]
333    pub retain_min_amount: Option<u64>,
334}
335
336/// Configuration for allowed token handling on Solana
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
338#[serde(deny_unknown_fields)]
339pub struct SolanaAllowedTokensPolicy {
340    pub mint: String,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    #[schema(nullable = false)]
343    pub decimals: Option<u8>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    #[schema(nullable = false)]
346    pub symbol: Option<String>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    #[schema(nullable = false)]
349    pub max_allowed_fee: Option<u64>,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    #[schema(nullable = false)]
352    pub swap_config: Option<SolanaAllowedTokensSwapConfig>,
353}
354
355impl SolanaAllowedTokensPolicy {
356    /// Create a new AllowedToken with required parameters
357    pub fn new(
358        mint: String,
359        max_allowed_fee: Option<u64>,
360        swap_config: Option<SolanaAllowedTokensSwapConfig>,
361    ) -> Self {
362        Self {
363            mint,
364            decimals: None,
365            symbol: None,
366            max_allowed_fee,
367            swap_config,
368        }
369    }
370
371    /// Create a new partial AllowedToken (alias for `new` for backward compatibility)
372    pub fn new_partial(
373        mint: String,
374        max_allowed_fee: Option<u64>,
375        swap_config: Option<SolanaAllowedTokensSwapConfig>,
376    ) -> Self {
377        Self::new(mint, max_allowed_fee, swap_config)
378    }
379}
380
381/// Solana fee payment strategy
382#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
383#[serde(rename_all = "lowercase")]
384pub enum SolanaFeePaymentStrategy {
385    #[default]
386    User,
387    Relayer,
388}
389
390/// Solana swap strategy
391#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
392#[serde(rename_all = "kebab-case")]
393pub enum SolanaSwapStrategy {
394    JupiterSwap,
395    JupiterUltra,
396    #[default]
397    Noop,
398}
399
400/// Jupiter swap options
401#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
402#[serde(deny_unknown_fields)]
403pub struct JupiterSwapOptions {
404    /// Maximum priority fee (in lamports) for a transaction. Optional.
405    #[schema(nullable = false)]
406    pub priority_fee_max_lamports: Option<u64>,
407    /// Priority. Optional.
408    #[schema(nullable = false)]
409    pub priority_level: Option<String>,
410    #[schema(nullable = false)]
411    pub dynamic_compute_unit_limit: Option<bool>,
412}
413
414/// Solana swap policy configuration
415#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
416#[serde(deny_unknown_fields)]
417pub struct RelayerSolanaSwapConfig {
418    /// DEX strategy to use for token swaps.
419    #[schema(nullable = false)]
420    pub strategy: Option<SolanaSwapStrategy>,
421    /// Cron schedule for executing token swap logic to keep relayer funded. Optional.
422    #[schema(nullable = false)]
423    pub cron_schedule: Option<String>,
424    /// Min sol balance to execute token swap logic to keep relayer funded. Optional.
425    #[schema(nullable = false)]
426    pub min_balance_threshold: Option<u64>,
427    /// Swap options for JupiterSwap strategy. Optional.
428    #[schema(nullable = false)]
429    pub jupiter_swap_options: Option<JupiterSwapOptions>,
430}
431
432/// Solana-specific relayer policy configuration
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Default)]
434#[serde(deny_unknown_fields)]
435pub struct RelayerSolanaPolicy {
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub allowed_programs: Option<Vec<String>>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub max_signatures: Option<u8>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub max_tx_data_size: Option<u16>,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub min_balance: Option<u64>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub fee_payment_strategy: Option<SolanaFeePaymentStrategy>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub fee_margin_percentage: Option<f32>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub allowed_accounts: Option<Vec<String>>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub disallowed_accounts: Option<Vec<String>>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub max_allowed_fee_lamports: Option<u64>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub swap_config: Option<RelayerSolanaSwapConfig>,
458}
459
460impl RelayerSolanaPolicy {
461    /// Get allowed tokens for this policy
462    pub fn get_allowed_tokens(&self) -> Vec<SolanaAllowedTokensPolicy> {
463        self.allowed_tokens.clone().unwrap_or_default()
464    }
465
466    /// Get allowed token entry by mint address
467    pub fn get_allowed_token_entry(&self, mint: &str) -> Option<SolanaAllowedTokensPolicy> {
468        self.allowed_tokens
469            .clone()
470            .unwrap_or_default()
471            .into_iter()
472            .find(|entry| entry.mint == mint)
473    }
474
475    /// Get swap configuration for this policy
476    pub fn get_swap_config(&self) -> Option<RelayerSolanaSwapConfig> {
477        self.swap_config.clone()
478    }
479
480    /// Get allowed token decimals by mint address
481    pub fn get_allowed_token_decimals(&self, mint: &str) -> Option<u8> {
482        self.get_allowed_token_entry(mint)
483            .and_then(|entry| entry.decimals)
484    }
485}
486/// Stellar-specific relayer policy configuration
487#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
488#[serde(deny_unknown_fields)]
489pub struct RelayerStellarPolicy {
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub min_balance: Option<u64>,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub max_fee: Option<u32>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub timeout_seconds: Option<u64>,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub concurrent_transactions: Option<bool>,
498}
499
500/// Network-specific policy for relayers
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
502#[serde(tag = "network_type")]
503pub enum RelayerNetworkPolicy {
504    #[serde(rename = "evm")]
505    Evm(RelayerEvmPolicy),
506    #[serde(rename = "solana")]
507    Solana(RelayerSolanaPolicy),
508    #[serde(rename = "stellar")]
509    Stellar(RelayerStellarPolicy),
510}
511
512impl RelayerNetworkPolicy {
513    /// Get EVM policy, returning default if not EVM
514    pub fn get_evm_policy(&self) -> RelayerEvmPolicy {
515        match self {
516            Self::Evm(policy) => policy.clone(),
517            _ => RelayerEvmPolicy::default(),
518        }
519    }
520
521    /// Get Solana policy, returning default if not Solana
522    pub fn get_solana_policy(&self) -> RelayerSolanaPolicy {
523        match self {
524            Self::Solana(policy) => policy.clone(),
525            _ => RelayerSolanaPolicy::default(),
526        }
527    }
528
529    /// Get Stellar policy, returning default if not Stellar
530    pub fn get_stellar_policy(&self) -> RelayerStellarPolicy {
531        match self {
532            Self::Stellar(policy) => policy.clone(),
533            _ => RelayerStellarPolicy::default(),
534        }
535    }
536}
537
538/// Core relayer domain model
539#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
540pub struct Relayer {
541    #[validate(
542        length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
543        regex(
544            path = "*ID_REGEX",
545            message = "ID must contain only letters, numbers, dashes and underscores"
546        )
547    )]
548    pub id: String,
549
550    #[validate(length(min = 1, message = "Name cannot be empty"))]
551    pub name: String,
552
553    #[validate(length(min = 1, message = "Network cannot be empty"))]
554    pub network: String,
555
556    pub paused: bool,
557    pub network_type: RelayerNetworkType,
558    pub policies: Option<RelayerNetworkPolicy>,
559
560    #[validate(length(min = 1, message = "Signer ID cannot be empty"))]
561    pub signer_id: String,
562
563    pub notification_id: Option<String>,
564    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
565}
566
567impl Relayer {
568    /// Creates a new relayer
569    #[allow(clippy::too_many_arguments)]
570    pub fn new(
571        id: String,
572        name: String,
573        network: String,
574        paused: bool,
575        network_type: RelayerNetworkType,
576        policies: Option<RelayerNetworkPolicy>,
577        signer_id: String,
578        notification_id: Option<String>,
579        custom_rpc_urls: Option<Vec<RpcConfig>>,
580    ) -> Self {
581        Self {
582            id,
583            name,
584            network,
585            paused,
586            network_type,
587            policies,
588            signer_id,
589            notification_id,
590            custom_rpc_urls,
591        }
592    }
593
594    /// Validates the relayer using both validator crate and custom validation
595    pub fn validate(&self) -> Result<(), RelayerValidationError> {
596        // Check for empty ID specifically first
597        if self.id.is_empty() {
598            return Err(RelayerValidationError::EmptyId);
599        }
600
601        // Check for ID too long
602        if self.id.len() > 36 {
603            return Err(RelayerValidationError::IdTooLong);
604        }
605
606        // First run validator crate validation
607        Validate::validate(self).map_err(|validation_errors| {
608            // Convert validator errors to our custom error type
609            for (field, errors) in validation_errors.field_errors() {
610                if let Some(error) = errors.first() {
611                    let field_str = field.as_ref();
612                    return match (field_str, error.code.as_ref()) {
613                        ("id", "regex") => RelayerValidationError::InvalidIdFormat,
614                        ("name", "length") => RelayerValidationError::EmptyName,
615                        ("network", "length") => RelayerValidationError::EmptyNetwork,
616                        ("signer_id", "length") => RelayerValidationError::InvalidPolicy(
617                            "Signer ID cannot be empty".to_string(),
618                        ),
619                        _ => RelayerValidationError::InvalidIdFormat, // fallback
620                    };
621                }
622            }
623            // Fallback error
624            RelayerValidationError::InvalidIdFormat
625        })?;
626
627        // Run custom validation
628        self.validate_policies()?;
629        self.validate_custom_rpc_urls()?;
630
631        Ok(())
632    }
633
634    /// Validates network-specific policies
635    fn validate_policies(&self) -> Result<(), RelayerValidationError> {
636        match (&self.network_type, &self.policies) {
637            (RelayerNetworkType::Solana, Some(RelayerNetworkPolicy::Solana(policy))) => {
638                self.validate_solana_policy(policy)?;
639            }
640            (RelayerNetworkType::Evm, Some(RelayerNetworkPolicy::Evm(_))) => {
641                // EVM policies don't need special validation currently
642            }
643            (RelayerNetworkType::Stellar, Some(RelayerNetworkPolicy::Stellar(_))) => {
644                // Stellar policies don't need special validation currently
645            }
646            // Mismatched network type and policy type
647            (network_type, Some(policy)) => {
648                let policy_type = match policy {
649                    RelayerNetworkPolicy::Evm(_) => "EVM",
650                    RelayerNetworkPolicy::Solana(_) => "Solana",
651                    RelayerNetworkPolicy::Stellar(_) => "Stellar",
652                };
653                let network_type_str = format!("{:?}", network_type);
654                return Err(RelayerValidationError::InvalidPolicy(format!(
655                    "Network type {} does not match policy type {}",
656                    network_type_str, policy_type
657                )));
658            }
659            // No policies is fine
660            (_, None) => {}
661        }
662        Ok(())
663    }
664
665    /// Validates Solana-specific policies
666    fn validate_solana_policy(
667        &self,
668        policy: &RelayerSolanaPolicy,
669    ) -> Result<(), RelayerValidationError> {
670        // Validate public keys
671        self.validate_solana_pub_keys(&policy.allowed_accounts)?;
672        self.validate_solana_pub_keys(&policy.disallowed_accounts)?;
673        self.validate_solana_pub_keys(&policy.allowed_programs)?;
674
675        // Validate allowed tokens mint addresses
676        if let Some(tokens) = &policy.allowed_tokens {
677            let mint_keys: Vec<String> = tokens.iter().map(|t| t.mint.clone()).collect();
678            self.validate_solana_pub_keys(&Some(mint_keys))?;
679        }
680
681        // Validate fee margin percentage
682        if let Some(fee_margin) = policy.fee_margin_percentage {
683            if fee_margin < 0.0 {
684                return Err(RelayerValidationError::InvalidPolicy(
685                    "Negative fee margin percentage values are not accepted".into(),
686                ));
687            }
688        }
689
690        // Check for conflicting allowed/disallowed accounts
691        if policy.allowed_accounts.is_some() && policy.disallowed_accounts.is_some() {
692            return Err(RelayerValidationError::InvalidPolicy(
693                "allowed_accounts and disallowed_accounts cannot be both present".into(),
694            ));
695        }
696
697        // Validate swap configuration
698        if let Some(swap_config) = &policy.swap_config {
699            self.validate_solana_swap_config(swap_config, policy)?;
700        }
701
702        Ok(())
703    }
704
705    /// Validates Solana public key format
706    fn validate_solana_pub_keys(
707        &self,
708        keys: &Option<Vec<String>>,
709    ) -> Result<(), RelayerValidationError> {
710        if let Some(keys) = keys {
711            let solana_pub_key_regex =
712                Regex::new(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$").map_err(|e| {
713                    RelayerValidationError::InvalidPolicy(format!("Regex compilation error: {}", e))
714                })?;
715
716            for key in keys {
717                if !solana_pub_key_regex.is_match(key) {
718                    return Err(RelayerValidationError::InvalidPolicy(
719                        "Public key must be a valid Solana address".into(),
720                    ));
721                }
722            }
723        }
724        Ok(())
725    }
726
727    /// Validates Solana swap configuration
728    fn validate_solana_swap_config(
729        &self,
730        swap_config: &RelayerSolanaSwapConfig,
731        policy: &RelayerSolanaPolicy,
732    ) -> Result<(), RelayerValidationError> {
733        // Swap config only supported for user fee payment strategy
734        if let Some(fee_payment_strategy) = &policy.fee_payment_strategy {
735            if *fee_payment_strategy == SolanaFeePaymentStrategy::Relayer {
736                return Err(RelayerValidationError::InvalidPolicy(
737                    "Swap config only supported for user fee payment strategy".into(),
738                ));
739            }
740        }
741
742        // Validate strategy-specific restrictions
743        if let Some(strategy) = &swap_config.strategy {
744            match strategy {
745                SolanaSwapStrategy::JupiterSwap | SolanaSwapStrategy::JupiterUltra => {
746                    if self.network != "mainnet-beta" {
747                        return Err(RelayerValidationError::InvalidPolicy(format!(
748                            "{:?} strategy is only supported on mainnet-beta",
749                            strategy
750                        )));
751                    }
752                }
753                SolanaSwapStrategy::Noop => {
754                    // No-op strategy doesn't need validation
755                }
756            }
757        }
758
759        // Validate cron schedule
760        if let Some(cron_schedule) = &swap_config.cron_schedule {
761            if cron_schedule.is_empty() {
762                return Err(RelayerValidationError::InvalidPolicy(
763                    "Empty cron schedule is not accepted".into(),
764                ));
765            }
766
767            Schedule::from_str(cron_schedule).map_err(|_| {
768                RelayerValidationError::InvalidPolicy("Invalid cron schedule format".into())
769            })?;
770        }
771
772        // Validate Jupiter swap options
773        if let Some(jupiter_options) = &swap_config.jupiter_swap_options {
774            // Jupiter options only valid for JupiterSwap strategy
775            if swap_config.strategy != Some(SolanaSwapStrategy::JupiterSwap) {
776                return Err(RelayerValidationError::InvalidPolicy(
777                    "JupiterSwap options are only valid for JupiterSwap strategy".into(),
778                ));
779            }
780
781            if let Some(max_lamports) = jupiter_options.priority_fee_max_lamports {
782                if max_lamports == 0 {
783                    return Err(RelayerValidationError::InvalidPolicy(
784                        "Max lamports must be greater than 0".into(),
785                    ));
786                }
787            }
788
789            if let Some(priority_level) = &jupiter_options.priority_level {
790                if priority_level.is_empty() {
791                    return Err(RelayerValidationError::InvalidPolicy(
792                        "Priority level cannot be empty".into(),
793                    ));
794                }
795
796                let valid_levels = ["medium", "high", "veryHigh"];
797                if !valid_levels.contains(&priority_level.as_str()) {
798                    return Err(RelayerValidationError::InvalidPolicy(
799                        "Priority level must be one of: medium, high, veryHigh".into(),
800                    ));
801                }
802            }
803
804            // Priority level and max lamports must be used together
805            match (
806                &jupiter_options.priority_level,
807                jupiter_options.priority_fee_max_lamports,
808            ) {
809                (Some(_), None) => {
810                    return Err(RelayerValidationError::InvalidPolicy(
811                        "Priority Fee Max lamports must be set if priority level is set".into(),
812                    ));
813                }
814                (None, Some(_)) => {
815                    return Err(RelayerValidationError::InvalidPolicy(
816                        "Priority level must be set if priority fee max lamports is set".into(),
817                    ));
818                }
819                _ => {}
820            }
821        }
822
823        Ok(())
824    }
825
826    /// Validates custom RPC URL configurations
827    fn validate_custom_rpc_urls(&self) -> Result<(), RelayerValidationError> {
828        if let Some(configs) = &self.custom_rpc_urls {
829            for config in configs {
830                reqwest::Url::parse(&config.url)
831                    .map_err(|_| RelayerValidationError::InvalidRpcUrl(config.url.clone()))?;
832
833                if config.weight > 100 {
834                    return Err(RelayerValidationError::InvalidRpcWeight);
835                }
836            }
837        }
838        Ok(())
839    }
840
841    /// Apply JSON Merge Patch (RFC 7396) directly to the domain object
842    ///
843    /// This method:
844    /// 1. Converts domain object to JSON
845    /// 2. Applies JSON merge patch
846    /// 3. Converts back to domain object
847    /// 4. Validates the final result
848    ///
849    /// This approach provides true JSON Merge Patch semantics while maintaining validation.
850    pub fn apply_json_patch(
851        &self,
852        patch: &serde_json::Value,
853    ) -> Result<Self, RelayerValidationError> {
854        // 1. Convert current domain object to JSON
855        let mut domain_json = serde_json::to_value(self).map_err(|e| {
856            RelayerValidationError::InvalidField(format!("Serialization error: {}", e))
857        })?;
858
859        // 2. Apply JSON Merge Patch
860        json_patch::merge(&mut domain_json, patch);
861
862        // 3. Convert back to domain object
863        let updated: Relayer = serde_json::from_value(domain_json).map_err(|e| {
864            RelayerValidationError::InvalidField(format!("Invalid result after patch: {}", e))
865        })?;
866
867        // 4. Validate the final result
868        updated.validate()?;
869
870        Ok(updated)
871    }
872}
873
874/// Validation errors for relayers
875#[derive(Debug, thiserror::Error)]
876pub enum RelayerValidationError {
877    #[error("Relayer ID cannot be empty")]
878    EmptyId,
879    #[error("Relayer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
880    InvalidIdFormat,
881    #[error("Relayer ID must not exceed 36 characters")]
882    IdTooLong,
883    #[error("Relayer name cannot be empty")]
884    EmptyName,
885    #[error("Network cannot be empty")]
886    EmptyNetwork,
887    #[error("Invalid relayer policy: {0}")]
888    InvalidPolicy(String),
889    #[error("Invalid RPC URL: {0}")]
890    InvalidRpcUrl(String),
891    #[error("RPC URL weight must be in range 0-100")]
892    InvalidRpcWeight,
893    #[error("Invalid field: {0}")]
894    InvalidField(String),
895}
896
897/// Centralized conversion from RelayerValidationError to ApiError
898impl From<RelayerValidationError> for crate::models::ApiError {
899    fn from(error: RelayerValidationError) -> Self {
900        use crate::models::ApiError;
901
902        ApiError::BadRequest(match error {
903            RelayerValidationError::EmptyId => "ID cannot be empty".to_string(),
904            RelayerValidationError::InvalidIdFormat => {
905                "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
906            }
907            RelayerValidationError::IdTooLong => {
908                "ID must not exceed 36 characters".to_string()
909            }
910            RelayerValidationError::EmptyName => "Name cannot be empty".to_string(),
911            RelayerValidationError::EmptyNetwork => "Network cannot be empty".to_string(),
912            RelayerValidationError::InvalidPolicy(msg) => {
913                format!("Invalid relayer policy: {}", msg)
914            }
915            RelayerValidationError::InvalidRpcUrl(url) => {
916                format!("Invalid RPC URL: {}", url)
917            }
918            RelayerValidationError::InvalidRpcWeight => {
919                "RPC URL weight must be in range 0-100".to_string()
920            }
921            RelayerValidationError::InvalidField(msg) => msg.clone(),
922        })
923    }
924}
925
926#[cfg(test)]
927mod tests {
928    use super::*;
929    use serde_json::json;
930
931    #[test]
932    fn test_disabled_reason_serialization_sanitizes_details() {
933        // Test that serialization removes sensitive error details
934        let reason = DisabledReason::RpcValidationFailed(
935            "Connection failed to https://mainnet.infura.io/v3/SECRET_API_KEY: timeout".to_string(),
936        );
937
938        let serialized = serde_json::to_string(&reason).unwrap();
939
940        // Should not contain the sensitive URL or API key
941        assert!(!serialized.contains("SECRET_API_KEY"));
942        assert!(!serialized.contains("infura.io"));
943
944        // Should contain generic description
945        assert!(serialized.contains("RPC endpoint validation failed"));
946    }
947
948    #[test]
949    fn test_disabled_reason_safe_description() {
950        let reason = DisabledReason::BalanceCheckFailed(
951            "Insufficient balance: 0.001 ETH but need 0.1 ETH at address 0x123...".to_string(),
952        );
953
954        let safe = reason.safe_description();
955
956        // Should not contain specific details
957        assert!(!safe.contains("0.001"));
958        assert!(!safe.contains("0x123"));
959        assert_eq!(safe, "Insufficient balance");
960    }
961
962    #[test]
963    fn test_disabled_reason_same_variant_same_type_different_message() {
964        // Same variant type with different error messages should be considered the same
965        let reason1 = DisabledReason::RpcValidationFailed("Connection timeout".to_string());
966        let reason2 = DisabledReason::RpcValidationFailed("Connection refused".to_string());
967
968        assert!(
969            reason1.same_variant(&reason2),
970            "Same variant types with different messages should be considered the same"
971        );
972    }
973
974    #[test]
975    fn test_disabled_reason_same_variant_different_types() {
976        // Different variant types should not be considered the same
977        let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
978        let reason2 = DisabledReason::BalanceCheckFailed("Error".to_string());
979
980        assert!(
981            !reason1.same_variant(&reason2),
982            "Different variant types should not be considered the same"
983        );
984    }
985
986    #[test]
987    fn test_disabled_reason_same_variant_identical() {
988        // Identical reasons should obviously be the same variant
989        let reason1 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
990        let reason2 = DisabledReason::NonceSyncFailed("Nonce error".to_string());
991
992        assert!(
993            reason1.same_variant(&reason2),
994            "Identical reasons should be the same variant"
995        );
996    }
997
998    #[test]
999    fn test_disabled_reason_same_variant_multiple_same_order() {
1000        // Multiple reasons with same variants in same order
1001        let reason1 = DisabledReason::Multiple(vec![
1002            DisabledReason::RpcValidationFailed("Error 1".to_string()),
1003            DisabledReason::BalanceCheckFailed("Error 2".to_string()),
1004        ]);
1005        let reason2 = DisabledReason::Multiple(vec![
1006            DisabledReason::RpcValidationFailed("Different error 1".to_string()),
1007            DisabledReason::BalanceCheckFailed("Different error 2".to_string()),
1008        ]);
1009
1010        assert!(
1011            reason1.same_variant(&reason2),
1012            "Multiple with same variant types in same order should be considered the same"
1013        );
1014    }
1015
1016    #[test]
1017    fn test_disabled_reason_same_variant_multiple_different_order() {
1018        // Multiple reasons with same variants but different order
1019        let reason1 = DisabledReason::Multiple(vec![
1020            DisabledReason::RpcValidationFailed("Error".to_string()),
1021            DisabledReason::BalanceCheckFailed("Error".to_string()),
1022        ]);
1023        let reason2 = DisabledReason::Multiple(vec![
1024            DisabledReason::BalanceCheckFailed("Error".to_string()),
1025            DisabledReason::RpcValidationFailed("Error".to_string()),
1026        ]);
1027
1028        assert!(
1029            !reason1.same_variant(&reason2),
1030            "Multiple with different order should not be considered the same"
1031        );
1032    }
1033
1034    #[test]
1035    fn test_disabled_reason_same_variant_multiple_different_length() {
1036        // Multiple reasons with different lengths
1037        let reason1 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1038            "Error".to_string(),
1039        )]);
1040        let reason2 = DisabledReason::Multiple(vec![
1041            DisabledReason::RpcValidationFailed("Error".to_string()),
1042            DisabledReason::BalanceCheckFailed("Error".to_string()),
1043        ]);
1044
1045        assert!(
1046            !reason1.same_variant(&reason2),
1047            "Multiple with different lengths should not be considered the same"
1048        );
1049    }
1050
1051    #[test]
1052    fn test_disabled_reason_same_variant_single_vs_multiple() {
1053        // Single reason vs Multiple should not be the same even if they contain the same variant
1054        let reason1 = DisabledReason::RpcValidationFailed("Error".to_string());
1055        let reason2 = DisabledReason::Multiple(vec![DisabledReason::RpcValidationFailed(
1056            "Error".to_string(),
1057        )]);
1058
1059        assert!(
1060            !reason1.same_variant(&reason2),
1061            "Single variant vs Multiple should not be considered the same"
1062        );
1063    }
1064
1065    // ===== RelayerNetworkType Tests =====
1066
1067    #[test]
1068    fn test_relayer_network_type_display() {
1069        assert_eq!(RelayerNetworkType::Evm.to_string(), "evm");
1070        assert_eq!(RelayerNetworkType::Solana.to_string(), "solana");
1071        assert_eq!(RelayerNetworkType::Stellar.to_string(), "stellar");
1072    }
1073
1074    #[test]
1075    fn test_relayer_network_type_from_config_file_type() {
1076        assert_eq!(
1077            RelayerNetworkType::from(ConfigFileNetworkType::Evm),
1078            RelayerNetworkType::Evm
1079        );
1080        assert_eq!(
1081            RelayerNetworkType::from(ConfigFileNetworkType::Solana),
1082            RelayerNetworkType::Solana
1083        );
1084        assert_eq!(
1085            RelayerNetworkType::from(ConfigFileNetworkType::Stellar),
1086            RelayerNetworkType::Stellar
1087        );
1088    }
1089
1090    #[test]
1091    fn test_config_file_network_type_from_relayer_type() {
1092        assert_eq!(
1093            ConfigFileNetworkType::from(RelayerNetworkType::Evm),
1094            ConfigFileNetworkType::Evm
1095        );
1096        assert_eq!(
1097            ConfigFileNetworkType::from(RelayerNetworkType::Solana),
1098            ConfigFileNetworkType::Solana
1099        );
1100        assert_eq!(
1101            ConfigFileNetworkType::from(RelayerNetworkType::Stellar),
1102            ConfigFileNetworkType::Stellar
1103        );
1104    }
1105
1106    #[test]
1107    fn test_relayer_network_type_serialization() {
1108        let evm_type = RelayerNetworkType::Evm;
1109        let serialized = serde_json::to_string(&evm_type).unwrap();
1110        assert_eq!(serialized, "\"evm\"");
1111
1112        let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1113        assert_eq!(deserialized, RelayerNetworkType::Evm);
1114
1115        // Test all types
1116        let types = vec![
1117            (RelayerNetworkType::Evm, "\"evm\""),
1118            (RelayerNetworkType::Solana, "\"solana\""),
1119            (RelayerNetworkType::Stellar, "\"stellar\""),
1120        ];
1121
1122        for (network_type, expected_json) in types {
1123            let serialized = serde_json::to_string(&network_type).unwrap();
1124            assert_eq!(serialized, expected_json);
1125
1126            let deserialized: RelayerNetworkType = serde_json::from_str(&serialized).unwrap();
1127            assert_eq!(deserialized, network_type);
1128        }
1129    }
1130
1131    // ===== Policy Struct Tests =====
1132
1133    #[test]
1134    fn test_relayer_evm_policy_default() {
1135        let default_policy = RelayerEvmPolicy::default();
1136        assert_eq!(default_policy.min_balance, None);
1137        assert_eq!(default_policy.gas_limit_estimation, None);
1138        assert_eq!(default_policy.gas_price_cap, None);
1139        assert_eq!(default_policy.whitelist_receivers, None);
1140        assert_eq!(default_policy.eip1559_pricing, None);
1141        assert_eq!(default_policy.private_transactions, None);
1142    }
1143
1144    #[test]
1145    fn test_relayer_evm_policy_serialization() {
1146        let policy = RelayerEvmPolicy {
1147            min_balance: Some(1000000000000000000),
1148            gas_limit_estimation: Some(true),
1149            gas_price_cap: Some(50000000000),
1150            whitelist_receivers: Some(vec!["0x123".to_string(), "0x456".to_string()]),
1151            eip1559_pricing: Some(false),
1152            private_transactions: Some(true),
1153        };
1154
1155        let serialized = serde_json::to_string(&policy).unwrap();
1156        let deserialized: RelayerEvmPolicy = serde_json::from_str(&serialized).unwrap();
1157        assert_eq!(policy, deserialized);
1158    }
1159
1160    #[test]
1161    fn test_allowed_token_new() {
1162        let token = SolanaAllowedTokensPolicy::new(
1163            "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1164            Some(100000),
1165            None,
1166        );
1167
1168        assert_eq!(token.mint, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
1169        assert_eq!(token.max_allowed_fee, Some(100000));
1170        assert_eq!(token.decimals, None);
1171        assert_eq!(token.symbol, None);
1172        assert_eq!(token.swap_config, None);
1173    }
1174
1175    #[test]
1176    fn test_allowed_token_new_partial() {
1177        let swap_config = SolanaAllowedTokensSwapConfig {
1178            slippage_percentage: Some(0.5),
1179            min_amount: Some(1000),
1180            max_amount: Some(10000000),
1181            retain_min_amount: Some(500),
1182        };
1183
1184        let token = SolanaAllowedTokensPolicy::new_partial(
1185            "TokenMint123".to_string(),
1186            Some(50000),
1187            Some(swap_config.clone()),
1188        );
1189
1190        assert_eq!(token.mint, "TokenMint123");
1191        assert_eq!(token.max_allowed_fee, Some(50000));
1192        assert_eq!(token.swap_config, Some(swap_config));
1193    }
1194
1195    #[test]
1196    fn test_allowed_token_swap_config_default() {
1197        let config = AllowedTokenSwapConfig::default();
1198        assert_eq!(config.slippage_percentage, None);
1199        assert_eq!(config.min_amount, None);
1200        assert_eq!(config.max_amount, None);
1201        assert_eq!(config.retain_min_amount, None);
1202    }
1203
1204    #[test]
1205    fn test_relayer_solana_fee_payment_strategy_default() {
1206        let default_strategy = SolanaFeePaymentStrategy::default();
1207        assert_eq!(default_strategy, SolanaFeePaymentStrategy::User);
1208    }
1209
1210    #[test]
1211    fn test_relayer_solana_swap_strategy_default() {
1212        let default_strategy = SolanaSwapStrategy::default();
1213        assert_eq!(default_strategy, SolanaSwapStrategy::Noop);
1214    }
1215
1216    #[test]
1217    fn test_jupiter_swap_options_default() {
1218        let options = JupiterSwapOptions::default();
1219        assert_eq!(options.priority_fee_max_lamports, None);
1220        assert_eq!(options.priority_level, None);
1221        assert_eq!(options.dynamic_compute_unit_limit, None);
1222    }
1223
1224    #[test]
1225    fn test_relayer_solana_swap_policy_default() {
1226        let policy = RelayerSolanaSwapConfig::default();
1227        assert_eq!(policy.strategy, None);
1228        assert_eq!(policy.cron_schedule, None);
1229        assert_eq!(policy.min_balance_threshold, None);
1230        assert_eq!(policy.jupiter_swap_options, None);
1231    }
1232
1233    #[test]
1234    fn test_relayer_solana_policy_default() {
1235        let policy = RelayerSolanaPolicy::default();
1236        assert_eq!(policy.allowed_programs, None);
1237        assert_eq!(policy.max_signatures, None);
1238        assert_eq!(policy.max_tx_data_size, None);
1239        assert_eq!(policy.min_balance, None);
1240        assert_eq!(policy.allowed_tokens, None);
1241        assert_eq!(policy.fee_payment_strategy, None);
1242        assert_eq!(policy.fee_margin_percentage, None);
1243        assert_eq!(policy.allowed_accounts, None);
1244        assert_eq!(policy.disallowed_accounts, None);
1245        assert_eq!(policy.max_allowed_fee_lamports, None);
1246        assert_eq!(policy.swap_config, None);
1247    }
1248
1249    #[test]
1250    fn test_relayer_solana_policy_get_allowed_tokens() {
1251        let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1252        let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1253
1254        let policy = RelayerSolanaPolicy {
1255            allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1256            ..RelayerSolanaPolicy::default()
1257        };
1258
1259        let tokens = policy.get_allowed_tokens();
1260        assert_eq!(tokens.len(), 2);
1261        assert_eq!(tokens[0], token1);
1262        assert_eq!(tokens[1], token2);
1263
1264        // Test empty case
1265        let empty_policy = RelayerSolanaPolicy::default();
1266        let empty_tokens = empty_policy.get_allowed_tokens();
1267        assert_eq!(empty_tokens.len(), 0);
1268    }
1269
1270    #[test]
1271    fn test_relayer_solana_policy_get_allowed_token_entry() {
1272        let token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1273        let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1274
1275        let policy = RelayerSolanaPolicy {
1276            allowed_tokens: Some(vec![token1.clone(), token2.clone()]),
1277            ..RelayerSolanaPolicy::default()
1278        };
1279
1280        let found_token = policy.get_allowed_token_entry("mint1").unwrap();
1281        assert_eq!(found_token, token1);
1282
1283        let not_found = policy.get_allowed_token_entry("mint3");
1284        assert!(not_found.is_none());
1285
1286        // Test empty case
1287        let empty_policy = RelayerSolanaPolicy::default();
1288        let empty_result = empty_policy.get_allowed_token_entry("mint1");
1289        assert!(empty_result.is_none());
1290    }
1291
1292    #[test]
1293    fn test_relayer_solana_policy_get_swap_config() {
1294        let swap_config = RelayerSolanaSwapConfig {
1295            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1296            cron_schedule: Some("0 0 * * *".to_string()),
1297            min_balance_threshold: Some(1000000),
1298            jupiter_swap_options: None,
1299        };
1300
1301        let policy = RelayerSolanaPolicy {
1302            swap_config: Some(swap_config.clone()),
1303            ..RelayerSolanaPolicy::default()
1304        };
1305
1306        let retrieved_config = policy.get_swap_config().unwrap();
1307        assert_eq!(retrieved_config, swap_config);
1308
1309        // Test None case
1310        let empty_policy = RelayerSolanaPolicy::default();
1311        assert!(empty_policy.get_swap_config().is_none());
1312    }
1313
1314    #[test]
1315    fn test_relayer_solana_policy_get_allowed_token_decimals() {
1316        let mut token1 = SolanaAllowedTokensPolicy::new("mint1".to_string(), Some(1000), None);
1317        token1.decimals = Some(9);
1318
1319        let token2 = SolanaAllowedTokensPolicy::new("mint2".to_string(), Some(2000), None);
1320        // token2.decimals is None
1321
1322        let policy = RelayerSolanaPolicy {
1323            allowed_tokens: Some(vec![token1, token2]),
1324            ..RelayerSolanaPolicy::default()
1325        };
1326
1327        assert_eq!(policy.get_allowed_token_decimals("mint1"), Some(9));
1328        assert_eq!(policy.get_allowed_token_decimals("mint2"), None);
1329        assert_eq!(policy.get_allowed_token_decimals("mint3"), None);
1330    }
1331
1332    #[test]
1333    fn test_relayer_stellar_policy_default() {
1334        let policy = RelayerStellarPolicy::default();
1335        assert_eq!(policy.min_balance, None);
1336        assert_eq!(policy.max_fee, None);
1337        assert_eq!(policy.timeout_seconds, None);
1338    }
1339
1340    // ===== RelayerNetworkPolicy Tests =====
1341
1342    #[test]
1343    fn test_relayer_network_policy_get_evm_policy() {
1344        let evm_policy = RelayerEvmPolicy {
1345            gas_price_cap: Some(50000000000),
1346            ..RelayerEvmPolicy::default()
1347        };
1348
1349        let network_policy = RelayerNetworkPolicy::Evm(evm_policy.clone());
1350        assert_eq!(network_policy.get_evm_policy(), evm_policy);
1351
1352        // Test non-EVM policy returns default
1353        let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
1354        assert_eq!(solana_policy.get_evm_policy(), RelayerEvmPolicy::default());
1355
1356        let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
1357        assert_eq!(stellar_policy.get_evm_policy(), RelayerEvmPolicy::default());
1358    }
1359
1360    #[test]
1361    fn test_relayer_network_policy_get_solana_policy() {
1362        let solana_policy = RelayerSolanaPolicy {
1363            min_balance: Some(5000000),
1364            ..RelayerSolanaPolicy::default()
1365        };
1366
1367        let network_policy = RelayerNetworkPolicy::Solana(solana_policy.clone());
1368        assert_eq!(network_policy.get_solana_policy(), solana_policy);
1369
1370        // Test non-Solana policy returns default
1371        let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
1372        assert_eq!(
1373            evm_policy.get_solana_policy(),
1374            RelayerSolanaPolicy::default()
1375        );
1376
1377        let stellar_policy = RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default());
1378        assert_eq!(
1379            stellar_policy.get_solana_policy(),
1380            RelayerSolanaPolicy::default()
1381        );
1382    }
1383
1384    #[test]
1385    fn test_relayer_network_policy_get_stellar_policy() {
1386        let stellar_policy = RelayerStellarPolicy {
1387            min_balance: Some(20000000),
1388            max_fee: Some(100000),
1389            timeout_seconds: Some(30),
1390            concurrent_transactions: None,
1391        };
1392
1393        let network_policy = RelayerNetworkPolicy::Stellar(stellar_policy.clone());
1394        assert_eq!(network_policy.get_stellar_policy(), stellar_policy);
1395
1396        // Test non-Stellar policy returns default
1397        let evm_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default());
1398        assert_eq!(
1399            evm_policy.get_stellar_policy(),
1400            RelayerStellarPolicy::default()
1401        );
1402
1403        let solana_policy = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
1404        assert_eq!(
1405            solana_policy.get_stellar_policy(),
1406            RelayerStellarPolicy::default()
1407        );
1408    }
1409
1410    // ===== Relayer Construction and Basic Tests =====
1411
1412    #[test]
1413    fn test_relayer_new() {
1414        let relayer = Relayer::new(
1415            "test-relayer".to_string(),
1416            "Test Relayer".to_string(),
1417            "mainnet".to_string(),
1418            false,
1419            RelayerNetworkType::Evm,
1420            Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default())),
1421            "test-signer".to_string(),
1422            Some("test-notification".to_string()),
1423            None,
1424        );
1425
1426        assert_eq!(relayer.id, "test-relayer");
1427        assert_eq!(relayer.name, "Test Relayer");
1428        assert_eq!(relayer.network, "mainnet");
1429        assert!(!relayer.paused);
1430        assert_eq!(relayer.network_type, RelayerNetworkType::Evm);
1431        assert_eq!(relayer.signer_id, "test-signer");
1432        assert_eq!(
1433            relayer.notification_id,
1434            Some("test-notification".to_string())
1435        );
1436        assert!(relayer.policies.is_some());
1437        assert_eq!(relayer.custom_rpc_urls, None);
1438    }
1439
1440    // ===== Relayer Validation Tests =====
1441
1442    #[test]
1443    fn test_relayer_validation_success() {
1444        let relayer = Relayer::new(
1445            "valid-relayer-id".to_string(),
1446            "Valid Relayer".to_string(),
1447            "mainnet".to_string(),
1448            false,
1449            RelayerNetworkType::Evm,
1450            None,
1451            "valid-signer".to_string(),
1452            None,
1453            None,
1454        );
1455
1456        assert!(relayer.validate().is_ok());
1457    }
1458
1459    #[test]
1460    fn test_relayer_validation_empty_id() {
1461        let relayer = Relayer::new(
1462            "".to_string(), // Empty ID
1463            "Valid Relayer".to_string(),
1464            "mainnet".to_string(),
1465            false,
1466            RelayerNetworkType::Evm,
1467            None,
1468            "valid-signer".to_string(),
1469            None,
1470            None,
1471        );
1472
1473        let result = relayer.validate();
1474        assert!(result.is_err());
1475        assert!(matches!(
1476            result.unwrap_err(),
1477            RelayerValidationError::EmptyId
1478        ));
1479    }
1480
1481    #[test]
1482    fn test_relayer_validation_id_too_long() {
1483        let long_id = "a".repeat(37); // 37 characters, exceeds 36 limit
1484        let relayer = Relayer::new(
1485            long_id,
1486            "Valid Relayer".to_string(),
1487            "mainnet".to_string(),
1488            false,
1489            RelayerNetworkType::Evm,
1490            None,
1491            "valid-signer".to_string(),
1492            None,
1493            None,
1494        );
1495
1496        let result = relayer.validate();
1497        assert!(result.is_err());
1498        assert!(matches!(
1499            result.unwrap_err(),
1500            RelayerValidationError::IdTooLong
1501        ));
1502    }
1503
1504    #[test]
1505    fn test_relayer_validation_invalid_id_format() {
1506        let relayer = Relayer::new(
1507            "invalid@id".to_string(), // Contains invalid character @
1508            "Valid Relayer".to_string(),
1509            "mainnet".to_string(),
1510            false,
1511            RelayerNetworkType::Evm,
1512            None,
1513            "valid-signer".to_string(),
1514            None,
1515            None,
1516        );
1517
1518        let result = relayer.validate();
1519        assert!(result.is_err());
1520        assert!(matches!(
1521            result.unwrap_err(),
1522            RelayerValidationError::InvalidIdFormat
1523        ));
1524    }
1525
1526    #[test]
1527    fn test_relayer_validation_empty_name() {
1528        let relayer = Relayer::new(
1529            "valid-id".to_string(),
1530            "".to_string(), // Empty name
1531            "mainnet".to_string(),
1532            false,
1533            RelayerNetworkType::Evm,
1534            None,
1535            "valid-signer".to_string(),
1536            None,
1537            None,
1538        );
1539
1540        let result = relayer.validate();
1541        assert!(result.is_err());
1542        assert!(matches!(
1543            result.unwrap_err(),
1544            RelayerValidationError::EmptyName
1545        ));
1546    }
1547
1548    #[test]
1549    fn test_relayer_validation_empty_network() {
1550        let relayer = Relayer::new(
1551            "valid-id".to_string(),
1552            "Valid Relayer".to_string(),
1553            "".to_string(), // Empty network
1554            false,
1555            RelayerNetworkType::Evm,
1556            None,
1557            "valid-signer".to_string(),
1558            None,
1559            None,
1560        );
1561
1562        let result = relayer.validate();
1563        assert!(result.is_err());
1564        assert!(matches!(
1565            result.unwrap_err(),
1566            RelayerValidationError::EmptyNetwork
1567        ));
1568    }
1569
1570    #[test]
1571    fn test_relayer_validation_empty_signer_id() {
1572        let relayer = Relayer::new(
1573            "valid-id".to_string(),
1574            "Valid Relayer".to_string(),
1575            "mainnet".to_string(),
1576            false,
1577            RelayerNetworkType::Evm,
1578            None,
1579            "".to_string(), // Empty signer ID
1580            None,
1581            None,
1582        );
1583
1584        let result = relayer.validate();
1585        assert!(result.is_err());
1586        // This should trigger InvalidPolicy error due to empty signer ID
1587        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1588            assert!(msg.contains("Signer ID cannot be empty"));
1589        } else {
1590            panic!("Expected InvalidPolicy error for empty signer ID");
1591        }
1592    }
1593
1594    #[test]
1595    fn test_relayer_validation_mismatched_network_type_and_policy() {
1596        let relayer = Relayer::new(
1597            "valid-id".to_string(),
1598            "Valid Relayer".to_string(),
1599            "mainnet".to_string(),
1600            false,
1601            RelayerNetworkType::Evm, // EVM network type
1602            Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default())), // But Solana policy
1603            "valid-signer".to_string(),
1604            None,
1605            None,
1606        );
1607
1608        let result = relayer.validate();
1609        assert!(result.is_err());
1610        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1611            assert!(msg.contains("Network type") && msg.contains("does not match policy type"));
1612        } else {
1613            panic!("Expected InvalidPolicy error for mismatched network type and policy");
1614        }
1615    }
1616
1617    #[test]
1618    fn test_relayer_validation_invalid_rpc_url() {
1619        let relayer = Relayer::new(
1620            "valid-id".to_string(),
1621            "Valid Relayer".to_string(),
1622            "mainnet".to_string(),
1623            false,
1624            RelayerNetworkType::Evm,
1625            None,
1626            "valid-signer".to_string(),
1627            None,
1628            Some(vec![RpcConfig::new("invalid-url".to_string())]), // Invalid URL
1629        );
1630
1631        let result = relayer.validate();
1632        assert!(result.is_err());
1633        assert!(matches!(
1634            result.unwrap_err(),
1635            RelayerValidationError::InvalidRpcUrl(_)
1636        ));
1637    }
1638
1639    #[test]
1640    fn test_relayer_validation_invalid_rpc_weight() {
1641        let relayer = Relayer::new(
1642            "valid-id".to_string(),
1643            "Valid Relayer".to_string(),
1644            "mainnet".to_string(),
1645            false,
1646            RelayerNetworkType::Evm,
1647            None,
1648            "valid-signer".to_string(),
1649            None,
1650            Some(vec![RpcConfig {
1651                url: "https://example.com".to_string(),
1652                weight: 150,
1653            }]), // Weight > 100
1654        );
1655
1656        let result = relayer.validate();
1657        assert!(result.is_err());
1658        assert!(matches!(
1659            result.unwrap_err(),
1660            RelayerValidationError::InvalidRpcWeight
1661        ));
1662    }
1663
1664    // ===== Solana-specific Validation Tests =====
1665
1666    #[test]
1667    fn test_relayer_validation_solana_invalid_public_key() {
1668        let policy = RelayerSolanaPolicy {
1669            allowed_programs: Some(vec!["invalid-pubkey".to_string()]), // Invalid Solana pubkey
1670            ..RelayerSolanaPolicy::default()
1671        };
1672
1673        let relayer = Relayer::new(
1674            "valid-id".to_string(),
1675            "Valid Relayer".to_string(),
1676            "mainnet".to_string(),
1677            false,
1678            RelayerNetworkType::Solana,
1679            Some(RelayerNetworkPolicy::Solana(policy)),
1680            "valid-signer".to_string(),
1681            None,
1682            None,
1683        );
1684
1685        let result = relayer.validate();
1686        assert!(result.is_err());
1687        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1688            assert!(msg.contains("Public key must be a valid Solana address"));
1689        } else {
1690            panic!("Expected InvalidPolicy error for invalid Solana public key");
1691        }
1692    }
1693
1694    #[test]
1695    fn test_relayer_validation_solana_valid_public_key() {
1696        let policy = RelayerSolanaPolicy {
1697            allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]), // Valid Solana pubkey
1698            ..RelayerSolanaPolicy::default()
1699        };
1700
1701        let relayer = Relayer::new(
1702            "valid-id".to_string(),
1703            "Valid Relayer".to_string(),
1704            "mainnet".to_string(),
1705            false,
1706            RelayerNetworkType::Solana,
1707            Some(RelayerNetworkPolicy::Solana(policy)),
1708            "valid-signer".to_string(),
1709            None,
1710            None,
1711        );
1712
1713        assert!(relayer.validate().is_ok());
1714    }
1715
1716    #[test]
1717    fn test_relayer_validation_solana_negative_fee_margin() {
1718        let policy = RelayerSolanaPolicy {
1719            fee_margin_percentage: Some(-1.0), // Negative fee margin
1720            ..RelayerSolanaPolicy::default()
1721        };
1722
1723        let relayer = Relayer::new(
1724            "valid-id".to_string(),
1725            "Valid Relayer".to_string(),
1726            "mainnet".to_string(),
1727            false,
1728            RelayerNetworkType::Solana,
1729            Some(RelayerNetworkPolicy::Solana(policy)),
1730            "valid-signer".to_string(),
1731            None,
1732            None,
1733        );
1734
1735        let result = relayer.validate();
1736        assert!(result.is_err());
1737        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1738            assert!(msg.contains("Negative fee margin percentage values are not accepted"));
1739        } else {
1740            panic!("Expected InvalidPolicy error for negative fee margin");
1741        }
1742    }
1743
1744    #[test]
1745    fn test_relayer_validation_solana_conflicting_accounts() {
1746        let policy = RelayerSolanaPolicy {
1747            allowed_accounts: Some(vec!["11111111111111111111111111111111".to_string()]),
1748            disallowed_accounts: Some(vec!["22222222222222222222222222222222".to_string()]),
1749            ..RelayerSolanaPolicy::default()
1750        };
1751
1752        let relayer = Relayer::new(
1753            "valid-id".to_string(),
1754            "Valid Relayer".to_string(),
1755            "mainnet".to_string(),
1756            false,
1757            RelayerNetworkType::Solana,
1758            Some(RelayerNetworkPolicy::Solana(policy)),
1759            "valid-signer".to_string(),
1760            None,
1761            None,
1762        );
1763
1764        let result = relayer.validate();
1765        assert!(result.is_err());
1766        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1767            assert!(msg.contains("allowed_accounts and disallowed_accounts cannot be both present"));
1768        } else {
1769            panic!("Expected InvalidPolicy error for conflicting accounts");
1770        }
1771    }
1772
1773    #[test]
1774    fn test_relayer_validation_solana_swap_config_wrong_fee_payment_strategy() {
1775        let swap_config = RelayerSolanaSwapConfig {
1776            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1777            ..RelayerSolanaSwapConfig::default()
1778        };
1779
1780        let policy = RelayerSolanaPolicy {
1781            fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer), // Relayer strategy
1782            swap_config: Some(swap_config),                                // But has swap config
1783            ..RelayerSolanaPolicy::default()
1784        };
1785
1786        let relayer = Relayer::new(
1787            "valid-id".to_string(),
1788            "Valid Relayer".to_string(),
1789            "mainnet".to_string(),
1790            false,
1791            RelayerNetworkType::Solana,
1792            Some(RelayerNetworkPolicy::Solana(policy)),
1793            "valid-signer".to_string(),
1794            None,
1795            None,
1796        );
1797
1798        let result = relayer.validate();
1799        assert!(result.is_err());
1800        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1801            assert!(msg.contains("Swap config only supported for user fee payment strategy"));
1802        } else {
1803            panic!("Expected InvalidPolicy error for swap config with relayer fee payment");
1804        }
1805    }
1806
1807    #[test]
1808    fn test_relayer_validation_solana_jupiter_strategy_wrong_network() {
1809        let swap_config = RelayerSolanaSwapConfig {
1810            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1811            ..RelayerSolanaSwapConfig::default()
1812        };
1813
1814        let policy = RelayerSolanaPolicy {
1815            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1816            swap_config: Some(swap_config),
1817            ..RelayerSolanaPolicy::default()
1818        };
1819
1820        let relayer = Relayer::new(
1821            "valid-id".to_string(),
1822            "Valid Relayer".to_string(),
1823            "testnet".to_string(), // Not mainnet-beta
1824            false,
1825            RelayerNetworkType::Solana,
1826            Some(RelayerNetworkPolicy::Solana(policy)),
1827            "valid-signer".to_string(),
1828            None,
1829            None,
1830        );
1831
1832        let result = relayer.validate();
1833        assert!(result.is_err());
1834        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1835            assert!(msg.contains("strategy is only supported on mainnet-beta"));
1836        } else {
1837            panic!("Expected InvalidPolicy error for Jupiter strategy on wrong network");
1838        }
1839    }
1840
1841    #[test]
1842    fn test_relayer_validation_solana_empty_cron_schedule() {
1843        let swap_config = RelayerSolanaSwapConfig {
1844            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1845            cron_schedule: Some("".to_string()), // Empty cron schedule
1846            ..RelayerSolanaSwapConfig::default()
1847        };
1848
1849        let policy = RelayerSolanaPolicy {
1850            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1851            swap_config: Some(swap_config),
1852            ..RelayerSolanaPolicy::default()
1853        };
1854
1855        let relayer = Relayer::new(
1856            "valid-id".to_string(),
1857            "Valid Relayer".to_string(),
1858            "mainnet-beta".to_string(),
1859            false,
1860            RelayerNetworkType::Solana,
1861            Some(RelayerNetworkPolicy::Solana(policy)),
1862            "valid-signer".to_string(),
1863            None,
1864            None,
1865        );
1866
1867        let result = relayer.validate();
1868        assert!(result.is_err());
1869        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1870            assert!(msg.contains("Empty cron schedule is not accepted"));
1871        } else {
1872            panic!("Expected InvalidPolicy error for empty cron schedule");
1873        }
1874    }
1875
1876    #[test]
1877    fn test_relayer_validation_solana_invalid_cron_schedule() {
1878        let swap_config = RelayerSolanaSwapConfig {
1879            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1880            cron_schedule: Some("invalid cron".to_string()), // Invalid cron format
1881            ..RelayerSolanaSwapConfig::default()
1882        };
1883
1884        let policy = RelayerSolanaPolicy {
1885            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1886            swap_config: Some(swap_config),
1887            ..RelayerSolanaPolicy::default()
1888        };
1889
1890        let relayer = Relayer::new(
1891            "valid-id".to_string(),
1892            "Valid Relayer".to_string(),
1893            "mainnet-beta".to_string(),
1894            false,
1895            RelayerNetworkType::Solana,
1896            Some(RelayerNetworkPolicy::Solana(policy)),
1897            "valid-signer".to_string(),
1898            None,
1899            None,
1900        );
1901
1902        let result = relayer.validate();
1903        assert!(result.is_err());
1904        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1905            assert!(msg.contains("Invalid cron schedule format"));
1906        } else {
1907            panic!("Expected InvalidPolicy error for invalid cron schedule");
1908        }
1909    }
1910
1911    #[test]
1912    fn test_relayer_validation_solana_jupiter_options_wrong_strategy() {
1913        let jupiter_options = JupiterSwapOptions {
1914            priority_fee_max_lamports: Some(10000),
1915            priority_level: Some("high".to_string()),
1916            dynamic_compute_unit_limit: Some(true),
1917        };
1918
1919        let swap_config = RelayerSolanaSwapConfig {
1920            strategy: Some(SolanaSwapStrategy::JupiterUltra), // Wrong strategy
1921            jupiter_swap_options: Some(jupiter_options),
1922            ..RelayerSolanaSwapConfig::default()
1923        };
1924
1925        let policy = RelayerSolanaPolicy {
1926            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1927            swap_config: Some(swap_config),
1928            ..RelayerSolanaPolicy::default()
1929        };
1930
1931        let relayer = Relayer::new(
1932            "valid-id".to_string(),
1933            "Valid Relayer".to_string(),
1934            "mainnet-beta".to_string(),
1935            false,
1936            RelayerNetworkType::Solana,
1937            Some(RelayerNetworkPolicy::Solana(policy)),
1938            "valid-signer".to_string(),
1939            None,
1940            None,
1941        );
1942
1943        let result = relayer.validate();
1944        assert!(result.is_err());
1945        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1946            assert!(msg.contains("JupiterSwap options are only valid for JupiterSwap strategy"));
1947        } else {
1948            panic!("Expected InvalidPolicy error for Jupiter options with wrong strategy");
1949        }
1950    }
1951
1952    #[test]
1953    fn test_relayer_validation_solana_jupiter_zero_max_lamports() {
1954        let jupiter_options = JupiterSwapOptions {
1955            priority_fee_max_lamports: Some(0), // Zero is invalid
1956            priority_level: Some("high".to_string()),
1957            dynamic_compute_unit_limit: Some(true),
1958        };
1959
1960        let swap_config = RelayerSolanaSwapConfig {
1961            strategy: Some(SolanaSwapStrategy::JupiterSwap),
1962            jupiter_swap_options: Some(jupiter_options),
1963            ..RelayerSolanaSwapConfig::default()
1964        };
1965
1966        let policy = RelayerSolanaPolicy {
1967            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
1968            swap_config: Some(swap_config),
1969            ..RelayerSolanaPolicy::default()
1970        };
1971
1972        let relayer = Relayer::new(
1973            "valid-id".to_string(),
1974            "Valid Relayer".to_string(),
1975            "mainnet-beta".to_string(),
1976            false,
1977            RelayerNetworkType::Solana,
1978            Some(RelayerNetworkPolicy::Solana(policy)),
1979            "valid-signer".to_string(),
1980            None,
1981            None,
1982        );
1983
1984        let result = relayer.validate();
1985        assert!(result.is_err());
1986        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
1987            assert!(msg.contains("Max lamports must be greater than 0"));
1988        } else {
1989            panic!("Expected InvalidPolicy error for zero max lamports");
1990        }
1991    }
1992
1993    #[test]
1994    fn test_relayer_validation_solana_jupiter_empty_priority_level() {
1995        let jupiter_options = JupiterSwapOptions {
1996            priority_fee_max_lamports: Some(10000),
1997            priority_level: Some("".to_string()), // Empty priority level
1998            dynamic_compute_unit_limit: Some(true),
1999        };
2000
2001        let swap_config = RelayerSolanaSwapConfig {
2002            strategy: Some(SolanaSwapStrategy::JupiterSwap),
2003            jupiter_swap_options: Some(jupiter_options),
2004            ..RelayerSolanaSwapConfig::default()
2005        };
2006
2007        let policy = RelayerSolanaPolicy {
2008            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2009            swap_config: Some(swap_config),
2010            ..RelayerSolanaPolicy::default()
2011        };
2012
2013        let relayer = Relayer::new(
2014            "valid-id".to_string(),
2015            "Valid Relayer".to_string(),
2016            "mainnet-beta".to_string(),
2017            false,
2018            RelayerNetworkType::Solana,
2019            Some(RelayerNetworkPolicy::Solana(policy)),
2020            "valid-signer".to_string(),
2021            None,
2022            None,
2023        );
2024
2025        let result = relayer.validate();
2026        assert!(result.is_err());
2027        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2028            assert!(msg.contains("Priority level cannot be empty"));
2029        } else {
2030            panic!("Expected InvalidPolicy error for empty priority level");
2031        }
2032    }
2033
2034    #[test]
2035    fn test_relayer_validation_solana_jupiter_invalid_priority_level() {
2036        let jupiter_options = JupiterSwapOptions {
2037            priority_fee_max_lamports: Some(10000),
2038            priority_level: Some("invalid".to_string()), // Invalid priority level
2039            dynamic_compute_unit_limit: Some(true),
2040        };
2041
2042        let swap_config = RelayerSolanaSwapConfig {
2043            strategy: Some(SolanaSwapStrategy::JupiterSwap),
2044            jupiter_swap_options: Some(jupiter_options),
2045            ..RelayerSolanaSwapConfig::default()
2046        };
2047
2048        let policy = RelayerSolanaPolicy {
2049            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2050            swap_config: Some(swap_config),
2051            ..RelayerSolanaPolicy::default()
2052        };
2053
2054        let relayer = Relayer::new(
2055            "valid-id".to_string(),
2056            "Valid Relayer".to_string(),
2057            "mainnet-beta".to_string(),
2058            false,
2059            RelayerNetworkType::Solana,
2060            Some(RelayerNetworkPolicy::Solana(policy)),
2061            "valid-signer".to_string(),
2062            None,
2063            None,
2064        );
2065
2066        let result = relayer.validate();
2067        assert!(result.is_err());
2068        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2069            assert!(msg.contains("Priority level must be one of: medium, high, veryHigh"));
2070        } else {
2071            panic!("Expected InvalidPolicy error for invalid priority level");
2072        }
2073    }
2074
2075    #[test]
2076    fn test_relayer_validation_solana_jupiter_missing_priority_fee() {
2077        let jupiter_options = JupiterSwapOptions {
2078            priority_fee_max_lamports: None, // Missing
2079            priority_level: Some("high".to_string()),
2080            dynamic_compute_unit_limit: Some(true),
2081        };
2082
2083        let swap_config = RelayerSolanaSwapConfig {
2084            strategy: Some(SolanaSwapStrategy::JupiterSwap),
2085            jupiter_swap_options: Some(jupiter_options),
2086            ..RelayerSolanaSwapConfig::default()
2087        };
2088
2089        let policy = RelayerSolanaPolicy {
2090            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2091            swap_config: Some(swap_config),
2092            ..RelayerSolanaPolicy::default()
2093        };
2094
2095        let relayer = Relayer::new(
2096            "valid-id".to_string(),
2097            "Valid Relayer".to_string(),
2098            "mainnet-beta".to_string(),
2099            false,
2100            RelayerNetworkType::Solana,
2101            Some(RelayerNetworkPolicy::Solana(policy)),
2102            "valid-signer".to_string(),
2103            None,
2104            None,
2105        );
2106
2107        let result = relayer.validate();
2108        assert!(result.is_err());
2109        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2110            assert!(msg.contains("Priority Fee Max lamports must be set if priority level is set"));
2111        } else {
2112            panic!("Expected InvalidPolicy error for missing priority fee");
2113        }
2114    }
2115
2116    #[test]
2117    fn test_relayer_validation_solana_jupiter_missing_priority_level() {
2118        let jupiter_options = JupiterSwapOptions {
2119            priority_fee_max_lamports: Some(10000),
2120            priority_level: None, // Missing
2121            dynamic_compute_unit_limit: Some(true),
2122        };
2123
2124        let swap_config = RelayerSolanaSwapConfig {
2125            strategy: Some(SolanaSwapStrategy::JupiterSwap),
2126            jupiter_swap_options: Some(jupiter_options),
2127            ..RelayerSolanaSwapConfig::default()
2128        };
2129
2130        let policy = RelayerSolanaPolicy {
2131            fee_payment_strategy: Some(SolanaFeePaymentStrategy::User),
2132            swap_config: Some(swap_config),
2133            ..RelayerSolanaPolicy::default()
2134        };
2135
2136        let relayer = Relayer::new(
2137            "valid-id".to_string(),
2138            "Valid Relayer".to_string(),
2139            "mainnet-beta".to_string(),
2140            false,
2141            RelayerNetworkType::Solana,
2142            Some(RelayerNetworkPolicy::Solana(policy)),
2143            "valid-signer".to_string(),
2144            None,
2145            None,
2146        );
2147
2148        let result = relayer.validate();
2149        assert!(result.is_err());
2150        if let Err(RelayerValidationError::InvalidPolicy(msg)) = result {
2151            assert!(msg.contains("Priority level must be set if priority fee max lamports is set"));
2152        } else {
2153            panic!("Expected InvalidPolicy error for missing priority level");
2154        }
2155    }
2156
2157    // ===== Error Conversion Tests =====
2158
2159    #[test]
2160    fn test_relayer_validation_error_to_api_error() {
2161        use crate::models::ApiError;
2162
2163        // Test each variant
2164        let errors = vec![
2165            (RelayerValidationError::EmptyId, "ID cannot be empty"),
2166            (RelayerValidationError::InvalidIdFormat, "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long"),
2167            (RelayerValidationError::IdTooLong, "ID must not exceed 36 characters"),
2168            (RelayerValidationError::EmptyName, "Name cannot be empty"),
2169            (RelayerValidationError::EmptyNetwork, "Network cannot be empty"),
2170            (RelayerValidationError::InvalidPolicy("test error".to_string()), "Invalid relayer policy: test error"),
2171            (RelayerValidationError::InvalidRpcUrl("http://invalid".to_string()), "Invalid RPC URL: http://invalid"),
2172            (RelayerValidationError::InvalidRpcWeight, "RPC URL weight must be in range 0-100"),
2173            (RelayerValidationError::InvalidField("test field error".to_string()), "test field error"),
2174        ];
2175
2176        for (validation_error, expected_message) in errors {
2177            let api_error: ApiError = validation_error.into();
2178            if let ApiError::BadRequest(message) = api_error {
2179                assert_eq!(message, expected_message);
2180            } else {
2181                panic!("Expected BadRequest variant");
2182            }
2183        }
2184    }
2185
2186    // ===== JSON Patch Tests (already existing) =====
2187
2188    #[test]
2189    fn test_apply_json_patch_comprehensive() {
2190        // Create a sample relayer
2191        let relayer = Relayer {
2192            id: "test-relayer".to_string(),
2193            name: "Original Name".to_string(),
2194            network: "mainnet".to_string(),
2195            paused: false,
2196            network_type: RelayerNetworkType::Evm,
2197            policies: Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
2198                min_balance: Some(1000000000000000000),
2199                gas_limit_estimation: Some(true),
2200                gas_price_cap: Some(50000000000),
2201                whitelist_receivers: None,
2202                eip1559_pricing: Some(false),
2203                private_transactions: None,
2204            })),
2205            signer_id: "test-signer".to_string(),
2206            notification_id: Some("old-notification".to_string()),
2207            custom_rpc_urls: None,
2208        };
2209
2210        // Create a JSON patch
2211        let patch = json!({
2212            "name": "Updated Name via JSON Patch",
2213            "paused": true,
2214            "policies": {
2215                "min_balance": "2000000000000000000",
2216                "gas_price_cap": null,  // Remove this field
2217                "eip1559_pricing": true,  // Update this field
2218                "whitelist_receivers": ["0x123", "0x456"]  // Add this field
2219                // gas_limit_estimation not mentioned - should remain unchanged
2220            },
2221            "notification_id": null, // Remove notification
2222            "custom_rpc_urls": [{"url": "https://example.com", "weight": 100}]
2223        });
2224
2225        // Apply the JSON patch - all logic now handled uniformly!
2226        let updated_relayer = relayer.apply_json_patch(&patch).unwrap();
2227
2228        // Verify all updates were applied correctly
2229        assert_eq!(updated_relayer.name, "Updated Name via JSON Patch");
2230        assert!(updated_relayer.paused);
2231        assert_eq!(updated_relayer.notification_id, None); // Removed
2232        assert!(updated_relayer.custom_rpc_urls.is_some());
2233
2234        // Verify policy merge patch worked correctly
2235        if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = updated_relayer.policies {
2236            assert_eq!(evm_policy.min_balance, Some(2000000000000000000)); // Updated
2237            assert_eq!(evm_policy.gas_price_cap, None); // Removed (was null)
2238            assert_eq!(evm_policy.eip1559_pricing, Some(true)); // Updated
2239            assert_eq!(evm_policy.gas_limit_estimation, Some(true)); // Unchanged
2240            assert_eq!(
2241                evm_policy.whitelist_receivers,
2242                Some(vec!["0x123".to_string(), "0x456".to_string()])
2243            ); // Added
2244            assert_eq!(evm_policy.private_transactions, None); // Unchanged
2245        } else {
2246            panic!("Expected EVM policy");
2247        }
2248    }
2249
2250    #[test]
2251    fn test_apply_json_patch_validation_failure() {
2252        let relayer = Relayer {
2253            id: "test-relayer".to_string(),
2254            name: "Original Name".to_string(),
2255            network: "mainnet".to_string(),
2256            paused: false,
2257            network_type: RelayerNetworkType::Evm,
2258            policies: None,
2259            signer_id: "test-signer".to_string(),
2260            notification_id: None,
2261            custom_rpc_urls: None,
2262        };
2263
2264        // Invalid patch - field that would make the result invalid
2265        let invalid_patch = json!({
2266            "name": ""  // Empty name should fail validation
2267        });
2268
2269        // Should fail validation during final validation step
2270        let result = relayer.apply_json_patch(&invalid_patch);
2271        assert!(result.is_err());
2272        assert!(result
2273            .unwrap_err()
2274            .to_string()
2275            .contains("Relayer name cannot be empty"));
2276    }
2277
2278    #[test]
2279    fn test_apply_json_patch_invalid_result() {
2280        let relayer = Relayer {
2281            id: "test-relayer".to_string(),
2282            name: "Original Name".to_string(),
2283            network: "mainnet".to_string(),
2284            paused: false,
2285            network_type: RelayerNetworkType::Evm,
2286            policies: None,
2287            signer_id: "test-signer".to_string(),
2288            notification_id: None,
2289            custom_rpc_urls: None,
2290        };
2291
2292        // Patch that would create an invalid structure
2293        let invalid_patch = json!({
2294            "network_type": "invalid_type"  // Invalid enum value
2295        });
2296
2297        // Should fail when converting back to domain object
2298        let result = relayer.apply_json_patch(&invalid_patch);
2299        assert!(result.is_err());
2300        // The error now occurs during the initial validation step
2301        let error_msg = result.unwrap_err().to_string();
2302        assert!(
2303            error_msg.contains("Invalid patch format")
2304                || error_msg.contains("Invalid result after patch")
2305        );
2306    }
2307}