openzeppelin_relayer/models/transaction/request/
evm.rs

1use crate::{
2    constants::ZERO_ADDRESS,
3    models::{ApiError, RelayerNetworkPolicy, RelayerRepoModel, U256},
4    utils::calculate_intrinsic_gas,
5};
6use serde::{Deserialize, Serialize};
7use utoipa::{schema, ToSchema};
8
9#[derive(Deserialize, Serialize, Default, ToSchema)]
10pub struct EvmTransactionRequest {
11    #[schema(nullable = false)]
12    pub to: Option<String>,
13    #[schema(value_type = u128, format = "u128")]
14    pub value: U256,
15    #[schema(nullable = false)]
16    pub data: Option<String>,
17    pub gas_limit: Option<u64>,
18    #[schema(nullable = false)]
19    pub gas_price: Option<u128>,
20    #[schema(nullable = false)]
21    pub speed: Option<Speed>,
22    #[schema(nullable = false)]
23    pub max_fee_per_gas: Option<u128>,
24    #[schema(nullable = false)]
25    pub max_priority_fee_per_gas: Option<u128>,
26    #[schema(nullable = false)]
27    pub valid_until: Option<String>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
31#[serde(rename_all = "lowercase")]
32pub enum Speed {
33    Fastest,
34    Fast,
35    Average,
36    #[serde(rename = "safeLow")]
37    SafeLow,
38}
39impl EvmTransactionRequest {
40    pub fn validate(&self, relayer: &RelayerRepoModel) -> Result<(), ApiError> {
41        validate_target_address(self, relayer)?;
42        validate_evm_transaction_request(self, relayer)?;
43        validate_price_params(self, relayer)?;
44        Ok(())
45    }
46}
47
48pub fn validate_evm_transaction_request(
49    request: &EvmTransactionRequest,
50    relayer: &RelayerRepoModel,
51) -> Result<(), ApiError> {
52    if request.to.is_none() && request.data.is_none() {
53        return Err(ApiError::BadRequest(
54            "Both txs `to` and `data` fields are missing. At least one of them has to be set."
55                .to_string(),
56        ));
57    }
58
59    // Validate gas_limit based on gas_limit_estimation policy
60    if let RelayerNetworkPolicy::Evm(evm_policy) = &relayer.policies {
61        // If gas_limit_estimation is disabled (Some(false)), gas_limit must be provided
62        if evm_policy.gas_limit_estimation == Some(false) && request.gas_limit.is_none() {
63            return Err(ApiError::BadRequest(
64                "gas_limit is required when gas_limit_estimation policy is disabled".to_string(),
65            ));
66        }
67    }
68
69    // Validate intrinsic gas if gas_limit is provided
70    if let Some(gas_limit) = request.gas_limit {
71        let intrinsic_gas = calculate_intrinsic_gas(request);
72        if gas_limit < intrinsic_gas {
73            return Err(ApiError::BadRequest(format!(
74                "gas_limit is too low, intrinsic gas is {} and gas_limit is {}",
75                intrinsic_gas, gas_limit
76            )));
77        }
78    }
79
80    if let Some(valid_until) = &request.valid_until {
81        match chrono::DateTime::parse_from_rfc3339(valid_until) {
82            Ok(valid_until_dt) => {
83                let now = chrono::Utc::now();
84                if valid_until_dt < now {
85                    return Err(ApiError::BadRequest(
86                        "The validUntil time cannot be in the past".to_string(),
87                    ));
88                }
89            }
90            Err(_) => {
91                return Err(ApiError::BadRequest(
92                    "Invalid validUntil datetime format".to_string(),
93                ));
94            }
95        }
96    }
97
98    Ok(())
99}
100
101pub fn validate_target_address(
102    request: &EvmTransactionRequest,
103    relayer: &RelayerRepoModel,
104) -> Result<(), ApiError> {
105    if let RelayerNetworkPolicy::Evm(evm_policy) = &relayer.policies {
106        if let Some(whitelist) = &evm_policy.whitelist_receivers {
107            let target_address = request.to.clone().unwrap_or_default().to_lowercase();
108            let mut allowed_addresses: Vec<String> =
109                whitelist.iter().map(|addr| addr.to_lowercase()).collect();
110            allowed_addresses.push(ZERO_ADDRESS.to_string());
111            allowed_addresses.push(relayer.address.to_lowercase());
112
113            if !allowed_addresses.contains(&target_address) {
114                return Err(ApiError::BadRequest(
115                    "Transaction target address is not whitelisted".to_string(),
116                ));
117            }
118        }
119    }
120    Ok(())
121}
122
123pub fn validate_price_params(
124    request: &EvmTransactionRequest,
125    relayer: &RelayerRepoModel,
126) -> Result<(), ApiError> {
127    let is_eip1559 =
128        request.max_fee_per_gas.is_some() || request.max_priority_fee_per_gas.is_some();
129    let is_legacy = request.gas_price.is_some();
130    let is_speed = request.speed.is_some();
131
132    // count how many transaction types are present
133    let transaction_types = [is_eip1559, is_legacy, is_speed]
134        .iter()
135        .filter(|&&x| x)
136        .count();
137
138    // validate that only one transaction type is present
139    if transaction_types == 0 {
140        return Err(ApiError::BadRequest(
141            "Transaction must specify either gasPrice, speed, or EIP1559 parameters".to_string(),
142        ));
143    }
144
145    if transaction_types > 1 {
146        return Err(ApiError::BadRequest(
147            "Cannot mix different transaction types. Use either gasPrice, speed, or EIP1559 \
148             parameters"
149                .to_string(),
150        ));
151    }
152
153    // validate specific fields based on the type
154    if is_eip1559 {
155        // for eip1559, both fields must be present
156        match (request.max_fee_per_gas, request.max_priority_fee_per_gas) {
157            (Some(_), None) | (None, Some(_)) => {
158                return Err(ApiError::BadRequest(
159                    "EIP1559 transactions require both maxFeePerGas and maxPriorityFeePerGas"
160                        .to_string(),
161                ));
162            }
163            (Some(max_fee), Some(max_priority_fee)) => {
164                if max_fee < max_priority_fee {
165                    return Err(ApiError::BadRequest(
166                        "maxFeePerGas must be greater than or equal to maxPriorityFeePerGas"
167                            .to_string(),
168                    ));
169                }
170            }
171            _ => unreachable!(),
172        }
173    }
174
175    if is_legacy {
176        if let RelayerNetworkPolicy::Evm(evm_policy) = &relayer.policies {
177            if let Some(gas_price_cap) = evm_policy.gas_price_cap {
178                if request.gas_price.unwrap_or(0) > gas_price_cap {
179                    return Err(ApiError::BadRequest("Gas price is too high".to_string()));
180                }
181            }
182        }
183    }
184
185    Ok(())
186}
187
188#[cfg(test)]
189mod tests {
190    use crate::models::{NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy, RpcConfig};
191
192    use super::*;
193    use chrono::{Duration, Utc};
194
195    fn create_basic_request() -> EvmTransactionRequest {
196        EvmTransactionRequest {
197            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
198            value: U256::from(0),
199            data: Some("0x".to_string()),
200            gas_limit: Some(21000),
201            gas_price: Some(0),
202            speed: None,
203            max_fee_per_gas: None,
204            max_priority_fee_per_gas: None,
205            valid_until: None,
206        }
207    }
208
209    fn create_test_relayer(paused: bool, system_disabled: bool) -> RelayerRepoModel {
210        RelayerRepoModel {
211            id: "test_relayer".to_string(),
212            name: "Test Relayer".to_string(),
213            paused,
214            system_disabled,
215            network: "test_network".to_string(),
216            network_type: NetworkType::Evm,
217            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
218            signer_id: "test_signer".to_string(),
219            address: "0x".to_string(),
220            notification_id: None,
221            custom_rpc_urls: Some(vec![RpcConfig::new("https://test-rpc-url".to_string())]),
222            ..Default::default()
223        }
224    }
225
226    #[test]
227    fn test_validate_evm_transaction_request_valid() {
228        let request = create_basic_request();
229        assert!(
230            validate_evm_transaction_request(&request, &create_test_relayer(false, false)).is_ok()
231        );
232    }
233
234    #[test]
235    fn test_validate_missing_to_and_data() {
236        let mut request = create_basic_request();
237        request.to = None;
238        request.data = None;
239
240        let result = validate_evm_transaction_request(&request, &create_test_relayer(false, false));
241        assert!(result.is_err());
242        assert!(matches!(result, Err(ApiError::BadRequest(_))));
243    }
244
245    #[test]
246    fn test_validate_valid_until_past() {
247        let mut request = create_basic_request();
248        let past_time = Utc::now() - Duration::hours(1);
249        request.valid_until = Some(past_time.to_rfc3339());
250
251        let result = validate_evm_transaction_request(&request, &create_test_relayer(false, false));
252        assert!(result.is_err());
253        assert!(matches!(result, Err(ApiError::BadRequest(_))));
254    }
255
256    #[test]
257    fn test_validate_valid_until_future() {
258        let mut request = create_basic_request();
259        let future_time = Utc::now() + Duration::hours(1);
260        request.valid_until = Some(future_time.to_rfc3339());
261
262        assert!(
263            validate_evm_transaction_request(&request, &create_test_relayer(false, false)).is_ok()
264        );
265    }
266
267    #[test]
268    fn test_validate_target_address_whitelisted() {
269        let request = create_basic_request();
270        let relayer = create_test_relayer(false, false);
271
272        assert!(validate_target_address(&request, &relayer).is_ok());
273    }
274
275    #[test]
276    fn test_validate_target_address_not_whitelisted() {
277        let mut request = create_basic_request();
278        request.to = Some("0xNOTWHITELISTED123456789".to_string());
279
280        let mut relayer = create_test_relayer(false, false);
281
282        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
283            evm_policy.whitelist_receivers = Some(vec![
284                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
285            ]);
286        }
287
288        let result = validate_target_address(&request, &relayer);
289        assert!(result.is_err());
290        assert!(matches!(result, Err(ApiError::BadRequest(_))));
291    }
292
293    #[test]
294    fn test_validate_target_address_zero_address() {
295        let mut request = create_basic_request();
296        request.to = Some(ZERO_ADDRESS.to_string());
297        let relayer = create_test_relayer(false, false);
298
299        assert!(validate_target_address(&request, &relayer).is_ok());
300    }
301
302    #[test]
303    fn test_validate_target_address_relayer_address() {
304        let mut request = create_basic_request();
305        let relayer = create_test_relayer(false, false);
306        request.to = Some(relayer.address.clone());
307
308        assert!(validate_target_address(&request, &relayer).is_ok());
309    }
310
311    #[test]
312    fn test_validate_evm_transaction_request_gas_limit_too_low() {
313        let mut request = create_basic_request();
314        request.gas_limit = Some(20000);
315        let result = validate_evm_transaction_request(&request, &create_test_relayer(false, false));
316        assert!(result.is_err());
317
318        if let Err(ApiError::BadRequest(msg)) = result {
319            assert_eq!(
320                msg,
321                "gas_limit is too low, intrinsic gas is 21000 and gas_limit is 20000".to_string()
322            );
323        } else {
324            panic!("Expected BadRequest error");
325        }
326    }
327
328    #[test]
329    fn test_validate_legacy_transaction() {
330        let request = create_basic_request();
331        assert!(
332            validate_evm_transaction_request(&request, &create_test_relayer(false, false)).is_ok()
333        );
334    }
335
336    #[test]
337    fn test_validate_eip1559_transaction() {
338        let mut request = create_basic_request();
339        request.max_fee_per_gas = Some(30000000000);
340        request.max_priority_fee_per_gas = Some(20000000000);
341
342        assert!(
343            validate_evm_transaction_request(&request, &create_test_relayer(false, false)).is_ok()
344        );
345    }
346
347    #[test]
348    fn test_validate_eip1559_invalid_fees() {
349        let mut request = create_basic_request();
350        request.max_fee_per_gas = Some(20000000000);
351        request.max_priority_fee_per_gas = Some(30000000000); // max_fee_per_gas should be greater than max_priority_fee_per_gas
352        let relayer = create_test_relayer(false, false);
353        let result = validate_price_params(&request, &relayer);
354        assert!(result.is_err());
355        assert!(matches!(result, Err(ApiError::BadRequest(_))));
356    }
357    #[test]
358    fn test_validate_speed_transaction() {
359        let mut request = create_basic_request();
360        request.speed = Some(Speed::Fast);
361
362        assert!(
363            validate_evm_transaction_request(&request, &create_test_relayer(false, false)).is_ok()
364        );
365    }
366
367    #[test]
368    fn test_validate_missing_required_fields() {
369        let mut request = create_basic_request();
370        request.to = None;
371        request.data = None;
372
373        let result = validate_evm_transaction_request(&request, &create_test_relayer(false, false));
374        assert!(result.is_err());
375        assert!(matches!(result, Err(ApiError::BadRequest(_))));
376    }
377
378    #[test]
379    fn test_validate_invalid_valid_until_format() {
380        let mut request = create_basic_request();
381        request.valid_until = Some("invalid-date-format".to_string());
382
383        let result = validate_evm_transaction_request(&request, &create_test_relayer(false, false));
384        assert!(result.is_err());
385        assert!(matches!(result, Err(ApiError::BadRequest(_))));
386    }
387
388    #[test]
389    fn test_validate_whitelisted_address() {
390        let request = create_basic_request();
391        let mut relayer = create_test_relayer(false, false);
392
393        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
394            evm_policy.whitelist_receivers = Some(vec![
395                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
396            ]);
397        }
398
399        assert!(validate_target_address(&request, &relayer).is_ok());
400    }
401
402    #[test]
403    fn test_validate_non_whitelisted_address() {
404        let mut request = create_basic_request();
405        request.to = Some("0x1234567890123456789012345678901234567890".to_string());
406        let mut relayer = create_test_relayer(false, false);
407
408        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
409            evm_policy.whitelist_receivers = Some(vec![
410                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
411            ]);
412        }
413
414        let result = validate_target_address(&request, &relayer);
415        assert!(result.is_err());
416        assert!(matches!(result, Err(ApiError::BadRequest(_))));
417    }
418
419    #[test]
420    fn test_validate_mixed_transaction_types() {
421        let mut request = create_basic_request();
422        request.gas_price = Some(20000000000);
423        request.max_fee_per_gas = Some(30000000000);
424
425        let relayer = create_test_relayer(false, false);
426        let result = validate_price_params(&request, &relayer);
427        assert!(result.is_err());
428        assert!(matches!(result, Err(ApiError::BadRequest(_))));
429    }
430
431    #[test]
432    fn test_validate_incomplete_eip1559() {
433        let mut request = create_basic_request();
434        request.max_fee_per_gas = Some(30000000000);
435        // Falta max_priority_fee_per_gas
436
437        let relayer = create_test_relayer(false, false);
438        let result = validate_price_params(&request, &relayer);
439        assert!(result.is_err());
440        assert!(matches!(result, Err(ApiError::BadRequest(_))));
441    }
442
443    #[test]
444    fn test_validate_invalid_eip1559_fees() {
445        let mut request = create_basic_request();
446        request.max_fee_per_gas = Some(20000000000);
447        request.max_priority_fee_per_gas = Some(30000000000); // Mayor que max_fee
448        let relayer = create_test_relayer(false, false);
449        let result = validate_price_params(&request, &relayer);
450        assert!(result.is_err());
451        assert!(matches!(result, Err(ApiError::BadRequest(_))));
452    }
453
454    #[test]
455    fn test_validate_speed_with_gas_price() {
456        let mut request = create_basic_request();
457        request.speed = Some(Speed::Fast);
458        request.gas_price = Some(20000000000);
459        let relayer = create_test_relayer(false, false);
460        let result = validate_price_params(&request, &relayer);
461        assert!(result.is_err());
462        assert!(matches!(result, Err(ApiError::BadRequest(_))));
463    }
464
465    #[test]
466    fn test_validate_gas_price_cap() {
467        let mut request = create_basic_request();
468        request.gas_price = Some(20000000000);
469        let mut relayer = create_test_relayer(false, false);
470        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
471            evm_policy.gas_price_cap = Some(10000000000);
472        }
473        let result = validate_price_params(&request, &relayer);
474        assert!(result.is_err());
475        assert!(matches!(result, Err(ApiError::BadRequest(_))));
476    }
477
478    #[test]
479    fn test_validate_gas_limit_optional_when_estimation_enabled() {
480        let mut request = create_basic_request();
481        request.gas_limit = None; // No gas_limit provided
482
483        // Create relayer with gas_limit_estimation enabled (default)
484        let relayer = create_test_relayer(false, false);
485        // Default RelayerEvmPolicy has gas_limit_estimation = Some(true)
486
487        let result = validate_evm_transaction_request(&request, &relayer);
488        assert!(
489            result.is_ok(),
490            "gas_limit should be optional when gas_limit_estimation is enabled"
491        );
492    }
493
494    #[test]
495    fn test_validate_gas_limit_optional_when_estimation_explicitly_enabled() {
496        let mut request = create_basic_request();
497        request.gas_limit = None; // No gas_limit provided
498
499        // Create relayer with gas_limit_estimation explicitly enabled
500        let mut relayer = create_test_relayer(false, false);
501        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
502            evm_policy.gas_limit_estimation = Some(true);
503        }
504
505        let result = validate_evm_transaction_request(&request, &relayer);
506        assert!(
507            result.is_ok(),
508            "gas_limit should be optional when gas_limit_estimation is explicitly enabled"
509        );
510    }
511
512    #[test]
513    fn test_validate_gas_limit_required_when_estimation_disabled() {
514        let mut request = create_basic_request();
515        request.gas_limit = None; // No gas_limit provided
516
517        // Create relayer with gas_limit_estimation disabled
518        let mut relayer = create_test_relayer(false, false);
519        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
520            evm_policy.gas_limit_estimation = Some(false);
521        }
522
523        let result = validate_evm_transaction_request(&request, &relayer);
524        assert!(
525            result.is_err(),
526            "gas_limit should be required when gas_limit_estimation is disabled"
527        );
528
529        if let Err(ApiError::BadRequest(msg)) = result {
530            assert!(
531                msg.contains("gas_limit is required when gas_limit_estimation policy is disabled"),
532                "Expected specific error message, got: {}",
533                msg
534            );
535        } else {
536            panic!("Expected BadRequest error");
537        }
538    }
539
540    #[test]
541    fn test_validate_gas_limit_provided_when_estimation_disabled() {
542        let mut request = create_basic_request();
543        request.gas_limit = Some(21000); // gas_limit provided
544
545        // Create relayer with gas_limit_estimation disabled
546        let mut relayer = create_test_relayer(false, false);
547        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
548            evm_policy.gas_limit_estimation = Some(false);
549        }
550
551        let result = validate_evm_transaction_request(&request, &relayer);
552        assert!(
553            result.is_ok(),
554            "validation should pass when gas_limit is provided and estimation is disabled"
555        );
556    }
557
558    #[test]
559    fn test_validate_gas_limit_provided_when_estimation_enabled() {
560        let mut request = create_basic_request();
561        request.gas_limit = Some(21000); // gas_limit provided
562
563        // Create relayer with gas_limit_estimation enabled
564        let mut relayer = create_test_relayer(false, false);
565        if let RelayerNetworkPolicy::Evm(ref mut evm_policy) = relayer.policies {
566            evm_policy.gas_limit_estimation = Some(true);
567        }
568
569        let result = validate_evm_transaction_request(&request, &relayer);
570        assert!(
571            result.is_ok(),
572            "validation should pass when gas_limit is provided even when estimation is enabled"
573        );
574    }
575}