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