Skip to content

Commit f94f589

Browse files
0xChaddBmattsse
andauthored
feat(anvil): eth_getTransactionBySenderAndNonce RPC method (#12497)
* feat(anvil): implement eth_getTransactionBySenderAndNonce RPC method * style: cargo fmt * chore: fix typo --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
1 parent 7418317 commit f94f589

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

crates/anvil/core/src/eth/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,14 @@ pub enum EthRequest {
678678
#[serde(deserialize_with = "deserialize_number")] U256,
679679
),
680680

681+
/// Returns the transaction by sender and nonce
682+
/// Returns the full transaction data.
683+
#[serde(rename = "eth_getTransactionBySenderAndNonce")]
684+
EthGetTransactionBySenderAndNonce(
685+
Address,
686+
#[serde(deserialize_with = "deserialize_number")] U256,
687+
),
688+
681689
/// Otterscan's `ots_getTransactionBySenderAndNonce` endpoint
682690
/// Given an ETH contract address, returns the tx hash and the direct address who created the
683691
/// contract.

crates/anvil/src/eth/api.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::{
3030
mem::transaction_build,
3131
};
3232
use alloy_consensus::{
33-
Account, Blob,
33+
Account, Blob, Transaction,
3434
transaction::{Recovered, eip4844::TxEip4844Variant},
3535
};
3636
use alloy_dyn_abi::TypedData;
@@ -501,6 +501,9 @@ impl EthApi {
501501
EthRequest::OtsGetTransactionBySenderAndNonce(address, nonce) => {
502502
self.ots_get_transaction_by_sender_and_nonce(address, nonce).await.to_rpc_result()
503503
}
504+
EthRequest::EthGetTransactionBySenderAndNonce(sender, nonce) => {
505+
self.transaction_by_sender_and_nonce(sender, nonce).await.to_rpc_result()
506+
}
504507
EthRequest::OtsGetContractCreator(address) => {
505508
self.ots_get_contract_creator(address).await.to_rpc_result()
506509
}
@@ -1447,6 +1450,91 @@ impl EthApi {
14471450
self.backend.transaction_by_block_number_and_index(block, idx).await
14481451
}
14491452

1453+
/// Returns the transaction by sender and nonce.
1454+
///
1455+
/// This will check the mempool for pending transactions first, then perform a binary search
1456+
/// over mined blocks to find the transaction.
1457+
///
1458+
/// Handler for ETH RPC call: `eth_getTransactionBySenderAndNonce`
1459+
pub async fn transaction_by_sender_and_nonce(
1460+
&self,
1461+
sender: Address,
1462+
nonce: U256,
1463+
) -> Result<Option<AnyRpcTransaction>> {
1464+
node_info!("eth_getTransactionBySenderAndNonce");
1465+
1466+
// check pending txs first
1467+
for pending_tx in self.pool.ready_transactions().chain(self.pool.pending_transactions()) {
1468+
if U256::from(pending_tx.pending_transaction.nonce()) == nonce
1469+
&& *pending_tx.pending_transaction.sender() == sender
1470+
{
1471+
let tx = transaction_build(
1472+
Some(*pending_tx.pending_transaction.hash()),
1473+
pending_tx.pending_transaction.transaction.clone(),
1474+
None,
1475+
None,
1476+
Some(self.backend.base_fee()),
1477+
);
1478+
1479+
let WithOtherFields { inner: mut tx, other } = tx.0;
1480+
// we set the from field here explicitly to the set sender of the pending
1481+
// transaction, in case the transaction is impersonated.
1482+
let from = *pending_tx.pending_transaction.sender();
1483+
tx.inner = Recovered::new_unchecked(tx.inner.into_inner(), from);
1484+
1485+
return Ok(Some(AnyRpcTransaction(WithOtherFields { inner: tx, other })));
1486+
}
1487+
}
1488+
1489+
let highest_nonce = self.transaction_count(sender, None).await?.saturating_to::<u64>();
1490+
let target_nonce = nonce.saturating_to::<u64>();
1491+
1492+
// if the nonce is higher or equal to the highest nonce, the transaction doesn't exist
1493+
if target_nonce >= highest_nonce {
1494+
return Ok(None);
1495+
}
1496+
1497+
// no mined blocks yet
1498+
let latest_block = self.backend.best_number();
1499+
if latest_block == 0 {
1500+
return Ok(None);
1501+
}
1502+
1503+
// binary search for the block containing the transaction
1504+
let mut low = 1u64;
1505+
let mut high = latest_block;
1506+
1507+
while low <= high {
1508+
let mid = low + (high - low) / 2;
1509+
let mid_nonce =
1510+
self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
1511+
1512+
if mid_nonce > target_nonce {
1513+
high = mid - 1;
1514+
} else {
1515+
low = mid + 1;
1516+
}
1517+
}
1518+
1519+
// search in the target block
1520+
let target_block = low;
1521+
if target_block <= latest_block
1522+
&& let Some(txs) =
1523+
self.backend.mined_transactions_by_block_number(target_block.into()).await
1524+
{
1525+
for tx in txs {
1526+
if tx.from() == sender
1527+
&& tx.nonce() == target_nonce
1528+
&& let Some(mined_tx) = self.backend.transaction_by_hash(tx.tx_hash()).await?
1529+
{
1530+
return Ok(Some(mined_tx));
1531+
}
1532+
}
1533+
}
1534+
1535+
Ok(None)
1536+
}
1537+
14501538
/// Returns transaction receipt by transaction hash.
14511539
///
14521540
/// Handler for ETH RPC call: `eth_getTransactionReceipt`

crates/anvil/tests/it/transaction.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::{
22
abi::{Greeter, Multicall, SimpleStorage},
33
utils::{connect_pubsub, http_provider_with_signer},
44
};
5+
use alloy_consensus::Transaction;
56
use alloy_hardforks::EthereumHardfork;
67
use alloy_network::{EthereumWallet, TransactionBuilder, TransactionResponse};
78
use alloy_primitives::{Address, Bytes, FixedBytes, U256, address, hex, map::B256HashSet};
@@ -1381,3 +1382,96 @@ async fn can_send_tx_osaka_valid_with_limit_disabled() {
13811382
let tx_receipt = pending_tx.get_receipt().await.unwrap();
13821383
assert!(tx_receipt.inner.inner.is_success());
13831384
}
1385+
1386+
#[tokio::test(flavor = "multi_thread")]
1387+
async fn can_get_tx_by_sender_and_nonce() {
1388+
let (api, handle) = spawn(NodeConfig::test()).await;
1389+
let provider = handle.http_provider();
1390+
1391+
let accounts = handle.dev_wallets().collect::<Vec<_>>();
1392+
let sender = accounts[0].address();
1393+
let recipient = accounts[1].address();
1394+
1395+
api.anvil_set_auto_mine(false).await.unwrap();
1396+
1397+
let mut tx_hashes = std::collections::BTreeMap::new();
1398+
1399+
// send 4 transactions from the same sender with consecutive nonces
1400+
for i in 0..4 {
1401+
let tx_request = TransactionRequest::default()
1402+
.to(recipient)
1403+
.value(U256::from(1000 + i))
1404+
.from(sender)
1405+
.nonce(i as u64);
1406+
1407+
let tx = WithOtherFields::new(tx_request);
1408+
let pending_tx = provider.send_transaction(tx).await.unwrap();
1409+
tx_hashes.insert(i as u64, *pending_tx.tx_hash());
1410+
}
1411+
1412+
// mine all transactions
1413+
api.mine_one().await;
1414+
1415+
for nonce in 0..4 {
1416+
let result: Option<alloy_network::AnyRpcTransaction> = provider
1417+
.client()
1418+
.request("eth_getTransactionBySenderAndNonce", (sender, U256::from(nonce)))
1419+
.await
1420+
.unwrap();
1421+
1422+
assert!(result.is_some());
1423+
let found_tx = result.unwrap();
1424+
1425+
assert_eq!(found_tx.inner.nonce(), nonce);
1426+
assert_eq!(found_tx.from(), sender);
1427+
assert_eq!(found_tx.inner.to(), Some(recipient));
1428+
assert_eq!(found_tx.inner.value(), U256::from(1000 + nonce));
1429+
assert_eq!(found_tx.inner.tx_hash(), tx_hashes[&nonce]);
1430+
}
1431+
1432+
let result: Option<alloy_network::AnyRpcTransaction> = provider
1433+
.client()
1434+
.request("eth_getTransactionBySenderAndNonce", (sender, U256::from(999)))
1435+
.await
1436+
.unwrap();
1437+
assert!(result.is_none());
1438+
1439+
let different_sender = accounts[2].address();
1440+
let result: Option<alloy_network::AnyRpcTransaction> = provider
1441+
.client()
1442+
.request("eth_getTransactionBySenderAndNonce", (different_sender, U256::from(0)))
1443+
.await
1444+
.unwrap();
1445+
assert!(result.is_none());
1446+
1447+
// send a pending transaction with explicit nonce 4
1448+
let pending_tx_request =
1449+
TransactionRequest::default().to(recipient).value(U256::from(5000)).from(sender).nonce(4);
1450+
1451+
let tx = WithOtherFields::new(pending_tx_request);
1452+
let pending_tx = provider.send_transaction(tx).await.unwrap();
1453+
1454+
// find the pending transaction with nonce 4
1455+
let result: Option<alloy_network::AnyRpcTransaction> = provider
1456+
.client()
1457+
.request("eth_getTransactionBySenderAndNonce", (sender, U256::from(4)))
1458+
.await
1459+
.unwrap();
1460+
1461+
assert!(result.is_some());
1462+
let found_tx = result.unwrap();
1463+
assert_eq!(found_tx.inner.nonce(), 4);
1464+
assert_eq!(found_tx.inner.tx_hash(), *pending_tx.tx_hash());
1465+
1466+
api.mine_one().await;
1467+
1468+
let result: Option<alloy_network::AnyRpcTransaction> = provider
1469+
.client()
1470+
.request("eth_getTransactionBySenderAndNonce", (sender, U256::from(4)))
1471+
.await
1472+
.unwrap();
1473+
1474+
assert!(result.is_some());
1475+
let found_tx = result.unwrap();
1476+
assert_eq!(found_tx.inner.nonce(), 4);
1477+
}

0 commit comments

Comments
 (0)