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::{SignerRecoverable, 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};
20use futures::{Future, StreamExt};
21use reth_chain_state::CanonStateSubscriptions;
22use reth_primitives_traits::{
23    BlockBody, Recovered, RecoveredBlock, SignedTransaction, TxTy, WithEncoded,
24};
25use reth_rpc_convert::{transaction::RpcConvert, RpcTxReq, TransactionConversionError};
26use reth_rpc_eth_types::{
27    block::convert_transaction_receipt,
28    utils::{binary_search, decode_raw_transaction},
29    EthApiError::{self, TransactionConfirmationTimeout},
30    EthResult, FillTransaction, SignError, TransactionSource,
31};
32use reth_storage_api::{
33    BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
34    TransactionsProvider,
35};
36use reth_transaction_pool::{
37    AddedTransactionOutcome, PoolPooledTx, PoolTransaction, PoolTx, TransactionOrigin,
38    TransactionPool,
39};
40use revm::state::bal::Bal as RevmBal;
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 + 'static,
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                        .map_err(Self::Error::from)?
168                    {
169                        return Ok(receipt);
170                    }
171                }
172                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
173                    hash,
174                    duration: timeout_duration,
175                }))
176            })
177            .await
178            .unwrap_or_else(|_elapsed| {
179                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
180                    hash,
181                    duration: timeout_duration,
182                }))
183            })
184        }
185    }
186
187    /// Returns the transaction by hash.
188    ///
189    /// Checks the pool and state.
190    ///
191    /// Returns `Ok(None)` if no matching transaction was found.
192    #[expect(clippy::complexity)]
193    fn transaction_by_hash(
194        &self,
195        hash: B256,
196    ) -> impl Future<
197        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
198    > + Send {
199        LoadTransaction::transaction_by_hash(self, hash)
200    }
201
202    /// Returns all transactions from the local pending pool.
203    fn pending_transactions(&self) -> Result<Vec<RpcTransaction<Self::NetworkTypes>>, Self::Error> {
204        self.pool()
205            .pending_transactions()
206            .into_iter()
207            .map(|tx| self.converter().fill_pending(tx.transaction.clone_into_consensus()))
208            .collect::<Result<Vec<_>, _>>()
209            .map_err(Self::Error::from)
210    }
211
212    /// Get all transactions in the block with the given hash.
213    ///
214    /// Returns `None` if block does not exist.
215    #[expect(clippy::type_complexity)]
216    fn transactions_by_block(
217        &self,
218        block: B256,
219    ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
220    {
221        async move {
222            self.cache()
223                .get_recovered_block(block)
224                .await
225                .map(|b| b.map(|b| b.body().transactions().to_vec()))
226                .map_err(Self::Error::from_eth_err)
227        }
228    }
229
230    /// Returns the EIP-2718 encoded transaction by hash.
231    ///
232    /// If this is a pooled EIP-4844 transaction, the blob sidecar is included.
233    ///
234    /// Checks the pool and state.
235    ///
236    /// Returns `Ok(None)` if no matching transaction was found.
237    fn raw_transaction_by_hash(
238        &self,
239        hash: B256,
240    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
241        async move {
242            // Note: this is mostly used to fetch pooled transactions so we check the pool first
243            if let Some(tx) =
244                self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
245            {
246                return Ok(Some(tx))
247            }
248
249            self.spawn_blocking_io(move |ref this| {
250                Ok(this
251                    .provider()
252                    .transaction_by_hash(hash)
253                    .map_err(Self::Error::from_eth_err)?
254                    .map(|tx| tx.encoded_2718().into()))
255            })
256            .await
257        }
258    }
259
260    /// Returns the _historical_ transaction and the block it was mined in
261    #[expect(clippy::type_complexity)]
262    fn historical_transaction_by_hash_at(
263        &self,
264        hash: B256,
265    ) -> impl Future<
266        Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
267    > + Send {
268        async move {
269            match self.transaction_by_hash_at(hash).await? {
270                None => Ok(None),
271                Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
272            }
273        }
274    }
275
276    /// Returns the transaction receipt for the given hash.
277    ///
278    /// Returns None if the transaction does not exist or is pending
279    /// Note: The tx receipt is not available for pending transactions.
280    fn transaction_receipt(
281        &self,
282        hash: B256,
283    ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
284    where
285        Self: LoadReceipt + 'static,
286    {
287        async move {
288            match self.load_transaction_and_receipt(hash).await? {
289                Some((tx, meta, receipt, all_receipts, block)) => self
290                    .build_transaction_receipt(tx, meta, receipt, all_receipts, block)
291                    .await
292                    .map(Some),
293                None => Ok(None),
294            }
295        }
296    }
297
298    /// Helper method that loads a transaction and its receipt.
299    ///
300    /// The returned transaction has its sender already recovered.
301    #[expect(clippy::complexity)]
302    fn load_transaction_and_receipt(
303        &self,
304        hash: TxHash,
305    ) -> impl Future<
306        Output = Result<
307            Option<(
308                Recovered<ProviderTx<Self::Provider>>,
309                TransactionMeta,
310                ProviderReceipt<Self::Provider>,
311                Option<Arc<Vec<ProviderReceipt<Self::Provider>>>>,
312                Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
313            )>,
314            Self::Error,
315        >,
316    > + Send
317    where
318        Self: 'static,
319    {
320        async move {
321            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
322                let Some(tx) = cached.recovered_transaction().map(|tx| tx.cloned())
323            {
324                let meta = cached.transaction_meta(hash);
325
326                // Best case: receipts are also cached.
327                if let Some(all_receipts) = cached.receipts.clone() &&
328                    let Some(receipt) = all_receipts.get(cached.tx_index).cloned()
329                {
330                    return Ok(Some((tx, meta, receipt, Some(all_receipts), Some(cached.block))));
331                }
332
333                // Block still cached but receipts evicted — fetch via cache since
334                // `build_transaction_receipt` needs all receipts for gas accounting
335                // anyway.
336                if let Some(receipts) = self
337                    .cache()
338                    .get_receipts(cached.block.hash())
339                    .await
340                    .map_err(Self::Error::from_eth_err)? &&
341                    let Some(receipt) = receipts.get(cached.tx_index).cloned()
342                {
343                    return Ok(Some((tx, meta, receipt, Some(receipts), Some(cached.block))));
344                }
345            }
346
347            // Full cache miss — fetch both from provider.
348            self.spawn_blocking_io(move |this| {
349                let provider = this.provider();
350                let Some((tx, meta)) = provider
351                    .transaction_by_hash_with_meta(hash)
352                    .map_err(Self::Error::from_eth_err)?
353                else {
354                    return Ok(None);
355                };
356
357                let tx = tx.try_into_recovered_unchecked().map_err(Self::Error::from_eth_err)?;
358
359                let receipt = provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)?;
360
361                Ok(receipt.map(|receipt| (tx, meta, receipt, None, None)))
362            })
363            .await
364        }
365    }
366
367    /// Get transaction by [`BlockId`] and index of transaction within that block.
368    ///
369    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
370    fn transaction_by_block_and_tx_index(
371        &self,
372        block_id: BlockId,
373        index: usize,
374    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
375    where
376        Self: LoadBlock,
377    {
378        async move {
379            if let Some(block) = self.recovered_block(block_id).await? {
380                let block_hash = block.hash();
381                let block_number = block.number();
382                let block_timestamp = block.timestamp();
383                let base_fee_per_gas = block.base_fee_per_gas();
384                if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
385                    let tx_info = TransactionInfo {
386                        hash: Some(*tx.tx_hash()),
387                        block_hash: Some(block_hash),
388                        block_number: Some(block_number),
389                        block_timestamp: Some(block_timestamp),
390                        base_fee: base_fee_per_gas,
391                        index: Some(index as u64),
392                    };
393
394                    return Ok(Some(
395                        self.converter().fill(tx.clone().with_signer(*signer), tx_info)?,
396                    ))
397                }
398            }
399
400            Ok(None)
401        }
402    }
403
404    /// Find a transaction by sender's address and nonce.
405    fn get_transaction_by_sender_and_nonce(
406        &self,
407        sender: Address,
408        nonce: u64,
409        include_pending: bool,
410    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
411    where
412        Self: LoadBlock + LoadState,
413    {
414        async move {
415            // Check the pool first
416            if include_pending &&
417                let Some(tx) =
418                    RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
419            {
420                let transaction = tx.transaction.clone_into_consensus();
421                return Ok(Some(self.converter().fill_pending(transaction)?));
422            }
423
424            // Note: we can't optimize for contracts (account with code) and cannot shortcircuit if
425            // the address has code, because with 7702 EOAs can also have code
426
427            let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
428
429            // If the nonce is higher or equal to the highest nonce, the transaction is pending or
430            // not exists.
431            if nonce >= highest {
432                return Ok(None);
433            }
434
435            let high = self.provider().best_block_number().map_err(Self::Error::from_eth_err)?;
436
437            // Perform a binary search over the block range to find the block in which the sender's
438            // nonce reached the requested nonce.
439            let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
440                let mid_nonce =
441                    self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
442
443                Ok(mid_nonce > nonce)
444            })
445            .await?;
446
447            let block_id = num.into();
448            let Some(block) = self.recovered_block(block_id).await? else {
449                return Err(EthApiError::HeaderNotFound(block_id).into())
450            };
451
452            let block_hash = block.hash();
453            let block_number = block.number();
454            let block_timestamp = block.timestamp();
455            let base_fee_per_gas = block.base_fee_per_gas();
456
457            // EIP-7702 authorizations can consume the account's nonce without a transaction
458            // from that sender, so an existing block may contain no matching transaction.
459            block
460                .transactions_with_sender()
461                .enumerate()
462                .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
463                .map(|(index, (signer, tx))| {
464                    let tx_info = TransactionInfo {
465                        hash: Some(*tx.tx_hash()),
466                        block_hash: Some(block_hash),
467                        block_number: Some(block_number),
468                        block_timestamp: Some(block_timestamp),
469                        base_fee: base_fee_per_gas,
470                        index: Some(index as u64),
471                    };
472                    Ok(self.converter().fill(tx.clone().with_signer(*signer), tx_info)?)
473                })
474                .transpose()
475        }
476    }
477
478    /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block.
479    ///
480    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
481    fn raw_transaction_by_block_and_tx_index(
482        &self,
483        block_id: BlockId,
484        index: usize,
485    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
486    where
487        Self: LoadBlock,
488    {
489        async move {
490            if let Some(block) = self.recovered_block(block_id).await? &&
491                let Some(tx) = block.body().transactions().get(index)
492            {
493                return Ok(Some(tx.encoded_2718().into()))
494            }
495
496            Ok(None)
497        }
498    }
499
500    /// Signs transaction with a matching signer, if any and submits the transaction to the pool.
501    /// Returns the hash of the signed transaction.
502    fn send_transaction_request(
503        &self,
504        mut request: RpcTxReq<Self::NetworkTypes>,
505    ) -> impl Future<Output = Result<B256, Self::Error>> + Send
506    where
507        Self: EthApiSpec + LoadBlock + EstimateCall + LoadFee,
508    {
509        async move {
510            let from = match request.as_ref().from() {
511                Some(from) => from,
512                None => return Err(SignError::NoAccount.into_eth_err()),
513            };
514
515            if self.find_signer(&from).is_err() {
516                return Err(SignError::NoAccount.into_eth_err())
517            }
518
519            // set nonce if not already set before
520            if request.as_ref().nonce().is_none() {
521                let nonce = self.next_available_nonce_for(&request).await?;
522                request.as_mut().set_nonce(nonce);
523            }
524
525            let chain_id = self.chain_id();
526            // A caller that pins `chainId` is asserting which chain it means to sign for.
527            // Silently rewriting it would sign and submit on a different chain than asked
528            // for, so reject the mismatch instead. Matches geth's `setDefaults`.
529            if let Some(request_chain_id) = request.as_ref().chain_id() &&
530                request_chain_id != chain_id.to::<u64>()
531            {
532                return Err(EthApiError::InvalidParams(format!(
533                    "chainId does not match node's (have={request_chain_id}, want={})",
534                    chain_id.to::<u64>()
535                ))
536                .into())
537            }
538            request.as_mut().set_chain_id(chain_id.to());
539
540            // set fee defaults if not already set before, same as `fill_transaction`
541            if request.as_ref().gas_price().is_none() {
542                let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
543                    tip
544                } else {
545                    let tip = self.suggested_priority_fee().await?.to::<u128>();
546                    request.as_mut().set_max_priority_fee_per_gas(tip);
547                    tip
548                };
549                if request.as_ref().max_fee_per_gas().is_none() {
550                    let header =
551                        self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
552                    let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
553                    request.as_mut().set_max_fee_per_gas(base_fee as u128 * 2 + tip);
554                }
555            }
556
557            if request.as_ref().gas_limit().is_none() {
558                let estimated_gas = self
559                    .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
560                    .await?;
561                request.as_mut().set_gas_limit(estimated_gas.to());
562            }
563
564            let transaction = self.sign_request(&from, request).await?.with_signer(from);
565
566            let pool_transaction =
567                <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
568                    transaction,
569                )
570                .map_err(|e| {
571                    Self::Error::from_eth_err(TransactionConversionError::Other(e.to_string()))
572                })?;
573
574            // submit the transaction to the pool with a `Local` origin
575            let AddedTransactionOutcome { hash, .. } = self
576                .pool()
577                .add_transaction(TransactionOrigin::Local, pool_transaction)
578                .await
579                .map_err(Self::Error::from_eth_err)?;
580
581            Ok(hash)
582        }
583    }
584
585    /// Fills the defaults on a given unsigned transaction.
586    fn fill_transaction(
587        &self,
588        mut request: RpcTxReq<Self::NetworkTypes>,
589    ) -> impl Future<Output = Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error>> + Send
590    where
591        Self: EthApiSpec + LoadBlock + EstimateCall + LoadFee,
592    {
593        async move {
594            if request.as_ref().value().is_none() {
595                request.as_mut().set_value(U256::ZERO);
596            }
597
598            if request.as_ref().nonce().is_none() {
599                let nonce = self.next_available_nonce_for(&request).await?;
600                request.as_mut().set_nonce(nonce);
601            }
602
603            let chain_id = self.chain_id();
604            // A caller that pins `chainId` is asserting which chain it means to sign for.
605            // Silently rewriting it would sign and submit on a different chain than asked
606            // for, so reject the mismatch instead. Matches geth's `setDefaults`.
607            if let Some(request_chain_id) = request.as_ref().chain_id() &&
608                request_chain_id != chain_id.to::<u64>()
609            {
610                return Err(EthApiError::InvalidParams(format!(
611                    "chainId does not match node's (have={request_chain_id}, want={})",
612                    chain_id.to::<u64>()
613                ))
614                .into())
615            }
616            request.as_mut().set_chain_id(chain_id.to());
617
618            if request.as_ref().has_eip4844_fields() &&
619                request.as_ref().max_fee_per_blob_gas().is_none()
620            {
621                let blob_fee = self.blob_base_fee().await?;
622                request.as_mut().set_max_fee_per_blob_gas(blob_fee.to());
623            }
624
625            // Use `sidecar.is_some()` instead of `blob_sidecar().is_some()` to handle
626            // both EIP-4844 (v0) and EIP-7594 (v1) sidecar formats
627            if request.as_ref().sidecar.is_some() &&
628                request.as_ref().blob_versioned_hashes.is_none()
629            {
630                request.as_mut().populate_blob_hashes();
631            }
632
633            if request.as_ref().gas_limit().is_none() {
634                let estimated_gas = self
635                    .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
636                    .await?;
637                request.as_mut().set_gas_limit(estimated_gas.to());
638            }
639
640            if request.as_ref().gas_price().is_none() {
641                let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
642                    tip
643                } else {
644                    let tip = self.suggested_priority_fee().await?.to::<u128>();
645                    request.as_mut().set_max_priority_fee_per_gas(tip);
646                    tip
647                };
648                if request.as_ref().max_fee_per_gas().is_none() {
649                    let header =
650                        self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
651                    let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
652                    // Use `2 * base_fee` as headroom, matching go-ethereum's
653                    // `setLondonFeeDefaults`, so the transaction does not
654                    // become invalid if the base fee rises before it is
655                    // included. This does not increase the effective price the sender pays:
656                    // `max_fee_per_gas` is only an upper bound and the sender still pays
657                    // `base_fee + min(tip, max_fee_per_gas - base_fee)`.
658                    request.as_mut().set_max_fee_per_gas(base_fee as u128 * 2 + tip);
659                }
660            }
661
662            let tx = self.converter().build_simulate_v1_transaction(request)?;
663
664            let raw = tx.encoded_2718().into();
665
666            Ok(FillTransaction { raw, tx })
667        }
668    }
669
670    /// Signs a transaction, with configured signers.
671    fn sign_request(
672        &self,
673        from: &Address,
674        txn: RpcTxReq<Self::NetworkTypes>,
675    ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
676        async move {
677            self.find_signer(from)?
678                .sign_transaction(txn, from)
679                .await
680                .map_err(Self::Error::from_eth_err)
681        }
682    }
683
684    /// Signs given message. Returns the signature.
685    fn sign(
686        &self,
687        account: Address,
688        message: Bytes,
689    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
690        async move {
691            Ok(self
692                .find_signer(&account)?
693                .sign(account, &message)
694                .await
695                .map_err(Self::Error::from_eth_err)?
696                .as_bytes()
697                .into())
698        }
699    }
700
701    /// Signs a transaction request using the given account in request
702    /// Returns the EIP-2718 encoded signed transaction.
703    fn sign_transaction(
704        &self,
705        request: RpcTxReq<Self::NetworkTypes>,
706    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
707        async move {
708            let from = match request.as_ref().from() {
709                Some(from) => from,
710                None => return Err(SignError::NoAccount.into_eth_err()),
711            };
712
713            Ok(self.sign_request(&from, request).await?.encoded_2718().into())
714        }
715    }
716
717    /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
718    fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
719        Ok(self
720            .find_signer(&account)?
721            .sign_typed_data(account, data)
722            .map_err(Self::Error::from_eth_err)?
723            .as_bytes()
724            .into())
725    }
726
727    /// Returns the signer for the given account, if found in configured signers.
728    #[expect(clippy::type_complexity)]
729    fn find_signer(
730        &self,
731        account: &Address,
732    ) -> Result<
733        Box<dyn EthSigner<ProviderTx<Self::Provider>, RpcTxReq<Self::NetworkTypes>> + 'static>,
734        Self::Error,
735    > {
736        self.signers()
737            .read()
738            .iter()
739            .find(|signer| signer.is_signer_for(account))
740            .map(|signer| dyn_clone::clone_box(&**signer))
741            .ok_or_else(|| SignError::NoAccount.into_eth_err())
742    }
743}
744
745/// Loads a transaction from database.
746///
747/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC
748/// methods.
749pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
750    /// Decodes and recovers a raw transaction, sharing sender recovery when configured.
751    fn recover_raw_transaction<T: SignedTransaction>(&self, tx: &[u8]) -> EthResult<Recovered<T>> {
752        let transaction = decode_raw_transaction::<T>(tx)?;
753        let signer = match self.eth_api_settings().sender_recovery_cache.as_ref() {
754            Some(cache) => cache.recover(&transaction),
755            None => transaction.try_recover(),
756        }
757        .map_err(|_| EthApiError::InvalidTransactionSignature)?;
758        Ok(Recovered::new_unchecked(transaction, signer))
759    }
760
761    /// Decodes and recovers raw transactions in order, stopping at the first error.
762    ///
763    /// Uses [`Self::recover_raw_transaction`] to share sender recovery when configured.
764    fn recover_raw_transactions<T: SignedTransaction>(
765        &self,
766        txs: impl IntoIterator<Item = impl AsRef<[u8]>>,
767    ) -> EthResult<Vec<Recovered<T>>> {
768        txs.into_iter().map(|tx| self.recover_raw_transaction(tx.as_ref())).collect()
769    }
770
771    /// Returns the transaction by hash.
772    ///
773    /// Checks the pool and state.
774    ///
775    /// Returns `Ok(None)` if no matching transaction was found.
776    #[expect(clippy::complexity)]
777    fn transaction_by_hash(
778        &self,
779        hash: B256,
780    ) -> impl Future<
781        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
782    > + Send {
783        async move {
784            // First, try the RPC cache
785            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
786                let Some(source) = cached.to_transaction_source()
787            {
788                return Ok(Some(source));
789            }
790
791            // Cache miss - try to find the transaction on disk
792            if let Some((tx, meta)) = self
793                .spawn_blocking_io(move |this| {
794                    this.provider()
795                        .transaction_by_hash_with_meta(hash)
796                        .map_err(Self::Error::from_eth_err)
797                })
798                .await?
799            {
800                // Note: we assume this transaction is valid, because it's mined (or
801                // part of pending block) and already. We don't need to
802                // check for pre EIP-2 because this transaction could be pre-EIP-2.
803                let transaction = tx
804                    .try_into_recovered_unchecked()
805                    .map_err(|_| EthApiError::InvalidTransactionSignature)?;
806
807                return Ok(Some(TransactionSource::Block {
808                    transaction,
809                    index: meta.index,
810                    block_hash: meta.block_hash,
811                    block_number: meta.block_number,
812                    block_timestamp: meta.timestamp,
813                    base_fee: meta.base_fee,
814                }));
815            }
816
817            // tx not found on disk, check pool
818            if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus())
819            {
820                return Ok(Some(TransactionSource::Pool(tx.into())));
821            }
822
823            Ok(None)
824        }
825    }
826
827    /// Returns the transaction by including its corresponding [`BlockId`].
828    ///
829    /// Note: this supports pending transactions
830    #[expect(clippy::type_complexity)]
831    fn transaction_by_hash_at(
832        &self,
833        transaction_hash: B256,
834    ) -> impl Future<
835        Output = Result<
836            Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
837            Self::Error,
838        >,
839    > + Send {
840        async move {
841            Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
842                tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
843                tx @ TransactionSource::Block { block_hash, .. } => {
844                    (tx, BlockId::Hash(block_hash.into()))
845                }
846            }))
847        }
848    }
849
850    /// Fetches the transaction and the transaction's block
851    #[expect(clippy::type_complexity)]
852    fn transaction_and_block(
853        &self,
854        hash: B256,
855    ) -> impl Future<
856        Output = Result<
857            Option<(
858                TransactionSource<ProviderTx<Self::Provider>>,
859                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
860            )>,
861            Self::Error,
862        >,
863    > + Send {
864        async move {
865            Ok(self
866                .transaction_and_block_and_maybe_bal(hash)
867                .await?
868                .map(|(transaction, block, _)| (transaction, block)))
869        }
870    }
871
872    /// Fetches the transaction and the transaction's block, together with the block's cached
873    /// block access list, if any.
874    ///
875    /// The BAL is only returned if it is already cached, it is never fetched from the BAL store.
876    #[expect(clippy::type_complexity)]
877    fn transaction_and_block_and_maybe_bal(
878        &self,
879        hash: B256,
880    ) -> impl Future<
881        Output = Result<
882            Option<(
883                TransactionSource<ProviderTx<Self::Provider>>,
884                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
885                Option<Arc<DecodedBal<Arc<RevmBal>>>>,
886            )>,
887            Self::Error,
888        >,
889    > + Send {
890        async move {
891            let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
892                None => return Ok(None),
893                Some(res) => res,
894            };
895
896            // Note: this is always either hash or pending
897            let block_hash = match at {
898                BlockId::Hash(hash) => hash.block_hash,
899                _ => return Ok(None),
900            };
901            let block_and_bal = self
902                .cache()
903                .get_recovered_block_and_maybe_bal(block_hash)
904                .await
905                .map_err(Self::Error::from_eth_err)?;
906            Ok(block_and_bal.map(|(block, bal)| (transaction, block, bal)))
907        }
908    }
909}