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