openzeppelin_relayer/models/relayer/
request.rs

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