openzeppelin_relayer/models/relayer/
request.rs

1//! Request models for relayer API endpoints.
2//!
3//! This module provides request structures used by relayer CRUD API endpoints,
4//! including:
5//!
6//! - **Create Requests**: New relayer creation
7//! - **Update Requests**: Partial relayer updates
8//! - **Validation**: Input validation and error handling
9//! - **Conversions**: Mapping between API requests and domain models
10//!
11//! These models handle API-specific concerns like optional fields for updates
12//! while delegating business logic validation to the domain model.
13
14use super::{
15    Relayer, RelayerEvmPolicy, RelayerNetworkPolicy, RelayerNetworkType, RelayerSolanaPolicy,
16    RelayerStellarPolicy, RpcConfig,
17};
18use crate::{models::error::ApiError, utils::generate_uuid};
19use serde::{Deserialize, Serialize};
20use utoipa::ToSchema;
21
22/// Request model for creating a new relayer
23#[derive(Debug, Clone, Serialize, ToSchema)]
24#[serde(deny_unknown_fields)]
25pub struct CreateRelayerRequest {
26    #[schema(nullable = false)]
27    pub id: Option<String>,
28    pub name: String,
29    pub network: String,
30    pub paused: bool,
31    pub network_type: RelayerNetworkType,
32    /// Policies - will be deserialized based on the network_type field
33    #[serde(skip_serializing_if = "Option::is_none")]
34    #[schema(nullable = false)]
35    pub policies: Option<CreateRelayerPolicyRequest>,
36    #[schema(nullable = false)]
37    pub signer_id: String,
38    #[schema(nullable = false)]
39    pub notification_id: Option<String>,
40    #[schema(nullable = false)]
41    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
42}
43
44/// Helper struct for deserializing CreateRelayerRequest with raw policies JSON
45#[derive(Debug, Clone, Deserialize)]
46#[serde(deny_unknown_fields)]
47struct CreateRelayerRequestRaw {
48    pub id: Option<String>,
49    pub name: String,
50    pub network: String,
51    pub paused: bool,
52    pub network_type: RelayerNetworkType,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub policies: Option<serde_json::Value>,
55    pub signer_id: String,
56    pub notification_id: Option<String>,
57    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
58}
59
60impl<'de> serde::Deserialize<'de> for CreateRelayerRequest {
61    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62    where
63        D: serde::Deserializer<'de>,
64    {
65        let raw = CreateRelayerRequestRaw::deserialize(deserializer)?;
66
67        // Convert policies based on network_type using the existing utility function
68        let policies = if let Some(policies_value) = raw.policies {
69            let domain_policy =
70                deserialize_policy_for_network_type(&policies_value, raw.network_type)
71                    .map_err(serde::de::Error::custom)?;
72
73            // Convert from RelayerNetworkPolicy to CreateRelayerPolicyRequest
74            let policy = match domain_policy {
75                RelayerNetworkPolicy::Evm(evm_policy) => {
76                    CreateRelayerPolicyRequest::Evm(evm_policy)
77                }
78                RelayerNetworkPolicy::Solana(solana_policy) => {
79                    CreateRelayerPolicyRequest::Solana(solana_policy)
80                }
81                RelayerNetworkPolicy::Stellar(stellar_policy) => {
82                    CreateRelayerPolicyRequest::Stellar(stellar_policy)
83                }
84            };
85            Some(policy)
86        } else {
87            None
88        };
89
90        Ok(CreateRelayerRequest {
91            id: raw.id,
92            name: raw.name,
93            network: raw.network,
94            paused: raw.paused,
95            network_type: raw.network_type,
96            policies,
97            signer_id: raw.signer_id,
98            notification_id: raw.notification_id,
99            custom_rpc_urls: raw.custom_rpc_urls,
100        })
101    }
102}
103
104/// Policy types for create requests - deserialized based on network_type from parent request
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
106#[serde(deny_unknown_fields, untagged)]
107pub enum CreateRelayerPolicyRequest {
108    Evm(RelayerEvmPolicy),
109    Solana(RelayerSolanaPolicy),
110    Stellar(RelayerStellarPolicy),
111}
112
113impl CreateRelayerPolicyRequest {
114    /// Converts to domain RelayerNetworkPolicy using the provided network type
115    pub fn to_domain_policy(
116        &self,
117        network_type: RelayerNetworkType,
118    ) -> Result<RelayerNetworkPolicy, ApiError> {
119        match (self, network_type) {
120            (CreateRelayerPolicyRequest::Evm(policy), RelayerNetworkType::Evm) => {
121                Ok(RelayerNetworkPolicy::Evm(policy.clone()))
122            }
123            (CreateRelayerPolicyRequest::Solana(policy), RelayerNetworkType::Solana) => {
124                Ok(RelayerNetworkPolicy::Solana(policy.clone()))
125            }
126            (CreateRelayerPolicyRequest::Stellar(policy), RelayerNetworkType::Stellar) => {
127                Ok(RelayerNetworkPolicy::Stellar(policy.clone()))
128            }
129            _ => Err(ApiError::BadRequest(
130                "Policy type does not match relayer network type".to_string(),
131            )),
132        }
133    }
134}
135
136/// Utility function to deserialize policy JSON for a specific network type
137/// Used for update requests where we know the network type ahead of time
138pub fn deserialize_policy_for_network_type(
139    policies_value: &serde_json::Value,
140    network_type: RelayerNetworkType,
141) -> Result<RelayerNetworkPolicy, ApiError> {
142    match network_type {
143        RelayerNetworkType::Evm => {
144            let evm_policy: RelayerEvmPolicy = serde_json::from_value(policies_value.clone())
145                .map_err(|e| ApiError::BadRequest(format!("Invalid EVM policy: {e}")))?;
146            Ok(RelayerNetworkPolicy::Evm(evm_policy))
147        }
148        RelayerNetworkType::Solana => {
149            let solana_policy: RelayerSolanaPolicy = serde_json::from_value(policies_value.clone())
150                .map_err(|e| ApiError::BadRequest(format!("Invalid Solana policy: {e}")))?;
151            Ok(RelayerNetworkPolicy::Solana(solana_policy))
152        }
153        RelayerNetworkType::Stellar => {
154            let stellar_policy: RelayerStellarPolicy =
155                serde_json::from_value(policies_value.clone())
156                    .map_err(|e| ApiError::BadRequest(format!("Invalid Stellar policy: {e}")))?;
157            Ok(RelayerNetworkPolicy::Stellar(stellar_policy))
158        }
159    }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
163#[serde(deny_unknown_fields)]
164pub struct UpdateRelayerRequest {
165    pub name: Option<String>,
166    #[schema(nullable = false)]
167    pub paused: Option<bool>,
168    /// Raw policy JSON - will be validated against relayer's network type during application
169    #[serde(skip_serializing_if = "Option::is_none")]
170    #[schema(nullable = false)]
171    pub policies: Option<CreateRelayerPolicyRequest>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    #[schema(nullable = false)]
174    pub notification_id: Option<String>,
175    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
176}
177
178/// Request model for updating an existing relayer
179/// All fields are optional to allow partial updates
180/// Note: network and signer_id are not updateable after creation
181///
182/// ## Merge Patch Semantics for Policies
183/// The policies field uses JSON Merge Patch (RFC 7396) semantics:
184/// - Field not provided: no change to existing value
185/// - Field with null value: remove/clear the field
186/// - Field with value: update the field
187/// - Empty object {}: no changes to any policy fields
188///
189/// ## Merge Patch Semantics for notification_id
190/// The notification_id field also uses JSON Merge Patch semantics:
191/// - Field not provided: no change to existing value
192/// - Field with null value: remove notification (set to None)
193/// - Field with string value: set to that notification ID
194///
195/// ## Example Usage
196///
197/// ```json
198/// // Update request examples:
199/// {
200///   "notification_id": null,           // Remove notification
201///   "policies": { "min_balance": null } // Remove min_balance policy
202/// }
203///
204/// {
205///   "notification_id": "notif-123",    // Set notification
206///   "policies": { "min_balance": "2000000000000000000" } // Update min_balance
207/// }
208///
209/// {
210///   "name": "Updated Name"             // Only update name, leave others unchanged
211/// }
212/// ```
213#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
214#[serde(deny_unknown_fields)]
215pub struct UpdateRelayerRequestRaw {
216    pub name: Option<String>,
217    pub paused: Option<bool>,
218    /// Raw policy JSON - will be validated against relayer's network type during application
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub policies: Option<serde_json::Value>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub notification_id: Option<String>,
223    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
224}
225
226impl TryFrom<CreateRelayerRequest> for Relayer {
227    type Error = ApiError;
228
229    fn try_from(request: CreateRelayerRequest) -> Result<Self, Self::Error> {
230        let id = request.id.clone().unwrap_or_else(generate_uuid);
231
232        // Convert policies directly using the typed policy request
233        let policies = if let Some(policy_request) = &request.policies {
234            Some(policy_request.to_domain_policy(request.network_type)?)
235        } else {
236            None
237        };
238
239        // Create domain relayer
240        let relayer = Relayer::new(
241            id,
242            request.name,
243            request.network,
244            request.paused,
245            request.network_type,
246            policies,
247            request.signer_id,
248            request.notification_id,
249            request.custom_rpc_urls,
250        );
251
252        // Validate using domain model validation logic
253        relayer.validate().map_err(ApiError::from)?;
254
255        Ok(relayer)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::models::{
263        relayer::{
264            RelayerEvmPolicy, RelayerSolanaPolicy, RelayerStellarPolicy, SolanaFeePaymentStrategy,
265        },
266        StellarFeePaymentStrategy,
267    };
268
269    #[test]
270    fn test_valid_create_request() {
271        let request = CreateRelayerRequest {
272            id: Some("test-relayer".to_string()),
273            name: "Test Relayer".to_string(),
274            network: "mainnet".to_string(),
275            paused: false,
276            network_type: RelayerNetworkType::Evm,
277            policies: Some(CreateRelayerPolicyRequest::Evm(RelayerEvmPolicy {
278                gas_price_cap: Some(100),
279                whitelist_receivers: None,
280                eip1559_pricing: Some(true),
281                private_transactions: None,
282                min_balance: None,
283                gas_limit_estimation: None,
284            })),
285            signer_id: "test-signer".to_string(),
286            notification_id: None,
287            custom_rpc_urls: None,
288        };
289
290        // Convert to domain model and validate there
291        let domain_relayer = Relayer::try_from(request);
292        assert!(domain_relayer.is_ok());
293    }
294
295    #[test]
296    fn test_valid_create_request_stellar() {
297        let request = CreateRelayerRequest {
298            id: Some("test-stellar-relayer".to_string()),
299            name: "Test Stellar Relayer".to_string(),
300            network: "mainnet".to_string(),
301            paused: false,
302            network_type: RelayerNetworkType::Stellar,
303            policies: Some(CreateRelayerPolicyRequest::Stellar(RelayerStellarPolicy {
304                min_balance: Some(20000000),
305                max_fee: Some(100000),
306                timeout_seconds: Some(30),
307                concurrent_transactions: None,
308                allowed_tokens: None,
309                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
310                slippage_percentage: None,
311                fee_margin_percentage: None,
312                swap_config: None,
313            })),
314            signer_id: "test-signer".to_string(),
315            notification_id: None,
316            custom_rpc_urls: None,
317        };
318
319        // Convert to domain model and validate there
320        let domain_relayer = Relayer::try_from(request);
321        assert!(domain_relayer.is_ok());
322
323        // Verify the domain model has correct values
324        let relayer = domain_relayer.unwrap();
325        assert_eq!(relayer.network_type, RelayerNetworkType::Stellar);
326        if let Some(RelayerNetworkPolicy::Stellar(stellar_policy)) = relayer.policies {
327            assert_eq!(stellar_policy.min_balance, Some(20000000));
328            assert_eq!(stellar_policy.max_fee, Some(100000));
329            assert_eq!(stellar_policy.timeout_seconds, Some(30));
330        } else {
331            panic!("Expected Stellar policy");
332        }
333    }
334
335    #[test]
336    fn test_valid_create_request_solana() {
337        let request = CreateRelayerRequest {
338            id: Some("test-solana-relayer".to_string()),
339            name: "Test Solana Relayer".to_string(),
340            network: "mainnet".to_string(),
341            paused: false,
342            network_type: RelayerNetworkType::Solana,
343            policies: Some(CreateRelayerPolicyRequest::Solana(RelayerSolanaPolicy {
344                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
345                min_balance: Some(1000000),
346                max_signatures: Some(5),
347                allowed_tokens: None,
348                allowed_programs: None,
349                allowed_accounts: None,
350                disallowed_accounts: None,
351                max_tx_data_size: None,
352                max_allowed_fee_lamports: None,
353                swap_config: None,
354                fee_margin_percentage: None,
355            })),
356            signer_id: "test-signer".to_string(),
357            notification_id: None,
358            custom_rpc_urls: None,
359        };
360
361        // Convert to domain model and validate there
362        let domain_relayer = Relayer::try_from(request);
363        assert!(domain_relayer.is_ok());
364
365        // Verify the domain model has correct values
366        let relayer = domain_relayer.unwrap();
367        assert_eq!(relayer.network_type, RelayerNetworkType::Solana);
368        if let Some(RelayerNetworkPolicy::Solana(solana_policy)) = relayer.policies {
369            assert_eq!(solana_policy.min_balance, Some(1000000));
370            assert_eq!(solana_policy.max_signatures, Some(5));
371            assert_eq!(
372                solana_policy.fee_payment_strategy,
373                Some(SolanaFeePaymentStrategy::Relayer)
374            );
375        } else {
376            panic!("Expected Solana policy");
377        }
378    }
379
380    #[test]
381    fn test_invalid_create_request_empty_id() {
382        let request = CreateRelayerRequest {
383            id: Some("".to_string()),
384            name: "Test Relayer".to_string(),
385            network: "mainnet".to_string(),
386            paused: false,
387            network_type: RelayerNetworkType::Evm,
388            policies: None,
389            signer_id: "test-signer".to_string(),
390            notification_id: None,
391            custom_rpc_urls: None,
392        };
393
394        // Convert to domain model and validate there - should fail due to empty ID
395        let domain_relayer = Relayer::try_from(request);
396        assert!(domain_relayer.is_err());
397    }
398
399    #[test]
400    fn test_create_request_policy_conversion() {
401        // Test that policies are correctly converted from request type to domain type
402        let request = CreateRelayerRequest {
403            id: Some("test-relayer".to_string()),
404            name: "Test Relayer".to_string(),
405            network: "mainnet".to_string(),
406            paused: false,
407            network_type: RelayerNetworkType::Solana,
408            policies: Some(CreateRelayerPolicyRequest::Solana(RelayerSolanaPolicy {
409                fee_payment_strategy: Some(
410                    crate::models::relayer::SolanaFeePaymentStrategy::Relayer,
411                ),
412                min_balance: Some(1000000),
413                allowed_tokens: None,
414                allowed_programs: None,
415                allowed_accounts: None,
416                disallowed_accounts: None,
417                max_signatures: None,
418                max_tx_data_size: None,
419                max_allowed_fee_lamports: None,
420                swap_config: None,
421                fee_margin_percentage: None,
422            })),
423            signer_id: "test-signer".to_string(),
424            notification_id: None,
425            custom_rpc_urls: None,
426        };
427
428        // Test policy conversion
429        if let Some(policy_request) = &request.policies {
430            let policy = policy_request
431                .to_domain_policy(request.network_type)
432                .unwrap();
433            if let RelayerNetworkPolicy::Solana(solana_policy) = policy {
434                assert_eq!(solana_policy.min_balance, Some(1000000));
435            } else {
436                panic!("Expected Solana policy");
437            }
438        } else {
439            panic!("Expected policies to be present");
440        }
441
442        // Test full conversion to domain relayer
443        let domain_relayer = Relayer::try_from(request);
444        assert!(domain_relayer.is_ok());
445    }
446
447    #[test]
448    fn test_create_request_stellar_policy_conversion() {
449        // Test that Stellar policies are correctly converted from request type to domain type
450        let request = CreateRelayerRequest {
451            id: Some("test-stellar-relayer".to_string()),
452            name: "Test Stellar Relayer".to_string(),
453            network: "mainnet".to_string(),
454            paused: false,
455            network_type: RelayerNetworkType::Stellar,
456            policies: Some(CreateRelayerPolicyRequest::Stellar(RelayerStellarPolicy {
457                min_balance: Some(50000000),
458                max_fee: Some(150000),
459                timeout_seconds: Some(60),
460                concurrent_transactions: None,
461                allowed_tokens: None,
462                fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
463                slippage_percentage: None,
464                fee_margin_percentage: None,
465                swap_config: None,
466            })),
467            signer_id: "test-signer".to_string(),
468            notification_id: None,
469            custom_rpc_urls: None,
470        };
471
472        // Test policy conversion
473        if let Some(policy_request) = &request.policies {
474            let policy = policy_request
475                .to_domain_policy(request.network_type)
476                .unwrap();
477            if let RelayerNetworkPolicy::Stellar(stellar_policy) = policy {
478                assert_eq!(stellar_policy.min_balance, Some(50000000));
479                assert_eq!(stellar_policy.max_fee, Some(150000));
480                assert_eq!(stellar_policy.timeout_seconds, Some(60));
481            } else {
482                panic!("Expected Stellar policy");
483            }
484        } else {
485            panic!("Expected policies to be present");
486        }
487
488        // Test full conversion to domain relayer
489        let domain_relayer = Relayer::try_from(request);
490        assert!(domain_relayer.is_ok());
491    }
492
493    #[test]
494    fn test_create_request_wrong_policy_type() {
495        // Test that providing wrong policy type for network type fails
496        let request = CreateRelayerRequest {
497            id: Some("test-relayer".to_string()),
498            name: "Test Relayer".to_string(),
499            network: "mainnet".to_string(),
500            paused: false,
501            network_type: RelayerNetworkType::Evm, // EVM network type
502            policies: Some(CreateRelayerPolicyRequest::Solana(
503                RelayerSolanaPolicy::default(),
504            )), // But Solana policy
505            signer_id: "test-signer".to_string(),
506            notification_id: None,
507            custom_rpc_urls: None,
508        };
509
510        // Should fail during policy conversion - since the policy was auto-detected as Solana
511        // but the network type is EVM, the conversion should fail
512        if let Some(policy_request) = &request.policies {
513            let result = policy_request.to_domain_policy(request.network_type);
514            assert!(result.is_err());
515            assert!(result
516                .unwrap_err()
517                .to_string()
518                .contains("Policy type does not match relayer network type"));
519        } else {
520            panic!("Expected policies to be present");
521        }
522    }
523
524    #[test]
525    fn test_create_request_stellar_wrong_policy_type() {
526        // Test that providing Stellar policy for EVM network type fails
527        let request = CreateRelayerRequest {
528            id: Some("test-relayer".to_string()),
529            name: "Test Relayer".to_string(),
530            network: "mainnet".to_string(),
531            paused: false,
532            network_type: RelayerNetworkType::Evm, // EVM network type
533            policies: Some(CreateRelayerPolicyRequest::Stellar(
534                RelayerStellarPolicy::default(),
535            )), // But Stellar policy
536            signer_id: "test-signer".to_string(),
537            notification_id: None,
538            custom_rpc_urls: None,
539        };
540
541        // Should fail during policy conversion
542        if let Some(policy_request) = &request.policies {
543            let result = policy_request.to_domain_policy(request.network_type);
544            assert!(result.is_err());
545            assert!(result
546                .unwrap_err()
547                .to_string()
548                .contains("Policy type does not match relayer network type"));
549        } else {
550            panic!("Expected policies to be present");
551        }
552    }
553
554    #[test]
555    fn test_create_request_json_deserialization() {
556        // Test that JSON without network_type in policies deserializes correctly
557        let json_input = r#"{
558            "name": "Test Relayer",
559            "network": "mainnet",
560            "paused": false,
561            "network_type": "evm",
562            "signer_id": "test-signer",
563            "policies": {
564                "gas_price_cap": 100000000000,
565                "eip1559_pricing": true,
566                "min_balance": 1000000000000000000
567            }
568        }"#;
569
570        let request: CreateRelayerRequest = serde_json::from_str(json_input).unwrap();
571        assert_eq!(request.network_type, RelayerNetworkType::Evm);
572        assert!(request.policies.is_some());
573
574        // Test that it converts to domain model correctly
575        let domain_relayer = Relayer::try_from(request).unwrap();
576        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Evm);
577
578        if let Some(RelayerNetworkPolicy::Evm(evm_policy)) = domain_relayer.policies {
579            assert_eq!(evm_policy.gas_price_cap, Some(100000000000));
580            assert_eq!(evm_policy.eip1559_pricing, Some(true));
581        } else {
582            panic!("Expected EVM policy");
583        }
584    }
585
586    #[test]
587    fn test_create_request_stellar_json_deserialization() {
588        // Test that Stellar JSON deserializes correctly
589        let json_input = r#"{
590            "name": "Test Stellar Relayer",
591            "network": "mainnet",
592            "paused": false,
593            "network_type": "stellar",
594            "signer_id": "test-signer",
595            "policies": {
596                "fee_payment_strategy": "relayer",
597                "min_balance": 25000000,
598                "max_fee": 200000,
599                "timeout_seconds": 45
600            }
601        }"#;
602
603        let request: CreateRelayerRequest = serde_json::from_str(json_input).unwrap();
604        assert_eq!(request.network_type, RelayerNetworkType::Stellar);
605        assert!(request.policies.is_some());
606
607        // Test that it converts to domain model correctly
608        let domain_relayer = Relayer::try_from(request).unwrap();
609        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Stellar);
610
611        if let Some(RelayerNetworkPolicy::Stellar(stellar_policy)) = domain_relayer.policies {
612            assert_eq!(stellar_policy.min_balance, Some(25000000));
613            assert_eq!(stellar_policy.max_fee, Some(200000));
614            assert_eq!(stellar_policy.timeout_seconds, Some(45));
615        } else {
616            panic!("Expected Stellar policy");
617        }
618    }
619
620    #[test]
621    fn test_create_request_solana_json_deserialization() {
622        // Test that Solana JSON deserializes correctly with complex policy
623        let json_input = r#"{
624            "name": "Test Solana Relayer",
625            "network": "mainnet",
626            "paused": false,
627            "network_type": "solana",
628            "signer_id": "test-signer",
629            "policies": {
630                "fee_payment_strategy": "relayer",
631                "min_balance": 5000000,
632                "max_signatures": 8,
633                "max_tx_data_size": 1024,
634                "fee_margin_percentage": 2.5
635            }
636        }"#;
637
638        let request: CreateRelayerRequest = serde_json::from_str(json_input).unwrap();
639        assert_eq!(request.network_type, RelayerNetworkType::Solana);
640        assert!(request.policies.is_some());
641
642        // Test that it converts to domain model correctly
643        let domain_relayer = Relayer::try_from(request).unwrap();
644        assert_eq!(domain_relayer.network_type, RelayerNetworkType::Solana);
645
646        if let Some(RelayerNetworkPolicy::Solana(solana_policy)) = domain_relayer.policies {
647            assert_eq!(solana_policy.min_balance, Some(5000000));
648            assert_eq!(solana_policy.max_signatures, Some(8));
649            assert_eq!(solana_policy.max_tx_data_size, Some(1024));
650            assert_eq!(solana_policy.fee_margin_percentage, Some(2.5));
651            assert_eq!(
652                solana_policy.fee_payment_strategy,
653                Some(SolanaFeePaymentStrategy::Relayer)
654            );
655        } else {
656            panic!("Expected Solana policy");
657        }
658    }
659
660    #[test]
661    fn test_valid_update_request() {
662        let request = UpdateRelayerRequestRaw {
663            name: Some("Updated Name".to_string()),
664            paused: Some(true),
665            policies: None,
666            notification_id: Some("new-notification".to_string()),
667            custom_rpc_urls: None,
668        };
669
670        // Should serialize/deserialize without errors
671        let serialized = serde_json::to_string(&request).unwrap();
672        let _deserialized: UpdateRelayerRequest = serde_json::from_str(&serialized).unwrap();
673    }
674
675    #[test]
676    fn test_update_request_all_none() {
677        let request = UpdateRelayerRequestRaw {
678            name: None,
679            paused: None,
680            policies: None,
681            notification_id: None,
682            custom_rpc_urls: None,
683        };
684
685        // Should serialize/deserialize without errors - all fields are optional
686        let serialized = serde_json::to_string(&request).unwrap();
687        let _deserialized: UpdateRelayerRequest = serde_json::from_str(&serialized).unwrap();
688    }
689
690    #[test]
691    fn test_update_request_policy_deserialization() {
692        // Test EVM policy deserialization without network_type in user input
693        let json_input = r#"{
694            "name": "Updated Relayer",
695            "policies": {
696                "gas_price_cap": 100000000000,
697                "eip1559_pricing": true
698            }
699        }"#;
700
701        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
702        assert!(request.policies.is_some());
703
704        // Validation happens during domain conversion based on network type
705        // Test with the utility function
706        if let Some(policies_json) = &request.policies {
707            let network_policy =
708                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Evm)
709                    .unwrap();
710            if let RelayerNetworkPolicy::Evm(evm_policy) = network_policy {
711                assert_eq!(evm_policy.gas_price_cap, Some(100000000000));
712                assert_eq!(evm_policy.eip1559_pricing, Some(true));
713            } else {
714                panic!("Expected EVM policy");
715            }
716        }
717    }
718
719    #[test]
720    fn test_update_request_policy_deserialization_solana() {
721        // Test Solana policy deserialization without network_type in user input
722        let json_input = r#"{
723            "policies": {
724                "fee_payment_strategy": "relayer",
725                "min_balance": 1000000
726            }
727        }"#;
728
729        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
730
731        // Validation happens during domain conversion based on network type
732        // Test with the utility function for Solana
733        if let Some(policies_json) = &request.policies {
734            let network_policy =
735                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Solana)
736                    .unwrap();
737            if let RelayerNetworkPolicy::Solana(solana_policy) = network_policy {
738                assert_eq!(solana_policy.min_balance, Some(1000000));
739            } else {
740                panic!("Expected Solana policy");
741            }
742        }
743    }
744
745    #[test]
746    fn test_update_request_policy_deserialization_stellar() {
747        // Test Stellar policy deserialization without network_type in user input
748        let json_input = r#"{
749            "policies": {
750                "max_fee": 75000,
751                "timeout_seconds": 120,
752                "min_balance": 15000000
753            }
754        }"#;
755
756        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
757
758        // Validation happens during domain conversion based on network type
759        // Test with the utility function for Stellar
760        if let Some(policies_json) = &request.policies {
761            let network_policy =
762                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Stellar)
763                    .unwrap();
764            if let RelayerNetworkPolicy::Stellar(stellar_policy) = network_policy {
765                assert_eq!(stellar_policy.max_fee, Some(75000));
766                assert_eq!(stellar_policy.timeout_seconds, Some(120));
767                assert_eq!(stellar_policy.min_balance, Some(15000000));
768            } else {
769                panic!("Expected Stellar policy");
770            }
771        }
772    }
773
774    #[test]
775    fn test_update_request_invalid_policy_format() {
776        // Test that invalid policy format fails during validation with utility function
777        let valid_json = r#"{
778            "name": "Test",
779            "policies": "invalid_not_an_object"
780        }"#;
781
782        let request: UpdateRelayerRequestRaw = serde_json::from_str(valid_json).unwrap();
783
784        // Should fail when trying to validate the policy against a network type
785        if let Some(policies_json) = &request.policies {
786            let result =
787                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Evm);
788            assert!(result.is_err());
789        }
790    }
791
792    #[test]
793    fn test_update_request_wrong_network_type() {
794        // Test that EVM policy deserializes correctly as EVM type
795        let json_input = r#"{
796            "policies": {
797                "gas_price_cap": 100000000000,
798                "eip1559_pricing": true
799            }
800        }"#;
801
802        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
803
804        // Should correctly deserialize as raw JSON - validation happens during domain conversion
805        assert!(request.policies.is_some());
806    }
807
808    #[test]
809    fn test_update_request_stellar_policy() {
810        // Test Stellar policy deserialization
811        let json_input = r#"{
812            "policies": {
813                "max_fee": 10000,
814                "timeout_seconds": 300,
815                "min_balance": 5000000
816            }
817        }"#;
818
819        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
820
821        // Should correctly deserialize as raw JSON - validation happens during domain conversion
822        assert!(request.policies.is_some());
823    }
824
825    #[test]
826    fn test_update_request_stellar_policy_partial() {
827        // Test Stellar policy with only some fields (partial update)
828        let json_input = r#"{
829            "policies": {
830                "max_fee": 50000
831            }
832        }"#;
833
834        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
835
836        // Should correctly deserialize as raw JSON
837        assert!(request.policies.is_some());
838
839        // Test domain conversion with utility function
840        if let Some(policies_json) = &request.policies {
841            let network_policy =
842                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Stellar)
843                    .unwrap();
844            if let RelayerNetworkPolicy::Stellar(stellar_policy) = network_policy {
845                assert_eq!(stellar_policy.max_fee, Some(50000));
846                assert_eq!(stellar_policy.timeout_seconds, None);
847                assert_eq!(stellar_policy.min_balance, None);
848            } else {
849                panic!("Expected Stellar policy");
850            }
851        }
852    }
853
854    #[test]
855    fn test_notification_id_deserialization() {
856        // Test valid notification_id deserialization
857        let json_with_notification = r#"{
858            "name": "Test Relayer",
859            "notification_id": "notif-123"
860        }"#;
861
862        let request: UpdateRelayerRequestRaw =
863            serde_json::from_str(json_with_notification).unwrap();
864        assert_eq!(request.notification_id, Some("notif-123".to_string()));
865
866        // Test without notification_id
867        let json_without_notification = r#"{
868            "name": "Test Relayer"
869        }"#;
870
871        let request: UpdateRelayerRequestRaw =
872            serde_json::from_str(json_without_notification).unwrap();
873        assert_eq!(request.notification_id, None);
874
875        // Test invalid notification_id type should fail deserialization
876        let invalid_json = r#"{
877            "name": "Test Relayer",
878            "notification_id": 123
879        }"#;
880
881        let result = serde_json::from_str::<UpdateRelayerRequestRaw>(invalid_json);
882        assert!(result.is_err());
883    }
884
885    #[test]
886    fn test_comprehensive_update_request() {
887        // Test a comprehensive update request with multiple fields
888        let json_input = r#"{
889            "name": "Updated Relayer",
890            "paused": true,
891            "notification_id": "new-notification-id",
892            "policies": {
893                "min_balance": "5000000000000000000",
894                "gas_limit_estimation": false
895            },
896            "custom_rpc_urls": [
897                {"url": "https://example.com", "weight": 100}
898            ]
899        }"#;
900
901        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
902
903        // Verify all fields are correctly deserialized
904        assert_eq!(request.name, Some("Updated Relayer".to_string()));
905        assert_eq!(request.paused, Some(true));
906        assert_eq!(
907            request.notification_id,
908            Some("new-notification-id".to_string())
909        );
910        assert!(request.policies.is_some());
911        assert!(request.custom_rpc_urls.is_some());
912
913        // Policies are now raw JSON - validation happens during domain conversion
914        if let Some(policies_json) = &request.policies {
915            // Just verify it's a JSON object with expected fields
916            assert!(policies_json.get("min_balance").is_some());
917            assert!(policies_json.get("gas_limit_estimation").is_some());
918        } else {
919            panic!("Expected policies");
920        }
921    }
922
923    #[test]
924    fn test_comprehensive_update_request_stellar() {
925        // Test a comprehensive Stellar update request
926        let json_input = r#"{
927            "name": "Updated Stellar Relayer",
928            "paused": false,
929            "notification_id": "stellar-notification",
930            "policies": {
931                "min_balance": 30000000,
932                "max_fee": 250000,
933                "timeout_seconds": 90
934            },
935            "custom_rpc_urls": [
936                {"url": "https://stellar-node.example.com", "weight": 100}
937            ]
938        }"#;
939
940        let request: UpdateRelayerRequestRaw = serde_json::from_str(json_input).unwrap();
941
942        // Verify all fields are correctly deserialized
943        assert_eq!(request.name, Some("Updated Stellar Relayer".to_string()));
944        assert_eq!(request.paused, Some(false));
945        assert_eq!(
946            request.notification_id,
947            Some("stellar-notification".to_string())
948        );
949        assert!(request.policies.is_some());
950        assert!(request.custom_rpc_urls.is_some());
951
952        // Test domain conversion
953        if let Some(policies_json) = &request.policies {
954            let network_policy =
955                deserialize_policy_for_network_type(policies_json, RelayerNetworkType::Stellar)
956                    .unwrap();
957            if let RelayerNetworkPolicy::Stellar(stellar_policy) = network_policy {
958                assert_eq!(stellar_policy.min_balance, Some(30000000));
959                assert_eq!(stellar_policy.max_fee, Some(250000));
960                assert_eq!(stellar_policy.timeout_seconds, Some(90));
961            } else {
962                panic!("Expected Stellar policy");
963            }
964        }
965    }
966
967    #[test]
968    fn test_create_request_network_type_based_policy_deserialization() {
969        // Test that policies are correctly deserialized based on network_type
970        // EVM network with EVM policy fields
971        let evm_json = r#"{
972            "name": "EVM Relayer",
973            "network": "mainnet",
974            "paused": false,
975            "network_type": "evm",
976            "signer_id": "test-signer",
977            "policies": {
978                "gas_price_cap": 50000000000,
979                "eip1559_pricing": true,
980                "min_balance": "1000000000000000000"
981            }
982        }"#;
983
984        let evm_request: CreateRelayerRequest = serde_json::from_str(evm_json).unwrap();
985        assert_eq!(evm_request.network_type, RelayerNetworkType::Evm);
986
987        if let Some(CreateRelayerPolicyRequest::Evm(evm_policy)) = evm_request.policies {
988            assert_eq!(evm_policy.gas_price_cap, Some(50000000000));
989            assert_eq!(evm_policy.eip1559_pricing, Some(true));
990            assert_eq!(evm_policy.min_balance, Some(1000000000000000000));
991        } else {
992            panic!("Expected EVM policy");
993        }
994
995        // Solana network with Solana policy fields
996        let solana_json = r#"{
997            "name": "Solana Relayer",
998            "network": "mainnet",
999            "paused": false,
1000            "network_type": "solana",
1001            "signer_id": "test-signer",
1002            "policies": {
1003                "fee_payment_strategy": "relayer",
1004                "min_balance": 5000000,
1005                "max_signatures": 10
1006            }
1007        }"#;
1008
1009        let solana_request: CreateRelayerRequest = serde_json::from_str(solana_json).unwrap();
1010        assert_eq!(solana_request.network_type, RelayerNetworkType::Solana);
1011
1012        if let Some(CreateRelayerPolicyRequest::Solana(solana_policy)) = solana_request.policies {
1013            assert_eq!(solana_policy.min_balance, Some(5000000));
1014            assert_eq!(solana_policy.max_signatures, Some(10));
1015        } else {
1016            panic!("Expected Solana policy");
1017        }
1018
1019        // Stellar network with Stellar policy fields
1020        let stellar_json = r#"{
1021            "name": "Stellar Relayer",
1022            "network": "mainnet",
1023            "paused": false,
1024            "network_type": "stellar",
1025            "signer_id": "test-signer",
1026            "policies": {
1027                "min_balance": 40000000,
1028                "max_fee": 300000,
1029                "timeout_seconds": 180
1030            }
1031        }"#;
1032
1033        let stellar_request: CreateRelayerRequest = serde_json::from_str(stellar_json).unwrap();
1034        assert_eq!(stellar_request.network_type, RelayerNetworkType::Stellar);
1035
1036        if let Some(CreateRelayerPolicyRequest::Stellar(stellar_policy)) = stellar_request.policies
1037        {
1038            assert_eq!(stellar_policy.min_balance, Some(40000000));
1039            assert_eq!(stellar_policy.max_fee, Some(300000));
1040            assert_eq!(stellar_policy.timeout_seconds, Some(180));
1041        } else {
1042            panic!("Expected Stellar policy");
1043        }
1044
1045        // Test that wrong policy fields for network type fails
1046        let invalid_json = r#"{
1047            "name": "Invalid Relayer",
1048            "network": "mainnet",
1049            "paused": false,
1050            "network_type": "evm",
1051            "signer_id": "test-signer",
1052            "policies": {
1053                "fee_payment_strategy": "relayer"
1054            }
1055        }"#;
1056
1057        let result = serde_json::from_str::<CreateRelayerRequest>(invalid_json);
1058        assert!(result.is_err());
1059        assert!(result.unwrap_err().to_string().contains("unknown field"));
1060    }
1061
1062    #[test]
1063    fn test_create_request_invalid_stellar_policy_fields() {
1064        // Test that invalid Stellar policy fields fail during deserialization
1065        let invalid_json = r#"{
1066            "name": "Invalid Stellar Relayer",
1067            "network": "mainnet",
1068            "paused": false,
1069            "network_type": "stellar",
1070            "signer_id": "test-signer",
1071            "policies": {
1072                "gas_price_cap": 100000000000
1073            }
1074        }"#;
1075
1076        let result = serde_json::from_str::<CreateRelayerRequest>(invalid_json);
1077        assert!(result.is_err());
1078        assert!(result.unwrap_err().to_string().contains("unknown field"));
1079    }
1080
1081    #[test]
1082    fn test_create_request_empty_policies() {
1083        // Test create request with empty policies for each network type
1084        let evm_json = r#"{
1085            "name": "EVM Relayer No Policies",
1086            "network": "mainnet",
1087            "paused": false,
1088            "network_type": "evm",
1089            "signer_id": "test-signer"
1090        }"#;
1091
1092        let evm_request: CreateRelayerRequest = serde_json::from_str(evm_json).unwrap();
1093        assert_eq!(evm_request.network_type, RelayerNetworkType::Evm);
1094        assert!(evm_request.policies.is_none());
1095
1096        let stellar_json = r#"{
1097            "name": "Stellar Relayer No Policies",
1098            "network": "mainnet",
1099            "paused": false,
1100            "network_type": "stellar",
1101            "signer_id": "test-signer"
1102        }"#;
1103
1104        let stellar_request: CreateRelayerRequest = serde_json::from_str(stellar_json).unwrap();
1105        assert_eq!(stellar_request.network_type, RelayerNetworkType::Stellar);
1106        assert!(stellar_request.policies.is_none());
1107
1108        let solana_json = r#"{
1109            "name": "Solana Relayer No Policies",
1110            "network": "mainnet",
1111            "paused": false,
1112            "network_type": "solana",
1113            "signer_id": "test-signer"
1114        }"#;
1115
1116        let solana_request: CreateRelayerRequest = serde_json::from_str(solana_json).unwrap();
1117        assert_eq!(solana_request.network_type, RelayerNetworkType::Solana);
1118        assert!(solana_request.policies.is_none());
1119    }
1120
1121    #[test]
1122    fn test_deserialize_policy_utility_function_all_networks() {
1123        // Test the utility function with all network types
1124
1125        // EVM policy
1126        let evm_json = serde_json::json!({
1127            "gas_price_cap": "75000000000",
1128            "private_transactions": false,
1129            "min_balance": "2000000000000000000"
1130        });
1131
1132        let evm_policy =
1133            deserialize_policy_for_network_type(&evm_json, RelayerNetworkType::Evm).unwrap();
1134        if let RelayerNetworkPolicy::Evm(policy) = evm_policy {
1135            assert_eq!(policy.gas_price_cap, Some(75000000000));
1136            assert_eq!(policy.private_transactions, Some(false));
1137            assert_eq!(policy.min_balance, Some(2000000000000000000));
1138        } else {
1139            panic!("Expected EVM policy");
1140        }
1141
1142        // Solana policy
1143        let solana_json = serde_json::json!({
1144            "fee_payment_strategy": "user",
1145            "max_tx_data_size": 512,
1146            "fee_margin_percentage": 1.5
1147        });
1148
1149        let solana_policy =
1150            deserialize_policy_for_network_type(&solana_json, RelayerNetworkType::Solana).unwrap();
1151        if let RelayerNetworkPolicy::Solana(policy) = solana_policy {
1152            assert_eq!(
1153                policy.fee_payment_strategy,
1154                Some(SolanaFeePaymentStrategy::User)
1155            );
1156            assert_eq!(policy.max_tx_data_size, Some(512));
1157            assert_eq!(policy.fee_margin_percentage, Some(1.5));
1158        } else {
1159            panic!("Expected Solana policy");
1160        }
1161
1162        // Stellar policy
1163        let stellar_json = serde_json::json!({
1164            "max_fee": 125000,
1165            "timeout_seconds": 240
1166        });
1167
1168        let stellar_policy =
1169            deserialize_policy_for_network_type(&stellar_json, RelayerNetworkType::Stellar)
1170                .unwrap();
1171        if let RelayerNetworkPolicy::Stellar(policy) = stellar_policy {
1172            assert_eq!(policy.max_fee, Some(125000));
1173            assert_eq!(policy.timeout_seconds, Some(240));
1174            assert_eq!(policy.min_balance, None);
1175        } else {
1176            panic!("Expected Stellar policy");
1177        }
1178    }
1179}