1mod 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#[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#[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#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
110#[serde(deny_unknown_fields)]
111pub struct SolanaAllowedTokensSwapConfig {
112 #[schema(nullable = false)]
114 pub slippage_percentage: Option<f32>,
115 #[schema(nullable = false)]
117 pub min_amount: Option<u64>,
118 #[schema(nullable = false)]
120 pub max_amount: Option<u64>,
121 #[schema(nullable = false)]
123 pub retain_min_amount: Option<u64>,
124}
125
126#[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 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 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#[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#[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#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
192#[serde(deny_unknown_fields)]
193pub struct JupiterSwapOptions {
194 #[schema(nullable = false)]
196 pub priority_fee_max_lamports: Option<u64>,
197 #[schema(nullable = false)]
199 pub priority_level: Option<String>,
200 #[schema(nullable = false)]
201 pub dynamic_compute_unit_limit: Option<bool>,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
206#[serde(deny_unknown_fields)]
207pub struct RelayerSolanaSwapConfig {
208 #[schema(nullable = false)]
210 pub strategy: Option<SolanaSwapStrategy>,
211 #[schema(nullable = false)]
213 pub cron_schedule: Option<String>,
214 #[schema(nullable = false)]
216 pub min_balance_threshold: Option<u64>,
217 #[schema(nullable = false)]
219 pub jupiter_swap_options: Option<JupiterSwapOptions>,
220}
221
222#[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 pub fn get_allowed_tokens(&self) -> Vec<SolanaAllowedTokensPolicy> {
253 self.allowed_tokens.clone().unwrap_or_default()
254 }
255
256 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 pub fn get_swap_config(&self) -> Option<RelayerSolanaSwapConfig> {
267 self.swap_config.clone()
268 }
269
270 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#[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#[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 pub fn get_evm_policy(&self) -> RelayerEvmPolicy {
303 match self {
304 Self::Evm(policy) => policy.clone(),
305 _ => RelayerEvmPolicy::default(),
306 }
307 }
308
309 pub fn get_solana_policy(&self) -> RelayerSolanaPolicy {
311 match self {
312 Self::Solana(policy) => policy.clone(),
313 _ => RelayerSolanaPolicy::default(),
314 }
315 }
316
317 pub fn get_stellar_policy(&self) -> RelayerStellarPolicy {
319 match self {
320 Self::Stellar(policy) => policy.clone(),
321 _ => RelayerStellarPolicy::default(),
322 }
323 }
324}
325
326#[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 #[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 pub fn validate(&self) -> Result<(), RelayerValidationError> {
384 if self.id.is_empty() {
386 return Err(RelayerValidationError::EmptyId);
387 }
388
389 if self.id.len() > 36 {
391 return Err(RelayerValidationError::IdTooLong);
392 }
393
394 Validate::validate(self).map_err(|validation_errors| {
396 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, };
409 }
410 }
411 RelayerValidationError::InvalidIdFormat
413 })?;
414
415 self.validate_policies()?;
417 self.validate_custom_rpc_urls()?;
418
419 Ok(())
420 }
421
422 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 }
431 (RelayerNetworkType::Stellar, Some(RelayerNetworkPolicy::Stellar(_))) => {
432 }
434 (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 (_, None) => {}
449 }
450 Ok(())
451 }
452
453 fn validate_solana_policy(
455 &self,
456 policy: &RelayerSolanaPolicy,
457 ) -> Result<(), RelayerValidationError> {
458 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 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 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 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 if let Some(swap_config) = &policy.swap_config {
487 self.validate_solana_swap_config(swap_config, policy)?;
488 }
489
490 Ok(())
491 }
492
493 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 fn validate_solana_swap_config(
517 &self,
518 swap_config: &RelayerSolanaSwapConfig,
519 policy: &RelayerSolanaPolicy,
520 ) -> Result<(), RelayerValidationError> {
521 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 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 }
544 }
545 }
546
547 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 if let Some(jupiter_options) = &swap_config.jupiter_swap_options {
562 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 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 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 pub fn apply_json_patch(
639 &self,
640 patch: &serde_json::Value,
641 ) -> Result<Self, RelayerValidationError> {
642 let mut domain_json = serde_json::to_value(self).map_err(|e| {
644 RelayerValidationError::InvalidField(format!("Serialization error: {}", e))
645 })?;
646
647 json_patch::merge(&mut domain_json, patch);
649
650 let updated: Relayer = serde_json::from_value(domain_json).map_err(|e| {
652 RelayerValidationError::InvalidField(format!("Invalid result after patch: {}", e))
653 })?;
654
655 updated.validate()?;
657
658 Ok(updated)
659 }
660}
661
662#[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
685impl 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 #[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 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 #[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 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 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 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 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 #[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 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 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 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 #[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 #[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(), "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); 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(), "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(), "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(), 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(), None,
1234 None,
1235 );
1236
1237 let result = relayer.validate();
1238 assert!(result.is_err());
1239 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, Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default())), "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())]), );
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 }]), );
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 #[test]
1320 fn test_relayer_validation_solana_invalid_public_key() {
1321 let policy = RelayerSolanaPolicy {
1322 allowed_programs: Some(vec!["invalid-pubkey".to_string()]), ..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()]), ..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), ..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), swap_config: Some(swap_config), ..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(), 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()), ..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()), ..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), 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), 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()), 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()), 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, 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, 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 #[test]
1813 fn test_relayer_validation_error_to_api_error() {
1814 use crate::models::ApiError;
1815
1816 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 #[test]
1842 fn test_apply_json_patch_comprehensive() {
1843 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 let patch = json!({
1865 "name": "Updated Name via JSON Patch",
1866 "paused": true,
1867 "policies": {
1868 "min_balance": "2000000000000000000",
1869 "gas_price_cap": null, "eip1559_pricing": true, "whitelist_receivers": ["0x123", "0x456"] },
1874 "notification_id": null, "custom_rpc_urls": [{"url": "https://example.com", "weight": 100}]
1876 });
1877
1878 let updated_relayer = relayer.apply_json_patch(&patch).unwrap();
1880
1881 assert_eq!(updated_relayer.name, "Updated Name via JSON Patch");
1883 assert!(updated_relayer.paused);
1884 assert_eq!(updated_relayer.notification_id, None); assert!(updated_relayer.custom_rpc_urls.is_some());
1886
1887 if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = updated_relayer.policies {
1889 assert_eq!(evm_policy.min_balance, Some(2000000000000000000)); assert_eq!(evm_policy.gas_price_cap, None); assert_eq!(evm_policy.eip1559_pricing, Some(true)); assert_eq!(evm_policy.gas_limit_estimation, Some(true)); assert_eq!(
1894 evm_policy.whitelist_receivers,
1895 Some(vec!["0x123".to_string(), "0x456".to_string()])
1896 ); assert_eq!(evm_policy.private_transactions, None); } 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 let invalid_patch = json!({
1919 "name": "" });
1921
1922 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 let invalid_patch = json!({
1947 "network_type": "invalid_type" });
1949
1950 let result = relayer.apply_json_patch(&invalid_patch);
1952 assert!(result.is_err());
1953 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}