Skip to content

Commit 16cf9fb

Browse files
authored
feat(anvil): add eth_fillTransaction support (#12595)
* feat(anvil): add `eth_fillTransaction` method - Introduced `EthFillTransaction` to handle filling transaction fields with defaults. - Implemented `fill_transaction` method to populate missing fields like nonce, gas limit, and fees. - Added `FillTransactionResult` struct to encapsulate the response of the new RPC method. - Updated tests to verify the functionality of the `fill_transaction` method, ensuring it correctly fills in required fields and preserves provided values. * fix: use `build_typed_transaction` w/ dummy sig to get `TypedTransaction`
1 parent a335575 commit 16cf9fb

File tree

4 files changed

+257
-6
lines changed

4 files changed

+257
-6
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ pub enum EthRequest {
187187
#[serde(default)] Option<Box<BlockOverrides>>,
188188
),
189189

190+
#[serde(rename = "eth_fillTransaction", with = "sequence")]
191+
EthFillTransaction(WithOtherFields<TransactionRequest>),
192+
190193
#[serde(rename = "eth_getTransactionByHash", with = "sequence")]
191194
EthGetTransactionByHash(TxHash),
192195

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,18 @@ impl Decodable2718 for TypedReceipt {
12731273

12741274
pub type ReceiptResponse = WithOtherFields<TransactionReceipt<TypedReceiptRpc>>;
12751275

1276+
/// Response type for `eth_fillTransaction` RPC method.
1277+
///
1278+
/// This type represents a transaction that has been "filled" with default values
1279+
/// for missing fields like nonce, gas limit, and fee parameters.
1280+
#[derive(Debug, Clone, Serialize, Deserialize)]
1281+
pub struct FillTransactionResult<T> {
1282+
/// RLP-encoded transaction bytes
1283+
pub raw: Bytes,
1284+
/// Filled transaction request
1285+
pub tx: T,
1286+
}
1287+
12761288
pub fn convert_to_anvil_receipt(receipt: AnyTransactionReceipt) -> Option<ReceiptResponse> {
12771289
let WithOtherFields {
12781290
inner:

crates/anvil/src/eth/api.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ use alloy_eips::{
4141
};
4242
use alloy_evm::overrides::{OverrideBlockHashes, apply_state_overrides};
4343
use alloy_network::{
44-
AnyRpcBlock, AnyRpcTransaction, BlockResponse, TransactionBuilder, TransactionResponse,
45-
eip2718::Decodable2718,
44+
AnyRpcBlock, AnyRpcTransaction, BlockResponse, TransactionBuilder, TransactionBuilder4844,
45+
TransactionResponse, eip2718::Decodable2718,
4646
};
4747
use alloy_primitives::{
4848
Address, B64, B256, Bytes, Signature, TxHash, TxKind, U64, U256,
@@ -72,8 +72,8 @@ use anvil_core::{
7272
EthRequest,
7373
block::BlockInfo,
7474
transaction::{
75-
PendingTransaction, ReceiptResponse, TypedTransaction, TypedTransactionRequest,
76-
transaction_request_to_typed,
75+
FillTransactionResult, PendingTransaction, ReceiptResponse, TypedTransaction,
76+
TypedTransactionRequest, transaction_request_to_typed,
7777
},
7878
wallet::WalletCapabilities,
7979
},
@@ -279,6 +279,9 @@ impl EthApi {
279279
.estimate_gas(call, block, EvmOverrides::new(state_override, block_overrides))
280280
.await
281281
.to_rpc_result(),
282+
EthRequest::EthFillTransaction(request) => {
283+
self.fill_transaction(request).await.to_rpc_result()
284+
}
282285
EthRequest::EthGetRawTransactionByHash(hash) => {
283286
self.raw_transaction(hash).await.to_rpc_result()
284287
}
@@ -1361,6 +1364,74 @@ impl EthApi {
13611364
.map(U256::from)
13621365
}
13631366

1367+
/// Fills a transaction request with default values for missing fields.
1368+
///
1369+
/// This method populates missing transaction fields like nonce, gas limit,
1370+
/// chain ID, and fee parameters with appropriate defaults.
1371+
///
1372+
/// Handler for ETH RPC call: `eth_fillTransaction`
1373+
pub async fn fill_transaction(
1374+
&self,
1375+
mut request: WithOtherFields<TransactionRequest>,
1376+
) -> Result<FillTransactionResult<TypedTransaction>> {
1377+
node_info!("eth_fillTransaction");
1378+
1379+
let from = match request.as_ref().from() {
1380+
Some(from) => from,
1381+
None => self.accounts()?.first().copied().ok_or(BlockchainError::NoSignerAvailable)?,
1382+
};
1383+
1384+
let nonce = if let Some(nonce) = request.as_ref().nonce() {
1385+
nonce
1386+
} else {
1387+
self.request_nonce(&request, from).await?.0
1388+
};
1389+
1390+
if request.as_ref().has_eip4844_fields()
1391+
&& request.as_ref().max_fee_per_blob_gas().is_none()
1392+
{
1393+
// Use the next block's blob base fee for better accuracy
1394+
let blob_fee = self.backend.fees().get_next_block_blob_base_fee_per_gas();
1395+
request.as_mut().set_max_fee_per_blob_gas(blob_fee);
1396+
}
1397+
1398+
if request.as_ref().blob_sidecar().is_some()
1399+
&& request.as_ref().blob_versioned_hashes.is_none()
1400+
{
1401+
request.as_mut().populate_blob_hashes();
1402+
}
1403+
1404+
if request.as_ref().gas_limit().is_none() {
1405+
let estimated_gas = self
1406+
.estimate_gas(request.clone(), Some(BlockId::latest()), EvmOverrides::default())
1407+
.await?;
1408+
request.as_mut().set_gas_limit(estimated_gas.to());
1409+
}
1410+
1411+
if request.as_ref().gas_price().is_none() {
1412+
let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
1413+
tip
1414+
} else {
1415+
let tip = self.lowest_suggestion_tip();
1416+
request.as_mut().set_max_priority_fee_per_gas(tip);
1417+
tip
1418+
};
1419+
if request.as_ref().max_fee_per_gas().is_none() {
1420+
request.as_mut().set_max_fee_per_gas(self.gas_price() + tip);
1421+
}
1422+
}
1423+
1424+
let typed_tx = self.build_typed_tx_request(request, nonce)?;
1425+
let tx = build_typed_transaction(
1426+
typed_tx,
1427+
Signature::new(Default::default(), Default::default(), false),
1428+
)?;
1429+
1430+
let raw = tx.encoded_2718().to_vec().into();
1431+
1432+
Ok(FillTransactionResult { raw, tx })
1433+
}
1434+
13641435
/// Handler for RPC call: `anvil_getBlobByHash`
13651436
pub fn anvil_get_blob_by_versioned_hash(
13661437
&self,

crates/anvil/tests/it/api.rs

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use crate::{
44
abi::{Multicall, SimpleStorage},
55
utils::{connect_pubsub_with_wallet, http_provider, http_provider_with_signer},
66
};
7-
use alloy_consensus::{SignableTransaction, Transaction, TxEip1559};
8-
use alloy_network::{EthereumWallet, TransactionBuilder, TxSignerSync};
7+
use alloy_consensus::{SidecarBuilder, SignableTransaction, SimpleCoder, Transaction, TxEip1559};
8+
use alloy_network::{EthereumWallet, TransactionBuilder, TransactionBuilder4844, TxSignerSync};
99
use alloy_primitives::{
1010
Address, B256, ChainId, U256, b256, bytes,
1111
map::{AddressHashMap, B256HashMap, HashMap},
@@ -466,3 +466,168 @@ async fn can_get_code_by_hash() {
466466
let code = api.debug_code_by_hash(code_hash, None).await.unwrap();
467467
assert_eq!(&code.unwrap(), foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER_RUNTIME_CODE);
468468
}
469+
470+
#[tokio::test(flavor = "multi_thread")]
471+
async fn test_fill_transaction_fills_chain_id() {
472+
let (api, handle) = spawn(NodeConfig::test()).await;
473+
let wallet = handle.dev_wallets().next().unwrap();
474+
let from = wallet.address();
475+
476+
let tx_req = TransactionRequest::default()
477+
.with_from(from)
478+
.with_to(Address::random())
479+
.with_gas_limit(21_000);
480+
481+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
482+
483+
// Should fill with the chain id from provider
484+
assert!(filled.tx.chain_id().is_some());
485+
assert_eq!(filled.tx.chain_id().unwrap(), CHAIN_ID);
486+
}
487+
488+
#[tokio::test(flavor = "multi_thread")]
489+
async fn test_fill_transaction_fills_nonce() {
490+
let (api, handle) = spawn(NodeConfig::test()).await;
491+
492+
let accounts: Vec<_> = handle.dev_wallets().collect();
493+
let signer: EthereumWallet = accounts[0].clone().into();
494+
let from = accounts[0].address();
495+
let to = accounts[1].address();
496+
497+
let provider = http_provider_with_signer(&handle.http_endpoint(), signer);
498+
499+
// Send a transaction to increment nonce
500+
let tx = TransactionRequest::default().with_from(from).with_to(to).with_value(U256::from(100));
501+
let tx = WithOtherFields::new(tx);
502+
provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
503+
504+
// Now the account should have nonce 1
505+
let tx_req = TransactionRequest::default()
506+
.with_from(from)
507+
.with_to(to)
508+
.with_value(U256::from(1000))
509+
.with_gas_limit(21_000);
510+
511+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
512+
513+
assert_eq!(filled.tx.nonce(), 1);
514+
}
515+
516+
#[tokio::test(flavor = "multi_thread")]
517+
async fn test_fill_transaction_preserves_provided_fields() {
518+
let (api, handle) = spawn(NodeConfig::test()).await;
519+
let wallet = handle.dev_wallets().next().unwrap();
520+
let from = wallet.address();
521+
522+
let provided_nonce = 100u64;
523+
let provided_gas_limit = 50_000u64;
524+
525+
let tx_req = TransactionRequest::default()
526+
.with_from(from)
527+
.with_to(Address::random())
528+
.with_value(U256::from(1000))
529+
.with_nonce(provided_nonce)
530+
.with_gas_limit(provided_gas_limit);
531+
532+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
533+
534+
// Should preserve the provided nonce and gas limit
535+
assert_eq!(filled.tx.nonce(), provided_nonce);
536+
assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
537+
}
538+
539+
#[tokio::test(flavor = "multi_thread")]
540+
async fn test_fill_transaction_fills_all_missing_fields() {
541+
let (api, handle) = spawn(NodeConfig::test()).await;
542+
let wallet = handle.dev_wallets().next().unwrap();
543+
let from = wallet.address();
544+
545+
// Create a simple transfer transaction with minimal fields
546+
let tx_req = TransactionRequest::default().with_from(from).with_to(Address::random());
547+
548+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
549+
550+
// Should fill all required fields and be EIP-1559
551+
assert!(filled.tx.is_eip1559());
552+
assert!(filled.tx.gas_limit() > 0);
553+
let essentials = filled.tx.essentials();
554+
assert!(essentials.max_fee_per_gas.is_some());
555+
assert!(essentials.max_priority_fee_per_gas.is_some());
556+
}
557+
558+
#[tokio::test(flavor = "multi_thread")]
559+
async fn test_fill_transaction_eip4844_blob_fee() {
560+
let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into()));
561+
let (api, handle) = spawn(node_config).await;
562+
let wallet = handle.dev_wallets().next().unwrap();
563+
let from = wallet.address();
564+
565+
let mut builder = SidecarBuilder::<SimpleCoder>::new();
566+
builder.ingest(b"dummy blob");
567+
let sidecar = builder.build().unwrap();
568+
569+
// EIP-4844 blob transaction with sidecar but no blob fee
570+
let mut tx_req = TransactionRequest::default().with_from(from).with_to(Address::random());
571+
tx_req.sidecar = Some(sidecar);
572+
tx_req.transaction_type = Some(3); // EIP-4844
573+
574+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
575+
576+
// Blob transaction should have max_fee_per_blob_gas filled
577+
assert!(
578+
filled.tx.max_fee_per_blob_gas().is_some(),
579+
"max_fee_per_blob_gas should be filled for blob tx"
580+
);
581+
let essentials = filled.tx.essentials();
582+
assert!(essentials.blob_versioned_hashes.is_some(), "blob_versioned_hashes should be present");
583+
}
584+
585+
#[tokio::test(flavor = "multi_thread")]
586+
async fn test_fill_transaction_eip4844_preserves_blob_fee() {
587+
let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into()));
588+
let (api, handle) = spawn(node_config).await;
589+
let wallet = handle.dev_wallets().next().unwrap();
590+
let from = wallet.address();
591+
592+
let provided_blob_fee = 5_000_000u128;
593+
594+
let mut builder = SidecarBuilder::<SimpleCoder>::new();
595+
builder.ingest(b"dummy blob");
596+
let sidecar = builder.build().unwrap();
597+
598+
// EIP-4844 blob transaction with blob fee already set
599+
let mut tx_req = TransactionRequest::default()
600+
.with_from(from)
601+
.with_to(Address::random())
602+
.with_max_fee_per_blob_gas(provided_blob_fee);
603+
tx_req.sidecar = Some(sidecar);
604+
tx_req.transaction_type = Some(3); // EIP-4844
605+
606+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
607+
608+
// Should preserve the provided blob fee
609+
assert_eq!(
610+
filled.tx.max_fee_per_blob_gas(),
611+
Some(provided_blob_fee),
612+
"should preserve provided max_fee_per_blob_gas"
613+
);
614+
}
615+
616+
#[tokio::test(flavor = "multi_thread")]
617+
async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
618+
let (api, handle) = spawn(NodeConfig::test()).await;
619+
let wallet = handle.dev_wallets().next().unwrap();
620+
let from = wallet.address();
621+
622+
// EIP-1559 transaction without blob fields
623+
let mut tx_req = TransactionRequest::default().with_from(from).with_to(Address::random());
624+
tx_req.transaction_type = Some(2); // EIP-1559
625+
626+
let filled = api.fill_transaction(WithOtherFields::new(tx_req)).await.unwrap();
627+
628+
// Non-blob transaction should NOT have blob fee filled
629+
assert!(
630+
filled.tx.max_fee_per_blob_gas().is_none(),
631+
"max_fee_per_blob_gas should not be set for non-blob tx"
632+
);
633+
}

0 commit comments

Comments
 (0)