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_eips::{eip2718::Encodable2718, BlockId};
16use alloy_network::{TransactionBuilder, TransactionBuilder4844};
17use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
18use alloy_rpc_types_eth::TransactionInfo;
19use futures::{Future, StreamExt};
20use reth_chain_state::CanonStateSubscriptions;
21use reth_primitives_traits::{
22    BlockBody, Recovered, RecoveredBlock, SignedTransaction, TxTy, WithEncoded,
23};
24use reth_rpc_convert::{transaction::RpcConvert, RpcTxReq, TransactionConversionError};
25use reth_rpc_eth_types::{
26    block::convert_transaction_receipt,
27    utils::{binary_search, recover_raw_transaction},
28    EthApiError::{self, TransactionConfirmationTimeout},
29    FillTransaction, SignError, TransactionSource,
30};
31use reth_storage_api::{
32    BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
33    TransactionsProvider,
34};
35use reth_transaction_pool::{
36    AddedTransactionOutcome, PoolPooledTx, PoolTransaction, TransactionOrigin, TransactionPool,
37};
38use std::{sync::Arc, time::Duration};
39
40/// Transaction related functions for the [`EthApiServer`](crate::EthApiServer) trait in
41/// the `eth_` namespace.
42///
43/// This includes utilities for transaction tracing, transacting and inspection.
44///
45/// Async functions that are spawned onto the
46/// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool) begin with `spawn_`
47///
48/// ## Calls
49///
50/// There are subtle differences between when transacting [`RpcTxReq`]:
51///
52/// The endpoints `eth_call` and `eth_estimateGas` and `eth_createAccessList` should always
53/// __disable__ the base fee check in the [`CfgEnv`](revm::context::CfgEnv).
54///
55/// The behaviour for tracing endpoints is not consistent across clients.
56/// Geth also disables the basefee check for tracing: <https://github.com/ethereum/go-ethereum/blob/bc0b87ca196f92e5af49bd33cc190ef0ec32b197/eth/tracers/api.go#L955-L955>
57/// Erigon does not: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/transactions/tracing.go#L209-L209>
58///
59/// See also <https://github.com/paradigmxyz/reth/issues/6240>
60///
61/// This implementation follows the behaviour of Geth and disables the basefee check for tracing.
62pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
63    /// Returns a handle for signing data.
64    ///
65    /// Signer access in default (L1) trait method implementations.
66    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes>;
67
68    /// Returns a list of addresses owned by provider.
69    fn accounts(&self) -> Vec<Address> {
70        self.signers().read().iter().flat_map(|s| s.accounts()).collect()
71    }
72
73    /// Returns the timeout duration for `send_raw_transaction_sync` RPC method.
74    fn send_raw_transaction_sync_timeout(&self) -> Duration;
75
76    /// Decodes and recovers the transaction and submits it to the pool.
77    ///
78    /// Returns the hash of the transaction.
79    fn send_raw_transaction(
80        &self,
81        tx: Bytes,
82    ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
83        async move {
84            let recovered = recover_raw_transaction::<PoolPooledTx<Self::Pool>>(&tx)?;
85            self.send_transaction(TransactionOrigin::External, WithEncoded::new(tx, recovered))
86                .await
87        }
88    }
89
90    /// Submits the transaction to the pool with the given [`TransactionOrigin`].
91    fn send_transaction(
92        &self,
93        origin: TransactionOrigin,
94        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
95    ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
96
97    /// Decodes and recovers the transaction and submits it to the pool.
98    ///
99    /// And awaits the receipt.
100    fn send_raw_transaction_sync(
101        &self,
102        tx: Bytes,
103    ) -> impl Future<Output = Result<RpcReceipt<Self::NetworkTypes>, Self::Error>> + Send
104    where
105        Self: LoadReceipt + 'static,
106    {
107        let this = self.clone();
108        let timeout_duration = self.send_raw_transaction_sync_timeout();
109        async move {
110            let mut stream = this.provider().canonical_state_stream();
111            let hash = EthTransactions::send_raw_transaction(&this, tx).await?;
112            tokio::time::timeout(timeout_duration, async {
113                while let Some(notification) = stream.next().await {
114                    let chain = notification.committed();
115                    if let Some((block, tx, receipt, all_receipts)) =
116                        chain.find_transaction_and_receipt_by_hash(hash) &&
117                        let Some(receipt) = convert_transaction_receipt(
118                            block,
119                            all_receipts,
120                            tx,
121                            receipt,
122                            this.converter(),
123                        )
124                        .transpose()
125                        .map_err(Self::Error::from)?
126                    {
127                        return Ok(receipt);
128                    }
129                }
130                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
131                    hash,
132                    duration: timeout_duration,
133                }))
134            })
135            .await
136            .unwrap_or_else(|_elapsed| {
137                Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
138                    hash,
139                    duration: timeout_duration,
140                }))
141            })
142        }
143    }
144
145    /// Returns the transaction by hash.
146    ///
147    /// Checks the pool and state.
148    ///
149    /// Returns `Ok(None)` if no matching transaction was found.
150    #[expect(clippy::complexity)]
151    fn transaction_by_hash(
152        &self,
153        hash: B256,
154    ) -> impl Future<
155        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
156    > + Send {
157        LoadTransaction::transaction_by_hash(self, hash)
158    }
159
160    /// Get all transactions in the block with the given hash.
161    ///
162    /// Returns `None` if block does not exist.
163    #[expect(clippy::type_complexity)]
164    fn transactions_by_block(
165        &self,
166        block: B256,
167    ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
168    {
169        async move {
170            self.cache()
171                .get_recovered_block(block)
172                .await
173                .map(|b| b.map(|b| b.body().transactions().to_vec()))
174                .map_err(Self::Error::from_eth_err)
175        }
176    }
177
178    /// Returns the EIP-2718 encoded transaction by hash.
179    ///
180    /// If this is a pooled EIP-4844 transaction, the blob sidecar is included.
181    ///
182    /// Checks the pool and state.
183    ///
184    /// Returns `Ok(None)` if no matching transaction was found.
185    fn raw_transaction_by_hash(
186        &self,
187        hash: B256,
188    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
189        async move {
190            // Note: this is mostly used to fetch pooled transactions so we check the pool first
191            if let Some(tx) =
192                self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
193            {
194                return Ok(Some(tx))
195            }
196
197            self.spawn_blocking_io(move |ref this| {
198                Ok(this
199                    .provider()
200                    .transaction_by_hash(hash)
201                    .map_err(Self::Error::from_eth_err)?
202                    .map(|tx| tx.encoded_2718().into()))
203            })
204            .await
205        }
206    }
207
208    /// Returns the _historical_ transaction and the block it was mined in
209    #[expect(clippy::type_complexity)]
210    fn historical_transaction_by_hash_at(
211        &self,
212        hash: B256,
213    ) -> impl Future<
214        Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
215    > + Send {
216        async move {
217            match self.transaction_by_hash_at(hash).await? {
218                None => Ok(None),
219                Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
220            }
221        }
222    }
223
224    /// Returns the transaction receipt for the given hash.
225    ///
226    /// Returns None if the transaction does not exist or is pending
227    /// Note: The tx receipt is not available for pending transactions.
228    fn transaction_receipt(
229        &self,
230        hash: B256,
231    ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
232    where
233        Self: LoadReceipt + 'static,
234    {
235        async move {
236            match self.load_transaction_and_receipt(hash).await? {
237                Some((tx, meta, receipt)) => {
238                    self.build_transaction_receipt(tx, meta, receipt).await.map(Some)
239                }
240                None => Ok(None),
241            }
242        }
243    }
244
245    /// Helper method that loads a transaction and its receipt.
246    #[expect(clippy::complexity)]
247    fn load_transaction_and_receipt(
248        &self,
249        hash: TxHash,
250    ) -> impl Future<
251        Output = Result<
252            Option<(ProviderTx<Self::Provider>, TransactionMeta, ProviderReceipt<Self::Provider>)>,
253            Self::Error,
254        >,
255    > + Send
256    where
257        Self: 'static,
258    {
259        async move {
260            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
261                let Some(tx) = cached.block.body().transactions().get(cached.tx_index).cloned()
262            {
263                let meta = cached.transaction_meta(hash);
264
265                // Best case: receipts are also cached.
266                if let Some(receipt) = cached.receipt().cloned() {
267                    return Ok(Some((tx, meta, receipt)));
268                }
269
270                // Block still cached but receipts evicted — fetch via cache since
271                // the block is recent and `build_transaction_receipt` needs all
272                // receipts for gas accounting anyway.
273                if let Some(receipts) = self
274                    .cache()
275                    .get_receipts(cached.block.hash())
276                    .await
277                    .map_err(Self::Error::from_eth_err)? &&
278                    let Some(receipt) = receipts.get(cached.tx_index).cloned()
279                {
280                    return Ok(Some((tx, meta, receipt)));
281                }
282            }
283
284            // Full cache miss — fetch both from provider.
285            self.spawn_blocking_io(move |this| {
286                let provider = this.provider();
287                let Some((tx, meta)) = provider
288                    .transaction_by_hash_with_meta(hash)
289                    .map_err(Self::Error::from_eth_err)?
290                else {
291                    return Ok(None);
292                };
293
294                let receipt = provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)?;
295
296                Ok(receipt.map(|receipt| (tx, meta, receipt)))
297            })
298            .await
299        }
300    }
301
302    /// Get transaction by [`BlockId`] and index of transaction within that block.
303    ///
304    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
305    fn transaction_by_block_and_tx_index(
306        &self,
307        block_id: BlockId,
308        index: usize,
309    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
310    where
311        Self: LoadBlock,
312    {
313        async move {
314            if let Some(block) = self.recovered_block(block_id).await? {
315                let block_hash = block.hash();
316                let block_number = block.number();
317                let base_fee_per_gas = block.base_fee_per_gas();
318                if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
319                    #[allow(clippy::needless_update)]
320                    let tx_info = TransactionInfo {
321                        hash: Some(*tx.tx_hash()),
322                        block_hash: Some(block_hash),
323                        block_number: Some(block_number),
324                        base_fee: base_fee_per_gas,
325                        index: Some(index as u64),
326                        ..Default::default()
327                    };
328
329                    return Ok(Some(
330                        self.converter().fill(tx.clone().with_signer(*signer), tx_info)?,
331                    ))
332                }
333            }
334
335            Ok(None)
336        }
337    }
338
339    /// Find a transaction by sender's address and nonce.
340    fn get_transaction_by_sender_and_nonce(
341        &self,
342        sender: Address,
343        nonce: u64,
344        include_pending: bool,
345    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
346    where
347        Self: LoadBlock + LoadState,
348    {
349        async move {
350            // Check the pool first
351            if include_pending &&
352                let Some(tx) =
353                    RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
354            {
355                let transaction = tx.transaction.clone_into_consensus();
356                return Ok(Some(self.converter().fill_pending(transaction)?));
357            }
358
359            // Note: we can't optimize for contracts (account with code) and cannot shortcircuit if
360            // the address has code, because with 7702 EOAs can also have code
361
362            let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
363
364            // If the nonce is higher or equal to the highest nonce, the transaction is pending or
365            // not exists.
366            if nonce >= highest {
367                return Ok(None);
368            }
369
370            let high = self.provider().best_block_number().map_err(Self::Error::from_eth_err)?;
371
372            // Perform a binary search over the block range to find the block in which the sender's
373            // nonce reached the requested nonce.
374            let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
375                let mid_nonce =
376                    self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
377
378                Ok(mid_nonce > nonce)
379            })
380            .await?;
381
382            let block_id = num.into();
383            self.recovered_block(block_id)
384                .await?
385                .and_then(|block| {
386                    let block_hash = block.hash();
387                    let block_number = block.number();
388                    let base_fee_per_gas = block.base_fee_per_gas();
389
390                    block
391                        .transactions_with_sender()
392                        .enumerate()
393                        .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
394                        .map(|(index, (signer, tx))| {
395                            #[allow(clippy::needless_update)]
396                            let tx_info = TransactionInfo {
397                                hash: Some(*tx.tx_hash()),
398                                block_hash: Some(block_hash),
399                                block_number: Some(block_number),
400                                base_fee: base_fee_per_gas,
401                                index: Some(index as u64),
402                                ..Default::default()
403                            };
404                            Ok(self.converter().fill(tx.clone().with_signer(*signer), tx_info)?)
405                        })
406                })
407                .ok_or(EthApiError::HeaderNotFound(block_id))?
408                .map(Some)
409        }
410    }
411
412    /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block.
413    ///
414    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
415    fn raw_transaction_by_block_and_tx_index(
416        &self,
417        block_id: BlockId,
418        index: usize,
419    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
420    where
421        Self: LoadBlock,
422    {
423        async move {
424            if let Some(block) = self.recovered_block(block_id).await? &&
425                let Some(tx) = block.body().transactions().get(index)
426            {
427                return Ok(Some(tx.encoded_2718().into()))
428            }
429
430            Ok(None)
431        }
432    }
433
434    /// Signs transaction with a matching signer, if any and submits the transaction to the pool.
435    /// Returns the hash of the signed transaction.
436    fn send_transaction_request(
437        &self,
438        mut request: RpcTxReq<Self::NetworkTypes>,
439    ) -> impl Future<Output = Result<B256, Self::Error>> + Send
440    where
441        Self: EthApiSpec + LoadBlock + EstimateCall,
442    {
443        async move {
444            let from = match request.as_ref().from() {
445                Some(from) => from,
446                None => return Err(SignError::NoAccount.into_eth_err()),
447            };
448
449            if self.find_signer(&from).is_err() {
450                return Err(SignError::NoAccount.into_eth_err())
451            }
452
453            // set nonce if not already set before
454            if request.as_ref().nonce().is_none() {
455                let nonce = self.next_available_nonce(from).await?;
456                request.as_mut().set_nonce(nonce);
457            }
458
459            let chain_id = self.chain_id();
460            request.as_mut().set_chain_id(chain_id.to());
461
462            let estimated_gas =
463                self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
464            let gas_limit = estimated_gas;
465            request.as_mut().set_gas_limit(gas_limit.to());
466
467            let transaction = self.sign_request(&from, request).await?.with_signer(from);
468
469            let pool_transaction =
470                <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
471                    transaction,
472                )
473                .map_err(|e| {
474                    Self::Error::from_eth_err(TransactionConversionError::Other(e.to_string()))
475                })?;
476
477            // submit the transaction to the pool with a `Local` origin
478            let AddedTransactionOutcome { hash, .. } = self
479                .pool()
480                .add_transaction(TransactionOrigin::Local, pool_transaction)
481                .await
482                .map_err(Self::Error::from_eth_err)?;
483
484            Ok(hash)
485        }
486    }
487
488    /// Fills the defaults on a given unsigned transaction.
489    fn fill_transaction(
490        &self,
491        mut request: RpcTxReq<Self::NetworkTypes>,
492    ) -> impl Future<Output = Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error>> + Send
493    where
494        Self: EthApiSpec + LoadBlock + EstimateCall + LoadFee,
495    {
496        async move {
497            let from = match request.as_ref().from() {
498                Some(from) => from,
499                None => return Err(SignError::NoAccount.into_eth_err()),
500            };
501
502            if request.as_ref().value().is_none() {
503                request.as_mut().set_value(U256::ZERO);
504            }
505
506            if request.as_ref().nonce().is_none() {
507                let nonce = self.next_available_nonce(from).await?;
508                request.as_mut().set_nonce(nonce);
509            }
510
511            let chain_id = self.chain_id();
512            request.as_mut().set_chain_id(chain_id.to());
513
514            if request.as_ref().has_eip4844_fields() &&
515                request.as_ref().max_fee_per_blob_gas().is_none()
516            {
517                let blob_fee = self.blob_base_fee().await?;
518                request.as_mut().set_max_fee_per_blob_gas(blob_fee.to());
519            }
520
521            // Use `sidecar.is_some()` instead of `blob_sidecar().is_some()` to handle
522            // both EIP-4844 (v0) and EIP-7594 (v1) sidecar formats
523            if request.as_ref().sidecar.is_some() &&
524                request.as_ref().blob_versioned_hashes.is_none()
525            {
526                request.as_mut().populate_blob_hashes();
527            }
528
529            if request.as_ref().gas_limit().is_none() {
530                let estimated_gas =
531                    self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
532                request.as_mut().set_gas_limit(estimated_gas.to());
533            }
534
535            if request.as_ref().gas_price().is_none() {
536                let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
537                    tip
538                } else {
539                    let tip = self.suggested_priority_fee().await?.to::<u128>();
540                    request.as_mut().set_max_priority_fee_per_gas(tip);
541                    tip
542                };
543                if request.as_ref().max_fee_per_gas().is_none() {
544                    let header =
545                        self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
546                    let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
547                    request.as_mut().set_max_fee_per_gas(base_fee as u128 + tip);
548                }
549            }
550
551            let tx = self.converter().build_simulate_v1_transaction(request)?;
552
553            let raw = tx.encoded_2718().into();
554
555            Ok(FillTransaction { raw, tx })
556        }
557    }
558
559    /// Signs a transaction, with configured signers.
560    fn sign_request(
561        &self,
562        from: &Address,
563        txn: RpcTxReq<Self::NetworkTypes>,
564    ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
565        async move {
566            self.find_signer(from)?
567                .sign_transaction(txn, from)
568                .await
569                .map_err(Self::Error::from_eth_err)
570        }
571    }
572
573    /// Signs given message. Returns the signature.
574    fn sign(
575        &self,
576        account: Address,
577        message: Bytes,
578    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
579        async move {
580            Ok(self
581                .find_signer(&account)?
582                .sign(account, &message)
583                .await
584                .map_err(Self::Error::from_eth_err)?
585                .as_bytes()
586                .into())
587        }
588    }
589
590    /// Signs a transaction request using the given account in request
591    /// Returns the EIP-2718 encoded signed transaction.
592    fn sign_transaction(
593        &self,
594        request: RpcTxReq<Self::NetworkTypes>,
595    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
596        async move {
597            let from = match request.as_ref().from() {
598                Some(from) => from,
599                None => return Err(SignError::NoAccount.into_eth_err()),
600            };
601
602            Ok(self.sign_request(&from, request).await?.encoded_2718().into())
603        }
604    }
605
606    /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
607    fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
608        Ok(self
609            .find_signer(&account)?
610            .sign_typed_data(account, data)
611            .map_err(Self::Error::from_eth_err)?
612            .as_bytes()
613            .into())
614    }
615
616    /// Returns the signer for the given account, if found in configured signers.
617    #[expect(clippy::type_complexity)]
618    fn find_signer(
619        &self,
620        account: &Address,
621    ) -> Result<
622        Box<dyn EthSigner<ProviderTx<Self::Provider>, RpcTxReq<Self::NetworkTypes>> + 'static>,
623        Self::Error,
624    > {
625        self.signers()
626            .read()
627            .iter()
628            .find(|signer| signer.is_signer_for(account))
629            .map(|signer| dyn_clone::clone_box(&**signer))
630            .ok_or_else(|| SignError::NoAccount.into_eth_err())
631    }
632}
633
634/// Loads a transaction from database.
635///
636/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC
637/// methods.
638pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
639    /// Returns the transaction by hash.
640    ///
641    /// Checks the pool and state.
642    ///
643    /// Returns `Ok(None)` if no matching transaction was found.
644    #[expect(clippy::complexity)]
645    fn transaction_by_hash(
646        &self,
647        hash: B256,
648    ) -> impl Future<
649        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
650    > + Send {
651        async move {
652            // First, try the RPC cache
653            if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
654                let Some(source) = cached.to_transaction_source()
655            {
656                return Ok(Some(source));
657            }
658
659            // Cache miss - try to find the transaction on disk
660            if let Some((tx, meta)) = self
661                .spawn_blocking_io(move |this| {
662                    this.provider()
663                        .transaction_by_hash_with_meta(hash)
664                        .map_err(Self::Error::from_eth_err)
665                })
666                .await?
667            {
668                // Note: we assume this transaction is valid, because it's mined (or
669                // part of pending block) and already. We don't need to
670                // check for pre EIP-2 because this transaction could be pre-EIP-2.
671                let transaction = tx
672                    .try_into_recovered_unchecked()
673                    .map_err(|_| EthApiError::InvalidTransactionSignature)?;
674
675                return Ok(Some(TransactionSource::Block {
676                    transaction,
677                    index: meta.index,
678                    block_hash: meta.block_hash,
679                    block_number: meta.block_number,
680                    base_fee: meta.base_fee,
681                }));
682            }
683
684            // tx not found on disk, check pool
685            if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus())
686            {
687                return Ok(Some(TransactionSource::Pool(tx.into())));
688            }
689
690            Ok(None)
691        }
692    }
693
694    /// Returns the transaction by including its corresponding [`BlockId`].
695    ///
696    /// Note: this supports pending transactions
697    #[expect(clippy::type_complexity)]
698    fn transaction_by_hash_at(
699        &self,
700        transaction_hash: B256,
701    ) -> impl Future<
702        Output = Result<
703            Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
704            Self::Error,
705        >,
706    > + Send {
707        async move {
708            Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
709                tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
710                tx @ TransactionSource::Block { block_hash, .. } => {
711                    (tx, BlockId::Hash(block_hash.into()))
712                }
713            }))
714        }
715    }
716
717    /// Fetches the transaction and the transaction's block
718    #[expect(clippy::type_complexity)]
719    fn transaction_and_block(
720        &self,
721        hash: B256,
722    ) -> impl Future<
723        Output = Result<
724            Option<(
725                TransactionSource<ProviderTx<Self::Provider>>,
726                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
727            )>,
728            Self::Error,
729        >,
730    > + Send {
731        async move {
732            let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
733                None => return Ok(None),
734                Some(res) => res,
735            };
736
737            // Note: this is always either hash or pending
738            let block_hash = match at {
739                BlockId::Hash(hash) => hash.block_hash,
740                _ => return Ok(None),
741            };
742            let block = self
743                .cache()
744                .get_recovered_block(block_hash)
745                .await
746                .map_err(Self::Error::from_eth_err)?;
747            Ok(block.map(|block| (transaction, block)))
748        }
749    }
750}