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