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::{
132        BlobTransactionSidecar, Block, Header, SidecarBuilder, SimpleCoder, Transaction,
133    };
134    use alloy_eips::eip2718::Encodable2718;
135    use alloy_primitives::{map::AddressMap, Address, Bytes, Signature, U256};
136    use alloy_rpc_types_eth::request::TransactionRequest;
137    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
138    use reth_ethereum_primitives::TransactionSigned;
139    use reth_evm::SenderRecoveryCache;
140    use reth_evm_ethereum::EthEvmConfig;
141    use reth_network_api::noop::NoopNetwork;
142    use reth_primitives_traits::SignedTransaction;
143    use reth_provider::{
144        test_utils::{ExtendedAccount, MockEthProvider},
145        ChainSpecProvider,
146    };
147    use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
148    use reth_transaction_pool::{
149        test_utils::{testing_pool, TestPool, TransactionGenerator},
150        EthPooledTransaction, PoolPooledTx, TransactionOrigin, TransactionPool,
151    };
152
153    fn mock_eth_api(
154        accounts: AddressMap<ExtendedAccount>,
155    ) -> EthApi<
156        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
157        EthRpcConverter<ChainSpec>,
158    > {
159        mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
160    }
161
162    fn mock_eth_api_with_sync_timeout(
163        accounts: AddressMap<ExtendedAccount>,
164        send_raw_transaction_sync_timeout: Duration,
165    ) -> EthApi<
166        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
167        EthRpcConverter<ChainSpec>,
168    > {
169        mock_eth_api_builder(accounts)
170            .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
171            .build()
172    }
173
174    fn mock_eth_api_builder(
175        accounts: AddressMap<ExtendedAccount>,
176    ) -> crate::EthApiBuilder<
177        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
178        EthRpcConverter<ChainSpec>,
179    > {
180        let mock_provider = MockEthProvider::default()
181            .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
182        mock_provider.extend_accounts(accounts);
183
184        let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
185        let pool = testing_pool();
186
187        let genesis_header = Header {
188            number: 0,
189            gas_limit: 30_000_000,
190            timestamp: 1,
191            excess_blob_gas: Some(0),
192            base_fee_per_gas: Some(1000000000),
193            blob_gas_used: Some(0),
194            ..Default::default()
195        };
196
197        let genesis_hash = B256::ZERO;
198        mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
199
200        EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
201    }
202
203    fn raw_transfer_tx() -> Bytes {
204        // https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
205        Bytes::from(hex!(
206            "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
207        ))
208    }
209
210    #[tokio::test]
211    async fn raw_transaction_recovery_shares_sender_cache() {
212        let cache = SenderRecoveryCache::new(16);
213        let eth_api = mock_eth_api_builder(Default::default())
214            .sender_recovery_cache(Some(cache.clone()))
215            .build();
216        let mut generator = TransactionGenerator::new(rand::rng());
217
218        for (transaction, warm_cache) in [
219            (generator.transaction().into_legacy(), false),
220            (generator.transaction().nonce(1).into_eip1559(), true),
221        ] {
222            let raw = Bytes::from(transaction.encoded_2718());
223            let hash = *transaction.tx_hash();
224            let sender = transaction.try_recover().unwrap();
225            assert_eq!(cache.get(&hash), None);
226            if warm_cache {
227                let recovered =
228                    eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
229                assert_eq!(recovered.signer(), sender);
230                assert_eq!(cache.get(&hash), Some(sender));
231            }
232
233            assert_eq!(eth_api.send_raw_transaction(raw.clone()).await.unwrap(), hash);
234            assert_eq!(cache.get(&hash), Some(sender));
235            let recovered =
236                eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
237            assert_eq!(recovered.signer(), sender);
238            assert_eq!(eth_api.recover_raw_pool_transaction(&raw).unwrap().sender(), sender);
239        }
240    }
241
242    #[tokio::test]
243    async fn raw_transaction_recovery_uses_pool_overrides() {
244        let raw = raw_transfer_tx();
245        for enabled in [false, true] {
246            let cache = SenderRecoveryCache::new(16);
247            let eth_api = mock_eth_api_builder(Default::default())
248                .sender_recovery_cache(enabled.then(|| cache.clone()))
249                .build();
250
251            // MockTransaction's raw hooks retain the input length instead of its in-memory size.
252            for _ in 0..2 {
253                let transaction = eth_api.recover_raw_pool_transaction(&raw).unwrap();
254                assert_eq!(transaction.encoded_length(), raw.len());
255                assert_eq!(cache.get(transaction.hash()), enabled.then_some(transaction.sender()));
256            }
257        }
258    }
259
260    #[tokio::test]
261    async fn raw_transaction_recovery_preserves_errors() {
262        let invalid = TransactionSigned::new_unhashed(
263            alloy_consensus::TxLegacy::default().into(),
264            Signature::new(U256::ZERO, U256::ZERO, false),
265        );
266        let mut trailing = raw_transfer_tx().to_vec();
267        trailing.push(0);
268
269        for enabled in [false, true] {
270            let cache = SenderRecoveryCache::new(16);
271            let eth_api = mock_eth_api_builder(Default::default())
272                .sender_recovery_cache(enabled.then(|| cache.clone()))
273                .build();
274            let valid = eth_api.recover_raw_pool_transaction(&raw_transfer_tx()).unwrap();
275            assert_eq!(cache.get(valid.hash()), enabled.then_some(valid.sender()));
276            for (raw, expected) in [
277                (vec![], EthApiError::EmptyRawTransactionData),
278                (vec![2], EthApiError::FailedToDecodeSignedTransaction),
279                (trailing.clone(), EthApiError::FailedToDecodeSignedTransaction),
280                (invalid.encoded_2718(), EthApiError::InvalidTransactionSignature),
281            ] {
282                let err = eth_api.send_raw_transaction(raw.clone().into()).await.unwrap_err();
283                assert_eq!(err.to_string(), expected.to_string());
284                let err =
285                    eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap_err();
286                assert_eq!(err.to_string(), expected.to_string());
287            }
288            assert!(eth_api.pool().is_empty());
289            assert_eq!(cache.get(invalid.tx_hash()), None);
290        }
291    }
292
293    #[tokio::test]
294    async fn cached_raw_transaction_preserves_blob_sidecar() {
295        let cache = SenderRecoveryCache::new(16);
296        let provider = MockEthProvider::default();
297        let evm_config = EthEvmConfig::new(provider.chain_spec());
298        let eth_api = EthApi::builder(
299            provider,
300            reth_transaction_pool::noop::NoopTransactionPool::default(),
301            NoopNetwork::default(),
302            evm_config,
303        )
304        .sender_recovery_cache(Some(cache.clone()))
305        .build();
306        let mut builder = SidecarBuilder::<SimpleCoder>::new();
307        builder.ingest(b"cached blob transaction");
308        let sidecar =
309            BlobTransactionSidecarVariant::from(builder.build::<BlobTransactionSidecar>().unwrap());
310        let transaction = TransactionSigned::new_unhashed(
311            alloy_consensus::TxEip4844 {
312                blob_versioned_hashes: sidecar.versioned_hashes().collect(),
313                ..Default::default()
314            }
315            .into(),
316            Signature::test_signature(),
317        );
318        let hash = *transaction.tx_hash();
319        let sender = transaction.try_recover().unwrap();
320        let raw = transaction.try_into_pooled_eip4844(sidecar.clone()).unwrap().encoded_2718();
321
322        for _ in 0..2 {
323            let mut recovered = eth_api.recover_raw_pool_transaction(&raw).unwrap();
324            assert_eq!(recovered.sender(), sender);
325            assert_eq!(cache.get(&hash), Some(sender));
326            let EthBlobTransactionSidecar::Present(actual) = recovered.take_blob() else {
327                panic!("missing blob sidecar")
328            };
329            assert_eq!(actual.into_sidecar(), sidecar);
330        }
331    }
332
333    #[tokio::test]
334    async fn send_raw_transaction_sync_populates_sender_cache() {
335        let cache = SenderRecoveryCache::new(16);
336        let eth_api = mock_eth_api_builder(Default::default())
337            .sender_recovery_cache(Some(cache.clone()))
338            .build();
339        let raw = raw_transfer_tx();
340        let transaction = EthPooledTransaction::decode_raw_transaction(&raw).unwrap();
341        let sender = transaction.try_recover().unwrap();
342
343        let err = eth_api.send_raw_transaction_sync(raw, Some(1)).await.unwrap_err();
344        assert!(matches!(err, EthApiError::TransactionConfirmationTimeout { .. }));
345        assert_eq!(cache.get(transaction.tx_hash()), Some(sender));
346        assert_eq!(eth_api.pool().len(), 1);
347    }
348
349    #[tokio::test]
350    async fn send_raw_transaction() {
351        let eth_api = mock_eth_api(Default::default());
352        let pool = eth_api.pool();
353
354        let tx_1 = raw_transfer_tx();
355
356        let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
357        assert_eq!(
358            pool.len(),
359            1,
360            "expect 1 transaction in the pool, but pool size is {}",
361            pool.len()
362        );
363
364        // https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
365        let tx_2 = Bytes::from(hex!(
366            "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
367        ));
368
369        let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
370        assert_eq!(
371            pool.len(),
372            2,
373            "expect 2 transactions in the pool, but pool size is {}",
374            pool.len()
375        );
376
377        assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
378        assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
379        assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
380        assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
381    }
382
383    #[tokio::test]
384    async fn send_raw_transaction_sync_uses_request_timeout() {
385        let eth_api = mock_eth_api(Default::default());
386
387        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
388
389        assert!(matches!(
390            err,
391            EthApiError::TransactionConfirmationTimeout { duration, .. }
392                if duration == Duration::from_millis(1)
393        ));
394        assert_eq!(eth_api.pool().len(), 1);
395    }
396
397    #[tokio::test]
398    async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
399        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
400
401        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
402
403        assert!(matches!(
404            err,
405            EthApiError::TransactionConfirmationTimeout { duration, .. }
406                if duration == Duration::from_millis(1)
407        ));
408        assert_eq!(eth_api.pool().len(), 1);
409    }
410
411    #[tokio::test]
412    async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
413        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
414
415        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
416
417        assert!(matches!(
418            err,
419            EthApiError::TransactionConfirmationTimeout { duration, .. }
420                if duration == Duration::from_millis(1)
421        ));
422        assert_eq!(eth_api.pool().len(), 1);
423    }
424
425    #[tokio::test]
426    async fn send_raw_transaction_sync_caps_request_timeout() {
427        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
428
429        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
430
431        assert!(matches!(
432            err,
433            EthApiError::TransactionConfirmationTimeout { duration, .. }
434                if duration == Duration::from_millis(1)
435        ));
436        assert_eq!(eth_api.pool().len(), 1);
437    }
438
439    #[tokio::test]
440    async fn send_transaction_preserves_provided_gas_limit() {
441        let signers = DevSigner::random_signers(1);
442        let address = signers[0].accounts()[0];
443        let accounts = AddressMap::from_iter([(
444            address,
445            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
446        )]);
447        let eth_api = mock_eth_api(accounts);
448        eth_api.signers().write().extend(signers);
449
450        let provided_gas_limit = 90_000;
451        let tx_req = TransactionRequest {
452            from: Some(address),
453            to: Some(address.into()),
454            gas: Some(provided_gas_limit),
455            gas_price: Some(1_000_000_000),
456            ..Default::default()
457        };
458
459        let hash = eth_api
460            .send_transaction_request(tx_req)
461            .await
462            .expect("send_transaction should succeed");
463        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
464
465        assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
466    }
467
468    #[tokio::test]
469    async fn send_transaction_fills_missing_fee_fields() {
470        let signers = DevSigner::random_signers(1);
471        let address = signers[0].accounts()[0];
472        let accounts = AddressMap::from_iter([(
473            address,
474            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
475        )]);
476        let eth_api = mock_eth_api(accounts);
477        eth_api.signers().write().extend(signers);
478
479        // no gas, gasPrice or 1559 fee fields: the node is expected to fill them
480        let tx_req = TransactionRequest {
481            from: Some(address),
482            to: Some(address.into()),
483            value: Some(U256::from(1)),
484            ..Default::default()
485        };
486
487        let hash = eth_api
488            .send_transaction_request(tx_req)
489            .await
490            .expect("send_transaction should fill the missing fee fields");
491        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
492        assert!(pooled.transaction.max_fee_per_gas() > 0);
493    }
494
495    #[tokio::test]
496    async fn send_transaction_rejects_mismatched_chain_id() {
497        let signers = DevSigner::random_signers(1);
498        let address = signers[0].accounts()[0];
499        let accounts = AddressMap::from_iter([(
500            address,
501            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
502        )]);
503        let eth_api = mock_eth_api(accounts);
504        eth_api.signers().write().extend(signers);
505
506        // The mock node is mainnet (chain id 1); the caller pins a different chain.
507        let tx_req = TransactionRequest {
508            from: Some(address),
509            to: Some(address.into()),
510            gas: Some(90_000),
511            gas_price: Some(1_000_000_000),
512            chain_id: Some(999),
513            ..Default::default()
514        };
515
516        let err = eth_api
517            .send_transaction_request(tx_req)
518            .await
519            .expect_err("a chain id that is not the node's must be rejected, not rewritten");
520        assert!(
521            err.to_string().contains("chainId does not match node's"),
522            "unexpected error: {err}"
523        );
524        assert!(eth_api.pool().is_empty(), "no transaction should have been submitted");
525    }
526
527    #[tokio::test]
528    async fn send_transaction_accepts_matching_chain_id() {
529        let signers = DevSigner::random_signers(1);
530        let address = signers[0].accounts()[0];
531        let accounts = AddressMap::from_iter([(
532            address,
533            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
534        )]);
535        let eth_api = mock_eth_api(accounts);
536        eth_api.signers().write().extend(signers);
537
538        let tx_req = TransactionRequest {
539            from: Some(address),
540            to: Some(address.into()),
541            gas: Some(90_000),
542            gas_price: Some(1_000_000_000),
543            chain_id: Some(1),
544            ..Default::default()
545        };
546
547        let hash = eth_api
548            .send_transaction_request(tx_req)
549            .await
550            .expect("a matching chain id must still be accepted");
551        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
552        assert_eq!(pooled.transaction.chain_id(), Some(1));
553    }
554
555    #[tokio::test]
556    async fn test_fill_transaction_fills_chain_id() {
557        let address = Address::random();
558        let accounts = AddressMap::from_iter([(
559            address,
560            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), // 10 ETH
561        )]);
562
563        let eth_api = mock_eth_api(accounts);
564
565        let tx_req = TransactionRequest {
566            from: Some(address),
567            to: Some(Address::random().into()),
568            gas: Some(21_000),
569            ..Default::default()
570        };
571
572        let filled =
573            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
574
575        // Should fill with the chain id from provider
576        assert!(filled.tx.chain_id().is_some());
577    }
578
579    #[tokio::test]
580    async fn test_fill_transaction_fills_nonce() {
581        let address = Address::random();
582        let nonce = 42u64;
583
584        let accounts = AddressMap::from_iter([(
585            address,
586            ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), // 1 ETH
587        )]);
588
589        let eth_api = mock_eth_api(accounts);
590
591        let tx_req = TransactionRequest {
592            from: Some(address),
593            to: Some(Address::random().into()),
594            value: Some(U256::from(1000)),
595            gas: Some(21_000),
596            ..Default::default()
597        };
598
599        let filled =
600            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
601
602        assert_eq!(filled.tx.nonce(), nonce);
603    }
604
605    #[tokio::test]
606    async fn test_fill_transaction_preserves_provided_fields() {
607        let address = Address::random();
608        let provided_nonce = 100u64;
609        let provided_gas_limit = 50_000u64;
610
611        let accounts = AddressMap::from_iter([(
612            address,
613            ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
614        )]);
615
616        let eth_api = mock_eth_api(accounts);
617
618        let tx_req = TransactionRequest {
619            from: Some(address),
620            to: Some(Address::random().into()),
621            value: Some(U256::from(1000)),
622            nonce: Some(provided_nonce),
623            gas: Some(provided_gas_limit),
624            ..Default::default()
625        };
626
627        let filled =
628            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
629
630        // Should preserve the provided nonce and gas limit
631        assert_eq!(filled.tx.nonce(), provided_nonce);
632        assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
633    }
634
635    #[tokio::test]
636    async fn test_fill_transaction_fills_all_missing_fields() {
637        let address = Address::random();
638
639        let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
640        let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
641
642        let eth_api = mock_eth_api(accounts);
643
644        // Create a simple transfer transaction
645        let tx_req = TransactionRequest {
646            from: Some(address),
647            to: Some(Address::random().into()),
648            ..Default::default()
649        };
650
651        let filled =
652            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
653
654        assert!(filled.tx.is_eip1559());
655    }
656
657    #[tokio::test]
658    async fn test_fill_transaction_eip4844_blob_fee() {
659        let address = Address::random();
660        let accounts = AddressMap::from_iter([(
661            address,
662            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
663        )]);
664
665        let eth_api = mock_eth_api(accounts);
666
667        let mut builder = SidecarBuilder::<SimpleCoder>::new();
668        builder.ingest(b"dummy blob");
669
670        // EIP-4844 blob transaction with versioned hashes but no blob fee
671        let tx_req = TransactionRequest {
672            from: Some(address),
673            to: Some(Address::random().into()),
674            sidecar: Some(BlobTransactionSidecarVariant::from(
675                builder.build::<BlobTransactionSidecar>().unwrap(),
676            )),
677            ..Default::default()
678        };
679
680        let filled =
681            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
682
683        // Blob transaction should have max_fee_per_blob_gas filled
684        assert!(
685            filled.tx.max_fee_per_blob_gas().is_some(),
686            "max_fee_per_blob_gas should be filled for blob tx"
687        );
688        assert!(
689            filled.tx.blob_versioned_hashes().is_some(),
690            "blob_versioned_hashes should be preserved"
691        );
692    }
693
694    #[tokio::test]
695    async fn test_fill_transaction_eip4844_preserves_blob_fee() {
696        let address = Address::random();
697        let accounts = AddressMap::from_iter([(
698            address,
699            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
700        )]);
701
702        let eth_api = mock_eth_api(accounts);
703
704        let provided_blob_fee = 5000000u128;
705
706        let mut builder = SidecarBuilder::<SimpleCoder>::new();
707        builder.ingest(b"dummy blob");
708
709        // EIP-4844 blob transaction with blob fee already set
710        let tx_req = TransactionRequest {
711            from: Some(address),
712            to: Some(Address::random().into()),
713            transaction_type: Some(3), // EIP-4844
714            sidecar: Some(BlobTransactionSidecarVariant::from(
715                builder.build::<BlobTransactionSidecar>().unwrap(),
716            )),
717            max_fee_per_blob_gas: Some(provided_blob_fee), // Already set
718            ..Default::default()
719        };
720
721        let filled =
722            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
723
724        // Should preserve the provided blob fee
725        assert_eq!(
726            filled.tx.max_fee_per_blob_gas(),
727            Some(provided_blob_fee),
728            "should preserve provided max_fee_per_blob_gas"
729        );
730    }
731
732    #[tokio::test]
733    async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
734        let address = Address::random();
735        let accounts = AddressMap::from_iter([(
736            address,
737            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
738        )]);
739
740        let eth_api = mock_eth_api(accounts);
741
742        // EIP-1559 transaction without blob fields
743        let tx_req = TransactionRequest {
744            from: Some(address),
745            to: Some(Address::random().into()),
746            transaction_type: Some(2), // EIP-1559
747            ..Default::default()
748        };
749
750        let filled =
751            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
752
753        // Non-blob transaction should NOT have blob fee filled
754        assert!(
755            filled.tx.max_fee_per_blob_gas().is_none(),
756            "max_fee_per_blob_gas should not be set for non-blob tx"
757        );
758    }
759}