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
13pub const TEST_MNEMONIC: &str = "test test test test test test test test test test test junk";
15
16#[derive(Debug)]
18pub struct Wallet {
19 pub inner: PrivateKeySigner,
21 pub inner_nonce: u64,
23 pub chain_id: u64,
25 amount: usize,
26}
27
28impl Wallet {
29 pub fn new(amount: usize) -> Self {
31 Self { inner: test_signer(0), chain_id: 1, amount, inner_nonce: 0 }
32 }
33
34 pub const fn with_chain_id(mut self, chain_id: u64) -> Self {
36 self.chain_id = chain_id;
37 self
38 }
39
40 pub fn wallet_gen(&self) -> Vec<PrivateKeySigner> {
42 (0..self.amount as u32).map(|idx| self.signer(idx)).collect()
43 }
44
45 pub fn signer(&self, index: u32) -> PrivateKeySigner {
47 test_signer(index).with_chain_id(Some(self.chain_id))
48 }
49
50 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
62pub 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#[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 pub const DEFAULT_GAS_LIMIT: u64 = 1_000_000;
103
104 pub const DEFAULT_MAX_FEE_PER_GAS: u128 = 1_000_000_000_000;
108
109 pub const DEFAULT_MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000;
112
113 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 pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
127 self.gas_limit = gas_limit;
128 self
129 }
130
131 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 pub const fn signer(&self) -> &PrivateKeySigner {
145 &self.signer
146 }
147
148 pub const fn address(&self) -> Address {
150 self.signer.address()
151 }
152
153 pub const fn chain_id(&self) -> u64 {
155 self.chain_id
156 }
157
158 pub const fn nonce(&self) -> u64 {
160 self.nonce
161 }
162
163 pub const fn next_nonce(&mut self) -> u64 {
165 let nonce = self.nonce;
166 self.nonce += 1;
167 nonce
168 }
169
170 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 pub async fn sign_tx_bytes(&mut self, tx: TransactionRequest) -> Bytes {
194 self.sign_request::<Ethereum>(tx).await
195 }
196
197 pub fn next_contract_address(&self) -> Address {
200 self.address().create(self.nonce)
201 }
202
203 pub fn tx(&mut self) -> TestTx<'_> {
207 self.tx_for()
208 }
209
210 pub fn call(&mut self, to: Address, input: impl Into<Bytes>) -> TestTx<'_> {
212 self.tx().to(to).input(input)
213 }
214
215 pub fn transfer(&mut self, to: Address, value: U256) -> TestTx<'_> {
217 self.tx().to(to).value(value)
218 }
219
220 pub fn deploy(&mut self, init_code: impl Into<Bytes>) -> TestTx<'_> {
224 self.tx().create().input(init_code)
225 }
226
227 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 pub fn tx_for<N: Network>(&mut self) -> TestTx<'_, N> {
273 TestTx { account: self, request: Default::default() }
274 }
275}
276
277#[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 pub fn to(mut self, to: Address) -> Self {
293 self.request.set_to(to);
294 self
295 }
296
297 pub fn create(mut self) -> Self {
299 self.request.set_create();
300 self
301 }
302
303 pub fn value(mut self, value: U256) -> Self {
305 self.request.set_value(value);
306 self
307 }
308
309 pub fn input(mut self, input: impl Into<Bytes>) -> Self {
311 self.request.set_input(input);
312 self
313 }
314
315 pub fn gas_limit(mut self, gas_limit: u64) -> Self {
317 self.request.set_gas_limit(gas_limit);
318 self
319 }
320
321 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 pub fn gas_price(mut self, gas_price: u128) -> Self {
331 self.request.set_gas_price(gas_price);
332 self
333 }
334
335 pub fn nonce(mut self, nonce: u64) -> Self {
337 self.request.set_nonce(nonce);
338 self
339 }
340
341 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 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 #[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 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 #[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}