Skip to main content

reth_rpc_eth_api/helpers/
transaction.rs

1//! Database access for `eth_` transaction RPC methods. Loads transaction and receipt data w.r.t.
2//! network.
3
4use super::{EthApiSpec, EthSigner, LoadBlock, LoadFee, LoadReceipt, LoadState, SpawnBlocking};
5use crate::{
6    helpers::{estimate::EstimateCall, spec::SignersForRpc},
7    FromEthApiError, FullEthApiTypes, IntoEthApiError, RpcNodeCore, RpcNodeCoreExt, RpcReceipt,
8    RpcTransaction,
9};
10use alloy_consensus::{
11    transaction::{TransactionMeta, TxHashRef},
12    BlockHeader, Transaction,
13};
14use alloy_dyn_abi::TypedData;
15use alloy_eip7928::bal::DecodedBal;
16use alloy_eips::{eip2718::Encodable2718, BlockId};
17use alloy_network::{TransactionBuilder, TransactionBuilder4844};
18use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
19use alloy_rpc_types_eth::{state::EvmOverrides, TransactionInfo, TransactionRequest};
20use evm2::evm::Bal as EvmBal;
21use futures::{Future, StreamExt};
22use reth_chain_state::CanonStateSubscriptions;
23use reth_primitives_traits::{
24    BlockBody, Recovered, RecoveredBlock, SignedTransaction, TxTy, WithEncoded,
25};
26use reth_rpc_convert::{transaction::RpcConvert, RpcTxReq, TransactionConversionError};
27use reth_rpc_eth_types::{
28    block::convert_transaction_receipt,
29    utils::{binary_search, decode_raw_transaction},
30    EthApiError::{self, TransactionConfirmationTimeout},
31    EthResult, FillTransaction, SignError, TransactionSource,
32};
33use reth_storage_api::{
34    BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
35    TransactionsProvider,
36};
37use reth_transaction_pool::{
38    AddedTransactionOutcome, PoolPooledTx, PoolTransaction, PoolTx, TransactionOrigin,
39    TransactionPool,
40};
41use std::{sync::Arc, time::Duration};
42
43/// Transaction related functions for the [`EthApiServer`](crate::EthApiServer) trait in
44/// the `eth_` namespace.
45///
46/// This includes utilities for transaction tracing, transacting and inspection.
47///
48/// Async functions that are spawned onto the
49/// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool) begin with `spawn_`
50///
51/// ## Calls
52///
53/// There are subtle differences between when transacting [`RpcTxReq`]:
54///
55/// The endpoints `eth_call` and `eth_estimateGas` and `eth_createAccessList` should always
56/// __disable__ the base fee check in the EVM environment.
57///
58/// The behaviour for tracing endpoints is not consistent across clients.
59/// Geth also disables the basefee check for tracing: <https://github.com/ethereum/go-ethereum/blob/bc0b87ca196f92e5af49bd33cc190ef0ec32b197/eth/tracers/api.go#L955-L955>
60/// Erigon does not: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/transactions/tracing.go#L209-L209>
61///
62/// See also <https://github.com/paradigmxyz/reth/issues/6240>
63///
64/// This implementation follows the behaviour of Geth and disables the basefee check for tracing.
65pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
66    /// Returns a handle for signing data.
67    ///
68    /// Signer access in default (L1) trait method implementations.
69    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes>;
70
71    /// Returns a list of addresses owned by provider.
72    fn accounts(&self) -> Vec<Address> {
73        self.signers().read().iter().flat_map(|s| s.accounts()).collect()
74    }
75
76    /// Returns the timeout duration for `send_raw_transaction_sync` RPC method.
77    fn send_raw_transaction_sync_timeout(&self) -> Duration {
78        self.eth_api_settings().send_raw_transaction_sync_timeout
79    }
80
81    /// Decodes and recovers the transaction and submits it to the pool.
82    ///
83    /// Returns the hash of the transaction.
84    fn send_raw_transaction(
85        &self,
86        tx: Bytes,
87    ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
88        async move {
89            let pool_transaction = self.recover_raw_pool_transaction(&tx)?;
90            self.send_pool_transaction(
91                TransactionOrigin::Local,
92                WithEncoded::new(tx, pool_transaction),
93            )
94            .await
95        }
96    }
97
98    /// Decodes and recovers a raw pool transaction, sharing sender recovery when configured.
99    fn recover_raw_pool_transaction(&self, tx: &[u8]) -> Result<PoolTx<Self::Pool>, Self::Error> {
100        match self.eth_api_settings().sender_recovery_cache.as_ref() {
101            Some(cache) => {
102                <PoolTx<Self::Pool> as PoolTransaction>::recover_raw_transaction_with_cache(
103                    tx, cache,
104                )
105            }
106            None => <PoolTx<Self::Pool> as PoolTransaction>::recover_raw_transaction(tx),
107        }
108        .map_err(Self::Error::from_eth_err)
109    }
110
111    /// Submits the transaction to the pool with the given [`TransactionOrigin`].
112    fn send_transaction(
113        &self,
114        origin: TransactionOrigin,
115        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
116    ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
117        async move {
118            let (encoded, recovered) = tx.split();
119            let pool_transaction =
120                <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
121
122            self.send_pool_transaction(origin, WithEncoded::new(encoded, pool_transaction)).await
123        }
124    }
125
126    /// Submits the pool transaction to the pool with the given [`TransactionOrigin`].
127    fn send_pool_transaction(
128        &self,
129        origin: TransactionOrigin,
130        tx: WithEncoded<PoolTx<Self::Pool>>,
131    ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
132
133    /// Decodes and recovers the transaction and submits it to the pool.
134    ///
135    /// And awaits the receipt.
136    fn send_raw_transaction_sync(
137        &self,
138        tx: Bytes,
139        timeout_ms: Option<u64>,
140    ) -> impl Future<Output = Result<RpcReceipt<Self::NetworkTypes>, Self::Error>> + Send
141    where
142        Self: LoadReceipt,
143    {
144        let this = self.clone();
145        let configured_timeout = self.send_raw_transaction_sync_timeout();
146        let timeout_duration = timeout_ms
147            .filter(|timeout_ms| *timeout_ms > 0)
148            .map(Duration::from_millis)
149            .map(|timeout| timeout.min(configured_timeout))
150            .unwrap_or(configured_timeout);
151        async move {
152            let mut stream = this.provider().canonical_state_stream();
153            let hash = EthTransactions::send_raw_transaction(&this, tx).await?;
154            tokio::time::timeout(timeout_duration, async {
155                while let Some(notification) = stream.next().await {
156                    let chain = notification.committed();
157                    if let Some((block, tx, receipt, all_receipts)) =
158                        chain.find_transaction_and_receipt_by_hash(hash) &&
159                        let Some(receipt) = convert_transaction_receipt(
160                            block,
161                            all_receipts,
162                            tx,
163                            receipt,
164                            this.converter(),
165                        )
166                        .transpose()?
167                    {
168                        return Ok(receipt);
169                    }
170                }
171                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
172                    hash,
173                    duration: timeout_duration,
174                }))
175            })
176            .await
177            .unwrap_or_else(|_elapsed| {
178                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
179                    hash,
180                    duration: timeout_duration,
181                }))
182            })
183        }
184    }
185
186    /// Returns the transaction by hash.
187    ///
188    /// Checks the pool and state.
189    ///
190    /// Returns `Ok(None)` if no matching transaction was found.
191    #[expect(clippy::complexity)]
192    fn transaction_by_hash(
193        &self,
194        hash: B256,
195    ) -> impl Future<
196        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
197    > + Send {
198        LoadTransaction::transaction_by_hash(self, hash)
199    }
200
201    /// Returns all transactions from the local pending pool.
202    fn pending_transactions(&self) -> Result<Vec<RpcTransaction<Self::NetworkTypes>>, Self::Error> {
203        self.pool()
204            .pending_transactions()
205            .into_iter()
206            .map(|tx| self.converter().fill_pending(tx.transaction.clone_into_consensus()))
207            .collect::<Result<Vec<_>, _>>()
208            .map_err(Self::Error::from)
209    }
210
211    /// Get all transactions in the block with the given hash.
212    ///
213    /// Returns `None` if block does not exist.
214    #[expect(clippy::type_complexity)]
215    fn transactions_by_block(
216        &self,
217        block: B256,
218    ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
219    {
220        async move {
221            self.cache()
222                .get_recovered_block(block)
223                .await
224                .map(|b| b.map(|b| b.body().transactions().to_vec()))
225                .map_err(Self::Error::from_eth_err)
226        }
227    }
228
229    /// Returns the EIP-2718 encoded transaction by hash.
230    ///
231    /// If this is a pooled EIP-4844 transaction, the blob sidecar is included.
232    ///
233    /// Checks the pool and state.
234    ///
235    /// Returns `Ok(None)` if no matching transaction was found.
236    fn raw_transaction_by_hash(
237        &self,
238        hash: B256,
239    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
240        async move {
241            // Note: this is mostly used to fetch pooled transactions so we check the pool first
242            if let Some(tx) =
243                self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
244            {
245                return Ok(Some(tx))
246            }
247
248            self.spawn_blocking_io(move |ref this| {
249                Ok(this
250                    .provider()
251                    .transaction_by_hash(hash)
252                    .map_err(Self::Error::from_eth_err)?
253                    .map(|tx| tx.encoded_2718().into()))
254            })
255            .await
256        }
257    }
258
259    /// Returns the _historical_ transaction and the block it was mined in
260    #[expect(clippy::type_complexity)]
261    fn historical_transaction_by_hash_at(
262        &self,
263        hash: B256,
264    ) -> impl Future<
265        Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
266    > + Send {
267        async move {
268            match self.transaction_by_hash_at(hash).await? {
269                None => Ok(None),
270                Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
271            }
272        }
273    }
274
275    /// Returns the transaction receipt for the given hash.
276    ///
277    /// Returns None if the transaction does not exist or is pending
278    /// Note: The tx receipt is not available for pending transactions.
279    fn transaction_receipt(
280        &self,
281        hash: B256,
282    ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
283    where
284        Self: LoadReceipt,
285    {
286        async move {
287            match self.load_transaction_and_receipt(hash).await? {
288                Some((tx, meta, receipt, all_receipts, block)) => self
289                    .build_transaction_receipt(tx, meta, receipt, all_receipts, block)
290                    .await
291                    .map(Some),
292                None => Ok(None),
293            }
294        }
295    }
296
297    /// Helper method that loads a transaction and its receipt.
298    ///
299    /// The returned transaction has its sender already recovered.
300    #[expect(clippy::complexity)]
301    fn load_transaction_and_receipt(
302        &self,
303        hash: TxHash,
304    ) -> impl Future<
305        Output = Result<
306            Option<(
307                Recovered<ProviderTx<Self::Provider>>,
308                TransactionMeta,
309                ProviderReceipt<Self::Provider>,
310                Option<Arc<Vec<ProviderReceipt<Self::Provider>>>>,
311                Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
312            )>,
313            Self::Error,
314        >,
315    > + Send
316    where
317        Self: 'static,
318    {
319        async move {
320            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
321                let Some(tx) = cached.recovered_transaction().map(|tx| tx.cloned())
322            {
323                let meta = cached.transaction_meta(hash);
324
325                // Best case: receipts are also cached.
326                if let Some(all_receipts) = cached.receipts.clone() &&
327                    let Some(receipt) = all_receipts.get(cached.tx_index).cloned()
328                {
329                    return Ok(Some((tx, meta, receipt, Some(all_receipts), Some(cached.block))));
330                }
331
332                // Block still cached but receipts evicted — fetch via cache since
333                // `build_transaction_receipt` needs all receipts for gas accounting
334                // anyway.
335                if let Some(receipts) = self
336                    .cache()
337                    .get_receipts(cached.block.hash())
338                    .await
339                    .map_err(Self::Error::from_eth_err)? &&
340                    let Some(receipt) = receipts.get(cached.tx_index).cloned()
341                {
342                    return Ok(Some((tx, meta, receipt, Some(receipts), Some(cached.block))));
343                }
344            }
345
346            // Full cache miss — fetch both from provider.
347            self.spawn_blocking_io(move |this| {
348                let provider = this.provider();
349                let Some((tx, meta)) = provider
350                    .transaction_by_hash_with_meta(hash)
351                    .map_err(Self::Error::from_eth_err)?
352                else {
353                    return Ok(None);
354                };
355
356                let tx = this.recover_mined_transaction(tx).map_err(Self::Error::from_eth_err)?;
357
358                let receipt = provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)?;
359
360                Ok(receipt.map(|receipt| (tx, meta, receipt, None, None)))
361            })
362            .await
363        }
364    }
365
366    /// Get transaction by [`BlockId`] and index of transaction within that block.
367    ///
368    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
369    fn transaction_by_block_and_tx_index(
370        &self,
371        block_id: BlockId,
372        index: usize,
373    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
374    where
375        Self: LoadBlock,
376    {
377        async move {
378            if let Some(block) = self.recovered_block(block_id).await? {
379                let block_hash = block.hash();
380                let block_number = block.number();
381                let block_timestamp = block.timestamp();
382                let base_fee_per_gas = block.base_fee_per_gas();
383                if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
384                    let tx_info = TransactionInfo {
385                        hash: Some(*tx.tx_hash()),
386                        block_hash: Some(block_hash),
387                        block_number: Some(block_number),
388                        block_timestamp: Some(block_timestamp),
389                        base_fee: base_fee_per_gas,
390                        index: Some(index as u64),
391                    };
392
393                    return Ok(Some(
394                        self.converter().fill(tx.clone().with_signer(*signer), tx_info)?,
395                    ))
396                }
397            }
398
399            Ok(None)
400        }
401    }
402
403    /// Find a transaction by sender's address and nonce.
404    fn get_transaction_by_sender_and_nonce(
405        &self,
406        sender: Address,
407        nonce: u64,
408        include_pending: bool,
409    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
410    where
411        Self: LoadBlock + LoadState,
412    {
413        async move {
414            // Check the pool first
415            if include_pending &&
416                let Some(tx) =
417                    RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
418            {
419                let transaction = tx.transaction.clone_into_consensus();
420                return Ok(Some(self.converter().fill_pending(transaction)?));
421            }
422
423            // Note: we can't optimize for contracts (account with code) and cannot shortcircuit if
424            // the address has code, because with 7702 EOAs can also have code
425
426            let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
427
428            // If the nonce is higher or equal to the highest nonce, the transaction is pending or
429            // not exists.
430            if nonce >= highest {
431                return Ok(None);
432            }
433
434            let high = self.provider().best_block_number().map_err(Self::Error::from_eth_err)?;
435
436            // Perform a binary search over the block range to find the block in which the sender's
437            // nonce reached the requested nonce.
438            let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
439                let mid_nonce =
440                    self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
441
442                Ok(mid_nonce > nonce)
443            })
444            .await?;
445
446            let block_id = num.into();
447            let Some(block) = self.recovered_block(block_id).await? else {
448                return Err(EthApiError::HeaderNotFound(block_id).into())
449            };
450
451            let block_hash = block.hash();
452            let block_number = block.number();
453            let block_timestamp = block.timestamp();
454            let base_fee_per_gas = block.base_fee_per_gas();
455
456            // EIP-7702 authorizations can consume the account's nonce without a transaction
457            // from that sender, so an existing block may contain no matching transaction.
458            block
459                .transactions_with_sender()
460                .enumerate()
461                .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
462                .map(|(index, (signer, tx))| {
463                    let tx_info = TransactionInfo {
464                        hash: Some(*tx.tx_hash()),
465                        block_hash: Some(block_hash),
466                        block_number: Some(block_number),
467                        block_timestamp: Some(block_timestamp),
468                        base_fee: base_fee_per_gas,
469                        index: Some(index as u64),
470                    };
471                    Ok(self.converter().fill(tx.clone().with_signer(*signer), tx_info)?)
472                })
473                .transpose()
474        }
475    }
476
477    /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block.
478    ///
479    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
480    fn raw_transaction_by_block_and_tx_index(
481        &self,
482        block_id: BlockId,
483        index: usize,
484    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
485    where
486        Self: LoadBlock,
487    {
488        async move {
489            if let Some(block) = self.recovered_block(block_id).await? &&
490                let Some(tx) = block.body().transactions().get(index)
491            {
492                return Ok(Some(tx.encoded_2718().into()))
493            }
494
495            Ok(None)
496        }
497    }
498
499    /// Signs transaction with a matching signer, if any and submits the transaction to the pool.
500    /// Returns the hash of the signed transaction.
501    ///
502    /// Rejects requests that mix legacy and EIP-1559 fee fields.
503    fn send_transaction_request(
504        &self,
505        mut request: RpcTxReq<Self::NetworkTypes>,
506    ) -> impl Future<Output = Result<B256, Self::Error>> + Send
507    where
508        Self: EthApiSpec + EstimateCall + LoadFee,
509    {
510        async move {
511            ensure_no_conflicting_fee_fields(request.as_ref())?;
512
513            let from = match request.as_ref().from() {
514                Some(from) => from,
515                None => return Err(SignError::NoAccount.into_eth_err()),
516            };
517
518            let signer = self.find_signer(&from)?;
519
520            // Held until the pool insert below, so a concurrent request from this account reads
521            // the pool only after this transaction is in it.
522            let _nonce_guard = match signer.nonce_lock() {
523                Some(lock) if request.as_ref().nonce().is_none() => Some(lock.lock_owned().await),
524                _ => None,
525            };
526
527            // set nonce if not already set before
528            if request.as_ref().nonce().is_none() {
529                let nonce = self.next_available_nonce_for(&request).await?;
530                request.as_mut().set_nonce(nonce);
531            }
532
533            let chain_id = self.chain_id();
534            // A caller that pins `chainId` is asserting which chain it means to sign for.
535            // Silently rewriting it would sign and submit on a different chain than asked
536            // for, so reject the mismatch instead. Matches geth's `setDefaults`.
537            if let Some(request_chain_id) = request.as_ref().chain_id() &&
538                request_chain_id != chain_id.to::<u64>()
539            {
540                return Err(EthApiError::InvalidParams(format!(
541                    "chainId does not match node's (have={request_chain_id}, want={})",
542                    chain_id.to::<u64>()
543                ))
544                .into())
545            }
546            request.as_mut().set_chain_id(chain_id.to());
547
548            // set fee defaults if not already set before, same as `fill_transaction`
549            if request.as_ref().gas_price().is_none() {
550                let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
551                    tip
552                } else {
553                    let tip = self.suggested_priority_fee().await?.to::<u128>();
554                    request.as_mut().set_max_priority_fee_per_gas(tip);
555                    tip
556                };
557                if request.as_ref().max_fee_per_gas().is_none() {
558                    let header =
559                        self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
560                    let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
561                    request.as_mut().set_max_fee_per_gas(base_fee as u128 * 2 + tip);
562                }
563            }
564
565            if request.as_ref().gas_limit().is_none() {
566                let estimated_gas = self
567                    .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
568                    .await?;
569                request.as_mut().set_gas_limit(estimated_gas.to());
570            }
571
572            let transaction = self.sign_request(&from, request).await?.with_signer(from);
573
574            let pool_transaction =
575                <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
576                    transaction,
577                )
578                .map_err(|e| {
579                    Self::Error::from_eth_err(TransactionConversionError::Other(e.to_string()))
580                })?;
581
582            // submit the transaction to the pool with a `Local` origin
583            let AddedTransactionOutcome { hash, .. } = self
584                .pool()
585                .add_transaction(TransactionOrigin::Local, pool_transaction)
586                .await
587                .map_err(Self::Error::from_eth_err)?;
588
589            Ok(hash)
590        }
591    }
592
593    /// Fills the defaults on a given unsigned transaction.
594    ///
595    /// Rejects requests that mix legacy and EIP-1559 fee fields.
596    fn fill_transaction(
597        &self,
598        mut request: RpcTxReq<Self::NetworkTypes>,
599    ) -> impl Future<Output = Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error>> + Send
600    where
601        Self: EthApiSpec + EstimateCall + LoadFee,
602    {
603        async move {
604            ensure_no_conflicting_fee_fields(request.as_ref())?;
605
606            if request.as_ref().value().is_none() {
607                request.as_mut().set_value(U256::ZERO);
608            }
609
610            if request.as_ref().nonce().is_none() {
611                let nonce = self.next_available_nonce_for(&request).await?;
612                request.as_mut().set_nonce(nonce);
613            }
614
615            let chain_id = self.chain_id();
616            // A caller that pins `chainId` is asserting which chain it means to sign for.
617            // Silently rewriting it would sign and submit on a different chain than asked
618            // for, so reject the mismatch instead. Matches geth's `setDefaults`.
619            if let Some(request_chain_id) = request.as_ref().chain_id() &&
620                request_chain_id != chain_id.to::<u64>()
621            {
622                return Err(EthApiError::InvalidParams(format!(
623                    "chainId does not match node's (have={request_chain_id}, want={})",
624                    chain_id.to::<u64>()
625                ))
626                .into())
627            }
628            request.as_mut().set_chain_id(chain_id.to());
629
630            if request.as_ref().has_eip4844_fields() &&
631                request.as_ref().max_fee_per_blob_gas().is_none()
632            {
633                let blob_fee = self.blob_base_fee().await?;
634                request.as_mut().set_max_fee_per_blob_gas(blob_fee.to());
635            }
636
637            // Use `sidecar.is_some()` instead of `blob_sidecar().is_some()` to handle
638            // both EIP-4844 (v0) and EIP-7594 (v1) sidecar formats
639            if request.as_ref().sidecar.is_some() &&
640                request.as_ref().blob_versioned_hashes.is_none()
641            {
642                request.as_mut().populate_blob_hashes();
643            }
644
645            if request.as_ref().gas_limit().is_none() {
646                let estimated_gas = self
647                    .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
648                    .await?;
649                request.as_mut().set_gas_limit(estimated_gas.to());
650            }
651
652            if request.as_ref().gas_price().is_none() {
653                let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
654                    tip
655                } else {
656                    let tip = self.suggested_priority_fee().await?.to::<u128>();
657                    request.as_mut().set_max_priority_fee_per_gas(tip);
658                    tip
659                };
660                if request.as_ref().max_fee_per_gas().is_none() {
661                    let header =
662                        self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
663                    let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
664                    // Use `2 * base_fee` as headroom, matching go-ethereum's
665                    // `setLondonFeeDefaults`, so the transaction does not
666                    // become invalid if the base fee rises before it is
667                    // included. This does not increase the effective price the sender pays:
668                    // `max_fee_per_gas` is only an upper bound and the sender still pays
669                    // `base_fee + min(tip, max_fee_per_gas - base_fee)`.
670                    request.as_mut().set_max_fee_per_gas(base_fee as u128 * 2 + tip);
671                }
672            }
673
674            let tx = self.converter().build_simulate_v1_transaction(request)?;
675
676            let raw = tx.encoded_2718().into();
677
678            Ok(FillTransaction { raw, tx })
679        }
680    }
681
682    /// Signs a transaction, with configured signers.
683    fn sign_request(
684        &self,
685        from: &Address,
686        txn: RpcTxReq<Self::NetworkTypes>,
687    ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
688        async move {
689            self.find_signer(from)?
690                .sign_transaction(txn, from)
691                .await
692                .map_err(Self::Error::from_eth_err)
693        }
694    }
695
696    /// Signs given message. Returns the signature.
697    fn sign(
698        &self,
699        account: Address,
700        message: Bytes,
701    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
702        async move {
703            Ok(self
704                .find_signer(&account)?
705                .sign(account, &message)
706                .await
707                .map_err(Self::Error::from_eth_err)?
708                .as_bytes()
709                .into())
710        }
711    }
712
713    /// Signs a transaction request using the given account in request
714    /// Returns the EIP-2718 encoded signed transaction.
715    ///
716    /// Rejects requests that mix legacy and EIP-1559 fee fields.
717    fn sign_transaction(
718        &self,
719        request: RpcTxReq<Self::NetworkTypes>,
720    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
721        async move {
722            ensure_no_conflicting_fee_fields(request.as_ref())?;
723
724            let from = match request.as_ref().from() {
725                Some(from) => from,
726                None => return Err(SignError::NoAccount.into_eth_err()),
727            };
728
729            Ok(self.sign_request(&from, request).await?.encoded_2718().into())
730        }
731    }
732
733    /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
734    fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
735        Ok(self
736            .find_signer(&account)?
737            .sign_typed_data(account, data)
738            .map_err(Self::Error::from_eth_err)?
739            .as_bytes()
740            .into())
741    }
742
743    /// Returns the signer for the given account, if found in configured signers.
744    #[expect(clippy::type_complexity)]
745    fn find_signer(
746        &self,
747        account: &Address,
748    ) -> Result<
749        Box<dyn EthSigner<ProviderTx<Self::Provider>, RpcTxReq<Self::NetworkTypes>> + 'static>,
750        Self::Error,
751    > {
752        self.signers()
753            .read()
754            .iter()
755            .find(|signer| signer.is_signer_for(account))
756            .map(|signer| dyn_clone::clone_box(&**signer))
757            .ok_or_else(|| SignError::NoAccount.into_eth_err())
758    }
759}
760
761/// Loads a transaction from database.
762///
763/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC
764/// methods.
765pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
766    /// Decodes and recovers a raw transaction, sharing sender recovery when configured.
767    fn recover_raw_transaction<T: SignedTransaction>(&self, tx: &[u8]) -> EthResult<Recovered<T>> {
768        let transaction = decode_raw_transaction::<T>(tx)?;
769        let signer = match self.eth_api_settings().sender_recovery_cache.as_ref() {
770            Some(cache) => cache.recover(&transaction),
771            None => transaction.try_recover(),
772        }
773        .map_err(|_| EthApiError::InvalidTransactionSignature)?;
774        Ok(Recovered::new_unchecked(transaction, signer))
775    }
776
777    /// Decodes and recovers raw transactions in order, stopping at the first error.
778    ///
779    /// Uses [`Self::recover_raw_transaction`] to share sender recovery when configured.
780    fn recover_raw_transactions<T: SignedTransaction>(
781        &self,
782        txs: impl IntoIterator<Item = impl AsRef<[u8]>>,
783    ) -> EthResult<Vec<Recovered<T>>> {
784        txs.into_iter().map(|tx| self.recover_raw_transaction(tx.as_ref())).collect()
785    }
786
787    /// Recovers the sender of a mined transaction, reusing a cached sender when configured.
788    ///
789    /// Mined transactions can predate EIP-2, so recovery does not reject high-s signatures. A
790    /// sender recovered on a cache miss is not cached: lookups of mined transactions rarely
791    /// repeat, and caching them would evict senders that ingress and payload execution still need.
792    fn recover_mined_transaction<T: SignedTransaction>(&self, tx: T) -> EthResult<Recovered<T>> {
793        let cached = self
794            .eth_api_settings()
795            .sender_recovery_cache
796            .as_ref()
797            .and_then(|cache| cache.get(tx.tx_hash()));
798        let signer = match cached {
799            Some(signer) => signer,
800            None => tx
801                .recover_signer_unchecked()
802                .map_err(|_| EthApiError::InvalidTransactionSignature)?,
803        };
804        Ok(Recovered::new_unchecked(tx, signer))
805    }
806
807    /// Returns the transaction by hash.
808    ///
809    /// Checks the pool and state.
810    ///
811    /// Returns `Ok(None)` if no matching transaction was found.
812    #[expect(clippy::complexity)]
813    fn transaction_by_hash(
814        &self,
815        hash: B256,
816    ) -> impl Future<
817        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
818    > + Send {
819        async move {
820            // First, try the RPC cache
821            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
822                let Some(source) = cached.to_transaction_source()
823            {
824                return Ok(Some(source));
825            }
826
827            // Cache miss - try to find the transaction on disk
828            if let Some((tx, meta)) = self
829                .spawn_blocking_io(move |this| {
830                    this.provider()
831                        .transaction_by_hash_with_meta(hash)
832                        .map_err(Self::Error::from_eth_err)
833                })
834                .await?
835            {
836                // Note: we assume this transaction is valid, because it's mined (or
837                // part of pending block) and already. We don't need to
838                // check for pre EIP-2 because this transaction could be pre-EIP-2.
839                let transaction =
840                    self.recover_mined_transaction(tx).map_err(Self::Error::from_eth_err)?;
841
842                return Ok(Some(TransactionSource::Block {
843                    transaction,
844                    index: meta.index,
845                    block_hash: meta.block_hash,
846                    block_number: meta.block_number,
847                    block_timestamp: meta.timestamp,
848                    base_fee: meta.base_fee,
849                }));
850            }
851
852            // tx not found on disk, check pool
853            if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus())
854            {
855                return Ok(Some(TransactionSource::Pool(tx.into())));
856            }
857
858            Ok(None)
859        }
860    }
861
862    /// Returns the transaction by including its corresponding [`BlockId`].
863    ///
864    /// Note: this supports pending transactions
865    #[expect(clippy::type_complexity)]
866    fn transaction_by_hash_at(
867        &self,
868        transaction_hash: B256,
869    ) -> impl Future<
870        Output = Result<
871            Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
872            Self::Error,
873        >,
874    > + Send {
875        async move {
876            Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
877                tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
878                tx @ TransactionSource::Block { block_hash, .. } => (tx, BlockId::hash(block_hash)),
879            }))
880        }
881    }
882
883    /// Fetches the transaction and the transaction's block
884    #[expect(clippy::type_complexity)]
885    fn transaction_and_block(
886        &self,
887        hash: B256,
888    ) -> impl Future<
889        Output = Result<
890            Option<(
891                TransactionSource<ProviderTx<Self::Provider>>,
892                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
893            )>,
894            Self::Error,
895        >,
896    > + Send {
897        async move {
898            Ok(self
899                .transaction_and_block_and_maybe_bal(hash)
900                .await?
901                .map(|(transaction, block, _)| (transaction, block)))
902        }
903    }
904
905    /// Fetches the transaction and the transaction's block, together with the block's cached
906    /// block access list, if any.
907    ///
908    /// The BAL is only returned if it is already cached, it is never fetched from the BAL store.
909    #[expect(clippy::type_complexity)]
910    fn transaction_and_block_and_maybe_bal(
911        &self,
912        hash: B256,
913    ) -> impl Future<
914        Output = Result<
915            Option<(
916                TransactionSource<ProviderTx<Self::Provider>>,
917                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
918                Option<Arc<DecodedBal<Arc<EvmBal>>>>,
919            )>,
920            Self::Error,
921        >,
922    > + Send {
923        async move {
924            let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
925                None => return Ok(None),
926                Some(res) => res,
927            };
928
929            // Note: this is always either hash or pending
930            let block_hash = match at {
931                BlockId::Hash(hash) => hash.block_hash,
932                _ => return Ok(None),
933            };
934            let block_and_bal = self
935                .cache()
936                .get_recovered_block_and_maybe_bal(block_hash)
937                .await
938                .map_err(Self::Error::from_eth_err)?;
939            Ok(block_and_bal.map(|(block, bal)| (transaction, block, bal)))
940        }
941    }
942}
943
944/// Rejects legacy gas prices combined with EIP-1559 fee fields before transaction construction.
945const fn ensure_no_conflicting_fee_fields(request: &TransactionRequest) -> EthResult<()> {
946    if request.gas_price.is_some() && request.has_eip1559_fields() {
947        return Err(EthApiError::ConflictingFeeFieldsInRequest)
948    }
949    Ok(())
950}