reth_rpc/
otterscan.rs

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