Skip to main content

anvil/eth/otterscan/
api.rs

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