reth_rpc/
otterscan.rs

1use alloy_consensus::{BlockHeader, Transaction, Typed2718};
2use alloy_eips::{BlockId, BlockNumberOrTag};
3use alloy_network::{ReceiptResponse, TransactionResponse};
4use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
5use alloy_rpc_types_eth::{BlockTransactions, TransactionReceipt};
6use alloy_rpc_types_trace::{
7    otterscan::{
8        BlockDetails, ContractCreator, InternalOperation, OperationType, OtsBlockTransactions,
9        OtsReceipt, OtsTransactionReceipt, TraceEntry, TransactionsWithReceipts,
10    },
11    parity::{Action, CreateAction, CreateOutput, TraceOutput},
12};
13use async_trait::async_trait;
14use jsonrpsee::{core::RpcResult, types::ErrorObjectOwned};
15use reth_rpc_api::{EthApiServer, OtterscanServer};
16use reth_rpc_eth_api::{
17    helpers::{EthTransactions, TraceExt},
18    FullEthApiTypes, RpcBlock, RpcHeader, RpcReceipt, RpcTransaction, TransactionCompat,
19};
20use reth_rpc_eth_types::{utils::binary_search, EthApiError};
21use reth_rpc_server_types::result::internal_rpc_err;
22use revm_inspectors::{
23    tracing::{types::CallTraceNode, TracingInspectorConfig},
24    transfer::{TransferInspector, TransferKind},
25};
26use revm_primitives::{ExecutionResult, SignedAuthorization};
27
28const API_LEVEL: u64 = 8;
29
30/// Otterscan API.
31#[derive(Debug)]
32pub struct OtterscanApi<Eth> {
33    eth: Eth,
34}
35
36impl<Eth> OtterscanApi<Eth> {
37    /// Creates a new instance of `Otterscan`.
38    pub const fn new(eth: Eth) -> Self {
39        Self { eth }
40    }
41}
42
43impl<Eth> OtterscanApi<Eth>
44where
45    Eth: FullEthApiTypes,
46{
47    /// Constructs a `BlockDetails` from a block and its receipts.
48    fn block_details(
49        &self,
50        block: RpcBlock<Eth::NetworkTypes>,
51        receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
52    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
53        // blob fee is burnt, so we don't need to calculate it
54        let total_fees = receipts
55            .iter()
56            .map(|receipt| receipt.gas_used().saturating_mul(receipt.effective_gas_price()))
57            .sum::<u128>();
58
59        Ok(BlockDetails::new(block, Default::default(), U256::from(total_fees)))
60    }
61}
62
63#[async_trait]
64impl<Eth> OtterscanServer<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>
65    for OtterscanApi<Eth>
66where
67    Eth: EthApiServer<
68            RpcTransaction<Eth::NetworkTypes>,
69            RpcBlock<Eth::NetworkTypes>,
70            RpcReceipt<Eth::NetworkTypes>,
71            RpcHeader<Eth::NetworkTypes>,
72        > + EthTransactions
73        + TraceExt
74        + 'static,
75{
76    /// Handler for `{ots,erigon}_getHeaderByNumber`
77    async fn get_header_by_number(
78        &self,
79        block_number: u64,
80    ) -> RpcResult<Option<RpcHeader<Eth::NetworkTypes>>> {
81        self.eth.header_by_number(BlockNumberOrTag::Number(block_number)).await
82    }
83
84    /// Handler for `ots_hasCode`
85    async fn has_code(&self, address: Address, block_id: Option<BlockId>) -> RpcResult<bool> {
86        EthApiServer::get_code(&self.eth, address, block_id).await.map(|code| !code.is_empty())
87    }
88
89    /// Handler for `ots_getApiLevel`
90    async fn get_api_level(&self) -> RpcResult<u64> {
91        Ok(API_LEVEL)
92    }
93
94    /// Handler for `ots_getInternalOperations`
95    async fn get_internal_operations(&self, tx_hash: TxHash) -> RpcResult<Vec<InternalOperation>> {
96        let internal_operations = self
97            .eth
98            .spawn_trace_transaction_in_block_with_inspector(
99                tx_hash,
100                TransferInspector::new(false),
101                |_tx_info, inspector, _, _| Ok(inspector.into_transfers()),
102            )
103            .await
104            .map_err(Into::into)?
105            .map(|transfer_operations| {
106                transfer_operations
107                    .iter()
108                    .map(|op| InternalOperation {
109                        from: op.from,
110                        to: op.to,
111                        value: op.value,
112                        r#type: match op.kind {
113                            TransferKind::Call => OperationType::OpTransfer,
114                            TransferKind::Create => OperationType::OpCreate,
115                            TransferKind::Create2 => OperationType::OpCreate2,
116                            TransferKind::SelfDestruct => OperationType::OpSelfDestruct,
117                            TransferKind::EofCreate => OperationType::OpEofCreate,
118                        },
119                    })
120                    .collect::<Vec<_>>()
121            })
122            .unwrap_or_default();
123        Ok(internal_operations)
124    }
125
126    /// Handler for `ots_getTransactionError`
127    async fn get_transaction_error(&self, tx_hash: TxHash) -> RpcResult<Option<Bytes>> {
128        let maybe_revert = self
129            .eth
130            .spawn_replay_transaction(tx_hash, |_tx_info, res, _| match res.result {
131                ExecutionResult::Revert { output, .. } => Ok(Some(output)),
132                _ => Ok(None),
133            })
134            .await
135            .map(Option::flatten)
136            .map_err(Into::into)?;
137        Ok(maybe_revert)
138    }
139
140    /// Handler for `ots_traceTransaction`
141    async fn trace_transaction(&self, tx_hash: TxHash) -> RpcResult<Option<Vec<TraceEntry>>> {
142        let traces = self
143            .eth
144            .spawn_trace_transaction_in_block(
145                tx_hash,
146                TracingInspectorConfig::default_parity(),
147                move |_tx_info, inspector, _, _| Ok(inspector.into_traces().into_nodes()),
148            )
149            .await
150            .map_err(Into::into)?
151            .map(|traces| {
152                traces
153                    .into_iter()
154                    .map(|CallTraceNode { trace, .. }| TraceEntry {
155                        r#type: if trace.is_selfdestruct() {
156                            "SELFDESTRUCT".to_string()
157                        } else {
158                            trace.kind.to_string()
159                        },
160                        depth: trace.depth as u32,
161                        from: trace.caller,
162                        to: trace.address,
163                        value: trace.value,
164                        input: trace.data,
165                        output: trace.output,
166                    })
167                    .collect::<Vec<_>>()
168            });
169        Ok(traces)
170    }
171
172    /// Handler for `ots_getBlockDetails`
173    async fn get_block_details(
174        &self,
175        block_number: u64,
176    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
177        let block_id = block_number.into();
178        let block = self.eth.block_by_number(block_id, true);
179        let block_id = block_id.into();
180        let receipts = self.eth.block_receipts(block_id);
181        let (block, receipts) = futures::try_join!(block, receipts)?;
182        self.block_details(
183            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
184            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
185        )
186    }
187
188    /// Handler for `getBlockDetailsByHash`
189    async fn get_block_details_by_hash(
190        &self,
191        block_hash: B256,
192    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
193        let block = self.eth.block_by_hash(block_hash, true);
194        let block_id = block_hash.into();
195        let receipts = self.eth.block_receipts(block_id);
196        let (block, receipts) = futures::try_join!(block, receipts)?;
197        self.block_details(
198            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
199            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
200        )
201    }
202
203    /// Handler for `getBlockTransactions`
204    async fn get_block_transactions(
205        &self,
206        block_number: u64,
207        page_number: usize,
208        page_size: usize,
209    ) -> RpcResult<
210        OtsBlockTransactions<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>,
211    > {
212        let block_id = block_number.into();
213        // retrieve full block and its receipts
214        let block = self.eth.block_by_number(block_id, true);
215        let block_id = block_id.into();
216        let receipts = self.eth.block_receipts(block_id);
217        let (block, receipts) = futures::try_join!(block, receipts)?;
218
219        let mut block = block.ok_or(EthApiError::HeaderNotFound(block_id))?;
220        let mut receipts = receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?;
221
222        // check if the number of transactions matches the number of receipts
223        let tx_len = block.transactions.len();
224        if tx_len != receipts.len() {
225            return Err(internal_rpc_err(
226                "the number of transactions does not match the number of receipts",
227            ))
228        }
229
230        // make sure the block is full
231        let BlockTransactions::Full(transactions) = &mut block.transactions else {
232            return Err(internal_rpc_err("block is not full"));
233        };
234
235        // Crop page
236        let page_end = tx_len.saturating_sub(page_number * page_size);
237        let page_start = page_end.saturating_sub(page_size);
238
239        // Crop transactions
240        *transactions = transactions.drain(page_start..page_end).collect::<Vec<_>>();
241
242        // The input field returns only the 4 bytes method selector instead of the entire
243        // calldata byte blob
244        // See also: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/jsonrpc/otterscan_api.go#L610-L617>
245        for tx in transactions.iter_mut() {
246            if tx.input().len() > 4 {
247                Eth::TransactionCompat::otterscan_api_truncate_input(tx);
248            }
249        }
250
251        // Crop receipts and transform them into OtsTransactionReceipt
252        let timestamp = Some(block.header.timestamp());
253        let receipts = receipts
254            .drain(page_start..page_end)
255            .zip(transactions.iter().map(Typed2718::ty))
256            .map(|(receipt, tx_ty)| {
257                let inner = OtsReceipt {
258                    status: receipt.status(),
259                    cumulative_gas_used: receipt.cumulative_gas_used() as u64,
260                    logs: None,
261                    logs_bloom: None,
262                    r#type: tx_ty,
263                };
264
265                let receipt = TransactionReceipt {
266                    inner,
267                    transaction_hash: receipt.transaction_hash(),
268                    transaction_index: receipt.transaction_index(),
269                    block_hash: receipt.block_hash(),
270                    block_number: receipt.block_number(),
271                    gas_used: receipt.gas_used(),
272                    effective_gas_price: receipt.effective_gas_price(),
273                    blob_gas_used: receipt.blob_gas_used(),
274                    blob_gas_price: receipt.blob_gas_price(),
275                    from: receipt.from(),
276                    to: receipt.to(),
277                    contract_address: receipt.contract_address(),
278                    authorization_list: receipt
279                        .authorization_list()
280                        .map(<[SignedAuthorization]>::to_vec),
281                };
282
283                OtsTransactionReceipt { receipt, timestamp }
284            })
285            .collect();
286
287        // use `transaction_count` to indicate the paginate information
288        let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
289        block.fullblock.transaction_count = tx_len;
290        Ok(block)
291    }
292
293    /// Handler for `searchTransactionsBefore`
294    async fn search_transactions_before(
295        &self,
296        _address: Address,
297        _block_number: u64,
298        _page_size: usize,
299    ) -> RpcResult<TransactionsWithReceipts> {
300        Err(internal_rpc_err("unimplemented"))
301    }
302
303    /// Handler for `searchTransactionsAfter`
304    async fn search_transactions_after(
305        &self,
306        _address: Address,
307        _block_number: u64,
308        _page_size: usize,
309    ) -> RpcResult<TransactionsWithReceipts> {
310        Err(internal_rpc_err("unimplemented"))
311    }
312
313    /// Handler for `getTransactionBySenderAndNonce`
314    async fn get_transaction_by_sender_and_nonce(
315        &self,
316        sender: Address,
317        nonce: u64,
318    ) -> RpcResult<Option<TxHash>> {
319        Ok(self
320            .eth
321            .get_transaction_by_sender_and_nonce(sender, nonce, false)
322            .await
323            .map_err(Into::into)?
324            .map(|tx| tx.tx_hash()))
325    }
326
327    /// Handler for `getContractCreator`
328    async fn get_contract_creator(&self, address: Address) -> RpcResult<Option<ContractCreator>> {
329        if !self.has_code(address, None).await? {
330            return Ok(None);
331        }
332
333        let num = binary_search::<_, _, ErrorObjectOwned>(
334            1,
335            self.eth.block_number()?.saturating_to(),
336            |mid| {
337                Box::pin(async move {
338                    Ok(!EthApiServer::get_code(&self.eth, address, Some(mid.into()))
339                        .await?
340                        .is_empty())
341                })
342            },
343        )
344        .await?;
345
346        let traces = self
347            .eth
348            .trace_block_with(
349                num.into(),
350                None,
351                TracingInspectorConfig::default_parity(),
352                |tx_info, inspector, _, _, _| {
353                    Ok(inspector.into_parity_builder().into_localized_transaction_traces(tx_info))
354                },
355            )
356            .await
357            .map_err(Into::into)?
358            .map(|traces| {
359                traces
360                    .into_iter()
361                    .flatten()
362                    .map(|tx_trace| {
363                        let trace = tx_trace.trace;
364                        Ok(match (trace.action, trace.result, trace.error) {
365                            (
366                                Action::Create(CreateAction { from: creator, .. }),
367                                Some(TraceOutput::Create(CreateOutput {
368                                    address: contract, ..
369                                })),
370                                None,
371                            ) if contract == address => Some(ContractCreator {
372                                hash: tx_trace
373                                    .transaction_hash
374                                    .ok_or(EthApiError::TransactionNotFound)?,
375                                creator,
376                            }),
377                            _ => None,
378                        })
379                    })
380                    .filter_map(Result::transpose)
381                    .collect::<Result<Vec<_>, EthApiError>>()
382            })
383            .transpose()?;
384
385        // A contract maybe created and then destroyed in multiple transactions, here we
386        // return the first found transaction, this behavior is consistent with etherscan's
387        let found = traces.and_then(|traces| traces.first().copied());
388        Ok(found)
389    }
390}