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