Skip to main content

reth_e2e_test_utils/
transaction.rs

1use alloy_consensus::{
2    EnvKzgSettings, EthereumTxEnvelope, SidecarBuilder, SimpleCoder, TxEip4844Variant, TxEnvelope,
3};
4use alloy_eips::{eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization};
5use alloy_network::{
6    eip2718::Encodable2718, Ethereum, EthereumWallet, NetworkTransactionBuilder,
7    TransactionBuilder4844,
8};
9use alloy_primitives::{hex, Address, Bytes, TxKind, B256, U256};
10use alloy_rpc_types_eth::{Authorization, TransactionInput, TransactionRequest};
11use alloy_signer::SignerSync;
12use alloy_signer_local::PrivateKeySigner;
13use eyre::Ok;
14
15/// Helper for transaction operations
16#[derive(Debug)]
17pub struct TransactionTestContext;
18
19impl TransactionTestContext {
20    /// Creates a static transfer and signs it, returning an envelope.
21    pub async fn transfer_tx(chain_id: u64, wallet: PrivateKeySigner) -> TxEnvelope {
22        let tx = tx(chain_id, 21000, None, None, 0, Some(20e9 as u128));
23        Self::sign_tx(wallet, tx).await
24    }
25
26    /// Same as `transfer_tx`, but could set max fee per gas.
27    pub async fn transfer_tx_with_gas_fee(
28        chain_id: u64,
29        max_fee_per_gas: Option<u128>,
30        wallet: PrivateKeySigner,
31    ) -> TxEnvelope {
32        let tx = tx(chain_id, 21000, None, None, 0, max_fee_per_gas);
33        Self::sign_tx(wallet, tx).await
34    }
35
36    /// Creates a static transfer and signs it, returning bytes.
37    pub async fn transfer_tx_bytes(chain_id: u64, wallet: PrivateKeySigner) -> Bytes {
38        let signed = Self::transfer_tx(chain_id, wallet).await;
39        signed.encoded_2718().into()
40    }
41
42    /// Creates a transfer with a specific nonce and signs it, returning bytes.
43    /// Uses high `max_fee_per_gas` (1000 gwei) to ensure tx acceptance regardless of basefee.
44    pub async fn transfer_tx_bytes_with_nonce(
45        chain_id: u64,
46        wallet: PrivateKeySigner,
47        nonce: u64,
48    ) -> Bytes {
49        let tx = tx(chain_id, 21000, None, None, nonce, Some(1000e9 as u128));
50        let signed = Self::sign_tx(wallet, tx).await;
51        signed.encoded_2718().into()
52    }
53
54    /// Creates a deployment transaction and signs it, returning an envelope.
55    pub async fn deploy_tx(
56        chain_id: u64,
57        gas: u64,
58        init_code: Bytes,
59        wallet: PrivateKeySigner,
60    ) -> TxEnvelope {
61        let tx = tx(chain_id, gas, Some(init_code), None, 0, Some(20e9 as u128));
62        Self::sign_tx(wallet, tx).await
63    }
64
65    /// Creates a deployment transaction and signs it, returning bytes.
66    pub async fn deploy_tx_bytes(
67        chain_id: u64,
68        gas: u64,
69        init_code: Bytes,
70        wallet: PrivateKeySigner,
71    ) -> Bytes {
72        let signed = Self::deploy_tx(chain_id, gas, init_code, wallet).await;
73        signed.encoded_2718().into()
74    }
75
76    /// Creates an EIP-7702 set code transaction and signs it, returning an envelope.
77    ///
78    /// The EIP-7702 will delegate the code of the signer to the contract at `delegate_to`.
79    pub async fn set_code_tx(
80        chain_id: u64,
81        delegate_to: Address,
82        wallet: PrivateKeySigner,
83    ) -> TxEnvelope {
84        let authorization =
85            Authorization { chain_id: U256::from(chain_id), address: delegate_to, nonce: 0 };
86        let signature = wallet
87            .sign_hash_sync(&authorization.signature_hash())
88            .expect("could not sign authorization");
89        let tx = tx(
90            chain_id,
91            48100,
92            None,
93            Some(authorization.into_signed(signature)),
94            0,
95            Some(20e9 as u128),
96        );
97        Self::sign_tx(wallet, tx).await
98    }
99
100    /// Creates an EIP-7702 set code transaction and signs it, returning bytes.
101    ///
102    /// The EIP-7702 will delegate the code of the signer to the contract at `delegate_to`.
103    pub async fn set_code_tx_bytes(
104        chain_id: u64,
105        delegate_to: Address,
106        wallet: PrivateKeySigner,
107    ) -> Bytes {
108        let signed = Self::set_code_tx(chain_id, delegate_to, wallet).await;
109        signed.encoded_2718().into()
110    }
111
112    /// Creates a tx with blob sidecar and sign it
113    pub async fn tx_with_blobs(
114        chain_id: u64,
115        wallet: PrivateKeySigner,
116    ) -> eyre::Result<TxEnvelope> {
117        let mut tx = tx(chain_id, 210000, None, None, 0, Some(20e9 as u128));
118
119        let mut builder = SidecarBuilder::<SimpleCoder>::new();
120        builder.ingest(b"dummy blob");
121        let sidecar: alloy_consensus::BlobTransactionSidecar = builder.build()?;
122        tx.set_blob_sidecar(alloy_eips::eip7594::BlobTransactionSidecarVariant::Eip4844(sidecar));
123        tx.set_max_fee_per_blob_gas(15e9 as u128);
124
125        let signed = Self::sign_tx(wallet, tx).await;
126        Ok(signed)
127    }
128
129    /// Signs an arbitrary [`TransactionRequest`] using the provided wallet
130    pub async fn sign_tx(wallet: PrivateKeySigner, tx: TransactionRequest) -> TxEnvelope {
131        let signer = EthereumWallet::from(wallet);
132        <TransactionRequest as NetworkTransactionBuilder<Ethereum>>::build(tx, &signer)
133            .await
134            .unwrap()
135    }
136
137    /// Creates a tx with blob sidecar and sign it, returning bytes
138    pub async fn tx_with_blobs_bytes(
139        chain_id: u64,
140        wallet: PrivateKeySigner,
141    ) -> eyre::Result<Bytes> {
142        let signed = Self::tx_with_blobs(chain_id, wallet).await?;
143
144        Ok(signed.encoded_2718().into())
145    }
146
147    /// Creates and encodes an Optimism L1 block information transaction.
148    pub async fn optimism_l1_block_info_tx(
149        chain_id: u64,
150        wallet: PrivateKeySigner,
151        nonce: u64,
152    ) -> Bytes {
153        let l1_block_info = Bytes::from_static(&hex!(
154            "7ef9015aa044bae9d41b8380d781187b426c6fe43df5fb2fb57bd4466ef6a701e1f01e015694deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000001580808408f0d18001b90104015d8eb900000000000000000000000000000000000000000000000000000000008057650000000000000000000000000000000000000000000000000000000063d96d10000000000000000000000000000000000000000000000000000000000009f35273d89754a1e0387b89520d989d3be9c37c1f32495a88faf1ea05c61121ab0d1900000000000000000000000000000000000000000000000000000000000000010000000000000000000000002d679b567db6187c0c8323fa982cfb88b74dbcc7000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240"
155        ));
156        let tx = tx(chain_id, 210000, Some(l1_block_info), None, nonce, Some(20e9 as u128));
157        let signer = EthereumWallet::from(wallet);
158        <TransactionRequest as NetworkTransactionBuilder<Ethereum>>::build(tx, &signer)
159            .await
160            .unwrap()
161            .encoded_2718()
162            .into()
163    }
164
165    /// Validates the sidecar of a given tx envelope and returns the versioned hashes
166    #[track_caller]
167    pub fn validate_sidecar(
168        tx: EthereumTxEnvelope<TxEip4844Variant<BlobTransactionSidecarVariant>>,
169    ) -> Vec<B256> {
170        let proof_setting = EnvKzgSettings::Default;
171
172        match tx {
173            EthereumTxEnvelope::Eip4844(signed) => match signed.tx() {
174                TxEip4844Variant::TxEip4844WithSidecar(tx) => {
175                    tx.validate_blob(proof_setting.get()).unwrap();
176                    tx.sidecar.versioned_hashes().collect()
177                }
178                _ => panic!("Expected Eip4844 transaction with sidecar"),
179            },
180            _ => panic!("Expected Eip4844 transaction"),
181        }
182    }
183}
184
185/// Creates a type 2 transaction
186fn tx(
187    chain_id: u64,
188    gas: u64,
189    data: Option<Bytes>,
190    delegate_to: Option<SignedAuthorization>,
191    nonce: u64,
192    max_fee_per_gas: Option<u128>,
193) -> TransactionRequest {
194    TransactionRequest {
195        nonce: Some(nonce),
196        value: Some(U256::from(100)),
197        to: Some(TxKind::Call(Address::random())),
198        gas: Some(gas),
199        max_fee_per_gas,
200        max_priority_fee_per_gas: Some(20e9 as u128),
201        chain_id: Some(chain_id),
202        input: TransactionInput { input: None, data },
203        authorization_list: delegate_to.map(|addr| vec![addr]),
204        ..Default::default()
205    }
206}