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