openzeppelin_relayer/models/relayer/
mod.rs

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