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