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, LoadReceipt, LoadState, SpawnBlocking};
5use crate::{
6    helpers::estimate::EstimateCall, FromEthApiError, FullEthApiTypes, IntoEthApiError,
7    RpcNodeCore, RpcNodeCoreExt, RpcReceipt, RpcTransaction,
8};
9use alloy_consensus::{transaction::TransactionMeta, BlockHeader, Transaction};
10use alloy_dyn_abi::TypedData;
11use alloy_eips::{eip2718::Encodable2718, BlockId};
12use alloy_network::TransactionBuilder;
13use alloy_primitives::{Address, Bytes, TxHash, B256};
14use alloy_rpc_types_eth::{transaction::TransactionRequest, BlockNumberOrTag, TransactionInfo};
15use futures::Future;
16use reth_node_api::BlockBody;
17use reth_primitives_traits::{RecoveredBlock, SignedTransaction};
18use reth_provider::{
19    BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
20    TransactionsProvider,
21};
22use reth_rpc_eth_types::{utils::binary_search, EthApiError, SignError, TransactionSource};
23use reth_rpc_types_compat::transaction::TransactionCompat;
24use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool};
25use std::sync::Arc;
26
27/// Transaction related functions for the [`EthApiServer`](crate::EthApiServer) trait in
28/// the `eth_` namespace.
29///
30/// This includes utilities for transaction tracing, transacting and inspection.
31///
32/// Async functions that are spawned onto the
33/// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool) begin with `spawn_`
34///
35/// ## Calls
36///
37/// There are subtle differences between when transacting [`TransactionRequest`]:
38///
39/// The endpoints `eth_call` and `eth_estimateGas` and `eth_createAccessList` should always
40/// __disable__ the base fee check in the [`CfgEnv`](revm::context::CfgEnv).
41///
42/// The behaviour for tracing endpoints is not consistent across clients.
43/// Geth also disables the basefee check for tracing: <https://github.com/ethereum/go-ethereum/blob/bc0b87ca196f92e5af49bd33cc190ef0ec32b197/eth/tracers/api.go#L955-L955>
44/// Erigon does not: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/transactions/tracing.go#L209-L209>
45///
46/// See also <https://github.com/paradigmxyz/reth/issues/6240>
47///
48/// This implementation follows the behaviour of Geth and disables the basefee check for tracing.
49pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
50    /// Returns a handle for signing data.
51    ///
52    /// Singer access in default (L1) trait method implementations.
53    #[expect(clippy::type_complexity)]
54    fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner<ProviderTx<Self::Provider>>>>>;
55
56    /// Decodes and recovers the transaction and submits it to the pool.
57    ///
58    /// Returns the hash of the transaction.
59    fn send_raw_transaction(
60        &self,
61        tx: Bytes,
62    ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
63
64    /// Returns the transaction by hash.
65    ///
66    /// Checks the pool and state.
67    ///
68    /// Returns `Ok(None)` if no matching transaction was found.
69    #[expect(clippy::complexity)]
70    fn transaction_by_hash(
71        &self,
72        hash: B256,
73    ) -> impl Future<
74        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
75    > + Send {
76        LoadTransaction::transaction_by_hash(self, hash)
77    }
78
79    /// Get all transactions in the block with the given hash.
80    ///
81    /// Returns `None` if block does not exist.
82    #[expect(clippy::type_complexity)]
83    fn transactions_by_block(
84        &self,
85        block: B256,
86    ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
87    {
88        async move {
89            self.cache()
90                .get_recovered_block(block)
91                .await
92                .map(|b| b.map(|b| b.body().transactions().to_vec()))
93                .map_err(Self::Error::from_eth_err)
94        }
95    }
96
97    /// Returns the EIP-2718 encoded transaction by hash.
98    ///
99    /// If this is a pooled EIP-4844 transaction, the blob sidecar is included.
100    ///
101    /// Checks the pool and state.
102    ///
103    /// Returns `Ok(None)` if no matching transaction was found.
104    fn raw_transaction_by_hash(
105        &self,
106        hash: B256,
107    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
108        async move {
109            // Note: this is mostly used to fetch pooled transactions so we check the pool first
110            if let Some(tx) =
111                self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
112            {
113                return Ok(Some(tx))
114            }
115
116            self.spawn_blocking_io(move |ref this| {
117                Ok(this
118                    .provider()
119                    .transaction_by_hash(hash)
120                    .map_err(Self::Error::from_eth_err)?
121                    .map(|tx| tx.encoded_2718().into()))
122            })
123            .await
124        }
125    }
126
127    /// Returns the _historical_ transaction and the block it was mined in
128    #[expect(clippy::type_complexity)]
129    fn historical_transaction_by_hash_at(
130        &self,
131        hash: B256,
132    ) -> impl Future<
133        Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
134    > + Send {
135        async move {
136            match self.transaction_by_hash_at(hash).await? {
137                None => Ok(None),
138                Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
139            }
140        }
141    }
142
143    /// Returns the transaction receipt for the given hash.
144    ///
145    /// Returns None if the transaction does not exist or is pending
146    /// Note: The tx receipt is not available for pending transactions.
147    fn transaction_receipt(
148        &self,
149        hash: B256,
150    ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
151    where
152        Self: LoadReceipt + 'static,
153    {
154        async move {
155            match self.load_transaction_and_receipt(hash).await? {
156                Some((tx, meta, receipt)) => {
157                    self.build_transaction_receipt(tx, meta, receipt).await.map(Some)
158                }
159                None => Ok(None),
160            }
161        }
162    }
163
164    /// Helper method that loads a transaction and its receipt.
165    #[expect(clippy::complexity)]
166    fn load_transaction_and_receipt(
167        &self,
168        hash: TxHash,
169    ) -> impl Future<
170        Output = Result<
171            Option<(ProviderTx<Self::Provider>, TransactionMeta, ProviderReceipt<Self::Provider>)>,
172            Self::Error,
173        >,
174    > + Send
175    where
176        Self: 'static,
177    {
178        let provider = self.provider().clone();
179        self.spawn_blocking_io(move |_| {
180            let (tx, meta) = match provider
181                .transaction_by_hash_with_meta(hash)
182                .map_err(Self::Error::from_eth_err)?
183            {
184                Some((tx, meta)) => (tx, meta),
185                None => return Ok(None),
186            };
187
188            let receipt = match provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)? {
189                Some(recpt) => recpt,
190                None => return Ok(None),
191            };
192
193            Ok(Some((tx, meta, receipt)))
194        })
195    }
196
197    /// Get transaction by [`BlockId`] and index of transaction within that block.
198    ///
199    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
200    fn transaction_by_block_and_tx_index(
201        &self,
202        block_id: BlockId,
203        index: usize,
204    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
205    where
206        Self: LoadBlock,
207    {
208        async move {
209            if let Some(block) = self.recovered_block(block_id).await? {
210                let block_hash = block.hash();
211                let block_number = block.number();
212                let base_fee_per_gas = block.base_fee_per_gas();
213                if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
214                    let tx_info = TransactionInfo {
215                        hash: Some(*tx.tx_hash()),
216                        block_hash: Some(block_hash),
217                        block_number: Some(block_number),
218                        base_fee: base_fee_per_gas,
219                        index: Some(index as u64),
220                    };
221
222                    return Ok(Some(
223                        self.tx_resp_builder().fill(tx.clone().with_signer(*signer), tx_info)?,
224                    ))
225                }
226            }
227
228            Ok(None)
229        }
230    }
231
232    /// Find a transaction by sender's address and nonce.
233    fn get_transaction_by_sender_and_nonce(
234        &self,
235        sender: Address,
236        nonce: u64,
237        include_pending: bool,
238    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
239    where
240        Self: LoadBlock + LoadState,
241    {
242        async move {
243            // Check the pool first
244            if include_pending {
245                if let Some(tx) =
246                    RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
247                {
248                    let transaction = tx.transaction.clone_into_consensus();
249                    return Ok(Some(self.tx_resp_builder().fill_pending(transaction)?));
250                }
251            }
252
253            // Check if the sender is a contract
254            if !self.get_code(sender, None).await?.is_empty() {
255                return Ok(None);
256            }
257
258            let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
259
260            // If the nonce is higher or equal to the highest nonce, the transaction is pending or
261            // not exists.
262            if nonce >= highest {
263                return Ok(None);
264            }
265
266            let Ok(high) = self.provider().best_block_number() else {
267                return Err(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()).into());
268            };
269
270            // Perform a binary search over the block range to find the block in which the sender's
271            // nonce reached the requested nonce.
272            let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
273                let mid_nonce =
274                    self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
275
276                Ok(mid_nonce > nonce)
277            })
278            .await?;
279
280            let block_id = num.into();
281            self.recovered_block(block_id)
282                .await?
283                .and_then(|block| {
284                    let block_hash = block.hash();
285                    let block_number = block.number();
286                    let base_fee_per_gas = block.base_fee_per_gas();
287
288                    block
289                        .transactions_with_sender()
290                        .enumerate()
291                        .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
292                        .map(|(index, (signer, tx))| {
293                            let tx_info = TransactionInfo {
294                                hash: Some(*tx.tx_hash()),
295                                block_hash: Some(block_hash),
296                                block_number: Some(block_number),
297                                base_fee: base_fee_per_gas,
298                                index: Some(index as u64),
299                            };
300                            self.tx_resp_builder().fill(tx.clone().with_signer(*signer), tx_info)
301                        })
302                })
303                .ok_or(EthApiError::HeaderNotFound(block_id))?
304                .map(Some)
305        }
306    }
307
308    /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block.
309    ///
310    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
311    fn raw_transaction_by_block_and_tx_index(
312        &self,
313        block_id: BlockId,
314        index: usize,
315    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
316    where
317        Self: LoadBlock,
318    {
319        async move {
320            if let Some(block) = self.recovered_block(block_id).await? {
321                if let Some(tx) = block.body().transactions().get(index) {
322                    return Ok(Some(tx.encoded_2718().into()))
323                }
324            }
325
326            Ok(None)
327        }
328    }
329
330    /// Signs transaction with a matching signer, if any and submits the transaction to the pool.
331    /// Returns the hash of the signed transaction.
332    fn send_transaction(
333        &self,
334        mut request: TransactionRequest,
335    ) -> impl Future<Output = Result<B256, Self::Error>> + Send
336    where
337        Self: EthApiSpec + LoadBlock + EstimateCall,
338    {
339        async move {
340            let from = match request.from {
341                Some(from) => from,
342                None => return Err(SignError::NoAccount.into_eth_err()),
343            };
344
345            if self.find_signer(&from).is_err() {
346                return Err(SignError::NoAccount.into_eth_err())
347            }
348
349            // set nonce if not already set before
350            if request.nonce.is_none() {
351                let nonce = self.next_available_nonce(from).await?;
352                request.nonce = Some(nonce);
353            }
354
355            let chain_id = self.chain_id();
356            request.chain_id = Some(chain_id.to());
357
358            let estimated_gas =
359                self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
360            let gas_limit = estimated_gas;
361            request.set_gas_limit(gas_limit.to());
362
363            let transaction = self.sign_request(&from, request).await?.with_signer(from);
364
365            let pool_transaction =
366                <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
367                    transaction,
368                )
369                .map_err(|_| EthApiError::TransactionConversionError)?;
370
371            // submit the transaction to the pool with a `Local` origin
372            let hash = self
373                .pool()
374                .add_transaction(TransactionOrigin::Local, pool_transaction)
375                .await
376                .map_err(Self::Error::from_eth_err)?;
377
378            Ok(hash)
379        }
380    }
381
382    /// Signs a transaction, with configured signers.
383    fn sign_request(
384        &self,
385        from: &Address,
386        txn: TransactionRequest,
387    ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
388        async move {
389            self.find_signer(from)?
390                .sign_transaction(txn, from)
391                .await
392                .map_err(Self::Error::from_eth_err)
393        }
394    }
395
396    /// Signs given message. Returns the signature.
397    fn sign(
398        &self,
399        account: Address,
400        message: Bytes,
401    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
402        async move {
403            Ok(self
404                .find_signer(&account)?
405                .sign(account, &message)
406                .await
407                .map_err(Self::Error::from_eth_err)?
408                .as_bytes()
409                .into())
410        }
411    }
412
413    /// Signs a transaction request using the given account in request
414    /// Returns the EIP-2718 encoded signed transaction.
415    fn sign_transaction(
416        &self,
417        request: TransactionRequest,
418    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
419        async move {
420            let from = match request.from {
421                Some(from) => from,
422                None => return Err(SignError::NoAccount.into_eth_err()),
423            };
424
425            Ok(self.sign_request(&from, request).await?.encoded_2718().into())
426        }
427    }
428
429    /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
430    fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
431        Ok(self
432            .find_signer(&account)?
433            .sign_typed_data(account, data)
434            .map_err(Self::Error::from_eth_err)?
435            .as_bytes()
436            .into())
437    }
438
439    /// Returns the signer for the given account, if found in configured signers.
440    #[expect(clippy::type_complexity)]
441    fn find_signer(
442        &self,
443        account: &Address,
444    ) -> Result<Box<(dyn EthSigner<ProviderTx<Self::Provider>> + 'static)>, Self::Error> {
445        self.signers()
446            .read()
447            .iter()
448            .find(|signer| signer.is_signer_for(account))
449            .map(|signer| dyn_clone::clone_box(&**signer))
450            .ok_or_else(|| SignError::NoAccount.into_eth_err())
451    }
452}
453
454/// Loads a transaction from database.
455///
456/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC
457/// methods.
458pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
459    /// Returns the transaction by hash.
460    ///
461    /// Checks the pool and state.
462    ///
463    /// Returns `Ok(None)` if no matching transaction was found.
464    #[expect(clippy::complexity)]
465    fn transaction_by_hash(
466        &self,
467        hash: B256,
468    ) -> impl Future<
469        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
470    > + Send {
471        async move {
472            // Try to find the transaction on disk
473            let mut resp = self
474                .spawn_blocking_io(move |this| {
475                    match this
476                        .provider()
477                        .transaction_by_hash_with_meta(hash)
478                        .map_err(Self::Error::from_eth_err)?
479                    {
480                        None => Ok(None),
481                        Some((tx, meta)) => {
482                            // Note: we assume this transaction is valid, because it's mined (or
483                            // part of pending block) and already. We don't need to
484                            // check for pre EIP-2 because this transaction could be pre-EIP-2.
485                            let transaction = tx
486                                .into_recovered_unchecked()
487                                .map_err(|_| EthApiError::InvalidTransactionSignature)?;
488
489                            let tx = TransactionSource::Block {
490                                transaction,
491                                index: meta.index,
492                                block_hash: meta.block_hash,
493                                block_number: meta.block_number,
494                                base_fee: meta.base_fee,
495                            };
496                            Ok(Some(tx))
497                        }
498                    }
499                })
500                .await?;
501
502            if resp.is_none() {
503                // tx not found on disk, check pool
504                if let Some(tx) =
505                    self.pool().get(&hash).map(|tx| tx.transaction.clone().into_consensus())
506                {
507                    resp = Some(TransactionSource::Pool(tx.into()));
508                }
509            }
510
511            Ok(resp)
512        }
513    }
514
515    /// Returns the transaction by including its corresponding [`BlockId`].
516    ///
517    /// Note: this supports pending transactions
518    #[expect(clippy::type_complexity)]
519    fn transaction_by_hash_at(
520        &self,
521        transaction_hash: B256,
522    ) -> impl Future<
523        Output = Result<
524            Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
525            Self::Error,
526        >,
527    > + Send {
528        async move {
529            Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
530                tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
531                tx @ TransactionSource::Block { block_hash, .. } => {
532                    (tx, BlockId::Hash(block_hash.into()))
533                }
534            }))
535        }
536    }
537
538    /// Fetches the transaction and the transaction's block
539    #[expect(clippy::type_complexity)]
540    fn transaction_and_block(
541        &self,
542        hash: B256,
543    ) -> impl Future<
544        Output = Result<
545            Option<(
546                TransactionSource<ProviderTx<Self::Provider>>,
547                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
548            )>,
549            Self::Error,
550        >,
551    > + Send {
552        async move {
553            let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
554                None => return Ok(None),
555                Some(res) => res,
556            };
557
558            // Note: this is always either hash or pending
559            let block_hash = match at {
560                BlockId::Hash(hash) => hash.block_hash,
561                _ => return Ok(None),
562            };
563            let block = self
564                .cache()
565                .get_recovered_block(block_hash)
566                .await
567                .map_err(Self::Error::from_eth_err)?;
568            Ok(block.map(|block| (transaction, block)))
569        }
570    }
571}