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        }
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}