openzeppelin_relayer/models/relayer/
response.rs

1//! Response models for relayer API endpoints.
2//!
3//! This module provides response structures used by relayer API endpoints,
4//! including:
5//!
6//! - **Response Models**: Structures returned by API endpoints
7//! - **Status Models**: Relayer status and runtime information
8//! - **Conversions**: Mapping from domain and repository models to API responses
9//! - **API Compatibility**: Maintaining backward compatibility with existing API contracts
10//!
11//! These models handle API-specific formatting and serialization while working
12//! with the domain model for business logic.
13
14use super::{
15    Relayer, RelayerEvmPolicy, RelayerNetworkPolicy, RelayerNetworkType, RelayerRepoModel,
16    RelayerSolanaPolicy, RelayerSolanaSwapConfig, RelayerStellarPolicy, RpcConfig,
17    SolanaAllowedTokensPolicy, SolanaFeePaymentStrategy,
18};
19use crate::constants::{
20    DEFAULT_EVM_GAS_LIMIT_ESTIMATION, DEFAULT_EVM_MIN_BALANCE, DEFAULT_SOLANA_MAX_TX_DATA_SIZE,
21    DEFAULT_SOLANA_MIN_BALANCE, DEFAULT_STELLAR_MIN_BALANCE,
22};
23use serde::{Deserialize, Serialize};
24use utoipa::ToSchema;
25
26/// Response for delete pending transactions operation
27#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
28pub struct DeletePendingTransactionsResponse {
29    pub queued_for_cancellation_transaction_ids: Vec<String>,
30    pub failed_to_queue_transaction_ids: Vec<String>,
31    pub total_processed: u32,
32}
33
34/// Policy types for responses - these don't include network_type tags
35/// since the network_type is already available at the top level of RelayerResponse
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
37#[serde(untagged)]
38pub enum RelayerNetworkPolicyResponse {
39    // Order matters for untagged enums - put most distinctive variants first
40    // EVM has unique fields (gas_price_cap, whitelist_receivers, eip1559_pricing) so it should be tried first
41    Evm(EvmPolicyResponse),
42    // Stellar has unique fields (max_fee, timeout_seconds) so it should be tried next
43    Stellar(StellarPolicyResponse),
44    // Solana has many fields but some overlap with others, so it should be tried last
45    Solana(SolanaPolicyResponse),
46}
47
48impl From<RelayerNetworkPolicy> for RelayerNetworkPolicyResponse {
49    fn from(policy: RelayerNetworkPolicy) -> Self {
50        match policy {
51            RelayerNetworkPolicy::Evm(evm_policy) => {
52                RelayerNetworkPolicyResponse::Evm(evm_policy.into())
53            }
54            RelayerNetworkPolicy::Solana(solana_policy) => {
55                RelayerNetworkPolicyResponse::Solana(solana_policy.into())
56            }
57            RelayerNetworkPolicy::Stellar(stellar_policy) => {
58                RelayerNetworkPolicyResponse::Stellar(stellar_policy.into())
59            }
60        }
61    }
62}
63
64/// Relayer response model for API endpoints
65#[derive(Debug, Serialize, Clone, PartialEq, ToSchema)]
66pub struct RelayerResponse {
67    pub id: String,
68    pub name: String,
69    pub network: String,
70    pub network_type: RelayerNetworkType,
71    pub paused: bool,
72    /// Policies without redundant network_type tag - network type is available at top level
73    /// Only included if user explicitly provided policies (not shown for empty/default policies)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    #[schema(nullable = false)]
76    pub policies: Option<RelayerNetworkPolicyResponse>,
77    pub signer_id: String,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    #[schema(nullable = false)]
80    pub notification_id: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[schema(nullable = false)]
83    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
84    // Runtime fields from repository model
85    #[schema(nullable = false)]
86    pub address: Option<String>,
87    #[schema(nullable = false)]
88    pub system_disabled: Option<bool>,
89}
90
91/// Relayer status with runtime information
92#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
93#[serde(tag = "network_type")]
94pub enum RelayerStatus {
95    #[serde(rename = "evm")]
96    Evm {
97        balance: String,
98        pending_transactions_count: u64,
99        last_confirmed_transaction_timestamp: Option<String>,
100        system_disabled: bool,
101        paused: bool,
102        nonce: String,
103    },
104    #[serde(rename = "stellar")]
105    Stellar {
106        balance: String,
107        pending_transactions_count: u64,
108        last_confirmed_transaction_timestamp: Option<String>,
109        system_disabled: bool,
110        paused: bool,
111        sequence_number: String,
112    },
113    #[serde(rename = "solana")]
114    Solana {
115        balance: String,
116        pending_transactions_count: u64,
117        last_confirmed_transaction_timestamp: Option<String>,
118        system_disabled: bool,
119        paused: bool,
120    },
121}
122
123/// Convert RelayerNetworkPolicy to RelayerNetworkPolicyResponse based on network type
124fn convert_policy_to_response(
125    policy: RelayerNetworkPolicy,
126    network_type: RelayerNetworkType,
127) -> RelayerNetworkPolicyResponse {
128    match (policy, network_type) {
129        (RelayerNetworkPolicy::Evm(evm_policy), RelayerNetworkType::Evm) => {
130            RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse::from(evm_policy))
131        }
132        (RelayerNetworkPolicy::Solana(solana_policy), RelayerNetworkType::Solana) => {
133            RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse::from(solana_policy))
134        }
135        (RelayerNetworkPolicy::Stellar(stellar_policy), RelayerNetworkType::Stellar) => {
136            RelayerNetworkPolicyResponse::Stellar(StellarPolicyResponse::from(stellar_policy))
137        }
138        // Handle mismatched cases by falling back to the policy type
139        (RelayerNetworkPolicy::Evm(evm_policy), _) => {
140            RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse::from(evm_policy))
141        }
142        (RelayerNetworkPolicy::Solana(solana_policy), _) => {
143            RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse::from(solana_policy))
144        }
145        (RelayerNetworkPolicy::Stellar(stellar_policy), _) => {
146            RelayerNetworkPolicyResponse::Stellar(StellarPolicyResponse::from(stellar_policy))
147        }
148    }
149}
150
151impl From<Relayer> for RelayerResponse {
152    fn from(relayer: Relayer) -> Self {
153        Self {
154            id: relayer.id.clone(),
155            name: relayer.name.clone(),
156            network: relayer.network.clone(),
157            network_type: relayer.network_type,
158            paused: relayer.paused,
159            policies: relayer
160                .policies
161                .map(|policy| convert_policy_to_response(policy, relayer.network_type)),
162            signer_id: relayer.signer_id,
163            notification_id: relayer.notification_id,
164            custom_rpc_urls: relayer.custom_rpc_urls,
165            address: None,
166            system_disabled: None,
167        }
168    }
169}
170
171impl From<RelayerRepoModel> for RelayerResponse {
172    fn from(model: RelayerRepoModel) -> Self {
173        // Only include policies in response if they have actual user-provided values
174        let policies = if is_empty_policy(&model.policies) {
175            None // Don't return empty/default policies in API response
176        } else {
177            Some(convert_policy_to_response(
178                model.policies.clone(),
179                model.network_type,
180            ))
181        };
182
183        Self {
184            id: model.id,
185            name: model.name,
186            network: model.network,
187            network_type: model.network_type,
188            paused: model.paused,
189            policies,
190            signer_id: model.signer_id,
191            notification_id: model.notification_id,
192            custom_rpc_urls: model.custom_rpc_urls,
193            address: Some(model.address),
194            system_disabled: Some(model.system_disabled),
195        }
196    }
197}
198
199/// Custom Deserialize implementation for RelayerResponse that uses network_type to deserialize policies
200impl<'de> serde::Deserialize<'de> for RelayerResponse {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: serde::Deserializer<'de>,
204    {
205        use serde::de::Error;
206        use serde_json::Value;
207
208        // First, deserialize to a generic Value to extract network_type
209        let value: Value = Value::deserialize(deserializer)?;
210
211        // Extract the network_type field
212        let network_type: RelayerNetworkType = value
213            .get("network_type")
214            .and_then(|v| serde_json::from_value(v.clone()).ok())
215            .ok_or_else(|| D::Error::missing_field("network_type"))?;
216
217        // Extract policies field if present
218        let policies = if let Some(policies_value) = value.get("policies") {
219            if policies_value.is_null() {
220                None
221            } else {
222                // Deserialize policies based on network_type
223                let policy_response = match network_type {
224                    RelayerNetworkType::Evm => {
225                        let evm_policy: EvmPolicyResponse =
226                            serde_json::from_value(policies_value.clone())
227                                .map_err(D::Error::custom)?;
228                        RelayerNetworkPolicyResponse::Evm(evm_policy)
229                    }
230                    RelayerNetworkType::Solana => {
231                        let solana_policy: SolanaPolicyResponse =
232                            serde_json::from_value(policies_value.clone())
233                                .map_err(D::Error::custom)?;
234                        RelayerNetworkPolicyResponse::Solana(solana_policy)
235                    }
236                    RelayerNetworkType::Stellar => {
237                        let stellar_policy: StellarPolicyResponse =
238                            serde_json::from_value(policies_value.clone())
239                                .map_err(D::Error::custom)?;
240                        RelayerNetworkPolicyResponse::Stellar(stellar_policy)
241                    }
242                };
243                Some(policy_response)
244            }
245        } else {
246            None
247        };
248
249        // Deserialize all other fields normally
250        Ok(RelayerResponse {
251            id: value
252                .get("id")
253                .and_then(|v| serde_json::from_value(v.clone()).ok())
254                .ok_or_else(|| D::Error::missing_field("id"))?,
255            name: value
256                .get("name")
257                .and_then(|v| serde_json::from_value(v.clone()).ok())
258                .ok_or_else(|| D::Error::missing_field("name"))?,
259            network: value
260                .get("network")
261                .and_then(|v| serde_json::from_value(v.clone()).ok())
262                .ok_or_else(|| D::Error::missing_field("network"))?,
263            network_type,
264            paused: value
265                .get("paused")
266                .and_then(|v| serde_json::from_value(v.clone()).ok())
267                .ok_or_else(|| D::Error::missing_field("paused"))?,
268            policies,
269            signer_id: value
270                .get("signer_id")
271                .and_then(|v| serde_json::from_value(v.clone()).ok())
272                .ok_or_else(|| D::Error::missing_field("signer_id"))?,
273            notification_id: value
274                .get("notification_id")
275                .and_then(|v| serde_json::from_value(v.clone()).ok())
276                .unwrap_or(None),
277            custom_rpc_urls: value
278                .get("custom_rpc_urls")
279                .and_then(|v| serde_json::from_value(v.clone()).ok())
280                .unwrap_or(None),
281            address: value
282                .get("address")
283                .and_then(|v| serde_json::from_value(v.clone()).ok())
284                .unwrap_or(None),
285            system_disabled: value
286                .get("system_disabled")
287                .and_then(|v| serde_json::from_value(v.clone()).ok())
288                .unwrap_or(None),
289        })
290    }
291}
292
293/// Check if a policy is "empty" (all fields are None) indicating it's a default
294fn is_empty_policy(policy: &RelayerNetworkPolicy) -> bool {
295    match policy {
296        RelayerNetworkPolicy::Evm(evm_policy) => {
297            evm_policy.min_balance.is_none()
298                && evm_policy.gas_limit_estimation.is_none()
299                && evm_policy.gas_price_cap.is_none()
300                && evm_policy.whitelist_receivers.is_none()
301                && evm_policy.eip1559_pricing.is_none()
302                && evm_policy.private_transactions.is_none()
303        }
304        RelayerNetworkPolicy::Solana(solana_policy) => {
305            solana_policy.allowed_programs.is_none()
306                && solana_policy.max_signatures.is_none()
307                && solana_policy.max_tx_data_size.is_none()
308                && solana_policy.min_balance.is_none()
309                && solana_policy.allowed_tokens.is_none()
310                && solana_policy.fee_payment_strategy.is_none()
311                && solana_policy.fee_margin_percentage.is_none()
312                && solana_policy.allowed_accounts.is_none()
313                && solana_policy.disallowed_accounts.is_none()
314                && solana_policy.max_allowed_fee_lamports.is_none()
315                && solana_policy.swap_config.is_none()
316        }
317        RelayerNetworkPolicy::Stellar(stellar_policy) => {
318            stellar_policy.min_balance.is_none()
319                && stellar_policy.max_fee.is_none()
320                && stellar_policy.timeout_seconds.is_none()
321        }
322    }
323}
324
325/// Network policy response models for OpenAPI documentation
326#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
327pub struct NetworkPolicyResponse {
328    #[serde(flatten)]
329    pub policy: RelayerNetworkPolicy,
330}
331
332/// Default function for EVM min balance
333fn default_evm_min_balance() -> u128 {
334    DEFAULT_EVM_MIN_BALANCE
335}
336
337fn default_evm_gas_limit_estimation() -> bool {
338    DEFAULT_EVM_GAS_LIMIT_ESTIMATION
339}
340
341/// Default function for Solana min balance
342fn default_solana_min_balance() -> u64 {
343    DEFAULT_SOLANA_MIN_BALANCE
344}
345
346/// Default function for Stellar min balance
347fn default_stellar_min_balance() -> u64 {
348    DEFAULT_STELLAR_MIN_BALANCE
349}
350
351/// Default function for Solana max tx data size
352fn default_solana_max_tx_data_size() -> u16 {
353    DEFAULT_SOLANA_MAX_TX_DATA_SIZE
354}
355/// EVM policy response model for OpenAPI documentation
356#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
357#[serde(deny_unknown_fields)]
358pub struct EvmPolicyResponse {
359    #[serde(
360        default = "default_evm_min_balance",
361        serialize_with = "crate::utils::serialize_u128_as_number",
362        deserialize_with = "crate::utils::deserialize_u128_as_number"
363    )]
364    #[schema(nullable = false)]
365    pub min_balance: u128,
366    #[serde(default = "default_evm_gas_limit_estimation")]
367    #[schema(nullable = false)]
368    pub gas_limit_estimation: bool,
369    #[serde(
370        skip_serializing_if = "Option::is_none",
371        serialize_with = "crate::utils::serialize_optional_u128_as_number",
372        deserialize_with = "crate::utils::deserialize_optional_u128_as_number",
373        default
374    )]
375    #[schema(nullable = false)]
376    pub gas_price_cap: Option<u128>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    #[schema(nullable = false)]
379    pub whitelist_receivers: Option<Vec<String>>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    #[schema(nullable = false)]
382    pub eip1559_pricing: Option<bool>,
383    #[serde(skip_serializing_if = "Option::is_none")]
384    #[schema(nullable = false)]
385    pub private_transactions: Option<bool>,
386}
387
388/// Solana policy response model for OpenAPI documentation
389#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
390#[serde(deny_unknown_fields)]
391pub struct SolanaPolicyResponse {
392    #[serde(skip_serializing_if = "Option::is_none")]
393    #[schema(nullable = false)]
394    pub allowed_programs: Option<Vec<String>>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    #[schema(nullable = false)]
397    pub max_signatures: Option<u8>,
398    #[schema(nullable = false)]
399    #[serde(default = "default_solana_max_tx_data_size")]
400    pub max_tx_data_size: u16,
401    #[serde(default = "default_solana_min_balance")]
402    #[schema(nullable = false)]
403    pub min_balance: u64,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    #[schema(nullable = false)]
406    pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    #[schema(nullable = false)]
409    pub fee_payment_strategy: Option<SolanaFeePaymentStrategy>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    #[schema(nullable = false)]
412    pub fee_margin_percentage: Option<f32>,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    #[schema(nullable = false)]
415    pub allowed_accounts: Option<Vec<String>>,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    #[schema(nullable = false)]
418    pub disallowed_accounts: Option<Vec<String>>,
419    #[serde(skip_serializing_if = "Option::is_none")]
420    #[schema(nullable = false)]
421    pub max_allowed_fee_lamports: Option<u64>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    #[schema(nullable = false)]
424    pub swap_config: Option<RelayerSolanaSwapConfig>,
425}
426
427/// Stellar policy response model for OpenAPI documentation
428#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
429#[serde(deny_unknown_fields)]
430pub struct StellarPolicyResponse {
431    #[serde(skip_serializing_if = "Option::is_none")]
432    #[schema(nullable = false)]
433    pub max_fee: Option<u32>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    #[schema(nullable = false)]
436    pub timeout_seconds: Option<u64>,
437    #[serde(default = "default_stellar_min_balance")]
438    #[schema(nullable = false)]
439    pub min_balance: u64,
440}
441
442impl From<RelayerEvmPolicy> for EvmPolicyResponse {
443    fn from(policy: RelayerEvmPolicy) -> Self {
444        Self {
445            min_balance: policy.min_balance.unwrap_or(DEFAULT_EVM_MIN_BALANCE),
446            gas_limit_estimation: policy
447                .gas_limit_estimation
448                .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION),
449            gas_price_cap: policy.gas_price_cap,
450            whitelist_receivers: policy.whitelist_receivers,
451            eip1559_pricing: policy.eip1559_pricing,
452            private_transactions: policy.private_transactions,
453        }
454    }
455}
456
457impl From<RelayerSolanaPolicy> for SolanaPolicyResponse {
458    fn from(policy: RelayerSolanaPolicy) -> Self {
459        Self {
460            allowed_programs: policy.allowed_programs,
461            max_signatures: policy.max_signatures,
462            max_tx_data_size: policy
463                .max_tx_data_size
464                .unwrap_or(DEFAULT_SOLANA_MAX_TX_DATA_SIZE),
465            min_balance: policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE),
466            allowed_tokens: policy.allowed_tokens,
467            fee_payment_strategy: policy.fee_payment_strategy,
468            fee_margin_percentage: policy.fee_margin_percentage,
469            allowed_accounts: policy.allowed_accounts,
470            disallowed_accounts: policy.disallowed_accounts,
471            max_allowed_fee_lamports: policy.max_allowed_fee_lamports,
472            swap_config: policy.swap_config,
473        }
474    }
475}
476
477impl From<RelayerStellarPolicy> for StellarPolicyResponse {
478    fn from(policy: RelayerStellarPolicy) -> Self {
479        Self {
480            min_balance: policy.min_balance.unwrap_or(DEFAULT_STELLAR_MIN_BALANCE),
481            max_fee: policy.max_fee,
482            timeout_seconds: policy.timeout_seconds,
483        }
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::models::relayer::{
491        RelayerEvmPolicy, RelayerSolanaPolicy, RelayerSolanaSwapConfig, RelayerStellarPolicy,
492        SolanaAllowedTokensPolicy, SolanaFeePaymentStrategy, SolanaSwapStrategy,
493    };
494
495    #[test]
496    fn test_from_domain_relayer() {
497        let relayer = Relayer::new(
498            "test-relayer".to_string(),
499            "Test Relayer".to_string(),
500            "mainnet".to_string(),
501            false,
502            RelayerNetworkType::Evm,
503            Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
504                gas_price_cap: Some(100_000_000_000),
505                whitelist_receivers: None,
506                eip1559_pricing: Some(true),
507                private_transactions: None,
508                min_balance: None,
509                gas_limit_estimation: None,
510            })),
511            "test-signer".to_string(),
512            None,
513            None,
514        );
515
516        let response: RelayerResponse = relayer.clone().into();
517
518        assert_eq!(response.id, relayer.id);
519        assert_eq!(response.name, relayer.name);
520        assert_eq!(response.network, relayer.network);
521        assert_eq!(response.network_type, relayer.network_type);
522        assert_eq!(response.paused, relayer.paused);
523        assert_eq!(
524            response.policies,
525            Some(RelayerNetworkPolicyResponse::Evm(
526                RelayerEvmPolicy {
527                    gas_price_cap: Some(100_000_000_000),
528                    whitelist_receivers: None,
529                    eip1559_pricing: Some(true),
530                    private_transactions: None,
531                    min_balance: Some(DEFAULT_EVM_MIN_BALANCE),
532                    gas_limit_estimation: Some(DEFAULT_EVM_GAS_LIMIT_ESTIMATION),
533                }
534                .into()
535            ))
536        );
537        assert_eq!(response.signer_id, relayer.signer_id);
538        assert_eq!(response.notification_id, relayer.notification_id);
539        assert_eq!(response.custom_rpc_urls, relayer.custom_rpc_urls);
540        assert_eq!(response.address, None);
541        assert_eq!(response.system_disabled, None);
542    }
543
544    #[test]
545    fn test_from_domain_relayer_solana() {
546        let relayer = Relayer::new(
547            "test-solana-relayer".to_string(),
548            "Test Solana Relayer".to_string(),
549            "mainnet".to_string(),
550            false,
551            RelayerNetworkType::Solana,
552            Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
553                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
554                max_signatures: Some(5),
555                min_balance: Some(1000000),
556                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
557                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
558                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
559                    Some(100000),
560                    None,
561                )]),
562                max_tx_data_size: None,
563                fee_margin_percentage: None,
564                allowed_accounts: None,
565                disallowed_accounts: None,
566                max_allowed_fee_lamports: None,
567                swap_config: None,
568            })),
569            "test-signer".to_string(),
570            None,
571            None,
572        );
573
574        let response: RelayerResponse = relayer.clone().into();
575
576        assert_eq!(response.id, relayer.id);
577        assert_eq!(response.network_type, RelayerNetworkType::Solana);
578        assert!(response.policies.is_some());
579
580        if let Some(RelayerNetworkPolicyResponse::Solana(solana_response)) = response.policies {
581            assert_eq!(solana_response.min_balance, 1000000);
582            assert_eq!(solana_response.max_signatures, Some(5));
583        } else {
584            panic!("Expected Solana policy response");
585        }
586    }
587
588    #[test]
589    fn test_from_domain_relayer_stellar() {
590        let relayer = Relayer::new(
591            "test-stellar-relayer".to_string(),
592            "Test Stellar Relayer".to_string(),
593            "mainnet".to_string(),
594            false,
595            RelayerNetworkType::Stellar,
596            Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
597                min_balance: Some(20000000),
598                max_fee: Some(100000),
599                timeout_seconds: Some(30),
600            })),
601            "test-signer".to_string(),
602            None,
603            None,
604        );
605
606        let response: RelayerResponse = relayer.clone().into();
607
608        assert_eq!(response.id, relayer.id);
609        assert_eq!(response.network_type, RelayerNetworkType::Stellar);
610        assert!(response.policies.is_some());
611
612        if let Some(RelayerNetworkPolicyResponse::Stellar(stellar_response)) = response.policies {
613            assert_eq!(stellar_response.min_balance, 20000000);
614        } else {
615            panic!("Expected Stellar policy response");
616        }
617    }
618
619    #[test]
620    fn test_response_serialization() {
621        let response = RelayerResponse {
622            id: "test-relayer".to_string(),
623            name: "Test Relayer".to_string(),
624            network: "mainnet".to_string(),
625            network_type: RelayerNetworkType::Evm,
626            paused: false,
627            policies: Some(RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse {
628                gas_price_cap: Some(50000000000),
629                whitelist_receivers: None,
630                eip1559_pricing: Some(true),
631                private_transactions: None,
632                min_balance: DEFAULT_EVM_MIN_BALANCE,
633                gas_limit_estimation: DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
634            })),
635            signer_id: "test-signer".to_string(),
636            notification_id: None,
637            custom_rpc_urls: None,
638            address: Some("0x123...".to_string()),
639            system_disabled: Some(false),
640        };
641
642        // Should serialize without errors
643        let serialized = serde_json::to_string(&response).unwrap();
644        assert!(!serialized.is_empty());
645
646        // Should deserialize back to the same struct
647        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
648        assert_eq!(response.id, deserialized.id);
649        assert_eq!(response.name, deserialized.name);
650    }
651
652    #[test]
653    fn test_solana_response_serialization() {
654        let response = RelayerResponse {
655            id: "test-solana-relayer".to_string(),
656            name: "Test Solana Relayer".to_string(),
657            network: "mainnet".to_string(),
658            network_type: RelayerNetworkType::Solana,
659            paused: false,
660            policies: Some(RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse {
661                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
662                max_signatures: Some(5),
663                max_tx_data_size: DEFAULT_SOLANA_MAX_TX_DATA_SIZE,
664                min_balance: 1000000,
665                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
666                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
667                    Some(100000),
668                    None,
669                )]),
670                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
671                fee_margin_percentage: Some(5.0),
672                allowed_accounts: None,
673                disallowed_accounts: None,
674                max_allowed_fee_lamports: Some(500000),
675                swap_config: Some(RelayerSolanaSwapConfig {
676                    strategy: Some(SolanaSwapStrategy::JupiterSwap),
677                    cron_schedule: Some("0 0 * * *".to_string()),
678                    min_balance_threshold: Some(500000),
679                    jupiter_swap_options: None,
680                }),
681            })),
682            signer_id: "test-signer".to_string(),
683            notification_id: None,
684            custom_rpc_urls: None,
685            address: Some("SolanaAddress123...".to_string()),
686            system_disabled: Some(false),
687        };
688
689        // Should serialize without errors
690        let serialized = serde_json::to_string(&response).unwrap();
691        assert!(!serialized.is_empty());
692
693        // Should deserialize back to the same struct
694        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
695        assert_eq!(response.id, deserialized.id);
696        assert_eq!(response.network_type, RelayerNetworkType::Solana);
697    }
698
699    #[test]
700    fn test_stellar_response_serialization() {
701        let response = RelayerResponse {
702            id: "test-stellar-relayer".to_string(),
703            name: "Test Stellar Relayer".to_string(),
704            network: "mainnet".to_string(),
705            network_type: RelayerNetworkType::Stellar,
706            paused: false,
707            policies: Some(RelayerNetworkPolicyResponse::Stellar(
708                StellarPolicyResponse {
709                    max_fee: Some(5000),
710                    timeout_seconds: None,
711                    min_balance: 20000000,
712                },
713            )),
714            signer_id: "test-signer".to_string(),
715            notification_id: None,
716            custom_rpc_urls: None,
717            address: Some("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string()),
718            system_disabled: Some(false),
719        };
720
721        // Should serialize without errors
722        let serialized = serde_json::to_string(&response).unwrap();
723        assert!(!serialized.is_empty());
724
725        // Should deserialize back to the same struct
726        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
727        assert_eq!(response.id, deserialized.id);
728        assert_eq!(response.network_type, RelayerNetworkType::Stellar);
729
730        // Verify Stellar-specific fields
731        if let Some(RelayerNetworkPolicyResponse::Stellar(stellar_policy)) = deserialized.policies {
732            assert_eq!(stellar_policy.min_balance, 20000000);
733            assert_eq!(stellar_policy.max_fee, Some(5000));
734            assert_eq!(stellar_policy.timeout_seconds, None);
735        } else {
736            panic!("Expected Stellar policy in deserialized response");
737        }
738    }
739
740    #[test]
741    fn test_response_without_redundant_network_type() {
742        let response = RelayerResponse {
743            id: "test-relayer".to_string(),
744            name: "Test Relayer".to_string(),
745            network: "mainnet".to_string(),
746            network_type: RelayerNetworkType::Evm,
747            paused: false,
748            policies: Some(RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse {
749                gas_price_cap: Some(100_000_000_000),
750                whitelist_receivers: None,
751                eip1559_pricing: Some(true),
752                private_transactions: None,
753                min_balance: DEFAULT_EVM_MIN_BALANCE,
754                gas_limit_estimation: DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
755            })),
756            signer_id: "test-signer".to_string(),
757            notification_id: None,
758            custom_rpc_urls: None,
759            address: Some("0x123...".to_string()),
760            system_disabled: Some(false),
761        };
762
763        let serialized = serde_json::to_string_pretty(&response).unwrap();
764
765        assert!(serialized.contains(r#""network_type": "evm""#));
766
767        // Count occurrences - should only be 1 (at top level)
768        let network_type_count = serialized.matches(r#""network_type""#).count();
769        assert_eq!(
770            network_type_count, 1,
771            "Should only have one network_type field at top level, not in policies"
772        );
773
774        assert!(serialized.contains(r#""gas_price_cap": 100000000000"#));
775        assert!(serialized.contains(r#""eip1559_pricing": true"#));
776    }
777
778    #[test]
779    fn test_solana_response_without_redundant_network_type() {
780        let response = RelayerResponse {
781            id: "test-solana-relayer".to_string(),
782            name: "Test Solana Relayer".to_string(),
783            network: "mainnet".to_string(),
784            network_type: RelayerNetworkType::Solana,
785            paused: false,
786            policies: Some(RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse {
787                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
788                max_signatures: Some(5),
789                max_tx_data_size: DEFAULT_SOLANA_MAX_TX_DATA_SIZE,
790                min_balance: 1000000,
791                allowed_tokens: None,
792                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
793                fee_margin_percentage: None,
794                allowed_accounts: None,
795                disallowed_accounts: None,
796                max_allowed_fee_lamports: None,
797                swap_config: None,
798            })),
799            signer_id: "test-signer".to_string(),
800            notification_id: None,
801            custom_rpc_urls: None,
802            address: Some("SolanaAddress123...".to_string()),
803            system_disabled: Some(false),
804        };
805
806        let serialized = serde_json::to_string_pretty(&response).unwrap();
807
808        assert!(serialized.contains(r#""network_type": "solana""#));
809
810        // Count occurrences - should only be 1 (at top level)
811        let network_type_count = serialized.matches(r#""network_type""#).count();
812        assert_eq!(
813            network_type_count, 1,
814            "Should only have one network_type field at top level, not in policies"
815        );
816
817        assert!(serialized.contains(r#""max_signatures": 5"#));
818        assert!(serialized.contains(r#""fee_payment_strategy": "relayer""#));
819    }
820
821    #[test]
822    fn test_stellar_response_without_redundant_network_type() {
823        let response = RelayerResponse {
824            id: "test-stellar-relayer".to_string(),
825            name: "Test Stellar Relayer".to_string(),
826            network: "mainnet".to_string(),
827            network_type: RelayerNetworkType::Stellar,
828            paused: false,
829            policies: Some(RelayerNetworkPolicyResponse::Stellar(
830                StellarPolicyResponse {
831                    min_balance: 20000000,
832                    max_fee: Some(100000),
833                    timeout_seconds: Some(30),
834                },
835            )),
836            signer_id: "test-signer".to_string(),
837            notification_id: None,
838            custom_rpc_urls: None,
839            address: Some("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string()),
840            system_disabled: Some(false),
841        };
842
843        let serialized = serde_json::to_string_pretty(&response).unwrap();
844
845        assert!(serialized.contains(r#""network_type": "stellar""#));
846
847        // Count occurrences - should only be 1 (at top level)
848        let network_type_count = serialized.matches(r#""network_type""#).count();
849        assert_eq!(
850            network_type_count, 1,
851            "Should only have one network_type field at top level, not in policies"
852        );
853
854        assert!(serialized.contains(r#""min_balance": 20000000"#));
855        assert!(serialized.contains(r#""max_fee": 100000"#));
856        assert!(serialized.contains(r#""timeout_seconds": 30"#));
857    }
858
859    #[test]
860    fn test_empty_policies_not_returned_in_response() {
861        // Create a repository model with empty policies (all None - user didn't set any)
862        let repo_model = RelayerRepoModel {
863            id: "test-relayer".to_string(),
864            name: "Test Relayer".to_string(),
865            network: "mainnet".to_string(),
866            network_type: RelayerNetworkType::Evm,
867            paused: false,
868            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()), // All None values
869            signer_id: "test-signer".to_string(),
870            notification_id: None,
871            custom_rpc_urls: None,
872            address: "0x123...".to_string(),
873            system_disabled: false,
874        };
875
876        // Convert to response
877        let response = RelayerResponse::from(repo_model);
878
879        // Empty policies should not be included in response
880        assert_eq!(response.policies, None);
881
882        // Verify serialization doesn't include policies field
883        let serialized = serde_json::to_string(&response).unwrap();
884        assert!(
885            !serialized.contains("policies"),
886            "Empty policies should not appear in JSON response"
887        );
888    }
889
890    #[test]
891    fn test_empty_solana_policies_not_returned_in_response() {
892        // Create a repository model with empty Solana policies (all None - user didn't set any)
893        let repo_model = RelayerRepoModel {
894            id: "test-solana-relayer".to_string(),
895            name: "Test Solana Relayer".to_string(),
896            network: "mainnet".to_string(),
897            network_type: RelayerNetworkType::Solana,
898            paused: false,
899            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()), // All None values
900            signer_id: "test-signer".to_string(),
901            notification_id: None,
902            custom_rpc_urls: None,
903            address: "SolanaAddress123...".to_string(),
904            system_disabled: false,
905        };
906
907        // Convert to response
908        let response = RelayerResponse::from(repo_model);
909
910        // Empty policies should not be included in response
911        assert_eq!(response.policies, None);
912
913        // Verify serialization doesn't include policies field
914        let serialized = serde_json::to_string(&response).unwrap();
915        assert!(
916            !serialized.contains("policies"),
917            "Empty Solana policies should not appear in JSON response"
918        );
919    }
920
921    #[test]
922    fn test_empty_stellar_policies_not_returned_in_response() {
923        // Create a repository model with empty Stellar policies (all None - user didn't set any)
924        let repo_model = RelayerRepoModel {
925            id: "test-stellar-relayer".to_string(),
926            name: "Test Stellar Relayer".to_string(),
927            network: "mainnet".to_string(),
928            network_type: RelayerNetworkType::Stellar,
929            paused: false,
930            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()), // All None values
931            signer_id: "test-signer".to_string(),
932            notification_id: None,
933            custom_rpc_urls: None,
934            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
935            system_disabled: false,
936        };
937
938        // Convert to response
939        let response = RelayerResponse::from(repo_model);
940
941        // Empty policies should not be included in response
942        assert_eq!(response.policies, None);
943
944        // Verify serialization doesn't include policies field
945        let serialized = serde_json::to_string(&response).unwrap();
946        assert!(
947            !serialized.contains("policies"),
948            "Empty Stellar policies should not appear in JSON response"
949        );
950    }
951
952    #[test]
953    fn test_user_provided_policies_returned_in_response() {
954        // Create a repository model with user-provided policies
955        let repo_model = RelayerRepoModel {
956            id: "test-relayer".to_string(),
957            name: "Test Relayer".to_string(),
958            network: "mainnet".to_string(),
959            network_type: RelayerNetworkType::Evm,
960            paused: false,
961            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
962                gas_price_cap: Some(100_000_000_000),
963                eip1559_pricing: Some(true),
964                min_balance: None, // Some fields can still be None
965                gas_limit_estimation: None,
966                whitelist_receivers: None,
967                private_transactions: None,
968            }),
969            signer_id: "test-signer".to_string(),
970            notification_id: None,
971            custom_rpc_urls: None,
972            address: "0x123...".to_string(),
973            system_disabled: false,
974        };
975
976        // Convert to response
977        let response = RelayerResponse::from(repo_model);
978
979        // User-provided policies should be included in response
980        assert!(response.policies.is_some());
981
982        // Verify serialization includes policies field
983        let serialized = serde_json::to_string(&response).unwrap();
984        assert!(
985            serialized.contains("policies"),
986            "User-provided policies should appear in JSON response"
987        );
988        assert!(
989            serialized.contains("gas_price_cap"),
990            "User-provided policy values should appear in JSON response"
991        );
992    }
993
994    #[test]
995    fn test_user_provided_solana_policies_returned_in_response() {
996        // Create a repository model with user-provided Solana policies
997        let repo_model = RelayerRepoModel {
998            id: "test-solana-relayer".to_string(),
999            name: "Test Solana Relayer".to_string(),
1000            network: "mainnet".to_string(),
1001            network_type: RelayerNetworkType::Solana,
1002            paused: false,
1003            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1004                max_signatures: Some(5),
1005                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
1006                min_balance: Some(1000000),
1007                allowed_programs: None, // Some fields can still be None
1008                max_tx_data_size: None,
1009                allowed_tokens: None,
1010                fee_margin_percentage: None,
1011                allowed_accounts: None,
1012                disallowed_accounts: None,
1013                max_allowed_fee_lamports: None,
1014                swap_config: None,
1015            }),
1016            signer_id: "test-signer".to_string(),
1017            notification_id: None,
1018            custom_rpc_urls: None,
1019            address: "SolanaAddress123...".to_string(),
1020            system_disabled: false,
1021        };
1022
1023        // Convert to response
1024        let response = RelayerResponse::from(repo_model);
1025
1026        // User-provided policies should be included in response
1027        assert!(response.policies.is_some());
1028
1029        // Verify serialization includes policies field
1030        let serialized = serde_json::to_string(&response).unwrap();
1031        assert!(
1032            serialized.contains("policies"),
1033            "User-provided Solana policies should appear in JSON response"
1034        );
1035        assert!(
1036            serialized.contains("max_signatures"),
1037            "User-provided Solana policy values should appear in JSON response"
1038        );
1039        assert!(
1040            serialized.contains("fee_payment_strategy"),
1041            "User-provided Solana policy values should appear in JSON response"
1042        );
1043    }
1044
1045    #[test]
1046    fn test_user_provided_stellar_policies_returned_in_response() {
1047        // Create a repository model with user-provided Stellar policies
1048        let repo_model = RelayerRepoModel {
1049            id: "test-stellar-relayer".to_string(),
1050            name: "Test Stellar Relayer".to_string(),
1051            network: "mainnet".to_string(),
1052            network_type: RelayerNetworkType::Stellar,
1053            paused: false,
1054            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
1055                max_fee: Some(100000),
1056                timeout_seconds: Some(30),
1057                min_balance: None, // Some fields can still be None
1058            }),
1059            signer_id: "test-signer".to_string(),
1060            notification_id: None,
1061            custom_rpc_urls: None,
1062            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1063            system_disabled: false,
1064        };
1065
1066        // Convert to response
1067        let response = RelayerResponse::from(repo_model);
1068
1069        // User-provided policies should be included in response
1070        assert!(response.policies.is_some());
1071
1072        // Verify serialization includes policies field
1073        let serialized = serde_json::to_string(&response).unwrap();
1074        assert!(
1075            serialized.contains("policies"),
1076            "User-provided Stellar policies should appear in JSON response"
1077        );
1078        assert!(
1079            serialized.contains("max_fee"),
1080            "User-provided Stellar policy values should appear in JSON response"
1081        );
1082        assert!(
1083            serialized.contains("timeout_seconds"),
1084            "User-provided Stellar policy values should appear in JSON response"
1085        );
1086    }
1087
1088    #[test]
1089    fn test_relayer_status_serialization() {
1090        // Test EVM status
1091        let evm_status = RelayerStatus::Evm {
1092            balance: "1000000000000000000".to_string(),
1093            pending_transactions_count: 5,
1094            last_confirmed_transaction_timestamp: Some("2024-01-01T00:00:00Z".to_string()),
1095            system_disabled: false,
1096            paused: false,
1097            nonce: "42".to_string(),
1098        };
1099
1100        let serialized = serde_json::to_string(&evm_status).unwrap();
1101        assert!(serialized.contains(r#""network_type":"evm""#));
1102        assert!(serialized.contains(r#""nonce":"42""#));
1103        assert!(serialized.contains(r#""balance":"1000000000000000000""#));
1104
1105        // Test Solana status
1106        let solana_status = RelayerStatus::Solana {
1107            balance: "5000000000".to_string(),
1108            pending_transactions_count: 3,
1109            last_confirmed_transaction_timestamp: None,
1110            system_disabled: false,
1111            paused: true,
1112        };
1113
1114        let serialized = serde_json::to_string(&solana_status).unwrap();
1115        assert!(serialized.contains(r#""network_type":"solana""#));
1116        assert!(serialized.contains(r#""balance":"5000000000""#));
1117        assert!(serialized.contains(r#""paused":true"#));
1118
1119        // Test Stellar status
1120        let stellar_status = RelayerStatus::Stellar {
1121            balance: "1000000000".to_string(),
1122            pending_transactions_count: 2,
1123            last_confirmed_transaction_timestamp: Some("2024-01-01T12:00:00Z".to_string()),
1124            system_disabled: true,
1125            paused: false,
1126            sequence_number: "123456789".to_string(),
1127        };
1128
1129        let serialized = serde_json::to_string(&stellar_status).unwrap();
1130        assert!(serialized.contains(r#""network_type":"stellar""#));
1131        assert!(serialized.contains(r#""sequence_number":"123456789""#));
1132        assert!(serialized.contains(r#""system_disabled":true"#));
1133    }
1134
1135    #[test]
1136    fn test_relayer_status_deserialization() {
1137        // Test EVM status deserialization
1138        let evm_json = r#"{
1139            "network_type": "evm",
1140            "balance": "1000000000000000000",
1141            "pending_transactions_count": 5,
1142            "last_confirmed_transaction_timestamp": "2024-01-01T00:00:00Z",
1143            "system_disabled": false,
1144            "paused": false,
1145            "nonce": "42"
1146        }"#;
1147
1148        let status: RelayerStatus = serde_json::from_str(evm_json).unwrap();
1149        if let RelayerStatus::Evm { nonce, balance, .. } = status {
1150            assert_eq!(nonce, "42");
1151            assert_eq!(balance, "1000000000000000000");
1152        } else {
1153            panic!("Expected EVM status");
1154        }
1155
1156        // Test Solana status deserialization
1157        let solana_json = r#"{
1158            "network_type": "solana",
1159            "balance": "5000000000",
1160            "pending_transactions_count": 3,
1161            "last_confirmed_transaction_timestamp": null,
1162            "system_disabled": false,
1163            "paused": true
1164        }"#;
1165
1166        let status: RelayerStatus = serde_json::from_str(solana_json).unwrap();
1167        if let RelayerStatus::Solana {
1168            balance, paused, ..
1169        } = status
1170        {
1171            assert_eq!(balance, "5000000000");
1172            assert!(paused);
1173        } else {
1174            panic!("Expected Solana status");
1175        }
1176
1177        // Test Stellar status deserialization
1178        let stellar_json = r#"{
1179            "network_type": "stellar",
1180            "balance": "1000000000",
1181            "pending_transactions_count": 2,
1182            "last_confirmed_transaction_timestamp": "2024-01-01T12:00:00Z",
1183            "system_disabled": true,
1184            "paused": false,
1185            "sequence_number": "123456789"
1186        }"#;
1187
1188        let status: RelayerStatus = serde_json::from_str(stellar_json).unwrap();
1189        if let RelayerStatus::Stellar {
1190            sequence_number,
1191            system_disabled,
1192            ..
1193        } = status
1194        {
1195            assert_eq!(sequence_number, "123456789");
1196            assert!(system_disabled);
1197        } else {
1198            panic!("Expected Stellar status");
1199        }
1200    }
1201}