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