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