reth_optimism_rpc/eth/
transaction.rs

1//! Loads and formats OP transaction RPC response.
2
3use alloy_consensus::{transaction::Recovered, SignableTransaction, Transaction as _};
4use alloy_primitives::{Bytes, Sealable, Sealed, Signature, B256};
5use alloy_rpc_types_eth::TransactionInfo;
6use op_alloy_consensus::OpTxEnvelope;
7use op_alloy_rpc_types::{OpTransactionRequest, Transaction};
8use reth_node_api::FullNodeComponents;
9use reth_optimism_primitives::{OpReceipt, OpTransactionSigned};
10use reth_rpc_eth_api::{
11    helpers::{EthSigner, EthTransactions, LoadTransaction, SpawnBlocking},
12    EthApiTypes, FromEthApiError, FullEthApiTypes, RpcNodeCore, RpcNodeCoreExt, TransactionCompat,
13};
14use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError};
15use reth_storage_api::{
16    BlockReader, BlockReaderIdExt, ProviderTx, ReceiptProvider, TransactionsProvider,
17};
18use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool};
19
20use crate::{eth::OpNodeCore, OpEthApi, OpEthApiError, SequencerClient};
21
22impl<N> EthTransactions for OpEthApi<N>
23where
24    Self: LoadTransaction<Provider: BlockReaderIdExt> + EthApiTypes<Error = OpEthApiError>,
25    N: OpNodeCore<Provider: BlockReader<Transaction = ProviderTx<Self::Provider>>>,
26{
27    fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner<ProviderTx<Self::Provider>>>>> {
28        self.inner.eth_api.signers()
29    }
30
31    /// Decodes and recovers the transaction and submits it to the pool.
32    ///
33    /// Returns the hash of the transaction.
34    async fn send_raw_transaction(&self, tx: Bytes) -> Result<B256, Self::Error> {
35        let recovered = recover_raw_transaction(&tx)?;
36        let pool_transaction = <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
37
38        // On optimism, transactions are forwarded directly to the sequencer to be included in
39        // blocks that it builds.
40        if let Some(client) = self.raw_tx_forwarder().as_ref() {
41            tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to sequencer");
42            let hash = client.forward_raw_transaction(&tx).await.inspect_err(|err| {
43                    tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
44                })?;
45
46            // Retain tx in local tx pool after forwarding, for local RPC usage.
47            let _ = self
48                .pool()
49                .add_transaction(TransactionOrigin::Local, pool_transaction)
50                .await.inspect_err(|err| {
51                    tracing::warn!(target: "rpc::eth", %err, %hash, "successfully sent tx to sequencer, but failed to persist in local tx pool");
52            });
53
54            return Ok(hash)
55        }
56
57        // submit the transaction to the pool with a `Local` origin
58        let hash = self
59            .pool()
60            .add_transaction(TransactionOrigin::Local, pool_transaction)
61            .await
62            .map_err(Self::Error::from_eth_err)?;
63
64        Ok(hash)
65    }
66}
67
68impl<N> LoadTransaction for OpEthApi<N>
69where
70    Self: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt,
71    N: OpNodeCore<Provider: TransactionsProvider, Pool: TransactionPool>,
72    Self::Pool: TransactionPool,
73{
74}
75
76impl<N> OpEthApi<N>
77where
78    N: OpNodeCore,
79{
80    /// Returns the [`SequencerClient`] if one is set.
81    pub fn raw_tx_forwarder(&self) -> Option<SequencerClient> {
82        self.inner.sequencer_client.clone()
83    }
84}
85
86impl<N> TransactionCompat<OpTransactionSigned> for OpEthApi<N>
87where
88    N: FullNodeComponents<Provider: ReceiptProvider<Receipt = OpReceipt>>,
89{
90    type Transaction = Transaction;
91    type Error = OpEthApiError;
92
93    fn fill(
94        &self,
95        tx: Recovered<OpTransactionSigned>,
96        tx_info: TransactionInfo,
97    ) -> Result<Self::Transaction, Self::Error> {
98        let mut tx = tx.convert::<OpTxEnvelope>();
99        let mut deposit_receipt_version = None;
100        let mut deposit_nonce = None;
101
102        if let OpTxEnvelope::Deposit(tx) = tx.inner_mut() {
103            // for depost tx we need to fetch the receipt
104            self.inner
105                .eth_api
106                .provider()
107                .receipt_by_hash(tx.tx_hash())
108                .map_err(Self::Error::from_eth_err)?
109                .inspect(|receipt| {
110                    if let OpReceipt::Deposit(receipt) = receipt {
111                        deposit_receipt_version = receipt.deposit_receipt_version;
112                        deposit_nonce = receipt.deposit_nonce;
113                    }
114                });
115
116            // For consistency with op-geth, we always return `0x0` for mint if it is
117            // missing This is because op-geth does not distinguish
118            // between null and 0, because this value is decoded from RLP where null is
119            // represented as 0
120            tx.inner_mut().mint = Some(tx.mint.unwrap_or_default());
121        }
122
123        let TransactionInfo {
124            block_hash, block_number, index: transaction_index, base_fee, ..
125        } = tx_info;
126
127        let effective_gas_price = if tx.is_deposit() {
128            // For deposits, we must always set the `gasPrice` field to 0 in rpc
129            // deposit tx don't have a gas price field, but serde of `Transaction` will take care of
130            // it
131            0
132        } else {
133            base_fee
134                .map(|base_fee| {
135                    tx.effective_tip_per_gas(base_fee).unwrap_or_default() + base_fee as u128
136                })
137                .unwrap_or_else(|| tx.max_fee_per_gas())
138        };
139
140        Ok(Transaction {
141            inner: alloy_rpc_types_eth::Transaction {
142                inner: tx,
143                block_hash,
144                block_number,
145                transaction_index,
146                effective_gas_price: Some(effective_gas_price),
147            },
148            deposit_nonce,
149            deposit_receipt_version,
150        })
151    }
152
153    fn build_simulate_v1_transaction(
154        &self,
155        request: alloy_rpc_types_eth::TransactionRequest,
156    ) -> Result<OpTransactionSigned, Self::Error> {
157        let request: OpTransactionRequest = request.into();
158        let Ok(tx) = request.build_typed_tx() else {
159            return Err(OpEthApiError::Eth(EthApiError::TransactionConversionError))
160        };
161
162        // Create an empty signature for the transaction.
163        let signature = Signature::new(Default::default(), Default::default(), false);
164        Ok(tx.into_signed(signature).into())
165    }
166
167    fn otterscan_api_truncate_input(tx: &mut Self::Transaction) {
168        let input = match tx.inner.inner.inner_mut() {
169            OpTxEnvelope::Eip1559(tx) => &mut tx.tx_mut().input,
170            OpTxEnvelope::Eip2930(tx) => &mut tx.tx_mut().input,
171            OpTxEnvelope::Legacy(tx) => &mut tx.tx_mut().input,
172            OpTxEnvelope::Eip7702(tx) => &mut tx.tx_mut().input,
173            OpTxEnvelope::Deposit(tx) => {
174                let (mut deposit, hash) = std::mem::replace(
175                    tx,
176                    Sealed::new_unchecked(Default::default(), Default::default()),
177                )
178                .split();
179                deposit.input = deposit.input.slice(..4);
180                let mut deposit = deposit.seal_unchecked(hash);
181                std::mem::swap(tx, &mut deposit);
182                return
183            }
184        };
185        *input = input.slice(..4);
186    }
187}