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