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#[derive(Debug)]
33pub struct OtterscanApi<Eth> {
34 eth: Eth,
35}
36
37impl<Eth> OtterscanApi<Eth> {
38 pub const fn new(eth: Eth) -> Self {
40 Self { eth }
41 }
42}
43
44impl<Eth> OtterscanApi<Eth>
45where
46 Eth: FullEthApiTypes,
47{
48 fn block_details(
50 &self,
51 block: RpcBlock<Eth::NetworkTypes>,
52 receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
53 ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
54 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 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 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 async fn get_api_level(&self) -> RpcResult<u64> {
95 Ok(API_LEVEL)
96 }
97
98 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 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 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 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 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 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 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 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 let BlockTransactions::Full(transactions) = &mut block.transactions else {
235 return Err(internal_rpc_err("block is not full"));
236 };
237
238 let page_end = tx_len.saturating_sub(page_number * page_size);
240 let page_start = page_end.saturating_sub(page_size);
241
242 *transactions = transactions.drain(page_start..page_end).collect::<Vec<_>>();
244
245 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 let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
280 block.fullblock.transaction_count = tx_len;
281 Ok(block)
282 }
283
284 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 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 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 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 let found = traces.and_then(|traces| traces.first().copied());
382 Ok(found)
383 }
384}