Skip to main content

reth_rpc/eth/helpers/
transaction.rs

1//! Contains RPC handler implementations specific to transactions
2
3use crate::EthApi;
4use alloy_consensus::BlobTransactionValidationError;
5use alloy_eips::{eip7594::BlobTransactionSidecarVariant, BlockId, Typed2718};
6use alloy_primitives::{hex, B256};
7use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
8use reth_primitives_traits::{AlloyBlockHeader, WithEncoded};
9use reth_rpc_convert::RpcConvert;
10use reth_rpc_eth_api::{
11    helpers::{spec::SignersForRpc, EthTransactions, LoadTransaction},
12    FromEvmError, RpcNodeCore,
13};
14use reth_rpc_eth_types::{error::RpcPoolError, EthApiError};
15use reth_storage_api::BlockReaderIdExt;
16use reth_transaction_pool::{
17    error::Eip4844PoolTransactionError, AddedTransactionOutcome, EthBlobTransactionSidecar,
18    EthPoolTransaction, PoolTransaction, PoolTx,
19};
20
21impl<N, Rpc> EthTransactions for EthApi<N, Rpc>
22where
23    N: RpcNodeCore,
24    EthApiError: FromEvmError<N::Evm>,
25    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
26{
27    #[inline]
28    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
29        self.inner.signers()
30    }
31
32    async fn send_pool_transaction(
33        &self,
34        origin: reth_transaction_pool::TransactionOrigin,
35        tx: WithEncoded<PoolTx<Self::Pool>>,
36    ) -> Result<B256, Self::Error> {
37        let (tx, mut pool_transaction) = tx.split();
38
39        // Optionally convert legacy blob sidecars to EIP-7594 format when Osaka is active
40        // This is opt-in via --rpc.force-blob-sidecar-upcasting
41        if self.inner.force_blob_sidecar_upcasting() && pool_transaction.is_eip4844() {
42            let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
43                return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
44                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
45                )));
46            };
47            let sidecar = sidecar.into_sidecar();
48
49            let sidecar = match sidecar {
50                BlobTransactionSidecarVariant::Eip4844(sidecar) => {
51                    let latest = self
52                        .provider()
53                        .latest_header()?
54                        .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
55                    // Convert to EIP-7594 if next block is Osaka
56                    if self
57                        .provider()
58                        .chain_spec()
59                        .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
60                    {
61                        BlobTransactionSidecarVariant::Eip7594(
62                            self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
63                                || {
64                                    RpcPoolError::Eip4844(
65                                        Eip4844PoolTransactionError::InvalidEip4844Blob(
66                                            BlobTransactionValidationError::InvalidProof,
67                                        ),
68                                    )
69                                },
70                            )?,
71                        )
72                    } else {
73                        BlobTransactionSidecarVariant::Eip4844(sidecar)
74                    }
75                }
76                sidecar => sidecar,
77            };
78
79            pool_transaction =
80                EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
81                    .ok_or_else(|| {
82                        RpcPoolError::Eip4844(
83                            Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
84                        )
85                    })?;
86        }
87
88        // forward the transaction to the specific endpoint if configured.
89        if let Some(client) = self.raw_tx_forwarder() {
90            tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
91            let rlp_hex = hex::encode_prefixed(&tx);
92
93            // broadcast raw transaction to subscribers if there is any.
94            self.broadcast_raw_transaction(tx);
95
96            let hash =
97                client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
98                    tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
99                }).map_err(EthApiError::other)?;
100
101            // Retain tx in local tx pool after forwarding, for local RPC usage.
102            let _ = self.inner.add_pool_transaction(origin, pool_transaction).await;
103
104            return Ok(hash);
105        }
106
107        // broadcast raw transaction to subscribers if there is any.
108        self.broadcast_raw_transaction(tx);
109
110        let AddedTransactionOutcome { hash, .. } =
111            self.inner.add_pool_transaction(origin, pool_transaction).await?;
112
113        Ok(hash)
114    }
115}
116
117impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
118where
119    N: RpcNodeCore,
120    EthApiError: FromEvmError<N::Evm>,
121    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
122{
123}
124
125#[cfg(test)]
126mod tests {
127    use std::time::Duration;
128
129    use super::*;
130    use crate::eth::helpers::{signer::DevSigner, types::EthRpcConverter};
131    use alloy_consensus::{BlobTransactionSidecar, Block, Header, Transaction};
132    use alloy_eips::{
133        eip2718::Encodable2718,
134        eip4844::{Blob, Bytes48},
135    };
136    use alloy_primitives::{bytes, map::AddressMap, Address, Bytes, Signature, U256};
137    use alloy_rpc_types_eth::request::TransactionRequest;
138    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
139    use reth_ethereum_primitives::TransactionSigned;
140    use reth_evm::SenderRecoveryCache;
141    use reth_evm_ethereum::EthEvmConfig;
142    use reth_network_api::noop::NoopNetwork;
143    use reth_primitives_traits::SignedTransaction;
144    use reth_provider::{
145        test_utils::{ExtendedAccount, MockEthProvider},
146        ChainSpecProvider,
147    };
148    use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
149    use reth_transaction_pool::{
150        test_utils::{testing_pool, TestPool, TransactionGenerator},
151        PoolPooledTx, TransactionOrigin, TransactionPool,
152    };
153
154    /// A sidecar with one zero blob. Its commitment and proof are the point at infinity, so it is
155    /// valid without loading the KZG trusted setup.
156    fn zero_blob_sidecar() -> BlobTransactionSidecar {
157        let mut point_at_infinity = [0; 48];
158        point_at_infinity[0] = 0xc0;
159        BlobTransactionSidecar::new(
160            vec![Blob::default()],
161            vec![Bytes48::from(point_at_infinity)],
162            vec![Bytes48::from(point_at_infinity)],
163        )
164    }
165    fn mock_eth_api(
166        accounts: AddressMap<ExtendedAccount>,
167    ) -> EthApi<
168        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
169        EthRpcConverter<ChainSpec>,
170    > {
171        mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
172    }
173
174    fn mock_eth_api_with_sync_timeout(
175        accounts: AddressMap<ExtendedAccount>,
176        send_raw_transaction_sync_timeout: Duration,
177    ) -> EthApi<
178        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
179        EthRpcConverter<ChainSpec>,
180    > {
181        mock_eth_api_builder(accounts)
182            .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
183            .build()
184    }
185
186    fn mock_eth_api_builder(
187        accounts: AddressMap<ExtendedAccount>,
188    ) -> crate::EthApiBuilder<
189        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
190        EthRpcConverter<ChainSpec>,
191    > {
192        let mock_provider = MockEthProvider::default()
193            .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
194        mock_provider.extend_accounts(accounts);
195
196        let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
197        let pool = testing_pool();
198
199        let genesis_header = Header {
200            number: 0,
201            gas_limit: 30_000_000,
202            timestamp: 1,
203            excess_blob_gas: Some(0),
204            base_fee_per_gas: Some(1000000000),
205            blob_gas_used: Some(0),
206            ..Default::default()
207        };
208
209        let genesis_hash = B256::ZERO;
210        mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
211
212        EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
213    }
214
215    fn raw_transfer_tx() -> Bytes {
216        // https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
217        bytes!("02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3")
218    }
219
220    #[tokio::test]
221    async fn raw_transaction_recovery_shares_sender_cache() {
222        let cache = SenderRecoveryCache::new(16);
223        let eth_api = mock_eth_api_builder(Default::default())
224            .sender_recovery_cache(Some(cache.clone()))
225            .build();
226        let mut generator = TransactionGenerator::new(rand::rng());
227
228        for (transaction, warm_cache) in [
229            (generator.transaction().into_legacy(), false),
230            (generator.transaction().nonce(1).into_eip1559(), true),
231        ] {
232            let raw = Bytes::from(transaction.encoded_2718());
233            let hash = *transaction.tx_hash();
234            let sender = transaction.try_recover().unwrap();
235            assert_eq!(cache.get(&hash), None);
236            if warm_cache {
237                let recovered =
238                    eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
239                assert_eq!(recovered.signer(), sender);
240                assert_eq!(cache.get(&hash), Some(sender));
241            }
242
243            assert_eq!(eth_api.send_raw_transaction(raw.clone()).await.unwrap(), hash);
244            assert_eq!(cache.get(&hash), Some(sender));
245            let recovered =
246                eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
247            assert_eq!(recovered.signer(), sender);
248            assert_eq!(eth_api.recover_raw_pool_transaction(&raw).unwrap().sender(), sender);
249        }
250    }
251
252    #[tokio::test]
253    async fn raw_transaction_recovery_uses_pool_overrides() {
254        let raw = raw_transfer_tx();
255        for enabled in [false, true] {
256            let cache = SenderRecoveryCache::new(16);
257            let eth_api = mock_eth_api_builder(Default::default())
258                .sender_recovery_cache(enabled.then(|| cache.clone()))
259                .build();
260
261            // MockTransaction's raw hooks retain the input length instead of its in-memory size.
262            for _ in 0..2 {
263                let transaction = eth_api.recover_raw_pool_transaction(&raw).unwrap();
264                assert_eq!(transaction.encoded_length(), raw.len());
265                assert_eq!(cache.get(transaction.hash()), enabled.then_some(transaction.sender()));
266            }
267        }
268    }
269
270    #[tokio::test]
271    async fn raw_transaction_recovery_preserves_errors() {
272        let invalid = TransactionSigned::new_unhashed(
273            alloy_consensus::TxLegacy::default().into(),
274            Signature::new(U256::ZERO, U256::ZERO, false),
275        );
276        let mut trailing = raw_transfer_tx().to_vec();
277        trailing.push(0);
278
279        for enabled in [false, true] {
280            let cache = SenderRecoveryCache::new(16);
281            let eth_api = mock_eth_api_builder(Default::default())
282                .sender_recovery_cache(enabled.then(|| cache.clone()))
283                .build();
284            let valid = eth_api.recover_raw_pool_transaction(&raw_transfer_tx()).unwrap();
285            assert_eq!(cache.get(valid.hash()), enabled.then_some(valid.sender()));
286            for (raw, expected) in [
287                (vec![], EthApiError::EmptyRawTransactionData),
288                (vec![2], EthApiError::FailedToDecodeSignedTransaction),
289                (trailing.clone(), EthApiError::FailedToDecodeSignedTransaction),
290                (invalid.encoded_2718(), EthApiError::InvalidTransactionSignature),
291            ] {
292                let err = eth_api.send_raw_transaction(raw.clone().into()).await.unwrap_err();
293                assert_eq!(err.to_string(), expected.to_string());
294                let err =
295                    eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap_err();
296                assert_eq!(err.to_string(), expected.to_string());
297            }
298            assert!(eth_api.pool().is_empty());
299            assert_eq!(cache.get(invalid.tx_hash()), None);
300        }
301    }
302
303    #[tokio::test]
304    async fn cached_raw_transaction_preserves_blob_sidecar() {
305        let cache = SenderRecoveryCache::new(16);
306        let provider = MockEthProvider::default();
307        let evm_config = EthEvmConfig::new(provider.chain_spec());
308        let eth_api = EthApi::builder(
309            provider,
310            reth_transaction_pool::noop::NoopTransactionPool::default(),
311            NoopNetwork::default(),
312            evm_config,
313        )
314        .sender_recovery_cache(Some(cache.clone()))
315        .build();
316        let sidecar = BlobTransactionSidecarVariant::from(zero_blob_sidecar());
317        let transaction = TransactionSigned::new_unhashed(
318            alloy_consensus::TxEip4844 {
319                blob_versioned_hashes: sidecar.versioned_hashes().collect(),
320                ..Default::default()
321            }
322            .into(),
323            Signature::test_signature(),
324        );
325        let hash = *transaction.tx_hash();
326        let sender = transaction.try_recover().unwrap();
327        let raw = transaction.try_into_pooled_eip4844(sidecar.clone()).unwrap().encoded_2718();
328
329        for _ in 0..2 {
330            let mut recovered = eth_api.recover_raw_pool_transaction(&raw).unwrap();
331            assert_eq!(recovered.sender(), sender);
332            assert_eq!(cache.get(&hash), Some(sender));
333            let EthBlobTransactionSidecar::Present(actual) = recovered.take_blob() else {
334                panic!("missing blob sidecar")
335            };
336            assert_eq!(actual.into_sidecar(), sidecar);
337        }
338    }
339
340    #[tokio::test]
341    async fn send_raw_transaction() {
342        let eth_api = mock_eth_api(Default::default());
343        let pool = eth_api.pool();
344
345        let tx_1 = raw_transfer_tx();
346
347        let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
348        assert_eq!(
349            pool.len(),
350            1,
351            "expect 1 transaction in the pool, but pool size is {}",
352            pool.len()
353        );
354
355        // https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
356        let tx_2 = bytes!("02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce");
357
358        let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
359        assert_eq!(
360            pool.len(),
361            2,
362            "expect 2 transactions in the pool, but pool size is {}",
363            pool.len()
364        );
365
366        assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
367        assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
368        assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
369        assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
370    }
371
372    #[tokio::test]
373    async fn send_raw_transaction_sync_uses_request_timeout() {
374        let eth_api = mock_eth_api(Default::default());
375
376        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
377
378        assert!(matches!(
379            err,
380            EthApiError::TransactionConfirmationTimeout { duration, .. }
381                if duration == Duration::from_millis(1)
382        ));
383        assert_eq!(eth_api.pool().len(), 1);
384    }
385
386    #[tokio::test]
387    async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
388        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
389
390        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
391
392        assert!(matches!(
393            err,
394            EthApiError::TransactionConfirmationTimeout { duration, .. }
395                if duration == Duration::from_millis(1)
396        ));
397        assert_eq!(eth_api.pool().len(), 1);
398    }
399
400    #[tokio::test]
401    async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
402        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
403
404        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
405
406        assert!(matches!(
407            err,
408            EthApiError::TransactionConfirmationTimeout { duration, .. }
409                if duration == Duration::from_millis(1)
410        ));
411        assert_eq!(eth_api.pool().len(), 1);
412    }
413
414    #[tokio::test]
415    async fn send_raw_transaction_sync_caps_request_timeout() {
416        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
417
418        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
419
420        assert!(matches!(
421            err,
422            EthApiError::TransactionConfirmationTimeout { duration, .. }
423                if duration == Duration::from_millis(1)
424        ));
425        assert_eq!(eth_api.pool().len(), 1);
426    }
427
428    #[tokio::test]
429    async fn send_transaction_preserves_provided_gas_limit() {
430        let signers = DevSigner::random_signers(1);
431        let address = signers[0].accounts()[0];
432        let accounts = AddressMap::from_iter([(
433            address,
434            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
435        )]);
436        let eth_api = mock_eth_api(accounts);
437        eth_api.signers().write().extend(signers);
438
439        let provided_gas_limit = 90_000;
440        let tx_req = TransactionRequest {
441            from: Some(address),
442            to: Some(address.into()),
443            gas: Some(provided_gas_limit),
444            gas_price: Some(1_000_000_000),
445            ..Default::default()
446        };
447
448        let hash = eth_api
449            .send_transaction_request(tx_req)
450            .await
451            .expect("send_transaction should succeed");
452        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
453
454        assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
455    }
456
457    #[tokio::test]
458    async fn send_transaction_fills_missing_fee_fields() {
459        let signers = DevSigner::random_signers(1);
460        let address = signers[0].accounts()[0];
461        let accounts = AddressMap::from_iter([(
462            address,
463            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
464        )]);
465        let eth_api = mock_eth_api(accounts);
466        eth_api.signers().write().extend(signers);
467
468        // no gas, gasPrice or 1559 fee fields: the node is expected to fill them
469        let tx_req = TransactionRequest {
470            from: Some(address),
471            to: Some(address.into()),
472            value: Some(U256::ONE),
473            ..Default::default()
474        };
475
476        let hash = eth_api
477            .send_transaction_request(tx_req)
478            .await
479            .expect("send_transaction should fill the missing fee fields");
480        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
481        assert!(pooled.transaction.max_fee_per_gas() > 0);
482    }
483
484    #[tokio::test]
485    async fn send_transaction_rejects_mismatched_chain_id() {
486        let signers = DevSigner::random_signers(1);
487        let address = signers[0].accounts()[0];
488        let accounts = AddressMap::from_iter([(
489            address,
490            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
491        )]);
492        let eth_api = mock_eth_api(accounts);
493        eth_api.signers().write().extend(signers);
494
495        // The mock node is mainnet (chain id 1); the caller pins a different chain.
496        let tx_req = TransactionRequest {
497            from: Some(address),
498            to: Some(address.into()),
499            gas: Some(90_000),
500            gas_price: Some(1_000_000_000),
501            chain_id: Some(999),
502            ..Default::default()
503        };
504
505        let err = eth_api
506            .send_transaction_request(tx_req)
507            .await
508            .expect_err("a chain id that is not the node's must be rejected, not rewritten");
509        assert!(
510            err.to_string().contains("chainId does not match node's"),
511            "unexpected error: {err}"
512        );
513        assert!(eth_api.pool().is_empty(), "no transaction should have been submitted");
514    }
515
516    #[tokio::test]
517    async fn send_transaction_accepts_matching_chain_id() {
518        let signers = DevSigner::random_signers(1);
519        let address = signers[0].accounts()[0];
520        let accounts = AddressMap::from_iter([(
521            address,
522            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
523        )]);
524        let eth_api = mock_eth_api(accounts);
525        eth_api.signers().write().extend(signers);
526
527        let tx_req = TransactionRequest {
528            from: Some(address),
529            to: Some(address.into()),
530            gas: Some(90_000),
531            gas_price: Some(1_000_000_000),
532            chain_id: Some(1),
533            ..Default::default()
534        };
535
536        let hash = eth_api
537            .send_transaction_request(tx_req)
538            .await
539            .expect("a matching chain id must still be accepted");
540        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
541        assert_eq!(pooled.transaction.chain_id(), Some(1));
542    }
543
544    #[tokio::test]
545    async fn test_fill_transaction_fills_chain_id() {
546        let address = Address::random();
547        let accounts = AddressMap::from_iter([(
548            address,
549            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), // 10 ETH
550        )]);
551
552        let eth_api = mock_eth_api(accounts);
553
554        let tx_req = TransactionRequest {
555            from: Some(address),
556            to: Some(Address::random().into()),
557            gas: Some(21_000),
558            ..Default::default()
559        };
560
561        let filled =
562            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
563
564        // Should fill with the chain id from provider
565        assert!(filled.tx.chain_id().is_some());
566    }
567
568    #[tokio::test]
569    async fn test_fill_transaction_fills_nonce() {
570        let address = Address::random();
571        let nonce = 42u64;
572
573        let accounts = AddressMap::from_iter([(
574            address,
575            ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), // 1 ETH
576        )]);
577
578        let eth_api = mock_eth_api(accounts);
579
580        let tx_req = TransactionRequest {
581            from: Some(address),
582            to: Some(Address::random().into()),
583            value: Some(U256::from(1000)),
584            gas: Some(21_000),
585            ..Default::default()
586        };
587
588        let filled =
589            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
590
591        assert_eq!(filled.tx.nonce(), nonce);
592    }
593
594    #[tokio::test]
595    async fn test_fill_transaction_preserves_provided_fields() {
596        let address = Address::random();
597        let provided_nonce = 100u64;
598        let provided_gas_limit = 50_000u64;
599
600        let accounts = AddressMap::from_iter([(
601            address,
602            ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
603        )]);
604
605        let eth_api = mock_eth_api(accounts);
606
607        let tx_req = TransactionRequest {
608            from: Some(address),
609            to: Some(Address::random().into()),
610            value: Some(U256::from(1000)),
611            nonce: Some(provided_nonce),
612            gas: Some(provided_gas_limit),
613            ..Default::default()
614        };
615
616        let filled =
617            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
618
619        // Should preserve the provided nonce and gas limit
620        assert_eq!(filled.tx.nonce(), provided_nonce);
621        assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
622    }
623
624    #[tokio::test]
625    async fn test_fill_transaction_fills_all_missing_fields() {
626        let address = Address::random();
627
628        let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
629        let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
630
631        let eth_api = mock_eth_api(accounts);
632
633        // Create a simple transfer transaction
634        let tx_req = TransactionRequest {
635            from: Some(address),
636            to: Some(Address::random().into()),
637            ..Default::default()
638        };
639
640        let filled =
641            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
642
643        assert!(filled.tx.is_eip1559());
644    }
645
646    #[tokio::test]
647    async fn test_fill_transaction_eip4844_blob_fee() {
648        let address = Address::random();
649        let accounts = AddressMap::from_iter([(
650            address,
651            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
652        )]);
653
654        let eth_api = mock_eth_api(accounts);
655
656        // EIP-4844 blob transaction with versioned hashes but no blob fee
657        let tx_req = TransactionRequest {
658            from: Some(address),
659            to: Some(Address::random().into()),
660            sidecar: Some(BlobTransactionSidecarVariant::from(zero_blob_sidecar())),
661            ..Default::default()
662        };
663
664        let filled =
665            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
666
667        // Blob transaction should have max_fee_per_blob_gas filled
668        assert!(
669            filled.tx.max_fee_per_blob_gas().is_some(),
670            "max_fee_per_blob_gas should be filled for blob tx"
671        );
672        assert!(
673            filled.tx.blob_versioned_hashes().is_some(),
674            "blob_versioned_hashes should be preserved"
675        );
676    }
677
678    #[tokio::test]
679    async fn test_fill_transaction_eip4844_preserves_blob_fee() {
680        let address = Address::random();
681        let accounts = AddressMap::from_iter([(
682            address,
683            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
684        )]);
685
686        let eth_api = mock_eth_api(accounts);
687
688        let provided_blob_fee = 5000000u128;
689
690        // EIP-4844 blob transaction with blob fee already set
691        let tx_req = TransactionRequest {
692            from: Some(address),
693            to: Some(Address::random().into()),
694            transaction_type: Some(3), // EIP-4844
695            sidecar: Some(BlobTransactionSidecarVariant::from(zero_blob_sidecar())),
696            max_fee_per_blob_gas: Some(provided_blob_fee), // Already set
697            ..Default::default()
698        };
699
700        let filled =
701            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
702
703        // Should preserve the provided blob fee
704        assert_eq!(
705            filled.tx.max_fee_per_blob_gas(),
706            Some(provided_blob_fee),
707            "should preserve provided max_fee_per_blob_gas"
708        );
709    }
710
711    #[tokio::test]
712    async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
713        let address = Address::random();
714        let accounts = AddressMap::from_iter([(
715            address,
716            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
717        )]);
718
719        let eth_api = mock_eth_api(accounts);
720
721        // EIP-1559 transaction without blob fields
722        let tx_req = TransactionRequest {
723            from: Some(address),
724            to: Some(Address::random().into()),
725            transaction_type: Some(2), // EIP-1559
726            ..Default::default()
727        };
728
729        let filled =
730            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
731
732        // Non-blob transaction should NOT have blob fee filled
733        assert!(
734            filled.tx.max_fee_per_blob_gas().is_none(),
735            "max_fee_per_blob_gas should not be set for non-blob tx"
736        );
737    }
738}