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