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