openzeppelin_relayer/models/relayer/
response.rs

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