openzeppelin_relayer/models/relayer/
response.rs

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