anvil/eth/otterscan/
api.rs

1use crate::eth::{
2    EthApi,
3    error::{BlockchainError, Result},
4    macros::node_info,
5};
6use alloy_consensus::Transaction as TransactionTrait;
7use alloy_network::{
8    AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, BlockResponse,
9    TransactionResponse,
10};
11use alloy_primitives::{Address, B256, Bytes, U256};
12use alloy_rpc_types::{
13    Block, BlockId, BlockNumberOrTag as BlockNumber, BlockTransactions,
14    trace::{
15        otterscan::{
16            BlockDetails, ContractCreator, InternalOperation, OtsBlock, OtsBlockTransactions,
17            OtsReceipt, OtsSlimBlock, OtsTransactionReceipt, TraceEntry, TransactionsWithReceipts,
18        },
19        parity::{Action, CreateAction, CreateOutput, TraceOutput},
20    },
21};
22use futures::future::join_all;
23use itertools::Itertools;
24
25impl EthApi {
26    /// Otterscan currently requires this endpoint, even though it's not part of the `ots_*`.
27    /// Ref: <https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/useProvider.ts#L71>
28    ///
29    /// As a faster alternative to `eth_getBlockByNumber` (by excluding uncle block
30    /// information), which is not relevant in the context of an anvil node
31    pub async fn erigon_get_header_by_number(
32        &self,
33        number: BlockNumber,
34    ) -> Result<Option<AnyRpcBlock>> {
35        node_info!("ots_getApiLevel");
36
37        self.backend.block_by_number(number).await
38    }
39
40    /// As per the latest Otterscan source code, at least version 8 is needed.
41    /// Ref: <https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/params.ts#L1C2-L1C2>
42    pub async fn ots_get_api_level(&self) -> Result<u64> {
43        node_info!("ots_getApiLevel");
44
45        // as required by current otterscan's source code
46        Ok(8)
47    }
48
49    /// Trace internal ETH transfers, contracts creation (CREATE/CREATE2) and self-destructs for a
50    /// certain transaction.
51    pub async fn ots_get_internal_operations(&self, hash: B256) -> Result<Vec<InternalOperation>> {
52        node_info!("ots_getInternalOperations");
53
54        self.backend
55            .mined_transaction(hash)
56            .map(|tx| tx.ots_internal_operations())
57            .ok_or_else(|| BlockchainError::DataUnavailable)
58    }
59
60    /// Check if an ETH address contains code at a certain block number.
61    pub async fn ots_has_code(&self, address: Address, block_number: BlockNumber) -> Result<bool> {
62        node_info!("ots_hasCode");
63        let block_id = Some(BlockId::Number(block_number));
64        Ok(!self.get_code(address, block_id).await?.is_empty())
65    }
66
67    /// Trace a transaction and generate a trace call tree.
68    /// Converts the list of traces for a transaction into the expected Otterscan format.
69    ///
70    /// Follows format specified in the [`ots_traceTransaction`](https://docs.otterscan.io/api-docs/ots-api#ots_tracetransaction) spec.
71    pub async fn ots_trace_transaction(&self, hash: B256) -> Result<Vec<TraceEntry>> {
72        node_info!("ots_traceTransaction");
73        let traces = self
74            .backend
75            .trace_transaction(hash)
76            .await?
77            .into_iter()
78            .filter_map(|trace| TraceEntry::from_transaction_trace(&trace.trace))
79            .collect();
80        Ok(traces)
81    }
82
83    /// Given a transaction hash, returns its raw revert reason.
84    pub async fn ots_get_transaction_error(&self, hash: B256) -> Result<Bytes> {
85        node_info!("ots_getTransactionError");
86
87        if let Some(receipt) = self.backend.mined_transaction_receipt(hash)
88            && !receipt.inner.inner.as_receipt_with_bloom().receipt.status.coerce_status()
89        {
90            return Ok(receipt.out.map(|b| b.0.into()).unwrap_or(Bytes::default()));
91        }
92
93        Ok(Bytes::default())
94    }
95
96    /// For simplicity purposes, we return the entire block instead of emptying the values that
97    /// Otterscan doesn't want. This is the original purpose of the endpoint (to save bandwidth),
98    /// but it doesn't seem necessary in the context of an anvil node
99    pub async fn ots_get_block_details(
100        &self,
101        number: BlockNumber,
102    ) -> Result<BlockDetails<AnyRpcHeader>> {
103        node_info!("ots_getBlockDetails");
104
105        if let Some(block) = self.backend.block_by_number(number).await? {
106            let ots_block = self.build_ots_block_details(block).await?;
107            Ok(ots_block)
108        } else {
109            Err(BlockchainError::BlockNotFound)
110        }
111    }
112
113    /// For simplicity purposes, we return the entire block instead of emptying the values that
114    /// Otterscan doesn't want. This is the original purpose of the endpoint (to save bandwidth),
115    /// but it doesn't seem necessary in the context of an anvil node
116    pub async fn ots_get_block_details_by_hash(
117        &self,
118        hash: B256,
119    ) -> Result<BlockDetails<AnyRpcHeader>> {
120        node_info!("ots_getBlockDetailsByHash");
121
122        if let Some(block) = self.backend.block_by_hash(hash).await? {
123            let ots_block = self.build_ots_block_details(block).await?;
124            Ok(ots_block)
125        } else {
126            Err(BlockchainError::BlockNotFound)
127        }
128    }
129
130    /// Gets paginated transaction data for a certain block. Return data is similar to
131    /// eth_getBlockBy* + eth_getTransactionReceipt.
132    pub async fn ots_get_block_transactions(
133        &self,
134        number: u64,
135        page: usize,
136        page_size: usize,
137    ) -> Result<OtsBlockTransactions<AnyRpcTransaction, AnyRpcHeader>> {
138        node_info!("ots_getBlockTransactions");
139
140        match self.backend.block_by_number_full(number.into()).await? {
141            Some(block) => self.build_ots_block_tx(block, page, page_size).await,
142            None => Err(BlockchainError::BlockNotFound),
143        }
144    }
145
146    /// Address history navigation. searches backwards from certain point in time.
147    pub async fn ots_search_transactions_before(
148        &self,
149        address: Address,
150        block_number: u64,
151        page_size: usize,
152    ) -> Result<TransactionsWithReceipts<alloy_rpc_types::Transaction<AnyTxEnvelope>>> {
153        node_info!("ots_searchTransactionsBefore");
154
155        let best = self.backend.best_number();
156        // we go from given block (defaulting to best) down to first block
157        // considering only post-fork
158        let from = if block_number == 0 { best } else { block_number - 1 };
159        let to = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1);
160
161        let first_page = from >= best;
162        let mut last_page = false;
163
164        let mut res: Vec<_> = vec![];
165
166        for n in (to..=from).rev() {
167            if let Some(traces) = self.backend.mined_parity_trace_block(n) {
168                let hashes = traces
169                    .into_iter()
170                    .rev()
171                    .filter(|trace| trace.contains_address(address))
172                    .filter_map(|trace| trace.transaction_hash)
173                    .unique();
174
175                if res.len() >= page_size {
176                    break;
177                }
178
179                res.extend(hashes);
180            }
181
182            if n == to {
183                last_page = true;
184            }
185        }
186
187        self.build_ots_search_transactions(res, first_page, last_page).await
188    }
189
190    /// Address history navigation. searches forward from certain point in time.
191    pub async fn ots_search_transactions_after(
192        &self,
193        address: Address,
194        block_number: u64,
195        page_size: usize,
196    ) -> Result<TransactionsWithReceipts<alloy_rpc_types::Transaction<AnyTxEnvelope>>> {
197        node_info!("ots_searchTransactionsAfter");
198
199        let best = self.backend.best_number();
200        // we go from the first post-fork block, up to the tip
201        let first_block = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1);
202        let from = if block_number == 0 { first_block } else { block_number + 1 };
203        let to = best;
204
205        let mut first_page = from >= best;
206        let mut last_page = false;
207
208        let mut res: Vec<_> = vec![];
209
210        for n in from..=to {
211            if n == first_block {
212                last_page = true;
213            }
214
215            if let Some(traces) = self.backend.mined_parity_trace_block(n) {
216                let hashes = traces
217                    .into_iter()
218                    .rev()
219                    .filter(|trace| trace.contains_address(address))
220                    .filter_map(|trace| trace.transaction_hash)
221                    .unique();
222
223                if res.len() >= page_size {
224                    break;
225                }
226
227                res.extend(hashes);
228            }
229
230            if n == to {
231                first_page = true;
232            }
233        }
234
235        // Results are always sent in reverse chronological order, according to the Otterscan spec
236        res.reverse();
237        self.build_ots_search_transactions(res, first_page, last_page).await
238    }
239
240    /// Given a sender address and a nonce, returns the tx hash or null if not found. It returns
241    /// only the tx hash on success, you can use the standard eth_getTransactionByHash after that to
242    /// get the full transaction data.
243    pub async fn ots_get_transaction_by_sender_and_nonce(
244        &self,
245        address: Address,
246        nonce: U256,
247    ) -> Result<Option<B256>> {
248        node_info!("ots_getTransactionBySenderAndNonce");
249
250        let from = self.get_fork().map(|f| f.block_number() + 1).unwrap_or_default();
251        let to = self.backend.best_number();
252
253        for n in (from..=to).rev() {
254            if let Some(txs) = self.backend.mined_transactions_by_block_number(n.into()).await {
255                for tx in txs {
256                    if U256::from(tx.nonce()) == nonce && tx.from() == address {
257                        return Ok(Some(tx.tx_hash()));
258                    }
259                }
260            }
261        }
262
263        Ok(None)
264    }
265
266    /// Given an ETH contract address, returns the tx hash and the direct address who created the
267    /// contract.
268    pub async fn ots_get_contract_creator(&self, addr: Address) -> Result<Option<ContractCreator>> {
269        node_info!("ots_getContractCreator");
270
271        let from = self.get_fork().map(|f| f.block_number()).unwrap_or_default();
272        let to = self.backend.best_number();
273
274        // loop in reverse, since we want the latest deploy to the address
275        for n in (from..=to).rev() {
276            if let Some(traces) = self.backend.mined_parity_trace_block(n) {
277                for trace in traces.into_iter().rev() {
278                    match (trace.trace.action, trace.trace.result) {
279                        (
280                            Action::Create(CreateAction { from, .. }),
281                            Some(TraceOutput::Create(CreateOutput { address, .. })),
282                        ) if address == addr => {
283                            return Ok(Some(ContractCreator {
284                                hash: trace.transaction_hash.unwrap(),
285                                creator: from,
286                            }));
287                        }
288                        _ => {}
289                    }
290                }
291            }
292        }
293
294        Ok(None)
295    }
296    /// The response for ots_getBlockDetails includes an `issuance` object that requires computing
297    /// the total gas spent in a given block.
298    ///
299    /// The only way to do this with the existing API is to explicitly fetch all receipts, to get
300    /// their `gas_used`. This would be extremely inefficient in a real blockchain RPC, but we can
301    /// get away with that in this context.
302    ///
303    /// The [original spec](https://docs.otterscan.io/api-docs/ots-api#ots_getblockdetails)
304    /// also mentions we can hardcode `transactions` and `logsBloom` to an empty array to save
305    /// bandwidth, because fields weren't intended to be used in the Otterscan UI at this point.
306    ///
307    /// This has two problems though:
308    ///   - It makes the endpoint too specific to Otterscan's implementation
309    ///   - It breaks the abstraction built in `OtsBlock<TX>` which computes `transaction_count`
310    ///     based on the existing list.
311    ///
312    /// Therefore we keep it simple by keeping the data in the response
313    pub async fn build_ots_block_details(
314        &self,
315        block: AnyRpcBlock,
316    ) -> Result<BlockDetails<alloy_rpc_types::Header<AnyHeader>>> {
317        if block.transactions.is_uncle() {
318            return Err(BlockchainError::DataUnavailable);
319        }
320        let receipts_futs = block
321            .transactions
322            .hashes()
323            .map(|hash| async move { self.transaction_receipt(hash).await });
324
325        // fetch all receipts
326        let receipts = join_all(receipts_futs)
327            .await
328            .into_iter()
329            .map(|r| match r {
330                Ok(Some(r)) => Ok(r),
331                _ => Err(BlockchainError::DataUnavailable),
332            })
333            .collect::<Result<Vec<_>>>()?;
334
335        let total_fees = receipts
336            .iter()
337            .fold(0, |acc, receipt| acc + (receipt.gas_used as u128) * receipt.effective_gas_price);
338
339        let Block { header, uncles, transactions, withdrawals } = block.into_inner();
340
341        let block =
342            OtsSlimBlock { header, uncles, transaction_count: transactions.len(), withdrawals };
343
344        Ok(BlockDetails {
345            block,
346            total_fees: U256::from(total_fees),
347            // issuance has no meaningful value in anvil's backend. just default to 0
348            issuance: Default::default(),
349        })
350    }
351
352    /// Fetches all receipts for the blocks's transactions, as required by the
353    /// [`ots_getBlockTransactions`] endpoint spec, and returns the final response object.
354    ///
355    /// [`ots_getBlockTransactions`]: https://docs.otterscan.io/api-docs/ots-api#ots_getblocktransactions
356    pub async fn build_ots_block_tx(
357        &self,
358        mut block: AnyRpcBlock,
359        page: usize,
360        page_size: usize,
361    ) -> Result<OtsBlockTransactions<AnyRpcTransaction, AnyRpcHeader>> {
362        if block.transactions.is_uncle() {
363            return Err(BlockchainError::DataUnavailable);
364        }
365
366        block.transactions = match block.transactions() {
367            BlockTransactions::Full(txs) => BlockTransactions::Full(
368                txs.iter().skip(page * page_size).take(page_size).cloned().collect(),
369            ),
370            BlockTransactions::Hashes(txs) => BlockTransactions::Hashes(
371                txs.iter().skip(page * page_size).take(page_size).copied().collect(),
372            ),
373            BlockTransactions::Uncle => unreachable!(),
374        };
375
376        let receipt_futs = block.transactions.hashes().map(|hash| self.transaction_receipt(hash));
377
378        let receipts = join_all(receipt_futs.map(|r| async {
379            if let Ok(Some(r)) = r.await {
380                let block = self.block_by_number(r.block_number.unwrap().into()).await?;
381                let timestamp = block.ok_or(BlockchainError::BlockNotFound)?.header.timestamp;
382                let receipt = r.map_inner(OtsReceipt::from);
383                Ok(OtsTransactionReceipt { receipt, timestamp: Some(timestamp) })
384            } else {
385                Err(BlockchainError::BlockNotFound)
386            }
387        }))
388        .await
389        .into_iter()
390        .collect::<Result<Vec<_>>>()?;
391
392        let transaction_count = block.transactions().len();
393        let fullblock = OtsBlock { block: block.inner.clone(), transaction_count };
394
395        let ots_block_txs = OtsBlockTransactions { fullblock, receipts };
396
397        Ok(ots_block_txs)
398    }
399
400    pub async fn build_ots_search_transactions(
401        &self,
402        hashes: Vec<B256>,
403        first_page: bool,
404        last_page: bool,
405    ) -> Result<TransactionsWithReceipts<alloy_rpc_types::Transaction<AnyTxEnvelope>>> {
406        let txs_futs = hashes.iter().map(|hash| async { self.transaction_by_hash(*hash).await });
407
408        let txs = join_all(txs_futs)
409            .await
410            .into_iter()
411            .map(|t| match t {
412                Ok(Some(t)) => Ok(t.into_inner()),
413                _ => Err(BlockchainError::DataUnavailable),
414            })
415            .collect::<Result<Vec<_>>>()?;
416
417        let receipt_futs = hashes.iter().map(|hash| self.transaction_receipt(*hash));
418
419        let receipts = join_all(receipt_futs.map(|r| async {
420            if let Ok(Some(r)) = r.await {
421                let block = self.block_by_number(r.block_number.unwrap().into()).await?;
422                let timestamp = block.ok_or(BlockchainError::BlockNotFound)?.header.timestamp;
423                let receipt = r.map_inner(OtsReceipt::from);
424                Ok(OtsTransactionReceipt { receipt, timestamp: Some(timestamp) })
425            } else {
426                Err(BlockchainError::BlockNotFound)
427            }
428        }))
429        .await
430        .into_iter()
431        .collect::<Result<Vec<_>>>()?;
432
433        Ok(TransactionsWithReceipts { txs, receipts, first_page, last_page })
434    }
435}