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_INCLUDE_REVERT_DATA, DEFAULT_EVM_MIN_BALANCE,
22    DEFAULT_SOLANA_MAX_TX_DATA_SIZE, 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                && evm_policy.include_revert_data.is_none()
341        }
342        RelayerNetworkPolicy::Solana(solana_policy) => {
343            solana_policy.allowed_programs.is_none()
344                && solana_policy.max_signatures.is_none()
345                && solana_policy.max_tx_data_size.is_none()
346                && solana_policy.min_balance.is_none()
347                && solana_policy.allowed_tokens.is_none()
348                && solana_policy.fee_payment_strategy.is_none()
349                && solana_policy.fee_margin_percentage.is_none()
350                && solana_policy.allowed_accounts.is_none()
351                && solana_policy.disallowed_accounts.is_none()
352                && solana_policy.max_allowed_fee_lamports.is_none()
353                && solana_policy.swap_config.is_none()
354        }
355        RelayerNetworkPolicy::Stellar(stellar_policy) => {
356            stellar_policy.min_balance.is_none()
357                && stellar_policy.max_fee.is_none()
358                && stellar_policy.timeout_seconds.is_none()
359                && stellar_policy.concurrent_transactions.is_none()
360                && stellar_policy.allowed_tokens.is_none()
361                && stellar_policy.fee_payment_strategy.is_none()
362                && stellar_policy.slippage_percentage.is_none()
363                && stellar_policy.fee_margin_percentage.is_none()
364                && stellar_policy.swap_config.is_none()
365        }
366    }
367}
368
369/// Network policy response models for OpenAPI documentation
370#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
371pub struct NetworkPolicyResponse {
372    #[serde(flatten)]
373    pub policy: RelayerNetworkPolicy,
374}
375
376/// Default function for EVM min balance
377fn default_evm_min_balance() -> u128 {
378    DEFAULT_EVM_MIN_BALANCE
379}
380
381fn default_evm_gas_limit_estimation() -> bool {
382    DEFAULT_EVM_GAS_LIMIT_ESTIMATION
383}
384
385fn default_evm_include_revert_data() -> bool {
386    DEFAULT_EVM_INCLUDE_REVERT_DATA
387}
388
389/// Default function for Solana min balance
390fn default_solana_min_balance() -> u64 {
391    DEFAULT_SOLANA_MIN_BALANCE
392}
393
394/// Default function for Stellar min balance
395fn default_stellar_min_balance() -> u64 {
396    DEFAULT_STELLAR_MIN_BALANCE
397}
398
399/// Default function for Solana max tx data size
400fn default_solana_max_tx_data_size() -> u16 {
401    DEFAULT_SOLANA_MAX_TX_DATA_SIZE
402}
403/// EVM policy response model for OpenAPI documentation
404#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
405#[serde(deny_unknown_fields)]
406pub struct EvmPolicyResponse {
407    #[serde(
408        default = "default_evm_min_balance",
409        serialize_with = "crate::utils::serialize_u128_as_number",
410        deserialize_with = "crate::utils::deserialize_u128_as_number"
411    )]
412    #[schema(nullable = false)]
413    pub min_balance: u128,
414    #[serde(default = "default_evm_gas_limit_estimation")]
415    #[schema(nullable = false)]
416    pub gas_limit_estimation: bool,
417    #[serde(
418        skip_serializing_if = "Option::is_none",
419        serialize_with = "crate::utils::serialize_optional_u128_as_number",
420        deserialize_with = "crate::utils::deserialize_optional_u128_as_number",
421        default
422    )]
423    #[schema(nullable = false)]
424    pub gas_price_cap: Option<u128>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    #[schema(nullable = false)]
427    pub whitelist_receivers: Option<Vec<String>>,
428    #[serde(skip_serializing_if = "Option::is_none")]
429    #[schema(nullable = false)]
430    pub eip1559_pricing: Option<bool>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    #[schema(nullable = false)]
433    pub private_transactions: Option<bool>,
434    #[serde(default = "default_evm_include_revert_data")]
435    #[schema(nullable = false)]
436    pub include_revert_data: bool,
437}
438
439/// Solana policy response model for OpenAPI documentation
440#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
441#[serde(deny_unknown_fields)]
442pub struct SolanaPolicyResponse {
443    #[serde(skip_serializing_if = "Option::is_none")]
444    #[schema(nullable = false)]
445    pub allowed_programs: Option<Vec<String>>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    #[schema(nullable = false)]
448    pub max_signatures: Option<u8>,
449    #[schema(nullable = false)]
450    #[serde(default = "default_solana_max_tx_data_size")]
451    pub max_tx_data_size: u16,
452    #[serde(default = "default_solana_min_balance")]
453    #[schema(nullable = false)]
454    pub min_balance: u64,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    #[schema(nullable = false)]
457    pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    #[schema(nullable = false)]
460    pub fee_payment_strategy: Option<SolanaFeePaymentStrategy>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    #[schema(nullable = false)]
463    pub fee_margin_percentage: Option<f32>,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    #[schema(nullable = false)]
466    pub allowed_accounts: Option<Vec<String>>,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    #[schema(nullable = false)]
469    pub disallowed_accounts: Option<Vec<String>>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    #[schema(nullable = false)]
472    pub max_allowed_fee_lamports: Option<u64>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    #[schema(nullable = false)]
475    pub swap_config: Option<RelayerSolanaSwapConfig>,
476}
477
478/// Stellar policy response model for OpenAPI documentation
479#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
480#[serde(deny_unknown_fields)]
481pub struct StellarPolicyResponse {
482    #[serde(skip_serializing_if = "Option::is_none")]
483    #[schema(nullable = false)]
484    pub max_fee: Option<u32>,
485    #[serde(skip_serializing_if = "Option::is_none")]
486    #[schema(nullable = false)]
487    pub timeout_seconds: Option<u64>,
488    #[serde(default = "default_stellar_min_balance")]
489    #[schema(nullable = false)]
490    pub min_balance: u64,
491    #[serde(skip_serializing_if = "Option::is_none")]
492    #[schema(nullable = false)]
493    pub concurrent_transactions: Option<bool>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    #[schema(nullable = false)]
496    pub allowed_tokens: Option<Vec<StellarAllowedTokensPolicy>>,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    #[schema(nullable = false)]
499    pub fee_payment_strategy: Option<StellarFeePaymentStrategy>,
500    #[serde(skip_serializing_if = "Option::is_none")]
501    #[schema(nullable = false)]
502    pub slippage_percentage: Option<f32>,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    #[schema(nullable = false)]
505    pub fee_margin_percentage: Option<f32>,
506    #[serde(skip_serializing_if = "Option::is_none")]
507    #[schema(nullable = false)]
508    pub swap_config: Option<RelayerStellarSwapConfig>,
509}
510
511impl From<RelayerEvmPolicy> for EvmPolicyResponse {
512    fn from(policy: RelayerEvmPolicy) -> Self {
513        Self {
514            min_balance: policy.min_balance.unwrap_or(DEFAULT_EVM_MIN_BALANCE),
515            gas_limit_estimation: policy
516                .gas_limit_estimation
517                .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION),
518            gas_price_cap: policy.gas_price_cap,
519            whitelist_receivers: policy.whitelist_receivers,
520            eip1559_pricing: policy.eip1559_pricing,
521            private_transactions: policy.private_transactions,
522            include_revert_data: policy
523                .include_revert_data
524                .unwrap_or(DEFAULT_EVM_INCLUDE_REVERT_DATA),
525        }
526    }
527}
528
529impl From<RelayerSolanaPolicy> for SolanaPolicyResponse {
530    fn from(policy: RelayerSolanaPolicy) -> Self {
531        Self {
532            allowed_programs: policy.allowed_programs,
533            max_signatures: policy.max_signatures,
534            max_tx_data_size: policy
535                .max_tx_data_size
536                .unwrap_or(DEFAULT_SOLANA_MAX_TX_DATA_SIZE),
537            min_balance: policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE),
538            allowed_tokens: policy.allowed_tokens,
539            fee_payment_strategy: policy.fee_payment_strategy,
540            fee_margin_percentage: policy.fee_margin_percentage,
541            allowed_accounts: policy.allowed_accounts,
542            disallowed_accounts: policy.disallowed_accounts,
543            max_allowed_fee_lamports: policy.max_allowed_fee_lamports,
544            swap_config: policy.swap_config,
545        }
546    }
547}
548
549impl From<RelayerStellarPolicy> for StellarPolicyResponse {
550    fn from(policy: RelayerStellarPolicy) -> Self {
551        Self {
552            min_balance: policy.min_balance.unwrap_or(DEFAULT_STELLAR_MIN_BALANCE),
553            max_fee: policy.max_fee,
554            timeout_seconds: policy.timeout_seconds,
555            concurrent_transactions: policy.concurrent_transactions,
556            allowed_tokens: policy.allowed_tokens,
557            fee_payment_strategy: policy.fee_payment_strategy,
558            slippage_percentage: policy.slippage_percentage,
559            fee_margin_percentage: policy.fee_margin_percentage,
560            swap_config: policy.swap_config,
561        }
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use crate::models::{
569        relayer::{
570            RelayerEvmPolicy, RelayerSolanaPolicy, RelayerSolanaSwapConfig, RelayerStellarPolicy,
571            SolanaAllowedTokensPolicy, SolanaFeePaymentStrategy, SolanaSwapStrategy,
572            StellarAllowedTokensPolicy, StellarFeePaymentStrategy, StellarSwapStrategy,
573        },
574        StellarTokenKind, StellarTokenMetadata,
575    };
576
577    #[test]
578    fn test_from_domain_relayer() {
579        let relayer = Relayer::new(
580            "test-relayer".to_string(),
581            "Test Relayer".to_string(),
582            "mainnet".to_string(),
583            false,
584            RelayerNetworkType::Evm,
585            Some(RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
586                include_revert_data: None,
587                gas_price_cap: Some(100_000_000_000),
588                whitelist_receivers: None,
589                eip1559_pricing: Some(true),
590                private_transactions: None,
591                min_balance: None,
592                gas_limit_estimation: None,
593            })),
594            "test-signer".to_string(),
595            None,
596            None,
597        );
598
599        let response: RelayerResponse = relayer.clone().into();
600
601        assert_eq!(response.id, relayer.id);
602        assert_eq!(response.name, relayer.name);
603        assert_eq!(response.network, relayer.network);
604        assert_eq!(response.network_type, relayer.network_type);
605        assert_eq!(response.paused, relayer.paused);
606        assert_eq!(
607            response.policies,
608            Some(RelayerNetworkPolicyResponse::Evm(
609                RelayerEvmPolicy {
610                    include_revert_data: None,
611                    gas_price_cap: Some(100_000_000_000),
612                    whitelist_receivers: None,
613                    eip1559_pricing: Some(true),
614                    private_transactions: None,
615                    min_balance: Some(DEFAULT_EVM_MIN_BALANCE),
616                    gas_limit_estimation: Some(DEFAULT_EVM_GAS_LIMIT_ESTIMATION),
617                }
618                .into()
619            ))
620        );
621        assert_eq!(response.signer_id, relayer.signer_id);
622        assert_eq!(response.notification_id, relayer.notification_id);
623        // custom_rpc_urls is None in this test
624        assert_eq!(response.custom_rpc_urls, None);
625        assert_eq!(response.address, None);
626        assert_eq!(response.system_disabled, None);
627    }
628
629    #[test]
630    fn test_from_domain_relayer_solana() {
631        let relayer = Relayer::new(
632            "test-solana-relayer".to_string(),
633            "Test Solana Relayer".to_string(),
634            "mainnet".to_string(),
635            false,
636            RelayerNetworkType::Solana,
637            Some(RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
638                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
639                max_signatures: Some(5),
640                min_balance: Some(1000000),
641                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
642                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
643                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
644                    Some(100000),
645                    None,
646                )]),
647                max_tx_data_size: None,
648                fee_margin_percentage: None,
649                allowed_accounts: None,
650                disallowed_accounts: None,
651                max_allowed_fee_lamports: None,
652                swap_config: None,
653            })),
654            "test-signer".to_string(),
655            None,
656            None,
657        );
658
659        let response: RelayerResponse = relayer.clone().into();
660
661        assert_eq!(response.id, relayer.id);
662        assert_eq!(response.network_type, RelayerNetworkType::Solana);
663        assert!(response.policies.is_some());
664
665        if let Some(RelayerNetworkPolicyResponse::Solana(solana_response)) = response.policies {
666            assert_eq!(solana_response.min_balance, 1000000);
667            assert_eq!(solana_response.max_signatures, Some(5));
668        } else {
669            panic!("Expected Solana policy response");
670        }
671    }
672
673    #[test]
674    fn test_from_domain_relayer_stellar() {
675        let relayer = Relayer::new(
676            "test-stellar-relayer".to_string(),
677            "Test Stellar Relayer".to_string(),
678            "mainnet".to_string(),
679            false,
680            RelayerNetworkType::Stellar,
681            Some(RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
682                min_balance: Some(20000000),
683                max_fee: Some(100000),
684                timeout_seconds: Some(30),
685                concurrent_transactions: None,
686                allowed_tokens: None,
687                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
688                slippage_percentage: None,
689                fee_margin_percentage: None,
690                swap_config: None,
691            })),
692            "test-signer".to_string(),
693            None,
694            None,
695        );
696
697        let response: RelayerResponse = relayer.clone().into();
698
699        assert_eq!(response.id, relayer.id);
700        assert_eq!(response.network_type, RelayerNetworkType::Stellar);
701        assert!(response.policies.is_some());
702
703        if let Some(RelayerNetworkPolicyResponse::Stellar(stellar_response)) = response.policies {
704            assert_eq!(stellar_response.min_balance, 20000000);
705        } else {
706            panic!("Expected Stellar policy response");
707        }
708    }
709
710    #[test]
711    fn test_response_serialization() {
712        let response = RelayerResponse {
713            id: "test-relayer".to_string(),
714            name: "Test Relayer".to_string(),
715            network: "mainnet".to_string(),
716            network_type: RelayerNetworkType::Evm,
717            paused: false,
718            policies: Some(RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse {
719                include_revert_data: DEFAULT_EVM_INCLUDE_REVERT_DATA,
720                gas_price_cap: Some(50000000000),
721                whitelist_receivers: None,
722                eip1559_pricing: Some(true),
723                private_transactions: None,
724                min_balance: DEFAULT_EVM_MIN_BALANCE,
725                gas_limit_estimation: DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
726            })),
727            signer_id: "test-signer".to_string(),
728            notification_id: None,
729            custom_rpc_urls: None,
730            address: Some("0x123...".to_string()),
731            system_disabled: Some(false),
732            ..Default::default()
733        };
734
735        // Should serialize without errors
736        let serialized = serde_json::to_string(&response).unwrap();
737        assert!(!serialized.is_empty());
738
739        // Should deserialize back to the same struct
740        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
741        assert_eq!(response.id, deserialized.id);
742        assert_eq!(response.name, deserialized.name);
743    }
744
745    #[test]
746    fn test_solana_response_serialization() {
747        let response = RelayerResponse {
748            id: "test-solana-relayer".to_string(),
749            name: "Test Solana Relayer".to_string(),
750            network: "mainnet".to_string(),
751            network_type: RelayerNetworkType::Solana,
752            paused: false,
753            policies: Some(RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse {
754                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
755                max_signatures: Some(5),
756                max_tx_data_size: DEFAULT_SOLANA_MAX_TX_DATA_SIZE,
757                min_balance: 1000000,
758                allowed_tokens: Some(vec![SolanaAllowedTokensPolicy::new(
759                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
760                    Some(100000),
761                    None,
762                )]),
763                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
764                fee_margin_percentage: Some(5.0),
765                allowed_accounts: None,
766                disallowed_accounts: None,
767                max_allowed_fee_lamports: Some(500000),
768                swap_config: Some(RelayerSolanaSwapConfig {
769                    strategy: Some(SolanaSwapStrategy::JupiterSwap),
770                    cron_schedule: Some("0 0 * * *".to_string()),
771                    min_balance_threshold: Some(500000),
772                    jupiter_swap_options: None,
773                }),
774            })),
775            signer_id: "test-signer".to_string(),
776            notification_id: None,
777            custom_rpc_urls: None,
778            address: Some("SolanaAddress123...".to_string()),
779            system_disabled: Some(false),
780            ..Default::default()
781        };
782
783        // Should serialize without errors
784        let serialized = serde_json::to_string(&response).unwrap();
785        assert!(!serialized.is_empty());
786
787        // Should deserialize back to the same struct
788        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
789        assert_eq!(response.id, deserialized.id);
790        assert_eq!(response.network_type, RelayerNetworkType::Solana);
791    }
792
793    #[test]
794    fn test_stellar_response_serialization() {
795        let response = RelayerResponse {
796            id: "test-stellar-relayer".to_string(),
797            name: "Test Stellar Relayer".to_string(),
798            network: "mainnet".to_string(),
799            network_type: RelayerNetworkType::Stellar,
800            paused: false,
801            policies: Some(RelayerNetworkPolicyResponse::Stellar(
802                StellarPolicyResponse {
803                    max_fee: Some(5000),
804                    timeout_seconds: None,
805                    min_balance: 20000000,
806                    concurrent_transactions: None,
807                    allowed_tokens: None,
808                    fee_payment_strategy: None,
809                    slippage_percentage: None,
810                    fee_margin_percentage: None,
811                    swap_config: None,
812                },
813            )),
814            signer_id: "test-signer".to_string(),
815            notification_id: None,
816            custom_rpc_urls: None,
817            address: Some("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string()),
818            system_disabled: Some(false),
819            ..Default::default()
820        };
821
822        // Should serialize without errors
823        let serialized = serde_json::to_string(&response).unwrap();
824        assert!(!serialized.is_empty());
825
826        // Should deserialize back to the same struct
827        let deserialized: RelayerResponse = serde_json::from_str(&serialized).unwrap();
828        assert_eq!(response.id, deserialized.id);
829        assert_eq!(response.network_type, RelayerNetworkType::Stellar);
830
831        // Verify Stellar-specific fields
832        if let Some(RelayerNetworkPolicyResponse::Stellar(stellar_policy)) = deserialized.policies {
833            assert_eq!(stellar_policy.min_balance, 20000000);
834            assert_eq!(stellar_policy.max_fee, Some(5000));
835            assert_eq!(stellar_policy.timeout_seconds, None);
836        } else {
837            panic!("Expected Stellar policy in deserialized response");
838        }
839    }
840
841    #[test]
842    fn test_response_without_redundant_network_type() {
843        let response = RelayerResponse {
844            id: "test-relayer".to_string(),
845            name: "Test Relayer".to_string(),
846            network: "mainnet".to_string(),
847            network_type: RelayerNetworkType::Evm,
848            paused: false,
849            policies: Some(RelayerNetworkPolicyResponse::Evm(EvmPolicyResponse {
850                include_revert_data: DEFAULT_EVM_INCLUDE_REVERT_DATA,
851                gas_price_cap: Some(100_000_000_000),
852                whitelist_receivers: None,
853                eip1559_pricing: Some(true),
854                private_transactions: None,
855                min_balance: DEFAULT_EVM_MIN_BALANCE,
856                gas_limit_estimation: DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
857            })),
858            signer_id: "test-signer".to_string(),
859            notification_id: None,
860            custom_rpc_urls: None,
861            address: Some("0x123...".to_string()),
862            system_disabled: Some(false),
863            ..Default::default()
864        };
865
866        let serialized = serde_json::to_string_pretty(&response).unwrap();
867
868        assert!(serialized.contains(r#""network_type": "evm""#));
869
870        // Count occurrences - should only be 1 (at top level)
871        let network_type_count = serialized.matches(r#""network_type""#).count();
872        assert_eq!(
873            network_type_count, 1,
874            "Should only have one network_type field at top level, not in policies"
875        );
876
877        assert!(serialized.contains(r#""gas_price_cap": 100000000000"#));
878        assert!(serialized.contains(r#""eip1559_pricing": true"#));
879    }
880
881    #[test]
882    fn test_solana_response_without_redundant_network_type() {
883        let response = RelayerResponse {
884            id: "test-solana-relayer".to_string(),
885            name: "Test Solana Relayer".to_string(),
886            network: "mainnet".to_string(),
887            network_type: RelayerNetworkType::Solana,
888            paused: false,
889            policies: Some(RelayerNetworkPolicyResponse::Solana(SolanaPolicyResponse {
890                allowed_programs: Some(vec!["11111111111111111111111111111111".to_string()]),
891                max_signatures: Some(5),
892                max_tx_data_size: DEFAULT_SOLANA_MAX_TX_DATA_SIZE,
893                min_balance: 1000000,
894                allowed_tokens: None,
895                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
896                fee_margin_percentage: None,
897                allowed_accounts: None,
898                disallowed_accounts: None,
899                max_allowed_fee_lamports: None,
900                swap_config: None,
901            })),
902            signer_id: "test-signer".to_string(),
903            notification_id: None,
904            custom_rpc_urls: None,
905            address: Some("SolanaAddress123...".to_string()),
906            system_disabled: Some(false),
907            ..Default::default()
908        };
909
910        let serialized = serde_json::to_string_pretty(&response).unwrap();
911
912        assert!(serialized.contains(r#""network_type": "solana""#));
913
914        // Count occurrences - should only be 1 (at top level)
915        let network_type_count = serialized.matches(r#""network_type""#).count();
916        assert_eq!(
917            network_type_count, 1,
918            "Should only have one network_type field at top level, not in policies"
919        );
920
921        assert!(serialized.contains(r#""max_signatures": 5"#));
922        assert!(serialized.contains(r#""fee_payment_strategy": "relayer""#));
923    }
924
925    #[test]
926    fn test_stellar_response_without_redundant_network_type() {
927        let response = RelayerResponse {
928            id: "test-stellar-relayer".to_string(),
929            name: "Test Stellar Relayer".to_string(),
930            network: "mainnet".to_string(),
931            network_type: RelayerNetworkType::Stellar,
932            paused: false,
933            policies: Some(RelayerNetworkPolicyResponse::Stellar(
934                StellarPolicyResponse {
935                    min_balance: 20000000,
936                    max_fee: Some(100000),
937                    timeout_seconds: Some(30),
938                    concurrent_transactions: None,
939                    allowed_tokens: None,
940                    fee_payment_strategy: None,
941                    slippage_percentage: None,
942                    fee_margin_percentage: None,
943                    swap_config: None,
944                },
945            )),
946            signer_id: "test-signer".to_string(),
947            notification_id: None,
948            custom_rpc_urls: None,
949            address: Some("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string()),
950            system_disabled: Some(false),
951            ..Default::default()
952        };
953
954        let serialized = serde_json::to_string_pretty(&response).unwrap();
955
956        assert!(serialized.contains(r#""network_type": "stellar""#));
957
958        // Count occurrences - should only be 1 (at top level)
959        let network_type_count = serialized.matches(r#""network_type""#).count();
960        assert_eq!(
961            network_type_count, 1,
962            "Should only have one network_type field at top level, not in policies"
963        );
964
965        assert!(serialized.contains(r#""min_balance": 20000000"#));
966        assert!(serialized.contains(r#""max_fee": 100000"#));
967        assert!(serialized.contains(r#""timeout_seconds": 30"#));
968    }
969
970    #[test]
971    fn test_empty_policies_not_returned_in_response() {
972        // Create a repository model with empty policies (all None - user didn't set any)
973        let repo_model = RelayerRepoModel {
974            id: "test-relayer".to_string(),
975            name: "Test Relayer".to_string(),
976            network: "mainnet".to_string(),
977            network_type: RelayerNetworkType::Evm,
978            paused: false,
979            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()), // All None values
980            signer_id: "test-signer".to_string(),
981            notification_id: None,
982            custom_rpc_urls: None,
983            address: "0x123...".to_string(),
984            system_disabled: false,
985            ..Default::default()
986        };
987
988        // Convert to response
989        let response = RelayerResponse::from(repo_model);
990
991        // Empty policies should not be included in response
992        assert_eq!(response.policies, None);
993
994        // Verify serialization doesn't include policies field
995        let serialized = serde_json::to_string(&response).unwrap();
996        assert!(
997            !serialized.contains("policies"),
998            "Empty policies should not appear in JSON response"
999        );
1000    }
1001
1002    #[test]
1003    fn test_empty_solana_policies_not_returned_in_response() {
1004        // Create a repository model with empty Solana policies (all None - user didn't set any)
1005        let repo_model = RelayerRepoModel {
1006            id: "test-solana-relayer".to_string(),
1007            name: "Test Solana Relayer".to_string(),
1008            network: "mainnet".to_string(),
1009            network_type: RelayerNetworkType::Solana,
1010            paused: false,
1011            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()), // All None values
1012            signer_id: "test-signer".to_string(),
1013            notification_id: None,
1014            custom_rpc_urls: None,
1015            address: "SolanaAddress123...".to_string(),
1016            system_disabled: false,
1017            ..Default::default()
1018        };
1019
1020        // Convert to response
1021        let response = RelayerResponse::from(repo_model);
1022
1023        // Empty policies should not be included in response
1024        assert_eq!(response.policies, None);
1025
1026        // Verify serialization doesn't include policies field
1027        let serialized = serde_json::to_string(&response).unwrap();
1028        assert!(
1029            !serialized.contains("policies"),
1030            "Empty Solana policies should not appear in JSON response"
1031        );
1032    }
1033
1034    #[test]
1035    fn test_empty_stellar_policies_not_returned_in_response() {
1036        // Create a repository model with empty Stellar policies (all None - user didn't set any)
1037        let repo_model = RelayerRepoModel {
1038            id: "test-stellar-relayer".to_string(),
1039            name: "Test Stellar Relayer".to_string(),
1040            network: "mainnet".to_string(),
1041            network_type: RelayerNetworkType::Stellar,
1042            paused: false,
1043            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()), // All None values
1044            signer_id: "test-signer".to_string(),
1045            notification_id: None,
1046            custom_rpc_urls: None,
1047            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1048            system_disabled: false,
1049            ..Default::default()
1050        };
1051
1052        // Convert to response
1053        let response = RelayerResponse::from(repo_model);
1054
1055        // Empty policies should not be included in response
1056        assert_eq!(response.policies, None);
1057
1058        // Verify serialization doesn't include policies field
1059        let serialized = serde_json::to_string(&response).unwrap();
1060        assert!(
1061            !serialized.contains("policies"),
1062            "Empty Stellar policies should not appear in JSON response"
1063        );
1064    }
1065
1066    #[test]
1067    fn test_user_provided_policies_returned_in_response() {
1068        // Create a repository model with user-provided policies
1069        let repo_model = RelayerRepoModel {
1070            id: "test-relayer".to_string(),
1071            name: "Test Relayer".to_string(),
1072            network: "mainnet".to_string(),
1073            network_type: RelayerNetworkType::Evm,
1074            paused: false,
1075            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
1076                include_revert_data: None,
1077                gas_price_cap: Some(100_000_000_000),
1078                eip1559_pricing: Some(true),
1079                min_balance: None, // Some fields can still be None
1080                gas_limit_estimation: None,
1081                whitelist_receivers: None,
1082                private_transactions: None,
1083            }),
1084            signer_id: "test-signer".to_string(),
1085            notification_id: None,
1086            custom_rpc_urls: None,
1087            address: "0x123...".to_string(),
1088            system_disabled: false,
1089            ..Default::default()
1090        };
1091
1092        // Convert to response
1093        let response = RelayerResponse::from(repo_model);
1094
1095        // User-provided policies should be included in response
1096        assert!(response.policies.is_some());
1097
1098        // Verify serialization includes policies field
1099        let serialized = serde_json::to_string(&response).unwrap();
1100        assert!(
1101            serialized.contains("policies"),
1102            "User-provided policies should appear in JSON response"
1103        );
1104        assert!(
1105            serialized.contains("gas_price_cap"),
1106            "User-provided policy values should appear in JSON response"
1107        );
1108    }
1109
1110    #[test]
1111    fn test_user_provided_solana_policies_returned_in_response() {
1112        // Create a repository model with user-provided Solana policies
1113        let repo_model = RelayerRepoModel {
1114            id: "test-solana-relayer".to_string(),
1115            name: "Test Solana Relayer".to_string(),
1116            network: "mainnet".to_string(),
1117            network_type: RelayerNetworkType::Solana,
1118            paused: false,
1119            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1120                max_signatures: Some(5),
1121                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
1122                min_balance: Some(1000000),
1123                allowed_programs: None, // Some fields can still be None
1124                max_tx_data_size: None,
1125                allowed_tokens: None,
1126                fee_margin_percentage: None,
1127                allowed_accounts: None,
1128                disallowed_accounts: None,
1129                max_allowed_fee_lamports: None,
1130                swap_config: None,
1131            }),
1132            signer_id: "test-signer".to_string(),
1133            notification_id: None,
1134            custom_rpc_urls: None,
1135            address: "SolanaAddress123...".to_string(),
1136            system_disabled: false,
1137            ..Default::default()
1138        };
1139
1140        // Convert to response
1141        let response = RelayerResponse::from(repo_model);
1142
1143        // User-provided policies should be included in response
1144        assert!(response.policies.is_some());
1145
1146        // Verify serialization includes policies field
1147        let serialized = serde_json::to_string(&response).unwrap();
1148        assert!(
1149            serialized.contains("policies"),
1150            "User-provided Solana policies should appear in JSON response"
1151        );
1152        assert!(
1153            serialized.contains("max_signatures"),
1154            "User-provided Solana policy values should appear in JSON response"
1155        );
1156        assert!(
1157            serialized.contains("fee_payment_strategy"),
1158            "User-provided Solana policy values should appear in JSON response"
1159        );
1160    }
1161
1162    #[test]
1163    fn test_user_provided_stellar_policies_returned_in_response() {
1164        // Create a repository model with user-provided Stellar policies
1165        let repo_model = RelayerRepoModel {
1166            id: "test-stellar-relayer".to_string(),
1167            name: "Test Stellar Relayer".to_string(),
1168            network: "mainnet".to_string(),
1169            network_type: RelayerNetworkType::Stellar,
1170            paused: false,
1171            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
1172                max_fee: Some(100000),
1173                timeout_seconds: Some(30),
1174                min_balance: Some(20000000),
1175                concurrent_transactions: Some(true),
1176                allowed_tokens: Some(vec![StellarAllowedTokensPolicy::new(
1177                    "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN".to_string(),
1178                    Some(StellarTokenMetadata {
1179                        kind: StellarTokenKind::Classic {
1180                            code: "USDC".to_string(),
1181                            issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
1182                                .to_string(),
1183                        },
1184                        decimals: 6,
1185                        canonical_asset_id:
1186                            "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
1187                                .to_string(),
1188                    }),
1189                    None,
1190                    None,
1191                )]),
1192                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
1193                slippage_percentage: Some(0.5),
1194                fee_margin_percentage: Some(2.0),
1195                swap_config: Some(RelayerStellarSwapConfig {
1196                    strategies: vec![StellarSwapStrategy::Soroswap],
1197                    cron_schedule: Some("0 0 * * *".to_string()),
1198                    min_balance_threshold: Some(10000000),
1199                }),
1200            }),
1201            signer_id: "test-signer".to_string(),
1202            notification_id: None,
1203            custom_rpc_urls: None,
1204            address: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1205            system_disabled: false,
1206            ..Default::default()
1207        };
1208
1209        // Convert to response
1210        let response = RelayerResponse::from(repo_model);
1211
1212        // User-provided policies should be included in response
1213        assert!(response.policies.is_some());
1214
1215        // Verify serialization includes policies field
1216        let serialized = serde_json::to_string(&response).unwrap();
1217        assert!(
1218            serialized.contains("policies"),
1219            "User-provided Stellar policies should appear in JSON response"
1220        );
1221        assert!(
1222            serialized.contains("max_fee"),
1223            "User-provided Stellar policy values should appear in JSON response"
1224        );
1225        assert!(
1226            serialized.contains("timeout_seconds"),
1227            "User-provided Stellar policy values should appear in JSON response"
1228        );
1229        assert!(
1230            serialized.contains("allowed_tokens"),
1231            "User-provided Stellar policy values should appear in JSON response"
1232        );
1233        assert!(
1234            serialized.contains("fee_payment_strategy"),
1235            "User-provided Stellar policy values should appear in JSON response"
1236        );
1237        assert!(
1238            serialized.contains("slippage_percentage"),
1239            "User-provided Stellar policy values should appear in JSON response"
1240        );
1241        assert!(
1242            serialized.contains("fee_margin_percentage"),
1243            "User-provided Stellar policy values should appear in JSON response"
1244        );
1245        assert!(
1246            serialized.contains("swap_config"),
1247            "User-provided Stellar policy values should appear in JSON response"
1248        );
1249    }
1250
1251    #[test]
1252    fn test_stellar_fee_payment_strategy_explicitly_set_vs_omitted() {
1253        // Test 1: Explicitly set to User - should appear in serialization
1254        let policy_with_user = RelayerStellarPolicy {
1255            min_balance: Some(20000000),
1256            max_fee: Some(100000),
1257            timeout_seconds: Some(30),
1258            concurrent_transactions: None,
1259            allowed_tokens: None,
1260            fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
1261            slippage_percentage: None,
1262            fee_margin_percentage: None,
1263            swap_config: None,
1264        };
1265
1266        let response_with_user = StellarPolicyResponse::from(policy_with_user);
1267        let serialized_with_user = serde_json::to_string(&response_with_user).unwrap();
1268        assert!(
1269            serialized_with_user.contains(r#""fee_payment_strategy":"user""#),
1270            "Explicitly set User fee_payment_strategy should appear in JSON response"
1271        );
1272
1273        // Test 2: Explicitly set to Relayer - should appear in serialization
1274        let policy_with_relayer = RelayerStellarPolicy {
1275            min_balance: Some(20000000),
1276            max_fee: Some(100000),
1277            timeout_seconds: Some(30),
1278            concurrent_transactions: None,
1279            allowed_tokens: None,
1280            fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
1281            slippage_percentage: None,
1282            fee_margin_percentage: None,
1283            swap_config: None,
1284        };
1285
1286        let response_with_relayer = StellarPolicyResponse::from(policy_with_relayer);
1287        let serialized_with_relayer = serde_json::to_string(&response_with_relayer).unwrap();
1288        assert!(
1289            serialized_with_relayer.contains(r#""fee_payment_strategy":"relayer""#),
1290            "Explicitly set Relayer fee_payment_strategy should appear in JSON response"
1291        );
1292
1293        // Test 3: Not set (None) - should NOT appear in serialization due to skip_serializing_if
1294        let policy_omitted = RelayerStellarPolicy {
1295            min_balance: Some(20000000),
1296            max_fee: Some(100000),
1297            timeout_seconds: Some(30),
1298            concurrent_transactions: None,
1299            allowed_tokens: None,
1300            fee_payment_strategy: None,
1301            slippage_percentage: None,
1302            fee_margin_percentage: None,
1303            swap_config: None,
1304        };
1305
1306        let response_omitted = StellarPolicyResponse::from(policy_omitted);
1307        let serialized_omitted = serde_json::to_string(&response_omitted).unwrap();
1308        assert!(
1309            !serialized_omitted.contains("fee_payment_strategy"),
1310            "Omitted fee_payment_strategy (None) should NOT appear in JSON response"
1311        );
1312
1313        // Test 4: Verify is_empty_policy correctly identifies None vs Some(User)
1314        let empty_policy = RelayerStellarPolicy::default();
1315        assert!(
1316            is_empty_policy(&RelayerNetworkPolicy::Stellar(empty_policy)),
1317            "Policy with all None values should be considered empty"
1318        );
1319
1320        let policy_with_user_only = RelayerStellarPolicy {
1321            fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
1322            ..Default::default()
1323        };
1324        assert!(
1325            !is_empty_policy(&RelayerNetworkPolicy::Stellar(policy_with_user_only)),
1326            "Policy with explicitly set User fee_payment_strategy should NOT be considered empty"
1327        );
1328
1329        let policy_with_relayer_only = RelayerStellarPolicy {
1330            fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
1331            ..Default::default()
1332        };
1333        assert!(
1334            !is_empty_policy(&RelayerNetworkPolicy::Stellar(policy_with_relayer_only)),
1335            "Policy with explicitly set Relayer fee_payment_strategy should NOT be considered empty"
1336        );
1337    }
1338
1339    #[test]
1340    fn test_relayer_status_serialization() {
1341        // Test EVM status
1342        let evm_status = RelayerStatus::Evm {
1343            balance: "1000000000000000000".to_string(),
1344            pending_transactions_count: 5,
1345            last_confirmed_transaction_timestamp: Some("2024-01-01T00:00:00Z".to_string()),
1346            system_disabled: false,
1347            paused: false,
1348            nonce: "42".to_string(),
1349        };
1350
1351        let serialized = serde_json::to_string(&evm_status).unwrap();
1352        assert!(serialized.contains(r#""network_type":"evm""#));
1353        assert!(serialized.contains(r#""nonce":"42""#));
1354        assert!(serialized.contains(r#""balance":"1000000000000000000""#));
1355
1356        // Test Solana status
1357        let solana_status = RelayerStatus::Solana {
1358            balance: "5000000000".to_string(),
1359            pending_transactions_count: 3,
1360            last_confirmed_transaction_timestamp: None,
1361            system_disabled: false,
1362            paused: true,
1363        };
1364
1365        let serialized = serde_json::to_string(&solana_status).unwrap();
1366        assert!(serialized.contains(r#""network_type":"solana""#));
1367        assert!(serialized.contains(r#""balance":"5000000000""#));
1368        assert!(serialized.contains(r#""paused":true"#));
1369
1370        // Test Stellar status
1371        let stellar_status = RelayerStatus::Stellar {
1372            balance: "1000000000".to_string(),
1373            pending_transactions_count: 2,
1374            last_confirmed_transaction_timestamp: Some("2024-01-01T12:00:00Z".to_string()),
1375            system_disabled: true,
1376            paused: false,
1377            sequence_number: "123456789".to_string(),
1378        };
1379
1380        let serialized = serde_json::to_string(&stellar_status).unwrap();
1381        assert!(serialized.contains(r#""network_type":"stellar""#));
1382        assert!(serialized.contains(r#""sequence_number":"123456789""#));
1383        assert!(serialized.contains(r#""system_disabled":true"#));
1384    }
1385
1386    #[test]
1387    fn test_relayer_status_deserialization() {
1388        // Test EVM status deserialization
1389        let evm_json = r#"{
1390            "network_type": "evm",
1391            "balance": "1000000000000000000",
1392            "pending_transactions_count": 5,
1393            "last_confirmed_transaction_timestamp": "2024-01-01T00:00:00Z",
1394            "system_disabled": false,
1395            "paused": false,
1396            "nonce": "42"
1397        }"#;
1398
1399        let status: RelayerStatus = serde_json::from_str(evm_json).unwrap();
1400        if let RelayerStatus::Evm { nonce, balance, .. } = status {
1401            assert_eq!(nonce, "42");
1402            assert_eq!(balance, "1000000000000000000");
1403        } else {
1404            panic!("Expected EVM status");
1405        }
1406
1407        // Test Solana status deserialization
1408        let solana_json = r#"{
1409            "network_type": "solana",
1410            "balance": "5000000000",
1411            "pending_transactions_count": 3,
1412            "last_confirmed_transaction_timestamp": null,
1413            "system_disabled": false,
1414            "paused": true
1415        }"#;
1416
1417        let status: RelayerStatus = serde_json::from_str(solana_json).unwrap();
1418        if let RelayerStatus::Solana {
1419            balance, paused, ..
1420        } = status
1421        {
1422            assert_eq!(balance, "5000000000");
1423            assert!(paused);
1424        } else {
1425            panic!("Expected Solana status");
1426        }
1427
1428        // Test Stellar status deserialization
1429        let stellar_json = r#"{
1430            "network_type": "stellar",
1431            "balance": "1000000000",
1432            "pending_transactions_count": 2,
1433            "last_confirmed_transaction_timestamp": "2024-01-01T12:00:00Z",
1434            "system_disabled": true,
1435            "paused": false,
1436            "sequence_number": "123456789"
1437        }"#;
1438
1439        let status: RelayerStatus = serde_json::from_str(stellar_json).unwrap();
1440        if let RelayerStatus::Stellar {
1441            sequence_number,
1442            system_disabled,
1443            ..
1444        } = status
1445        {
1446            assert_eq!(sequence_number, "123456789");
1447            assert!(system_disabled);
1448        } else {
1449            panic!("Expected Stellar status");
1450        }
1451    }
1452}