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