Skip to main content

reth_e2e_test_utils/
wallet.rs

1use alloy_eips::{eip2718::Encodable2718, BlockId};
2use alloy_network::{
3    Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
4};
5use alloy_primitives::{Address, Bytes, U256};
6use alloy_provider::Provider;
7use alloy_rpc_types_eth::TransactionRequest;
8use alloy_signer::Signer;
9use alloy_signer_local::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner};
10use futures_util::future::BoxFuture;
11use std::future::IntoFuture;
12
13/// Mnemonic of the test accounts funded by the [`test_genesis`](crate::test_genesis).
14pub const TEST_MNEMONIC: &str = "test test test test test test test test test test test junk";
15
16/// One of the accounts of the genesis allocations.
17#[derive(Debug)]
18pub struct Wallet {
19    /// The signer
20    pub inner: PrivateKeySigner,
21    /// The nonce
22    pub inner_nonce: u64,
23    /// The chain id
24    pub chain_id: u64,
25    amount: usize,
26}
27
28impl Wallet {
29    /// Creates a new account from one of the secret/pubkeys of the genesis allocations (test.json)
30    pub fn new(amount: usize) -> Self {
31        Self { inner: test_signer(0), chain_id: 1, amount, inner_nonce: 0 }
32    }
33
34    /// Sets chain id
35    pub const fn with_chain_id(mut self, chain_id: u64) -> Self {
36        self.chain_id = chain_id;
37        self
38    }
39
40    /// Generates a list of wallets
41    pub fn wallet_gen(&self) -> Vec<PrivateKeySigner> {
42        (0..self.amount as u32).map(|idx| self.signer(idx)).collect()
43    }
44
45    /// Returns the signer of the test account at `index`, with the chain id of the wallet.
46    pub fn signer(&self, index: u32) -> PrivateKeySigner {
47        test_signer(index).with_chain_id(Some(self.chain_id))
48    }
49
50    /// Returns the test account at `index`, with the chain id of the wallet and nonce 0.
51    pub fn account(&self, index: u32) -> TestAccount {
52        TestAccount::new(self.signer(index), self.chain_id)
53    }
54}
55
56impl Default for Wallet {
57    fn default() -> Self {
58        Self::new(1)
59    }
60}
61
62/// Returns the signer of the test account at `index`, derived from [`TEST_MNEMONIC`] with the
63/// default Ethereum derivation path `m/44'/60'/0'/0/{index}`.
64pub fn test_signer(index: u32) -> PrivateKeySigner {
65    MnemonicBuilder::<English>::default()
66        .phrase(TEST_MNEMONIC)
67        .index(index)
68        .expect("valid derivation index")
69        .build()
70        .expect("valid test mnemonic")
71}
72
73/// A test account that tracks its nonce, e.g. one of the accounts funded by the
74/// [`test_genesis`](crate::test_genesis).
75///
76/// [`Self::sign_tx_bytes`] fills the nonce, chain id, gas limit and fees of transaction requests
77/// that do not set them, so tests only need to set the fields they care about. The gas limit and
78/// fees default to [`Self::DEFAULT_GAS_LIMIT`] and the `DEFAULT_*_FEE_PER_GAS` constants and can be
79/// changed once per account with [`Self::with_gas_limit`] and [`Self::with_fees`].
80///
81/// [`Self::tx`] and its shortcuts [`Self::call`], [`Self::transfer`] and [`Self::deploy`] build and
82/// sign a transaction in a single expression:
83///
84/// ```ignore
85/// let mut account = wallet.account(0).with_fees(max_fee_per_gas, max_priority_fee_per_gas);
86/// let contract = account.next_contract_address();
87/// let deploy = account.deploy(init_code).await;
88/// let call = account.call(contract, calldata).gas_limit(100_000).await;
89/// ```
90#[derive(Debug, Clone)]
91pub struct TestAccount {
92    signer: PrivateKeySigner,
93    chain_id: u64,
94    nonce: u64,
95    gas_limit: u64,
96    max_fee_per_gas: u128,
97    max_priority_fee_per_gas: u128,
98}
99
100impl TestAccount {
101    /// Initial gas limit of transactions signed by [`Self::sign_tx_bytes`] that do not set one.
102    pub const DEFAULT_GAS_LIMIT: u64 = 1_000_000;
103
104    /// Initial max fee per gas of transactions signed by [`Self::sign_tx_bytes`] that set no fees.
105    ///
106    /// High enough that transactions are accepted regardless of the base fee of test chains.
107    pub const DEFAULT_MAX_FEE_PER_GAS: u128 = 1_000_000_000_000;
108
109    /// Initial max priority fee per gas of transactions signed by [`Self::sign_tx_bytes`] that set
110    /// no fees.
111    pub const DEFAULT_MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000;
112
113    /// Creates a new account for the given signer and chain id, starting at nonce 0.
114    pub const fn new(signer: PrivateKeySigner, chain_id: u64) -> Self {
115        Self {
116            signer,
117            chain_id,
118            nonce: 0,
119            gas_limit: Self::DEFAULT_GAS_LIMIT,
120            max_fee_per_gas: Self::DEFAULT_MAX_FEE_PER_GAS,
121            max_priority_fee_per_gas: Self::DEFAULT_MAX_PRIORITY_FEE_PER_GAS,
122        }
123    }
124
125    /// Sets the gas limit of signed transactions that do not set one.
126    pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
127        self.gas_limit = gas_limit;
128        self
129    }
130
131    /// Sets the EIP-1559 fees of signed transactions that set neither these fees nor a legacy gas
132    /// price.
133    pub const fn with_fees(
134        mut self,
135        max_fee_per_gas: u128,
136        max_priority_fee_per_gas: u128,
137    ) -> Self {
138        self.max_fee_per_gas = max_fee_per_gas;
139        self.max_priority_fee_per_gas = max_priority_fee_per_gas;
140        self
141    }
142
143    /// Returns the signer of the account.
144    pub const fn signer(&self) -> &PrivateKeySigner {
145        &self.signer
146    }
147
148    /// Returns the address of the account.
149    pub const fn address(&self) -> Address {
150        self.signer.address()
151    }
152
153    /// Returns the chain id transactions of the account are signed for.
154    pub const fn chain_id(&self) -> u64 {
155        self.chain_id
156    }
157
158    /// Returns the nonce of the next transaction of the account.
159    pub const fn nonce(&self) -> u64 {
160        self.nonce
161    }
162
163    /// Returns the nonce of the next transaction and increments it.
164    pub const fn next_nonce(&mut self) -> u64 {
165        let nonce = self.nonce;
166        self.nonce += 1;
167        nonce
168    }
169
170    /// Sets the nonce to the pending transaction count of the account on the node behind
171    /// `provider`, and returns it.
172    pub async fn sync_nonce<N: Network>(
173        &mut self,
174        provider: &impl Provider<N>,
175    ) -> eyre::Result<u64> {
176        self.nonce =
177            provider.get_transaction_count(self.address()).block_id(BlockId::pending()).await?;
178        Ok(self.nonce)
179    }
180
181    /// Signs the transaction request, returning the EIP-2718 encoded bytes.
182    ///
183    /// Fills unset fields: the nonce with [`Self::next_nonce`], the chain id, gas limit and, unless
184    /// the request sets a legacy gas price, the EIP-1559 fees of the account. An explicit nonce
185    /// does not advance the tracked nonce. Contract creations must set `to`, e.g. with
186    /// [`TransactionRequest::create`] or [`Self::deploy`].
187    ///
188    /// See [`Self::sign_request`] for other networks.
189    ///
190    /// # Panics
191    ///
192    /// If the request can not be built into a signed transaction.
193    pub async fn sign_tx_bytes(&mut self, tx: TransactionRequest) -> Bytes {
194        self.sign_request::<Ethereum>(tx).await
195    }
196
197    /// Returns the address of the contract created by the next transaction of the account, if it
198    /// is a contract creation that does not set an explicit nonce.
199    pub fn next_contract_address(&self) -> Address {
200        self.address().create(self.nonce)
201    }
202
203    /// Starts building a transaction of the account that sets no fields yet.
204    ///
205    /// See [`Self::tx_for`] for other networks.
206    pub fn tx(&mut self) -> TestTx<'_> {
207        self.tx_for()
208    }
209
210    /// Starts building a call of `to` with the given input.
211    pub fn call(&mut self, to: Address, input: impl Into<Bytes>) -> TestTx<'_> {
212        self.tx().to(to).input(input)
213    }
214
215    /// Starts building a transfer of `value` to `to`.
216    pub fn transfer(&mut self, to: Address, value: U256) -> TestTx<'_> {
217        self.tx().to(to).value(value)
218    }
219
220    /// Starts building a contract creation with the given init code.
221    ///
222    /// See [`Self::next_contract_address`] for the address of the created contract.
223    pub fn deploy(&mut self, init_code: impl Into<Bytes>) -> TestTx<'_> {
224        self.tx().create().input(init_code)
225    }
226
227    /// Signs the transaction request of the network `N` like [`Self::sign_tx_bytes`], returning
228    /// the EIP-2718 encoded bytes.
229    ///
230    /// Only the secp256k1 key of the account signs, so transactions of networks that are signed
231    /// with other keys, e.g. P256 or passkeys, need their own wallets. Transactions using a
232    /// network-specific nonce scheme, such as 2D nonces, should set an explicit nonce so that the
233    /// tracked protocol nonce is not advanced.
234    ///
235    /// # Panics
236    ///
237    /// If the request can not be built into a signed transaction.
238    pub async fn sign_request<N: Network>(&mut self, mut tx: N::TransactionRequest) -> Bytes
239    where
240        EthereumWallet: NetworkWallet<N>,
241    {
242        if tx.nonce().is_none() {
243            tx.set_nonce(self.next_nonce());
244        }
245        if tx.chain_id().is_none() {
246            tx.set_chain_id(self.chain_id);
247        }
248        if tx.gas_limit().is_none() {
249            tx.set_gas_limit(self.gas_limit);
250        }
251        if tx.gas_price().is_none() {
252            if tx.max_fee_per_gas().is_none() {
253                tx.set_max_fee_per_gas(self.max_fee_per_gas);
254            }
255            if tx.max_priority_fee_per_gas().is_none() {
256                tx.set_max_priority_fee_per_gas(self.max_priority_fee_per_gas);
257            }
258        }
259        let wallet = EthereumWallet::from(self.signer.clone());
260        NetworkTransactionBuilder::<N>::build(tx, &wallet)
261            .await
262            .unwrap_or_else(|err| panic!("failed to sign the test transaction: {err}"))
263            .encoded_2718()
264            .into()
265    }
266
267    /// Starts building a transaction of the network `N` that sets no fields yet.
268    ///
269    /// The shortcuts [`Self::call`], [`Self::transfer`] and [`Self::deploy`] build Ethereum
270    /// transactions; for other networks, use the setters of [`TestTx`], e.g.
271    /// `account.tx_for::<N>().to(to).input(input)`. See [`Self::sign_request`] for the limits.
272    pub fn tx_for<N: Network>(&mut self) -> TestTx<'_, N> {
273        TestTx { account: self, request: Default::default() }
274    }
275}
276
277/// A transaction of a [`TestAccount`] being built for the network `N`, see [`TestAccount::tx`]
278/// and [`TestAccount::tx_for`].
279///
280/// Awaiting it signs the transaction with [`TestAccount::sign_request`], like [`Self::sign`],
281/// which fills the fields that are not set from the account. Network-specific fields can be set
282/// with [`Self::map_request`].
283#[derive(Debug)]
284#[must_use = "the transaction is only signed when awaited"]
285pub struct TestTx<'a, N: Network = Ethereum> {
286    account: &'a mut TestAccount,
287    request: N::TransactionRequest,
288}
289
290impl<N: Network> TestTx<'_, N> {
291    /// Sets the recipient.
292    pub fn to(mut self, to: Address) -> Self {
293        self.request.set_to(to);
294        self
295    }
296
297    /// Makes the transaction a contract creation, with the input as init code.
298    pub fn create(mut self) -> Self {
299        self.request.set_create();
300        self
301    }
302
303    /// Sets the value transferred with the transaction.
304    pub fn value(mut self, value: U256) -> Self {
305        self.request.set_value(value);
306        self
307    }
308
309    /// Sets the input, i.e. the calldata of a call or the init code of a contract creation.
310    pub fn input(mut self, input: impl Into<Bytes>) -> Self {
311        self.request.set_input(input);
312        self
313    }
314
315    /// Sets the gas limit instead of the gas limit of the account.
316    pub fn gas_limit(mut self, gas_limit: u64) -> Self {
317        self.request.set_gas_limit(gas_limit);
318        self
319    }
320
321    /// Sets the EIP-1559 fees instead of the fees of the account.
322    pub fn fees(mut self, max_fee_per_gas: u128, max_priority_fee_per_gas: u128) -> Self {
323        self.request.set_max_fee_per_gas(max_fee_per_gas);
324        self.request.set_max_priority_fee_per_gas(max_priority_fee_per_gas);
325        self
326    }
327
328    /// Sets a legacy gas price, which makes this a legacy transaction without the EIP-1559 fees of
329    /// the account.
330    pub fn gas_price(mut self, gas_price: u128) -> Self {
331        self.request.set_gas_price(gas_price);
332        self
333    }
334
335    /// Sets an explicit nonce, which does not advance the tracked nonce of the account.
336    pub fn nonce(mut self, nonce: u64) -> Self {
337        self.request.set_nonce(nonce);
338        self
339    }
340
341    /// Modifies the underlying transaction request, e.g. to set network-specific fields or fields
342    /// without a dedicated setter.
343    pub fn map_request(
344        mut self,
345        f: impl FnOnce(N::TransactionRequest) -> N::TransactionRequest,
346    ) -> Self {
347        self.request = f(self.request);
348        self
349    }
350
351    /// Signs the transaction, returning the EIP-2718 encoded bytes.
352    ///
353    /// See [`TestAccount::sign_request`].
354    pub async fn sign(self) -> Bytes
355    where
356        EthereumWallet: NetworkWallet<N>,
357    {
358        self.account.sign_request::<N>(self.request).await
359    }
360}
361
362impl<'a, N: Network> IntoFuture for TestTx<'a, N>
363where
364    EthereumWallet: NetworkWallet<N>,
365{
366    type Output = Bytes;
367    type IntoFuture = BoxFuture<'a, Bytes>;
368
369    fn into_future(self) -> Self::IntoFuture {
370        Box::pin(self.sign())
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use alloy_consensus::{transaction::SignerRecoverable, Transaction, TxEnvelope};
378    use alloy_eips::eip2718::Decodable2718;
379    use alloy_network::AnyNetwork;
380    use alloy_primitives::TxKind;
381
382    fn decode(raw: Bytes) -> TxEnvelope {
383        let tx = TxEnvelope::decode_2718(&mut raw.as_ref()).unwrap();
384        assert_eq!(tx.recover_signer().unwrap(), Wallet::default().account(0).address());
385        tx
386    }
387
388    fn assert_send<T: Send>(_: T) {}
389
390    /// Tests of downstream nodes sign transactions in spawned tasks, so the futures must be
391    /// `Send`.
392    #[expect(dead_code)]
393    fn test_sign_futures_are_send(account: &mut TestAccount) {
394        assert_send(account.sign_tx_bytes(TransactionRequest::default()));
395        assert_send(account.tx().sign());
396        assert_send(account.tx().into_future());
397        assert_send(account.sign_request::<AnyNetwork>(Default::default()));
398        assert_send(account.tx_for::<AnyNetwork>().into_future());
399    }
400
401    #[tokio::test]
402    async fn tx_builder_fills_account_defaults() {
403        let mut account = Wallet::default()
404            .with_chain_id(1337)
405            .account(0)
406            .with_gas_limit(300_000)
407            .with_fees(7, 3);
408        let to = Address::repeat_byte(0x11);
409
410        let call = decode(account.call(to, [0xab, 0xcd]).await);
411        assert!(call.is_eip1559());
412        assert_eq!(call.chain_id(), Some(1337));
413        assert_eq!(call.nonce(), 0);
414        assert_eq!(call.kind(), TxKind::Call(to));
415        assert_eq!(call.input().as_ref(), [0xab, 0xcd]);
416        assert_eq!(call.value(), U256::ZERO);
417        assert_eq!(call.gas_limit(), 300_000);
418        assert_eq!(call.max_fee_per_gas(), 7);
419        assert_eq!(call.max_priority_fee_per_gas(), Some(3));
420
421        let transfer =
422            decode(account.transfer(to, U256::from(5)).gas_limit(21_000).fees(9, 1).sign().await);
423        assert_eq!(transfer.nonce(), 1);
424        assert_eq!(transfer.value(), U256::from(5));
425        assert_eq!(transfer.gas_limit(), 21_000);
426        assert_eq!(transfer.max_fee_per_gas(), 9);
427        assert_eq!(transfer.max_priority_fee_per_gas(), Some(1));
428
429        assert_eq!(account.next_contract_address(), account.address().create(2));
430        let deploy = decode(account.deploy([0x00]).await);
431        assert_eq!(deploy.nonce(), 2);
432        assert_eq!(deploy.kind(), TxKind::Create);
433        assert_eq!(deploy.input().as_ref(), [0x00]);
434
435        // An explicit nonce does not advance the tracked nonce.
436        let legacy = decode(account.transfer(to, U256::ZERO).gas_price(11).nonce(9).await);
437        assert!(legacy.is_legacy());
438        assert_eq!(legacy.nonce(), 9);
439        assert_eq!(legacy.gas_price(), Some(11));
440        assert_eq!(account.nonce(), 3);
441
442        let mapped = decode(account.tx().map_request(|tx| tx.to(to).value(U256::ONE)).await);
443        assert_eq!(mapped.nonce(), 3);
444        assert_eq!(mapped.value(), U256::ONE);
445    }
446
447    /// The builder works for any network whose transactions the account's wallet can sign.
448    #[tokio::test]
449    async fn tx_builder_is_network_generic() {
450        let mut account = Wallet::default().account(0);
451        let to = Address::repeat_byte(0x11);
452
453        let ethereum = account.transfer(to, U256::ONE).input([0xab]).nonce(0).await;
454        let any = account.tx_for::<AnyNetwork>().to(to).value(U256::ONE).input([0xab]).await;
455        assert_eq!(any, ethereum);
456        assert_eq!(account.nonce(), 1);
457
458        let deploy = account.tx_for::<AnyNetwork>().create().input([0x00]).fees(9, 1).sign().await;
459        let deploy = decode(deploy);
460        assert_eq!(deploy.kind(), TxKind::Create);
461        assert_eq!(deploy.nonce(), 1);
462        assert_eq!(deploy.max_fee_per_gas(), 9);
463    }
464}