1use alloy::{
19 consensus::{SignableTransaction, TxEip1559, TxEnvelope},
20 eips::eip2718::{Decodable2718, Encodable2718},
21 primitives::{Address, B256, Bytes, TxKind, U256},
22 signers::{Signer, local::PrivateKeySigner},
23};
24use anyhow::Context;
25
26const BPS_DENOMINATOR: u128 = 10_000;
27
28pub const EXECUTION_SCHEMA_VERSION: i16 = 2;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TransactionPurpose {
34 Wrap,
36 Approve,
38 Swap,
40}
41
42impl TransactionPurpose {
43 #[must_use]
45 pub const fn as_str(&self) -> &'static str {
46 match self {
47 Self::Wrap => "wrap",
48 Self::Approve => "approve",
49 Self::Swap => "swap",
50 }
51 }
52
53 #[must_use]
55 pub fn parse(value: &str) -> Option<Self> {
56 match value {
57 "wrap" => Some(Self::Wrap),
58 "approve" => Some(Self::Approve),
59 "swap" => Some(Self::Swap),
60 _ => None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum TransactionStatus {
68 Prepared,
70 Signed,
72 Broadcast,
74 Pending,
78 Rejected,
82 Included,
84 Finalized,
86 Reverted,
88 Replaced,
90 Dropped,
92 Reorged,
94 Recoverable,
96}
97
98impl TransactionStatus {
99 #[must_use]
101 pub const fn as_str(&self) -> &'static str {
102 match self {
103 Self::Prepared => "prepared",
104 Self::Signed => "signed",
105 Self::Broadcast => "broadcast",
106 Self::Pending => "pending",
107 Self::Rejected => "rejected",
108 Self::Included => "included",
109 Self::Finalized => "finalized",
110 Self::Reverted => "reverted",
111 Self::Replaced => "replaced",
112 Self::Dropped => "dropped",
113 Self::Reorged => "reorged",
114 Self::Recoverable => "recoverable",
115 }
116 }
117}
118
119#[must_use]
121#[expect(clippy::too_many_arguments)]
122pub fn build_eip1559_transaction(
123 chain_id: u64,
124 nonce: u64,
125 gas_limit: u64,
126 max_fee_per_gas: u128,
127 max_priority_fee_per_gas: u128,
128 to: Address,
129 value: U256,
130 input: Bytes,
131) -> TxEip1559 {
132 TxEip1559 {
133 chain_id,
134 nonce,
135 gas_limit,
136 max_fee_per_gas,
137 max_priority_fee_per_gas,
138 to: TxKind::Call(to),
139 value,
140 access_list: Default::default(),
141 input,
142 }
143}
144
145pub async fn sign_eip1559_transaction(
152 tx: TxEip1559,
153 signer: &PrivateKeySigner,
154) -> anyhow::Result<(B256, Vec<u8>)> {
155 let signature = signer
156 .sign_hash(&tx.signature_hash())
157 .await
158 .context("failed to sign transaction")?;
159 let signed = tx.into_signed(signature);
160 let tx_hash = *signed.hash();
161
162 Ok((tx_hash, signed.encoded_2718()))
163}
164
165pub(super) struct SignedTransactionIntent {
167 pub hash: B256,
168 pub signer: Address,
169 pub durable_signer: Address,
170 pub chain_id: u32,
171 pub intent_chain_id: u32,
172 pub row_chain_id: u32,
173 pub nonce: u64,
174 pub to: Address,
175 pub value: U256,
176 pub input: Bytes,
177 pub gas_limit: u64,
178 pub max_fee_per_gas: u64,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub(super) struct DecodedSignedTransaction {
184 pub hash: B256,
185 pub signer: Address,
186 pub chain_id: u64,
187 pub nonce: u64,
188 pub to: Address,
189 pub value: U256,
190 pub input: Bytes,
191 pub gas_limit: u64,
192 pub max_fee_per_gas: u128,
193 pub max_priority_fee_per_gas: u128,
194}
195
196pub(super) fn decode_signed_transaction(
198 raw_transaction: &[u8],
199) -> anyhow::Result<DecodedSignedTransaction> {
200 let envelope = TxEnvelope::decode_2718_exact(raw_transaction).map_err(|_| {
201 anyhow::anyhow!("Persisted signed transaction is not a complete EIP-2718 envelope")
202 })?;
203 let TxEnvelope::Eip1559(signed) = envelope else {
204 anyhow::bail!("Persisted signed transaction is not EIP-1559");
205 };
206 anyhow::ensure!(
207 signed.signature().normalize_s().is_none(),
208 "Persisted transaction signature is not EIP-2 normalized"
209 );
210 let signer = signed
211 .signature()
212 .recover_address_from_prehash(&signed.signature_hash())
213 .context("failed to recover persisted transaction signer")?;
214 let hash = *signed.hash();
215 let tx = signed.tx();
216 let TxKind::Call(to) = tx.to else {
217 anyhow::bail!("Signed transaction creates a contract instead of calling a destination");
218 };
219 anyhow::ensure!(
220 tx.access_list.is_empty(),
221 "Signed transaction access list is not empty"
222 );
223
224 Ok(DecodedSignedTransaction {
225 hash,
226 signer,
227 chain_id: tx.chain_id,
228 nonce: tx.nonce,
229 to,
230 value: tx.value,
231 input: tx.input.clone(),
232 gas_limit: tx.gas_limit,
233 max_fee_per_gas: tx.max_fee_per_gas,
234 max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
235 })
236}
237
238pub(super) fn validate_signed_transaction(
240 raw_transaction: &[u8],
241 intent: &SignedTransactionIntent,
242) -> anyhow::Result<()> {
243 let tx = decode_signed_transaction(raw_transaction)?;
244
245 anyhow::ensure!(
246 tx.hash == intent.hash,
247 "Persisted transaction hash {} does not match signed transaction hash {}",
248 intent.hash,
249 tx.hash
250 );
251 anyhow::ensure!(
252 intent.durable_signer == intent.signer,
253 "Persisted transaction signer {} does not match configured wallet {}",
254 intent.durable_signer,
255 intent.signer
256 );
257 anyhow::ensure!(
258 tx.signer == intent.signer,
259 "Signed transaction signer {} does not match configured wallet {}",
260 tx.signer,
261 intent.signer
262 );
263 anyhow::ensure!(
264 intent.intent_chain_id == intent.chain_id,
265 "Persisted intent chain ID {} does not match configured chain ID {}",
266 intent.intent_chain_id,
267 intent.chain_id
268 );
269 anyhow::ensure!(
270 intent.row_chain_id == intent.chain_id,
271 "Persisted transaction row chain ID {} does not match configured chain ID {}",
272 intent.row_chain_id,
273 intent.chain_id
274 );
275
276 anyhow::ensure!(
277 tx.chain_id == u64::from(intent.chain_id),
278 "Signed transaction chain ID {} does not match configured chain ID {}",
279 tx.chain_id,
280 intent.chain_id
281 );
282 anyhow::ensure!(
283 tx.nonce == intent.nonce,
284 "Signed transaction nonce {} does not match persisted nonce {}",
285 tx.nonce,
286 intent.nonce
287 );
288 anyhow::ensure!(
289 tx.to == intent.to,
290 "Signed transaction destination {} does not match persisted destination {}",
291 tx.to,
292 intent.to
293 );
294 anyhow::ensure!(
295 tx.value == intent.value,
296 "Signed transaction value does not match persisted value"
297 );
298 anyhow::ensure!(
299 tx.input == intent.input,
300 "Signed transaction calldata does not match persisted calldata"
301 );
302 anyhow::ensure!(
303 tx.gas_limit <= intent.gas_limit,
304 "Signed transaction gas limit {} exceeds configured ceiling {}",
305 tx.gas_limit,
306 intent.gas_limit
307 );
308 anyhow::ensure!(
309 tx.max_fee_per_gas <= u128::from(intent.max_fee_per_gas),
310 "Signed transaction max fee per gas {} wei exceeds configured ceiling {} wei",
311 tx.max_fee_per_gas,
312 intent.max_fee_per_gas
313 );
314 anyhow::ensure!(
315 tx.max_priority_fee_per_gas <= tx.max_fee_per_gas,
316 "Signed transaction priority fee per gas {} wei exceeds max fee per gas {} wei",
317 tx.max_priority_fee_per_gas,
318 tx.max_fee_per_gas
319 );
320
321 Ok(())
322}
323
324pub fn derive_gas_limit(estimate: u64, gas_buffer_bps: u32, gas_limit: u64) -> anyhow::Result<u64> {
334 let buffered = apply_buffer_bps(u128::from(estimate), gas_buffer_bps)?;
335
336 if buffered > u128::from(gas_limit) {
337 anyhow::bail!(
338 "Estimated gas {estimate} with {gas_buffer_bps} bps buffer ({buffered}) exceeds gas limit {gas_limit}"
339 );
340 }
341
342 u64::try_from(buffered).context("buffered gas limit overflow")
343}
344
345pub fn derive_fees(
356 base_fee_per_gas_wei: u128,
357 priority_fee_per_gas_wei: u128,
358 base_fee_buffer_bps: u32,
359 max_fee_per_gas_wei: u128,
360) -> anyhow::Result<(u128, u128)> {
361 let max_fee = compute_max_fee(
362 base_fee_per_gas_wei,
363 priority_fee_per_gas_wei,
364 base_fee_buffer_bps,
365 )?;
366
367 if max_fee > max_fee_per_gas_wei {
368 anyhow::bail!(
369 "Derived max fee per gas {max_fee} wei exceeds ceiling {max_fee_per_gas_wei} wei"
370 );
371 }
372
373 Ok((max_fee, priority_fee_per_gas_wei))
374}
375
376pub fn compute_max_fee(
383 base_fee_per_gas_wei: u128,
384 priority_fee_per_gas_wei: u128,
385 base_fee_buffer_bps: u32,
386) -> anyhow::Result<u128> {
387 let buffered_base_fee = apply_buffer_bps(base_fee_per_gas_wei, base_fee_buffer_bps)?;
388
389 buffered_base_fee
390 .checked_add(priority_fee_per_gas_wei)
391 .context("max fee per gas overflow")
392}
393
394fn apply_buffer_bps(value: u128, buffer_bps: u32) -> anyhow::Result<u128> {
395 let buffer_bps = u128::from(buffer_bps);
396 let buffer_whole = (value / BPS_DENOMINATOR)
397 .checked_mul(buffer_bps)
398 .context("buffered value overflow")?;
399
400 let buffer_remainder = (value % BPS_DENOMINATOR)
402 .checked_mul(buffer_bps)
403 .and_then(|v| v.checked_add(BPS_DENOMINATOR - 1))
404 .map(|v| v / BPS_DENOMINATOR)
405 .context("buffered value overflow")?;
406
407 value
408 .checked_add(buffer_whole)
409 .and_then(|v| v.checked_add(buffer_remainder))
410 .context("buffered value overflow")
411}
412
413#[cfg(test)]
414mod tests {
415 use std::str::FromStr;
416
417 use alloy::{
418 consensus::TxEip2930,
419 eips::eip2930::{AccessList, AccessListItem},
420 primitives::{Signature, address, b256},
421 };
422 use nautilus_core::hex;
423 use rstest::rstest;
424
425 use super::*;
426
427 const TEST_PRIVATE_KEY: &str =
430 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
431 const EXPECTED_RAW_TX: &str = "02f87682a4b10783989680840bebc20082fde89482af49447d8a07e3bd95bd0d56f35241523fbab187038d7ea4c6800084d0e30db0c080a0ecbbf3b95a4509c94cf0fe219c93a404c09de776a7073f2765709fe04f32f024a07b6a1f8332b39ca80ad3e61d124147af984ffcba1dd5579dbcf11e921ea3cecb";
432
433 #[derive(Debug, Clone, Copy)]
434 enum InvalidSignedField {
435 Hash,
436 Signer,
437 DurableSigner,
438 IntentChain,
439 RowChain,
440 TransactionChain,
441 Nonce,
442 CallType,
443 Destination,
444 Value,
445 Input,
446 GasLimit,
447 MaxFee,
448 PriorityFee,
449 AccessList,
450 }
451
452 fn validation_transaction() -> TxEip1559 {
453 build_eip1559_transaction(
454 42161,
455 7,
456 65_000,
457 200_000_000,
458 10_000_000,
459 address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
460 U256::from(1_000_000_000_000_000u64),
461 Bytes::from(hex::decode("d0e30db0").unwrap()),
462 )
463 }
464
465 fn validation_intent(hash: B256, signer: Address) -> SignedTransactionIntent {
466 SignedTransactionIntent {
467 hash,
468 signer,
469 durable_signer: signer,
470 chain_id: 42161,
471 intent_chain_id: 42161,
472 row_chain_id: 42161,
473 nonce: 7,
474 to: address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
475 value: U256::from(1_000_000_000_000_000u64),
476 input: Bytes::from(hex::decode("d0e30db0").unwrap()),
477 gas_limit: 1_000_000,
478 max_fee_per_gas: 1_000_000_000,
479 }
480 }
481
482 #[rstest]
483 fn test_derive_gas_limit_applies_buffer_rounding_up() {
484 assert_eq!(derive_gas_limit(50_001, 2_000, 1_000_000).unwrap(), 60_002);
485 }
486
487 #[rstest]
488 fn test_derive_gas_limit_allows_estimate_at_ceiling() {
489 assert_eq!(
490 derive_gas_limit(1_000_000, 0, 1_000_000).unwrap(),
491 1_000_000
492 );
493 }
494
495 #[rstest]
496 fn test_derive_gas_limit_rejects_above_ceiling_without_clamping() {
497 let result = derive_gas_limit(900_000, 2_000, 1_000_000);
498
499 assert!(result.is_err());
500 assert!(
501 result
502 .unwrap_err()
503 .to_string()
504 .contains("exceeds gas limit 1000000")
505 );
506 }
507
508 #[rstest]
509 fn test_derive_fees_combines_buffered_base_fee_and_priority_fee() {
510 let (max_fee, priority_fee) = derive_fees(100, 5, 2_000, 1_000).unwrap();
511
512 assert_eq!(max_fee, 125);
513 assert_eq!(priority_fee, 5);
514 }
515
516 #[rstest]
517 fn test_derive_fees_rejects_above_ceiling() {
518 let result = derive_fees(100, 5, 2_000, 124);
519
520 assert!(result.is_err());
521 assert!(
522 result
523 .unwrap_err()
524 .to_string()
525 .contains("exceeds ceiling 124 wei")
526 );
527 }
528
529 #[rstest]
530 fn test_derive_fees_allows_fee_at_ceiling() {
531 assert!(derive_fees(100, 5, 2_000, 125).is_ok());
532 }
533
534 #[rstest]
535 fn test_compute_max_fee_zero_buffer_accepts_maximum_value() {
536 assert_eq!(compute_max_fee(u128::MAX, 0, 0).unwrap(), u128::MAX);
537 }
538
539 #[rstest]
540 fn test_compute_max_fee_rejects_buffered_value_overflow() {
541 let error = compute_max_fee(u128::MAX, 0, 1).unwrap_err();
542
543 assert_eq!(error.to_string(), "buffered value overflow");
544 }
545
546 #[rstest]
547 fn test_build_eip1559_transaction_populates_all_fields() {
548 let to = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
549 let input = Bytes::from(hex::decode("d0e30db0").unwrap());
550
551 let tx = build_eip1559_transaction(
552 42161,
553 7,
554 65_000,
555 200_000_000,
556 10_000_000,
557 to,
558 U256::from(1_000_000_000_000_000u64),
559 input.clone(),
560 );
561
562 assert_eq!(tx.chain_id, 42161);
563 assert_eq!(tx.nonce, 7);
564 assert_eq!(tx.gas_limit, 65_000);
565 assert_eq!(tx.max_fee_per_gas, 200_000_000);
566 assert_eq!(tx.max_priority_fee_per_gas, 10_000_000);
567 assert_eq!(tx.to, TxKind::Call(to));
568 assert_eq!(tx.value, U256::from(1_000_000_000_000_000u64));
569 assert!(tx.access_list.is_empty());
570 assert_eq!(tx.input, input);
571 }
572
573 #[tokio::test]
574 async fn test_sign_eip1559_transaction_matches_reference_vector() {
575 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
576 assert_eq!(
577 signer.address(),
578 address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
579 );
580
581 let tx = build_eip1559_transaction(
582 42161,
583 7,
584 65_000,
585 200_000_000,
586 10_000_000,
587 address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
588 U256::from(1_000_000_000_000_000u64),
589 Bytes::from(hex::decode("d0e30db0").unwrap()),
590 );
591
592 let (tx_hash, raw_tx) = sign_eip1559_transaction(tx, &signer).await.unwrap();
593
594 assert_eq!(
595 tx_hash,
596 b256!("9da4b71be3336357259f56bda5cfbd3803c211ce09b510c43e6fb2af84088c6a")
597 );
598 assert_eq!(hex::encode(&raw_tx), EXPECTED_RAW_TX);
599 }
600
601 #[tokio::test]
602 async fn test_validate_signed_transaction_accepts_builder_output_at_policy_ceilings() {
603 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
604 let mut transaction = validation_transaction();
605 transaction.gas_limit = 1_000_000;
606 transaction.max_fee_per_gas = 1_000_000_000;
607 transaction.max_priority_fee_per_gas = 1_000_000_000;
608 let (hash, raw_transaction) = sign_eip1559_transaction(transaction, &signer)
609 .await
610 .unwrap();
611 let intent = validation_intent(hash, signer.address());
612
613 validate_signed_transaction(&raw_transaction, &intent).unwrap();
614 }
615
616 #[rstest]
617 fn test_validate_signed_transaction_rejects_malformed_bytes() {
618 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
619 let intent = validation_intent(B256::ZERO, signer.address());
620
621 let error = validate_signed_transaction(&[0x02, 0xc0], &intent).unwrap_err();
622
623 assert_eq!(
624 error.to_string(),
625 "Persisted signed transaction is not a complete EIP-2718 envelope"
626 );
627 }
628
629 #[tokio::test]
630 async fn test_validate_signed_transaction_rejects_trailing_bytes_without_exposing_them() {
631 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
632 let (hash, mut raw_transaction) =
633 sign_eip1559_transaction(validation_transaction(), &signer)
634 .await
635 .unwrap();
636 raw_transaction.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
637 let intent = validation_intent(hash, signer.address());
638
639 let error = validate_signed_transaction(&raw_transaction, &intent).unwrap_err();
640
641 assert_eq!(
642 error.to_string(),
643 "Persisted signed transaction is not a complete EIP-2718 envelope"
644 );
645 assert!(!error.to_string().contains("deadbeef"));
646 }
647
648 #[tokio::test]
649 async fn test_validate_signed_transaction_rejects_other_envelope_type() {
650 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
651 let transaction = validation_transaction();
652 let transaction = TxEip2930 {
653 chain_id: transaction.chain_id,
654 nonce: transaction.nonce,
655 gas_price: transaction.max_fee_per_gas,
656 gas_limit: transaction.gas_limit,
657 to: transaction.to,
658 value: transaction.value,
659 access_list: transaction.access_list,
660 input: transaction.input,
661 };
662 let signature = signer
663 .sign_hash(&transaction.signature_hash())
664 .await
665 .unwrap();
666 let raw_transaction = transaction.into_signed(signature).encoded_2718();
667 let intent = validation_intent(B256::ZERO, signer.address());
668
669 let error = validate_signed_transaction(&raw_transaction, &intent).unwrap_err();
670
671 assert_eq!(
672 error.to_string(),
673 "Persisted signed transaction is not EIP-1559"
674 );
675 }
676
677 #[tokio::test]
678 async fn test_validate_signed_transaction_rejects_noncanonical_signature() {
679 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
680 let transaction = validation_transaction();
681 let signature = signer
682 .sign_hash(&transaction.signature_hash())
683 .await
684 .unwrap();
685 let curve_order =
686 U256::from_str("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")
687 .unwrap();
688 let signature = Signature::new(signature.r(), curve_order - signature.s(), !signature.v());
689 let signed = transaction.into_signed(signature);
690 let hash = *signed.hash();
691 let raw_transaction = signed.encoded_2718();
692 let intent = validation_intent(hash, signer.address());
693
694 let error = validate_signed_transaction(&raw_transaction, &intent).unwrap_err();
695
696 assert_eq!(
697 error.to_string(),
698 "Persisted transaction signature is not EIP-2 normalized"
699 );
700 }
701
702 #[rstest]
703 #[case::hash(InvalidSignedField::Hash, "Persisted transaction hash")]
704 #[case::signer(InvalidSignedField::Signer, "does not match configured wallet")]
705 #[case::durable_signer(InvalidSignedField::DurableSigner, "Persisted transaction signer")]
706 #[case::intent_chain(InvalidSignedField::IntentChain, "Persisted intent chain ID")]
707 #[case::row_chain(InvalidSignedField::RowChain, "Persisted transaction row chain ID")]
708 #[case::transaction_chain(InvalidSignedField::TransactionChain, "Signed transaction chain ID")]
709 #[case::nonce(InvalidSignedField::Nonce, "Signed transaction nonce")]
710 #[case::call_type(InvalidSignedField::CallType, "creates a contract")]
711 #[case::destination(InvalidSignedField::Destination, "Signed transaction destination")]
712 #[case::value(InvalidSignedField::Value, "Signed transaction value")]
713 #[case::input(InvalidSignedField::Input, "Signed transaction calldata")]
714 #[case::gas_limit(InvalidSignedField::GasLimit, "Signed transaction gas limit")]
715 #[case::max_fee(InvalidSignedField::MaxFee, "Signed transaction max fee per gas")]
716 #[case::priority_fee(
717 InvalidSignedField::PriorityFee,
718 "Signed transaction priority fee per gas"
719 )]
720 #[case::access_list(InvalidSignedField::AccessList, "Signed transaction access list")]
721 #[tokio::test]
722 async fn test_validate_signed_transaction_rejects_field_mismatch(
723 #[case] field: InvalidSignedField,
724 #[case] expected_error: &str,
725 ) {
726 let signer = PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap();
727 let other = address!("0000000000000000000000000000000000000001");
728 let mut transaction = validation_transaction();
729 let mut intent = validation_intent(B256::ZERO, signer.address());
730
731 match field {
732 InvalidSignedField::Hash => {}
733 InvalidSignedField::Signer => {
734 intent.signer = other;
735 intent.durable_signer = other;
736 }
737 InvalidSignedField::DurableSigner => intent.durable_signer = other,
738 InvalidSignedField::IntentChain => intent.intent_chain_id = 1,
739 InvalidSignedField::RowChain => intent.row_chain_id = 1,
740 InvalidSignedField::TransactionChain => transaction.chain_id = 1,
741 InvalidSignedField::Nonce => transaction.nonce = 8,
742 InvalidSignedField::CallType => transaction.to = TxKind::Create,
743 InvalidSignedField::Destination => transaction.to = TxKind::Call(other),
744 InvalidSignedField::Value => transaction.value = U256::from(2u64),
745 InvalidSignedField::Input => transaction.input = Bytes::from(vec![0x01]),
746 InvalidSignedField::GasLimit => transaction.gas_limit = 1_000_001,
747 InvalidSignedField::MaxFee => transaction.max_fee_per_gas = 1_000_000_001,
748 InvalidSignedField::PriorityFee => {
749 transaction.max_fee_per_gas = 10_000_000;
750 transaction.max_priority_fee_per_gas = 10_000_001;
751 }
752 InvalidSignedField::AccessList => {
753 transaction.access_list = AccessList(vec![AccessListItem {
754 address: other,
755 storage_keys: Vec::new(),
756 }]);
757 }
758 }
759
760 let (hash, raw_transaction) = sign_eip1559_transaction(transaction, &signer)
761 .await
762 .unwrap();
763
764 if !matches!(field, InvalidSignedField::Hash) {
765 intent.hash = hash;
766 }
767
768 let error = validate_signed_transaction(&raw_transaction, &intent).unwrap_err();
769
770 assert!(error.to_string().contains(expected_error), "was: {error}");
771 }
772}