Skip to main content

reth_rpc/
otterscan.rs

1use alloy_consensus::{BlockHeader, Typed2718};
2use alloy_eips::{eip1898::LenientBlockNumberOrTag, BlockId};
3use alloy_evm::block::calc::{base_block_reward, block_reward, ommer_reward};
4use alloy_network::{primitives::HeaderResponse, ReceiptResponse, TransactionResponse};
5use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
6use alloy_rpc_types_eth::{BlockTransactions, TransactionReceipt};
7use alloy_rpc_types_trace::{
8    otterscan::{
9        BlockDetails, ContractCreator, InternalIssuance, InternalOperation, OtsBlockTransactions,
10        OtsReceipt, OtsTransactionReceipt, TraceEntry, TransactionsWithReceipts,
11    },
12    parity::{Action, CreateAction, CreateOutput, LocalizedTransactionTrace, TraceOutput},
13};
14use async_trait::async_trait;
15use jsonrpsee::{core::RpcResult, types::ErrorObjectOwned};
16use reth_chainspec::ChainSpecProvider;
17use reth_primitives_traits::{BlockBody, TxTy};
18use reth_rpc_api::{EthApiServer, OtterscanServer};
19use reth_rpc_convert::RpcTxReq;
20use reth_rpc_eth_api::{
21    helpers::{EthTransactions, TraceExt},
22    FullEthApiTypes, RpcBlock, RpcHeader, RpcReceipt, RpcTransaction,
23};
24use reth_rpc_eth_types::{utils::binary_search, EthApiError};
25use reth_rpc_server_types::result::internal_rpc_err;
26use revm::context_interface::result::ExecutionResult;
27use revm_inspectors::{
28    otterscan::InternalOperationsInspector,
29    tracing::{
30        types::{CallKind, CallTraceNode},
31        TracingInspectorConfig,
32    },
33};
34use tokio::sync::OwnedSemaphorePermit;
35
36const API_LEVEL: u64 = 8;
37
38/// Otterscan API.
39#[derive(Debug)]
40pub struct OtterscanApi<Eth> {
41    eth: Eth,
42}
43
44impl<Eth> OtterscanApi<Eth> {
45    /// Creates a new instance of `Otterscan`.
46    pub const fn new(eth: Eth) -> Self {
47        Self { eth }
48    }
49}
50
51impl<Eth> OtterscanApi<Eth>
52where
53    Eth: FullEthApiTypes + TraceExt,
54{
55    /// Constructs a `BlockDetails` from a block and its receipts.
56    async fn block_details(
57        &self,
58        block: RpcBlock<Eth::NetworkTypes>,
59        receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
60    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
61        // Execution fees include the base fee; blob fees are not part of this field.
62        let total_fees = receipts
63            .iter()
64            .map(|receipt| {
65                U256::from(receipt.gas_used()) * U256::from(receipt.effective_gas_price())
66            })
67            .sum::<U256>();
68
69        let chain_spec = self.eth.provider().chain_spec();
70        let reward = if block.header.number() == 0 {
71            None
72        } else {
73            base_block_reward(&chain_spec, block.header.number())
74        };
75        let issuance = if let Some(reward) = reward {
76            let recovered = if block.uncles.is_empty() {
77                None
78            } else {
79                Some(
80                    self.eth
81                        .recovered_block(block.header.hash().into())
82                        .await
83                        .map_err(Into::<ErrorObjectOwned>::into)?
84                        .ok_or(EthApiError::HeaderNotFound(block.header.hash().into()))?,
85                )
86            };
87            let ommers =
88                recovered.as_ref().and_then(|block| block.body().ommers()).unwrap_or_default();
89            calculate_issuance(
90                reward,
91                block.header.number(),
92                ommers.iter().map(BlockHeader::number),
93            )
94        } else {
95            InternalIssuance::default()
96        };
97        Ok(BlockDetails::new(block, issuance, total_fees))
98    }
99
100    /// Acquires a permit to execute a tracing call.
101    ///
102    /// The permit should be moved into the spawned trace so it is held until the trace completes.
103    async fn acquire_trace_permit(&self) -> RpcResult<OwnedSemaphorePermit> {
104        Ok(self.eth.acquire_owned_tracing().await.map_err(|_| EthApiError::InternalEthError)?)
105    }
106}
107
108#[async_trait]
109impl<Eth> OtterscanServer<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>
110    for OtterscanApi<Eth>
111where
112    Eth: EthApiServer<
113            RpcTxReq<Eth::NetworkTypes>,
114            RpcTransaction<Eth::NetworkTypes>,
115            RpcBlock<Eth::NetworkTypes>,
116            RpcReceipt<Eth::NetworkTypes>,
117            RpcHeader<Eth::NetworkTypes>,
118            TxTy<Eth::Primitives>,
119        > + EthTransactions
120        + TraceExt
121        + 'static,
122{
123    /// Handler for `ots_getHeaderByNumber` and `erigon_getHeaderByNumber`
124    async fn get_header_by_number(
125        &self,
126        block_number: LenientBlockNumberOrTag,
127    ) -> RpcResult<Option<RpcHeader<Eth::NetworkTypes>>> {
128        self.eth.header_by_number(block_number.into()).await
129    }
130
131    /// Handler for `ots_hasCode`
132    async fn has_code(&self, address: Address, block_id: Option<BlockId>) -> RpcResult<bool> {
133        EthApiServer::get_code(&self.eth, address, block_id).await.map(|code| !code.is_empty())
134    }
135
136    /// Handler for `ots_getApiLevel`
137    async fn get_api_level(&self) -> RpcResult<u64> {
138        Ok(API_LEVEL)
139    }
140
141    /// Handler for `ots_getInternalOperations`
142    async fn get_internal_operations(&self, tx_hash: TxHash) -> RpcResult<Vec<InternalOperation>> {
143        let permit = self.acquire_trace_permit().await?;
144        self.eth
145            .spawn_trace_transaction_in_block_with_inspector(
146                tx_hash,
147                InternalOperationsInspector::default(),
148                move |_tx_info, inspector, _, _| {
149                    let _permit = permit;
150                    Ok(inspector.into_operations())
151                },
152            )
153            .await
154            .map_err(Into::into)
155            .map(Option::unwrap_or_default)
156    }
157
158    /// Handler for `ots_getTransactionError`
159    async fn get_transaction_error(&self, tx_hash: TxHash) -> RpcResult<Option<Bytes>> {
160        let permit = self.acquire_trace_permit().await?;
161        self.eth
162            .spawn_replay_transaction(tx_hash, move |_tx_info, res, _| {
163                let _permit = permit;
164                Ok(transaction_error(res.result))
165            })
166            .await
167            .map_err(Into::into)
168    }
169
170    /// Handler for `ots_traceTransaction`
171    async fn trace_transaction(&self, tx_hash: TxHash) -> RpcResult<Option<Vec<TraceEntry>>> {
172        let permit = self.acquire_trace_permit().await?;
173        self.eth
174            .spawn_trace_transaction_in_block(
175                tx_hash,
176                TracingInspectorConfig::default_parity(),
177                move |_tx_info, inspector, _, _| {
178                    let _permit = permit;
179                    Ok(otterscan_traces(inspector.into_traces().into_nodes()))
180                },
181            )
182            .await
183            .map_err(Into::into)
184    }
185
186    /// Handler for `ots_getBlockDetails`
187    async fn get_block_details(
188        &self,
189        block_number: LenientBlockNumberOrTag,
190    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
191        let block_number = block_number.into_inner();
192        let block = self.eth.block_by_number(block_number, false);
193        let block_id = block_number.into();
194        let receipts = self.eth.block_receipts(block_id);
195        let (block, receipts) = futures::try_join!(block, receipts)?;
196        self.block_details(
197            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
198            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
199        )
200        .await
201    }
202
203    /// Handler for `ots_getBlockDetailsByHash`
204    async fn get_block_details_by_hash(
205        &self,
206        block_hash: B256,
207    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
208        let block = self.eth.block_by_hash(block_hash, false);
209        let block_id = block_hash.into();
210        let receipts = self.eth.block_receipts(block_id);
211        let (block, receipts) = futures::try_join!(block, receipts)?;
212        self.block_details(
213            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
214            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
215        )
216        .await
217    }
218
219    /// Handler for `ots_getBlockTransactions`
220    async fn get_block_transactions(
221        &self,
222        block_number: LenientBlockNumberOrTag,
223        page_number: usize,
224        page_size: usize,
225    ) -> RpcResult<
226        OtsBlockTransactions<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>,
227    > {
228        let block_number = block_number.into_inner();
229        // retrieve full block and its receipts
230        let block = self.eth.block_by_number(block_number, true);
231        let block_id = block_number.into();
232        let receipts = self.eth.block_receipts(block_id);
233        let (block, receipts) = futures::try_join!(block, receipts)?;
234
235        let mut block = block.ok_or(EthApiError::HeaderNotFound(block_id))?;
236        let mut receipts = receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?;
237
238        // check if the number of transactions matches the number of receipts
239        let tx_len = block.transactions.len();
240        if tx_len != receipts.len() {
241            return Err(internal_rpc_err(
242                "the number of transactions does not match the number of receipts",
243            ))
244        }
245
246        // make sure the block is full
247        let BlockTransactions::Full(transactions) = &mut block.transactions else {
248            return Err(internal_rpc_err("block is not full"));
249        };
250
251        let page = block_transaction_page_range(tx_len, page_number, page_size);
252
253        // Crop transactions
254        *transactions = transactions.drain(page.clone()).collect::<Vec<_>>();
255
256        // Crop receipts and transform them into OtsTransactionReceipt
257        let timestamp = Some(block.header.timestamp());
258        let receipts = receipts
259            .drain(page)
260            .zip(transactions.iter().map(Typed2718::ty))
261            .map(|(receipt, tx_ty)| {
262                let inner = OtsReceipt {
263                    status: receipt.status(),
264                    cumulative_gas_used: receipt.cumulative_gas_used(),
265                    logs: None,
266                    logs_bloom: None,
267                    r#type: tx_ty,
268                };
269
270                let receipt = TransactionReceipt {
271                    inner,
272                    transaction_hash: receipt.transaction_hash(),
273                    transaction_index: receipt.transaction_index(),
274                    block_hash: receipt.block_hash(),
275                    block_number: receipt.block_number(),
276                    gas_used: receipt.gas_used(),
277                    effective_gas_price: receipt.effective_gas_price(),
278                    blob_gas_used: receipt.blob_gas_used(),
279                    blob_gas_price: receipt.blob_gas_price(),
280                    from: receipt.from(),
281                    to: receipt.to(),
282                    contract_address: receipt.contract_address(),
283                };
284
285                OtsTransactionReceipt { receipt, timestamp }
286            })
287            .collect();
288
289        // use `transaction_count` to indicate the paginate information
290        let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
291        block.fullblock.transaction_count = tx_len;
292        Ok(block)
293    }
294
295    /// Handler for `ots_searchTransactionsBefore`
296    async fn search_transactions_before(
297        &self,
298        _address: Address,
299        _block_number: LenientBlockNumberOrTag,
300        _page_size: usize,
301    ) -> RpcResult<TransactionsWithReceipts> {
302        Err(internal_rpc_err("unimplemented"))
303    }
304
305    /// Handler for `ots_searchTransactionsAfter`
306    async fn search_transactions_after(
307        &self,
308        _address: Address,
309        _block_number: LenientBlockNumberOrTag,
310        _page_size: usize,
311    ) -> RpcResult<TransactionsWithReceipts> {
312        Err(internal_rpc_err("unimplemented"))
313    }
314
315    /// Handler for `ots_getTransactionBySenderAndNonce`
316    async fn get_transaction_by_sender_and_nonce(
317        &self,
318        sender: Address,
319        nonce: u64,
320    ) -> RpcResult<Option<TxHash>> {
321        Ok(self
322            .eth
323            .get_transaction_by_sender_and_nonce(sender, nonce, false)
324            .await
325            .map_err(Into::into)?
326            .map(|tx| tx.tx_hash()))
327    }
328
329    /// Handler for `ots_getContractCreator`
330    async fn get_contract_creator(&self, address: Address) -> RpcResult<Option<ContractCreator>> {
331        if !self.has_code(address, None).await? {
332            return Ok(None);
333        }
334
335        let permit = self.acquire_trace_permit().await?;
336        let num = binary_search::<_, _, ErrorObjectOwned>(
337            1,
338            self.eth.block_number()?.saturating_to(),
339            |mid| {
340                Box::pin(async move {
341                    Ok(!EthApiServer::get_code(&self.eth, address, Some(mid.into()))
342                        .await?
343                        .is_empty())
344                })
345            },
346        )
347        .await?;
348
349        let traces = self
350            .eth
351            .trace_block_with(
352                num.into(),
353                None,
354                TracingInspectorConfig::default_parity(),
355                move |tx_info, mut ctx| {
356                    let _permit = &permit;
357                    Ok(ctx
358                        .take_inspector()
359                        .into_parity_builder()
360                        .into_localized_transaction_traces(tx_info))
361                },
362            )
363            .await
364            .map_err(Into::into)?
365            .map(|traces| find_contract_creator(address, traces))
366            .transpose()?;
367
368        // Code-presence search assumes a single deployment. It cannot reliably identify
369        // the first deployment of contracts that were destroyed and recreated.
370        Ok(traces.flatten())
371    }
372}
373
374/// Finds a creation that was not rolled back by its own frame or an enclosing frame.
375fn find_contract_creator(
376    address: Address,
377    transactions: Vec<Vec<LocalizedTransactionTrace>>,
378) -> Result<Option<ContractCreator>, EthApiError> {
379    for traces in transactions {
380        let mut reverted_path: Option<Vec<usize>> = None;
381        for tx_trace in traces {
382            let trace = tx_trace.trace;
383            // Parity traces are in preorder, so a failed frame precedes its entire subtree.
384            if reverted_path.as_ref().is_some_and(|path| trace.trace_address.starts_with(path)) {
385                continue
386            }
387            reverted_path = None;
388            if trace.error.is_some() {
389                reverted_path = Some(trace.trace_address);
390                continue
391            }
392            if let (
393                Action::Create(CreateAction { from: creator, .. }),
394                Some(TraceOutput::Create(CreateOutput { address: contract, .. })),
395            ) = (trace.action, trace.result) &&
396                contract == address
397            {
398                return Ok(Some(ContractCreator {
399                    hash: tx_trace.transaction_hash.ok_or(EthApiError::TransactionNotFound)?,
400                    creator,
401                }))
402            }
403        }
404    }
405    Ok(None)
406}
407
408/// Returns the transaction slice for an Otterscan block page.
409///
410/// Pages are selected from the end of the block, retaining block order within each page.
411/// The frontend reverses each page and uses this ordering for transaction-index links.
412const fn block_transaction_page_range(
413    tx_len: usize,
414    page_number: usize,
415    page_size: usize,
416) -> std::ops::Range<usize> {
417    let page_end = tx_len.saturating_sub(page_number.saturating_mul(page_size));
418    let page_start = page_end.saturating_sub(page_size);
419    page_start..page_end
420}
421
422/// Rewards issued to the miner (including ommer inclusion) and to the ommers themselves.
423fn calculate_issuance(
424    reward: u128,
425    number: u64,
426    ommer_numbers: impl ExactSizeIterator<Item = u64>,
427) -> InternalIssuance {
428    let block_reward = U256::from(block_reward(reward, ommer_numbers.len()));
429    let uncle_reward =
430        ommer_numbers.map(|ommer| U256::from(ommer_reward(reward, number, ommer))).sum::<U256>();
431    InternalIssuance { block_reward, uncle_reward, issuance: block_reward + uncle_reward }
432}
433
434fn transaction_error<H>(result: ExecutionResult<H>) -> Bytes {
435    match result {
436        ExecutionResult::Revert { output, .. } => output,
437        _ => Bytes::new(),
438    }
439}
440
441/// A self-destruct is a separate operation at the end of its enclosing call, after any children.
442fn otterscan_traces(nodes: Vec<CallTraceNode>) -> Vec<TraceEntry> {
443    let mut entries = Vec::with_capacity(nodes.len());
444    let mut selfdestructs: Vec<TraceEntry> = Vec::new();
445    for CallTraceNode { trace, .. } in nodes {
446        // The arena is in call-entry order. Emit completed calls' self-destructs before
447        // entering a sibling or returning to an ancestor.
448        while selfdestructs.last().is_some_and(|entry| entry.depth > trace.depth as u32) {
449            entries.push(selfdestructs.pop().expect("checked above"));
450        }
451        if let Some(to) = trace.selfdestruct_refund_target {
452            selfdestructs.push(TraceEntry {
453                r#type: "SELFDESTRUCT".to_string(),
454                depth: trace.depth as u32 + 1,
455                from: trace.selfdestruct_address.unwrap_or(trace.address),
456                to,
457                value: trace.selfdestruct_transferred_value,
458                input: Bytes::new(),
459                output: Bytes::new(),
460            });
461        }
462        entries.push(TraceEntry {
463            r#type: trace.kind.to_string(),
464            depth: trace.depth as u32,
465            from: trace.caller,
466            to: trace.address,
467            value: (!matches!(trace.kind, CallKind::StaticCall | CallKind::DelegateCall))
468                .then_some(trace.value),
469            input: trace.data,
470            output: trace.output,
471        });
472    }
473    entries.extend(selfdestructs.into_iter().rev());
474    entries
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use alloy_consensus::{constants::ETH_TO_WEI, Header};
481    use alloy_primitives::{hex, TxKind};
482    use alloy_rpc_types_trace::parity::TransactionTrace;
483    use reth_chainspec::MAINNET;
484    use reth_evm_ethereum::EthEvmConfig;
485    use reth_network_api::noop::NoopNetwork;
486    use reth_provider::test_utils::MockEthProvider;
487    use reth_transaction_pool::test_utils::testing_pool;
488    use revm::{
489        context::TxEnv,
490        context_interface::result::{HaltReason, Output, SuccessReason},
491        database::InMemoryDB,
492        inspector::InspectorEvmTr,
493        primitives::hardfork::SpecId,
494        state::{AccountInfo, Bytecode},
495        Context, InspectEvm, MainBuilder, MainContext,
496    };
497    use revm_inspectors::tracing::{types::CallTrace, TracingInspector};
498
499    #[test]
500    fn block_transaction_pages_match_frontend_index_navigation() {
501        // The frontend requests floor((count - 1 - index) / size), then reverses the page.
502        for count in [0, 1, 10, 25, 51] {
503            for size in [1, 10, 25] {
504                for index in 0..count {
505                    let reverse_index = count - 1 - index;
506                    let page = block_transaction_page_range(count, reverse_index / size, size);
507                    let displayed = page.rev().collect::<Vec<_>>();
508                    assert_eq!(displayed[reverse_index % size], index);
509                }
510            }
511        }
512        assert_eq!(block_transaction_page_range(25, 0, 10), 15..25);
513        assert_eq!(block_transaction_page_range(25, 1, 10), 5..15);
514        assert_eq!(block_transaction_page_range(25, 2, 10), 0..5);
515        assert_eq!(block_transaction_page_range(25, 3, 10), 0..0);
516        assert_eq!(block_transaction_page_range(25, usize::MAX, 10), 0..0);
517        assert_eq!(block_transaction_page_range(25, 0, usize::MAX), 0..25);
518        assert_eq!(block_transaction_page_range(25, 1, usize::MAX), 0..0);
519        assert_eq!(block_transaction_page_range(25, 0, 0), 25..25);
520    }
521
522    #[test]
523    fn transaction_error_returns_only_revert_data() {
524        let success: ExecutionResult = ExecutionResult::Success {
525            reason: SuccessReason::Return,
526            gas: Default::default(),
527            logs: vec![],
528            output: Output::Call(Bytes::from_static(b"successful return data")),
529        };
530        let halt = ExecutionResult::Halt {
531            reason: HaltReason::OutOfGas(revm::context_interface::result::OutOfGasError::Basic),
532            gas: Default::default(),
533            logs: vec![],
534        };
535        for result in [
536            success,
537            halt,
538            ExecutionResult::Revert { gas: Default::default(), logs: vec![], output: Bytes::new() },
539        ] {
540            assert_eq!(serde_json::to_value(Some(transaction_error(result))).unwrap(), "0x");
541        }
542        let output = Bytes::from_static(b"revert data");
543        assert_eq!(
544            transaction_error::<HaltReason>(ExecutionResult::Revert {
545                gas: Default::default(),
546                logs: vec![],
547                output: output.clone()
548            }),
549            output
550        );
551    }
552
553    async fn cache_empty_block(
554        cache: reth_rpc_eth_types::EthStateCache<reth_ethereum_primitives::EthPrimitives>,
555        block: reth_ethereum_primitives::Block,
556    ) {
557        // MockEthProvider does not implement recovered_block; seed the normal RPC cache.
558        let outcome = reth_execution_types::ExecutionOutcome::new(
559            Default::default(),
560            vec![vec![]],
561            block.header.number,
562            vec![],
563        );
564        let chain = reth_execution_types::Chain::new(
565            [reth_primitives_traits::RecoveredBlock::new_unhashed(block, vec![])],
566            outcome,
567            Default::default(),
568        );
569        reth_rpc_eth_types::cache::cache_new_blocks_task(
570            cache,
571            futures::stream::iter([reth_chain_state::CanonStateNotification::Commit {
572                new: std::sync::Arc::new(chain),
573            }]),
574        )
575        .await;
576    }
577
578    #[tokio::test]
579    async fn block_details_issuance_follows_chain_forks() {
580        for (spec, number, reward) in [
581            (0, 0),
582            (1, 5),
583            (4_369_999, 5),
584            (4_370_000, 3),
585            (7_279_999, 3),
586            (7_280_000, 2),
587            (15_537_393, 2),
588            (15_537_394, 0),
589        ]
590        .into_iter()
591        .map(|(number, reward)| (MAINNET.clone(), number, reward))
592        .chain(std::iter::once((
593            std::sync::Arc::new(
594                reth_chainspec::ChainSpecBuilder::mainnet().paris_activated().build(),
595            ),
596            1,
597            0,
598        ))) {
599            let provider = MockEthProvider::default().with_chain_spec((*spec).clone());
600            let header = Header { number, ..Default::default() };
601            let hash = header.hash_slow();
602            let block = reth_ethereum_primitives::Block { header, body: Default::default() };
603            provider.add_block(hash, block.clone());
604            provider.add_receipts(number, vec![]);
605            let api = OtterscanApi::new(
606                crate::eth::EthApiBuilder::new(
607                    provider,
608                    testing_pool(),
609                    NoopNetwork::default(),
610                    EthEvmConfig::new(spec),
611                )
612                .build(),
613            );
614            cache_empty_block(api.eth.cache().clone(), block).await;
615            let by_number = api
616                .get_block_details(alloy_eips::BlockNumberOrTag::Number(number).into())
617                .await
618                .unwrap();
619            let by_hash = api.get_block_details_by_hash(hash).await.unwrap();
620            assert_eq!(
621                by_number.issuance.block_reward,
622                U256::from(reward * ETH_TO_WEI),
623                "block {number}"
624            );
625            assert_eq!(by_number.issuance.uncle_reward, U256::ZERO);
626            assert_eq!(by_number.issuance.issuance, by_number.issuance.block_reward);
627            assert_eq!(by_number.issuance, by_hash.issuance);
628        }
629    }
630
631    #[tokio::test]
632    async fn block_details_loads_ommer_headers() {
633        let provider = MockEthProvider::default();
634        let block = reth_ethereum_primitives::Block {
635            header: Header { number: 126, ..Default::default() },
636            body: alloy_consensus::BlockBody {
637                ommers: vec![Header { number: 123, ..Default::default() }],
638                ..Default::default()
639            },
640        };
641        let hash = block.header.hash_slow();
642        provider.add_block(hash, block.clone());
643        provider.add_receipts(126, vec![]);
644        let api = OtterscanApi::new(
645            crate::eth::EthApiBuilder::new(
646                provider,
647                testing_pool(),
648                NoopNetwork::default(),
649                EthEvmConfig::new(MAINNET.clone()),
650            )
651            .build(),
652        );
653        cache_empty_block(api.eth.cache().clone(), block).await;
654        let expected = calculate_issuance(5 * ETH_TO_WEI, 126, [123].into_iter());
655        assert_eq!(
656            api.get_block_details(alloy_eips::BlockNumberOrTag::Number(126).into())
657                .await
658                .unwrap()
659                .issuance,
660            expected
661        );
662        assert_eq!(api.get_block_details_by_hash(hash).await.unwrap().issuance, expected);
663    }
664
665    #[tokio::test]
666    async fn unknown_transactions_and_unimplemented_history_remain_distinct() {
667        let api = OtterscanApi::new(
668            crate::eth::EthApiBuilder::new(
669                MockEthProvider::default(),
670                testing_pool(),
671                NoopNetwork::default(),
672                EthEvmConfig::new(MAINNET.clone()),
673            )
674            .build(),
675        );
676        assert_eq!(api.get_api_level().await.unwrap(), 8);
677        assert_eq!(api.get_transaction_error(B256::ZERO).await.unwrap(), None);
678        assert_eq!(api.trace_transaction(B256::ZERO).await.unwrap(), None);
679        assert!(api.get_internal_operations(B256::ZERO).await.unwrap().is_empty());
680        assert!(api
681            .get_block_details(alloy_eips::BlockNumberOrTag::Number(1).into())
682            .await
683            .is_err());
684        assert!(api
685            .get_block_transactions(alloy_eips::BlockNumberOrTag::Number(1).into(), 0, 10)
686            .await
687            .is_err());
688        for error in [
689            api.search_transactions_before(
690                Address::ZERO,
691                alloy_eips::BlockNumberOrTag::Number(0).into(),
692                10,
693            )
694            .await
695            .unwrap_err(),
696            api.search_transactions_after(
697                Address::ZERO,
698                alloy_eips::BlockNumberOrTag::Number(0).into(),
699                10,
700            )
701            .await
702            .unwrap_err(),
703        ] {
704            assert_eq!(error.code(), -32603);
705            assert_eq!(error.message(), "unimplemented");
706        }
707    }
708
709    #[tokio::test]
710    async fn trace_methods_wait_for_tracing_permit() {
711        use reth_rpc_eth_api::helpers::SpawnBlocking;
712        use std::time::Duration;
713
714        let api = OtterscanApi::new(
715            crate::eth::EthApiBuilder::new(
716                MockEthProvider::default(),
717                testing_pool(),
718                NoopNetwork::default(),
719                EthEvmConfig::new(MAINNET.clone()),
720            )
721            .proof_permits(1)
722            .build(),
723        );
724        let permit = api.eth.acquire_owned_tracing().await.unwrap();
725
726        let timeout = Duration::from_millis(100);
727        assert!(tokio::time::timeout(timeout, api.trace_transaction(B256::ZERO)).await.is_err());
728        assert!(tokio::time::timeout(timeout, api.get_transaction_error(B256::ZERO))
729            .await
730            .is_err());
731        assert!(tokio::time::timeout(timeout, api.get_internal_operations(B256::ZERO))
732            .await
733            .is_err());
734
735        drop(permit);
736        assert_eq!(api.trace_transaction(B256::ZERO).await.unwrap(), None);
737    }
738
739    #[tokio::test]
740    async fn execution_fees_use_full_u256_width() {
741        let api = OtterscanApi::new(
742            crate::eth::EthApiBuilder::new(
743                MockEthProvider::default(),
744                testing_pool(),
745                NoopNetwork::default(),
746                EthEvmConfig::new(MAINNET.clone()),
747            )
748            .build(),
749        );
750        let receipt = TransactionReceipt {
751            inner: alloy_consensus::ReceiptEnvelope::Legacy(
752                alloy_consensus::Receipt {
753                    status: true.into(),
754                    cumulative_gas_used: 2,
755                    logs: vec![],
756                }
757                .with_bloom(),
758            ),
759            transaction_hash: B256::ZERO,
760            transaction_index: Some(0),
761            block_hash: Some(B256::ZERO),
762            block_number: Some(0),
763            gas_used: 2,
764            effective_gas_price: u128::MAX,
765            blob_gas_used: None,
766            blob_gas_price: None,
767            from: Address::ZERO,
768            to: None,
769            contract_address: None,
770        };
771        let details =
772            api.block_details(Default::default(), vec![receipt.clone(), receipt]).await.unwrap();
773        assert_eq!(details.total_fees, U256::from(u128::MAX) * U256::from(4));
774    }
775
776    #[test]
777    fn issuance_includes_ommer_and_inclusion_rewards() {
778        // A Frontier reward with an ommer three blocks behind.
779        let issuance = calculate_issuance(5 * ETH_TO_WEI, 126, [123].into_iter());
780        assert_eq!(issuance.block_reward, U256::from(5_156_250_000_000_000_000u128));
781        assert_eq!(issuance.uncle_reward, U256::from(3_125_000_000_000_000_000u128));
782        assert_eq!(issuance.issuance, U256::from(8_281_250_000_000_000_000u128));
783        let issuance = calculate_issuance(2 * ETH_TO_WEI, 100, [99, 94].into_iter());
784        assert_eq!(issuance.block_reward, U256::from(2_125_000_000_000_000_000u128));
785        assert_eq!(issuance.uncle_reward, U256::from(2_250_000_000_000_000_000u128));
786    }
787
788    fn execute(code: Bytes, spec: SpecId) -> (ExecutionResult, Vec<TraceEntry>) {
789        let contract = Address::repeat_byte(0x11);
790        let mut db = InMemoryDB::default();
791        db.insert_account_info(
792            Address::ZERO,
793            AccountInfo { balance: U256::from(ETH_TO_WEI), ..Default::default() },
794        );
795        db.insert_account_info(
796            contract,
797            AccountInfo {
798                balance: U256::from(100),
799                code: Some(Bytecode::new_legacy(code)),
800                ..Default::default()
801            },
802        );
803        let mut evm = Context::mainnet()
804            .modify_cfg_chained(|cfg| cfg.spec = spec)
805            .with_db(db)
806            .build_mainnet_with_inspector(TracingInspector::new(
807                TracingInspectorConfig::default_parity(),
808            ));
809        let result = evm
810            .inspect_tx(TxEnv {
811                kind: TxKind::Call(contract),
812                value: U256::from(7),
813                gas_limit: 1_000_000,
814                ..Default::default()
815            })
816            .unwrap();
817        let (_, inspector) = evm.ctx_inspector();
818        (result.result, otterscan_traces(inspector.traces().nodes().to_vec()))
819    }
820
821    #[test]
822    fn selfdestruct_preserves_enclosing_call_and_beneficiary() {
823        for spec in [SpecId::SHANGHAI, SpecId::CANCUN] {
824            let (result, traces) = execute(hex!("6022ff").into(), spec);
825            assert!(result.is_success());
826            assert_eq!(traces.len(), 2);
827            assert_eq!(traces[0].r#type, "CALL");
828            assert_eq!(traces[0].depth, 0);
829            assert_eq!(traces[0].value, Some(U256::from(7)));
830            assert_eq!(traces[1].r#type, "SELFDESTRUCT");
831            assert_eq!(traces[1].depth, 1);
832            assert_eq!(traces[1].from, Address::repeat_byte(0x11));
833            assert_eq!(traces[1].to, Address::with_last_byte(0x22));
834            assert_eq!(traces[1].value, Some(U256::from(107)));
835        }
836    }
837
838    #[test]
839    fn selfdestruct_after_a_child_call_uses_enclosing_depth() {
840        // Call another account, then destroy the root contract. The destruction is a sibling
841        // of that call, not its child.
842        let (result, traces) =
843            execute(hex!("60006000600060006000603361fffff1506022ff").into(), SpecId::CANCUN);
844        assert!(result.is_success());
845        assert_eq!(traces.len(), 3);
846        assert_eq!(traces[1].r#type, "CALL");
847        assert_eq!(traces[1].depth, 1);
848        assert_eq!(traces[2].r#type, "SELFDESTRUCT");
849        assert_eq!(traces[2].depth, 1);
850        assert_eq!(traces[2].from, Address::repeat_byte(0x11));
851        assert_eq!(traces[2].to, Address::with_last_byte(0x22));
852    }
853
854    #[test]
855    fn selfdestructs_follow_children_and_precede_siblings() {
856        let nodes = [0, 1, 2, 1]
857            .into_iter()
858            .enumerate()
859            .map(|(idx, depth)| CallTraceNode {
860                trace: CallTrace {
861                    depth,
862                    address: Address::with_last_byte(idx as u8),
863                    selfdestruct_address: (idx < 3).then_some(Address::with_last_byte(idx as u8)),
864                    selfdestruct_refund_target: (idx < 3).then_some(Address::with_last_byte(0xff)),
865                    selfdestruct_transferred_value: (idx < 3).then_some(U256::ZERO),
866                    ..Default::default()
867                },
868                ..Default::default()
869            })
870            .collect();
871        let traces = otterscan_traces(nodes);
872        let order =
873            traces.iter().map(|trace| (trace.r#type.as_str(), trace.depth)).collect::<Vec<_>>();
874        assert_eq!(
875            order,
876            [
877                ("CALL", 0),
878                ("CALL", 1),
879                ("CALL", 2),
880                ("SELFDESTRUCT", 3),
881                ("SELFDESTRUCT", 2),
882                ("CALL", 1),
883                ("SELFDESTRUCT", 1)
884            ]
885        );
886        assert_eq!(
887            traces
888                .iter()
889                .filter(|trace| trace.r#type == "SELFDESTRUCT")
890                .map(|trace| trace.from)
891                .collect::<Vec<_>>(),
892            [Address::with_last_byte(2), Address::with_last_byte(1), Address::ZERO]
893        );
894    }
895
896    #[test]
897    fn precompile_calls_remain_in_transaction_traces() {
898        let (result, traces) =
899            execute(hex!("60006000600060006000600461fffff15000").into(), SpecId::CANCUN);
900        assert!(result.is_success());
901        assert_eq!(traces.len(), 2);
902        assert_eq!(traces[1].to, Address::with_last_byte(4));
903    }
904
905    #[test]
906    fn contract_creator_ignores_reverted_ancestors() {
907        let contract = Address::repeat_byte(0x11);
908        let creator = Address::repeat_byte(0x22);
909        let localize = |trace, hash| LocalizedTransactionTrace {
910            trace,
911            transaction_hash: Some(hash),
912            block_hash: None,
913            block_number: Some(1),
914            transaction_position: None,
915        };
916        let creation = TransactionTrace {
917            action: Action::Create(CreateAction { from: creator, ..Default::default() }),
918            result: Some(TraceOutput::Create(CreateOutput {
919                address: contract,
920                code: Bytes::new(),
921                gas_used: 0,
922            })),
923            trace_address: vec![0, 0],
924            ..Default::default()
925        };
926        for path in [vec![], vec![0]] {
927            let failed = TransactionTrace {
928                trace_address: path,
929                error: Some("Reverted".into()),
930                ..Default::default()
931            };
932            let reverted =
933                vec![localize(failed, B256::ZERO), localize(creation.clone(), B256::ZERO)];
934            assert_eq!(find_contract_creator(contract, vec![reverted.clone()]).unwrap(), None);
935            let successful = vec![localize(creation.clone(), B256::repeat_byte(1))];
936            assert_eq!(
937                find_contract_creator(contract, vec![reverted, successful]).unwrap(),
938                Some(ContractCreator { creator, hash: B256::repeat_byte(1) })
939            );
940        }
941        // A reverted sibling does not invalidate a subsequent successful creation.
942        let failed = TransactionTrace {
943            trace_address: vec![0],
944            error: Some("Reverted".into()),
945            ..Default::default()
946        };
947        let successful = TransactionTrace { trace_address: vec![1], ..creation };
948        assert!(find_contract_creator(
949            contract,
950            vec![vec![localize(failed, B256::ZERO), localize(successful, B256::ZERO)]]
951        )
952        .unwrap()
953        .is_some());
954    }
955
956    #[test]
957    fn static_and_delegate_calls_have_no_value() {
958        // STATICCALL, DELEGATECALL and CALLCODE to 0x22 do not transfer ETH.
959        let (result, traces) = execute(
960            hex!("6000600060006000602261fffffa506000600060006000602261fffff45060006000600060006001602261fffff25000").into(),
961            SpecId::CANCUN,
962        );
963        assert!(result.is_success());
964        assert_eq!(traces.len(), 4);
965        assert_eq!(traces[1].r#type, "STATICCALL");
966        assert_eq!(traces[1].value, None);
967        assert_eq!(traces[2].r#type, "DELEGATECALL");
968        assert_eq!(traces[2].value, None);
969        assert_eq!(traces[3].r#type, "CALLCODE");
970        assert_eq!(traces[3].value, Some(U256::from(1)));
971    }
972}