diff --git a/Cargo.lock b/Cargo.lock index b5a351a1f490..6600f154f429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ dependencies = [ "futures", "hash-db", "hyper", + "itertools", "memory-db", "parking_lot", "pretty_assertions", diff --git a/anvil/Cargo.toml b/anvil/Cargo.toml index 4d56f56f107b..660481086801 100644 --- a/anvil/Cargo.toml +++ b/anvil/Cargo.toml @@ -55,6 +55,7 @@ serde = { version = "1", features = ["derive"] } thiserror = "1" yansi = "0.5" tempfile = "3" +itertools = "0.10" # cli clap = { version = "4", features = ["derive", "env", "wrap_help"], optional = true } diff --git a/anvil/core/src/eth/mod.rs b/anvil/core/src/eth/mod.rs index d0e5aa1f1fa7..c0ea52c65e70 100644 --- a/anvil/core/src/eth/mod.rs +++ b/anvil/core/src/eth/mod.rs @@ -600,6 +600,93 @@ pub enum EthRequest { /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content) #[cfg_attr(feature = "serde", serde(rename = "txpool_content", with = "empty_params"))] TxPoolContent(()), + + /// Otterscan's `ots_getApiLevel` endpoint + /// Otterscan currently requires this endpoint, even though it's not part of the ots_* + /// https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/useProvider.ts#L71 + /// Related upstream issue: https://github.com/otterscan/otterscan/issues/1081 + #[cfg_attr(feature = "serde", serde(rename = "erigon_getHeaderByNumber"))] + ErigonGetHeaderByNumber( + #[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number_seq"))] + BlockNumber, + ), + + /// Otterscan's `ots_getApiLevel` endpoint + /// Used as a simple API versioning scheme for the ots_* namespace + #[cfg_attr(feature = "serde", serde(rename = "ots_getApiLevel", with = "empty_params"))] + OtsGetApiLevel(()), + + /// Otterscan's `ots_getInternalOperations` endpoint + /// Traces internal ETH transfers, contracts creation (CREATE/CREATE2) and self-destructs for a + /// certain transaction. + #[cfg_attr(feature = "serde", serde(rename = "ots_getInternalOperations", with = "sequence"))] + OtsGetInternalOperations(H256), + + /// Otterscan's `ots_hasCode` endpoint + /// Check if an ETH address contains code at a certain block number. + #[cfg_attr(feature = "serde", serde(rename = "ots_hasCode"))] + OtsHasCode( + Address, + #[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number", default))] + BlockNumber, + ), + + /// Otterscan's `ots_traceTransaction` endpoint + /// Trace a transaction and generate a trace call tree. + #[cfg_attr(feature = "serde", serde(rename = "ots_traceTransaction", with = "sequence"))] + OtsTraceTransaction(H256), + + /// Otterscan's `ots_getTransactionError` endpoint + /// Given a transaction hash, returns its raw revert reason. + #[cfg_attr(feature = "serde", serde(rename = "ots_getTransactionError", with = "sequence"))] + OtsGetTransactionError(H256), + + /// Otterscan's `ots_getBlockDetails` endpoint + /// Given a block number, return its data. Similar to the standard eth_getBlockByNumber/Hash + /// method, but can be optimized by excluding unnecessary data such as transactions and + /// logBloom + #[cfg_attr(feature = "serde", serde(rename = "ots_getBlockDetails"))] + OtsGetBlockDetails( + #[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number_seq"))] + BlockNumber, + ), + + /// Otterscan's `ots_getBlockDetails` endpoint + /// Same as `ots_getBlockDetails`, but receiving a block hash instead of number + #[cfg_attr(feature = "serde", serde(rename = "ots_getBlockDetailsByHash", with = "sequence"))] + OtsGetBlockDetailsByHash(H256), + + /// Otterscan's `ots_getBlockTransactions` endpoint + /// Gets paginated transaction data for a certain block. Return data is similar to + /// eth_getBlockBy* + eth_getTransactionReceipt. + #[cfg_attr(feature = "serde", serde(rename = "ots_getBlockTransactions"))] + OtsGetBlockTransactions(u64, usize, usize), + + /// Otterscan's `ots_searchTransactionsBefore` endpoint + /// Address history navigation. searches backwards from certain point in time. + #[cfg_attr(feature = "serde", serde(rename = "ots_searchTransactionsBefore"))] + OtsSearchTransactionsBefore(Address, u64, usize), + + /// Otterscan's `ots_searchTransactionsAfter` endpoint + /// Address history navigation. searches forward from certain point in time. + #[cfg_attr(feature = "serde", serde(rename = "ots_searchTransactionsAfter"))] + OtsSearchTransactionsAfter(Address, u64, usize), + + /// Otterscan's `ots_getTransactionBySenderAndNonce` endpoint + /// Given a sender address and a nonce, returns the tx hash or null if not found. It returns + /// only the tx hash on success, you can use the standard eth_getTransactionByHash after that + /// to get the full transaction data. + #[cfg_attr(feature = "serde", serde(rename = "ots_getTransactionBySenderAndNonce",))] + OtsGetTransactionBySenderAndNonce( + Address, + #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_number"))] U256, + ), + + /// Otterscan's `ots_getTransactionBySenderAndNonce` endpoint + /// Given an ETH contract address, returns the tx hash and the direct address who created the + /// contract. + #[cfg_attr(feature = "serde", serde(rename = "ots_getContractCreator", with = "sequence"))] + OtsGetContractCreator(Address), } /// Represents ethereum JSON-RPC API diff --git a/anvil/src/eth/api.rs b/anvil/src/eth/api.rs index 4cab9194c471..bd19d19ec35b 100644 --- a/anvil/src/eth/api.rs +++ b/anvil/src/eth/api.rs @@ -82,7 +82,7 @@ pub struct EthApi { pool: Arc, /// Holds all blockchain related data /// In-Memory only for now - backend: Arc, + pub(super) backend: Arc, /// Whether this node is mining is_mining: bool, /// available signers @@ -356,6 +356,41 @@ impl EthApi { EthRequest::TxPoolStatus(_) => self.txpool_status().await.to_rpc_result(), EthRequest::TxPoolInspect(_) => self.txpool_inspect().await.to_rpc_result(), EthRequest::TxPoolContent(_) => self.txpool_content().await.to_rpc_result(), + EthRequest::ErigonGetHeaderByNumber(num) => { + self.erigon_get_header_by_number(num).await.to_rpc_result() + } + EthRequest::OtsGetApiLevel(_) => self.ots_get_api_level().await.to_rpc_result(), + EthRequest::OtsGetInternalOperations(hash) => { + self.ots_get_internal_operations(hash).await.to_rpc_result() + } + EthRequest::OtsHasCode(addr, num) => self.ots_has_code(addr, num).await.to_rpc_result(), + EthRequest::OtsTraceTransaction(hash) => { + self.ots_trace_transaction(hash).await.to_rpc_result() + } + EthRequest::OtsGetTransactionError(hash) => { + self.ots_get_transaction_error(hash).await.to_rpc_result() + } + EthRequest::OtsGetBlockDetails(num) => { + self.ots_get_block_details(num).await.to_rpc_result() + } + EthRequest::OtsGetBlockDetailsByHash(hash) => { + self.ots_get_block_details_by_hash(hash).await.to_rpc_result() + } + EthRequest::OtsGetBlockTransactions(num, page, page_size) => { + self.ots_get_block_transactions(num, page, page_size).await.to_rpc_result() + } + EthRequest::OtsSearchTransactionsBefore(address, num, page_size) => { + self.ots_search_transactions_before(address, num, page_size).await.to_rpc_result() + } + EthRequest::OtsSearchTransactionsAfter(address, num, page_size) => { + self.ots_search_transactions_after(address, num, page_size).await.to_rpc_result() + } + EthRequest::OtsGetTransactionBySenderAndNonce(address, nonce) => { + self.ots_get_transaction_by_sender_and_nonce(address, nonce).await.to_rpc_result() + } + EthRequest::OtsGetContractCreator(address) => { + self.ots_get_contract_creator(address).await.to_rpc_result() + } } } diff --git a/anvil/src/eth/backend/mem/mod.rs b/anvil/src/eth/backend/mem/mod.rs index 8928e9fd308c..d5a3066a2967 100644 --- a/anvil/src/eth/backend/mem/mod.rs +++ b/anvil/src/eth/backend/mem/mod.rs @@ -1355,8 +1355,18 @@ impl Backend { Some(self.convert_block(block)) } + pub(crate) async fn mined_transactions_by_block_number( + &self, + number: BlockNumber, + ) -> Option> { + if let Some(block) = self.get_block(number) { + return self.mined_transactions_in_block(&block) + } + None + } + /// Returns all transactions given a block - fn mined_transactions_in_block(&self, block: &Block) -> Option> { + pub(crate) fn mined_transactions_in_block(&self, block: &Block) -> Option> { let mut transactions = Vec::with_capacity(block.transactions.len()); let base_fee = block.header.base_fee_per_gas; let storage = self.blockchain.storage.read(); @@ -1443,7 +1453,7 @@ impl Backend { self.blockchain.get_block_by_hash(&hash) } - fn mined_block_by_number(&self, number: BlockNumber) -> Option> { + pub fn mined_block_by_number(&self, number: BlockNumber) -> Option> { Some(self.convert_block(self.get_block(number)?)) } @@ -1752,12 +1762,12 @@ impl Backend { } /// Returns the traces for the given transaction - fn mined_parity_trace_transaction(&self, hash: H256) -> Option> { + pub(crate) fn mined_parity_trace_transaction(&self, hash: H256) -> Option> { self.blockchain.storage.read().transactions.get(&hash).map(|tx| tx.parity_traces()) } - /// Returns the traces for the given transaction - fn mined_parity_trace_block(&self, block: u64) -> Option> { + /// Returns the traces for the given block + pub(crate) fn mined_parity_trace_block(&self, block: u64) -> Option> { let block = self.get_block(block)?; let mut traces = vec![]; let storage = self.blockchain.storage.read(); diff --git a/anvil/src/eth/mod.rs b/anvil/src/eth/mod.rs index 0892b79a01cd..a9acd1d9af4c 100644 --- a/anvil/src/eth/mod.rs +++ b/anvil/src/eth/mod.rs @@ -1,4 +1,5 @@ pub mod api; +pub mod otterscan; pub use api::EthApi; pub mod backend; diff --git a/anvil/src/eth/otterscan/api.rs b/anvil/src/eth/otterscan/api.rs new file mode 100644 index 000000000000..f5da4b287a78 --- /dev/null +++ b/anvil/src/eth/otterscan/api.rs @@ -0,0 +1,294 @@ +use crate::eth::{ + error::{BlockchainError, Result}, + macros::node_info, + EthApi, +}; + +use ethers::types::{ + Action, Address, Block, BlockId, BlockNumber, Bytes, Call, Create, CreateResult, Res, Reward, + Transaction, TxHash, H256, U256, U64, +}; +use itertools::Itertools; + +use super::types::{ + OtsBlockDetails, OtsBlockTransactions, OtsContractCreator, OtsInternalOperation, + OtsSearchTransactions, OtsTrace, +}; + +impl EthApi { + /// Otterscan currently requires this endpoint, even though it's not part of the ots_* + /// https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/useProvider.ts#L71 + /// + /// As a faster alternative to eth_getBlockByNumber (by excluding uncle block + /// information), which is not relevant in the context of an anvil node + pub async fn erigon_get_header_by_number( + &self, + number: BlockNumber, + ) -> Result>> { + node_info!("ots_getApiLevel"); + + self.backend.block_by_number(number).await + } + + /// As per the latest Otterscan source code, at least version 8 is needed + /// https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/params.ts#L1C2-L1C2 + pub async fn ots_get_api_level(&self) -> Result { + node_info!("ots_getApiLevel"); + + // as required by current otterscan's source code + Ok(8) + } + + /// Trace internal ETH transfers, contracts creation (CREATE/CREATE2) and self-destructs for a + /// certain transaction. + pub async fn ots_get_internal_operations( + &self, + hash: H256, + ) -> Result> { + node_info!("ots_getInternalOperations"); + + self.backend + .mined_parity_trace_transaction(hash) + .map(OtsInternalOperation::batch_build) + .ok_or_else(|| BlockchainError::DataUnavailable) + } + + /// Check if an ETH address contains code at a certain block number. + pub async fn ots_has_code(&self, address: Address, block_number: BlockNumber) -> Result { + node_info!("ots_hasCode"); + let block_id = Some(BlockId::Number(block_number)); + Ok(self.get_code(address, block_id).await?.len() > 0) + } + + /// Trace a transaction and generate a trace call tree. + pub async fn ots_trace_transaction(&self, hash: H256) -> Result> { + node_info!("ots_traceTransaction"); + + Ok(OtsTrace::batch_build(self.backend.trace_transaction(hash).await?)) + } + + /// Given a transaction hash, returns its raw revert reason. + pub async fn ots_get_transaction_error(&self, hash: H256) -> Result> { + node_info!("ots_getTransactionError"); + + if let Some(receipt) = self.backend.mined_transaction_receipt(hash) { + if receipt.inner.status == Some(U64::zero()) { + return Ok(receipt.out) + } + } + + Ok(Default::default()) + } + + /// For simplicity purposes, we return the entire block instead of emptying the values that + /// Otterscan doesn't want. This is the original purpose of the endpoint (to save bandwidth), + /// but it doesn't seem necessary in the context of an anvil node + pub async fn ots_get_block_details(&self, number: BlockNumber) -> Result { + node_info!("ots_getBlockDetails"); + + if let Some(block) = self.backend.block_by_number_full(number).await? { + let ots_block = OtsBlockDetails::build(block, &self.backend).await?; + + Ok(ots_block) + } else { + Err(BlockchainError::BlockNotFound) + } + } + + /// For simplicity purposes, we return the entire block instead of emptying the values that + /// Otterscan doesn't want. This is the original purpose of the endpoint (to save bandwidth), + /// but it doesn't seem necessary in the context of an anvil node + pub async fn ots_get_block_details_by_hash(&self, hash: H256) -> Result { + node_info!("ots_getBlockDetailsByHash"); + + if let Some(block) = self.backend.block_by_hash_full(hash).await? { + let ots_block = OtsBlockDetails::build(block, &self.backend).await?; + + Ok(ots_block) + } else { + Err(BlockchainError::BlockNotFound) + } + } + + /// Gets paginated transaction data for a certain block. Return data is similar to + /// eth_getBlockBy* + eth_getTransactionReceipt. + pub async fn ots_get_block_transactions( + &self, + number: u64, + page: usize, + page_size: usize, + ) -> Result { + node_info!("ots_getBlockTransactions"); + + match self.backend.block_by_number_full(number.into()).await? { + Some(block) => OtsBlockTransactions::build(block, &self.backend, page, page_size).await, + None => Err(BlockchainError::BlockNotFound), + } + } + + /// Address history navigation. searches backwards from certain point in time. + pub async fn ots_search_transactions_before( + &self, + address: Address, + block_number: u64, + page_size: usize, + ) -> Result { + node_info!("ots_searchTransactionsBefore"); + + let best = self.backend.best_number().as_u64(); + // we go from given block (defaulting to best) down to first block + // considering only post-fork + let from = if block_number == 0 { best } else { block_number }; + let to = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + + let first_page = from == best; + let mut last_page = false; + + let mut res: Vec<_> = vec![]; + + dbg!(to, from); + for n in (to..=from).rev() { + if n == to { + last_page = true; + } + + if let Some(traces) = self.backend.mined_parity_trace_block(n) { + let hashes = traces + .into_iter() + .rev() + .filter_map(|trace| match trace.action { + Action::Call(Call { from, to, .. }) if from == address || to == address => { + trace.transaction_hash + } + _ => None, + }) + .unique(); + + res.extend(hashes); + + if res.len() >= page_size { + break + } + } + } + + OtsSearchTransactions::build(res, &self.backend, first_page, last_page).await + } + + /// Address history navigation. searches forward from certain point in time. + pub async fn ots_search_transactions_after( + &self, + address: Address, + block_number: u64, + page_size: usize, + ) -> Result { + node_info!("ots_searchTransactionsAfter"); + + let best = self.backend.best_number().as_u64(); + // we go from the first post-fork block, up to the tip + let from = if block_number == 0 { + self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1) + } else { + block_number + }; + let to = best; + + let first_page = from == best; + let mut last_page = false; + + let mut res: Vec<_> = vec![]; + + for n in from..=to { + if n == to { + last_page = true; + } + + if let Some(traces) = self.backend.mined_parity_trace_block(n) { + let hashes = traces + .into_iter() + .rev() + .filter_map(|trace| match trace.action { + Action::Call(Call { from, to, .. }) if from == address || to == address => { + trace.transaction_hash + } + Action::Create(Create { from, .. }) if from == address => { + trace.transaction_hash + } + Action::Reward(Reward { author, .. }) if author == address => { + trace.transaction_hash + } + _ => None, + }) + .unique(); + + res.extend(hashes); + + if res.len() >= page_size { + break + } + } + } + + OtsSearchTransactions::build(res, &self.backend, first_page, last_page).await + } + + /// Given a sender address and a nonce, returns the tx hash or null if not found. It returns + /// only the tx hash on success, you can use the standard eth_getTransactionByHash after that to + /// get the full transaction data. + pub async fn ots_get_transaction_by_sender_and_nonce( + &self, + address: Address, + nonce: U256, + ) -> Result> { + node_info!("ots_getTransactionBySenderAndNonce"); + + let from = self.get_fork().map(|f| f.block_number() + 1).unwrap_or_default(); + let to = self.backend.best_number().as_u64(); + + for n in (from..=to).rev() { + if let Some(txs) = self.backend.mined_transactions_by_block_number(n.into()).await { + for tx in txs { + if tx.nonce == nonce && tx.from == address { + return Ok(Some(tx)) + } + } + } + } + + Ok(None) + } + + /// Given an ETH contract address, returns the tx hash and the direct address who created the + /// contract. + pub async fn ots_get_contract_creator( + &self, + addr: Address, + ) -> Result> { + node_info!("ots_getContractCreator"); + + let from = self.get_fork().map(|f| f.block_number()).unwrap_or_default(); + let to = self.backend.best_number().as_u64(); + + // loop in reverse, since we want the latest deploy to the address + for n in (from..=to).rev() { + if let Some(traces) = dbg!(self.backend.mined_parity_trace_block(n)) { + for trace in traces.into_iter().rev() { + match (trace.action, trace.result) { + ( + Action::Create(Create { from, .. }), + Some(Res::Create(CreateResult { address, .. })), + ) if address == addr => { + return Ok(Some(OtsContractCreator { + hash: trace.transaction_hash.unwrap(), + creator: from, + })) + } + _ => {} + } + } + } + } + + Ok(None) + } +} diff --git a/anvil/src/eth/otterscan/mod.rs b/anvil/src/eth/otterscan/mod.rs new file mode 100644 index 000000000000..8389f117b557 --- /dev/null +++ b/anvil/src/eth/otterscan/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod types; diff --git a/anvil/src/eth/otterscan/types.rs b/anvil/src/eth/otterscan/types.rs new file mode 100644 index 000000000000..e1b9fd6039f6 --- /dev/null +++ b/anvil/src/eth/otterscan/types.rs @@ -0,0 +1,331 @@ +use ethers::types::{ + Action, Address, Block, Bytes, Call, CallType, Create, CreateResult, Res, Suicide, Trace, + Transaction, TransactionReceipt, H256, U256, +}; +use futures::future::join_all; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::eth::{ + backend::mem::Backend, + error::{BlockchainError, Result}, +}; + +/// Patched Block struct, to include the additional `transactionCount` field expected by Otterscan +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase", bound = "TX: Serialize + DeserializeOwned")] +pub struct OtsBlock { + #[serde(flatten)] + pub block: Block, + pub transaction_count: usize, +} + +/// Block structure with additional details regarding fees and issuance +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OtsBlockDetails { + pub block: OtsBlock, + pub total_fees: U256, + pub issuance: Issuance, +} + +/// Issuance information for a block. Expected by Otterscan in ots_getBlockDetails calls +#[derive(Debug, Serialize, Default)] +pub struct Issuance { + block_reward: U256, + uncle_reward: U256, + issuance: U256, +} + +/// Holds both transactions and receipts for a block +#[derive(Serialize, Debug)] +pub struct OtsBlockTransactions { + pub fullblock: OtsBlock, + pub receipts: Vec, +} + +/// Patched Receipt struct, to include the additional `timestamp` field expected by Otterscan +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OtsTransactionReceipt { + #[serde(flatten)] + receipt: TransactionReceipt, + timestamp: u64, +} + +/// Information about the creator address and transaction for a contract +#[derive(Serialize, Debug)] +pub struct OtsContractCreator { + pub hash: H256, + pub creator: Address, +} + +/// Paginated search results of an account's history +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OtsSearchTransactions { + pub txs: Vec, + pub receipts: Vec, + pub first_page: bool, + pub last_page: bool, +} + +/// Otterscan format for listing relevant internal operations +#[derive(Serialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct OtsInternalOperation { + pub r#type: OtsInternalOperationType, + pub from: Address, + pub to: Address, + pub value: U256, +} + +/// Types of internal operations recognized by Otterscan +#[derive(Serialize, Debug, PartialEq)] +pub enum OtsInternalOperationType { + Transfer = 0, + SelfDestruct = 1, + Create = 2, + // The spec asks for a Create2 entry as well, but we don't have that info +} + +/// Otterscan's representation of a trace +#[derive(Serialize, Debug, PartialEq)] +pub struct OtsTrace { + pub r#type: OtsTraceType, + pub depth: usize, + pub from: Address, + pub to: Address, + pub value: U256, + pub input: Bytes, +} + +/// The type of call being described by an Otterscan trace. Only CALL, STATICCALL and DELEGATECALL +/// are represented +#[derive(Serialize, Debug, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum OtsTraceType { + Call, + StaticCall, + DelegateCall, +} + +impl OtsBlockDetails { + /// The response for ots_getBlockDetails includes an `issuance` object that requires computing + /// the total gas spent in a given block. + /// The only way to do this with the existing API is to explicitly fetch all receipts, to get + /// their `gas_used`. This would be extremely inefficient in a real blockchain RPC, but we can + /// get away with that in this context. + /// + /// The [original spec](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_getblockdetails) also mentions we can hardcode `transactions` and `logsBloom` to an empty array to save bandwith, because fields weren't intended to be used in the Otterscan UI at this point. This has two problems though: + /// - It makes the endpoint too specific to Otterscan's implementation + /// - It breaks the abstraction built in `OtsBlock` which computes `transaction_count` + /// based on the existing list. + /// Therefore we keep it simple by keeping the data in the response + pub async fn build(block: Block, backend: &Backend) -> Result { + let receipts_futs = block + .transactions + .iter() + .map(|tx| async { backend.transaction_receipt(tx.hash).await }); + + // fetch all receipts + let receipts: Vec = join_all(receipts_futs) + .await + .into_iter() + .map(|r| match r { + Ok(Some(r)) => Ok(r), + _ => Err(BlockchainError::DataUnavailable), + }) + .collect::>()?; + + let total_fees = receipts.iter().fold(U256::zero(), |acc, receipt| { + acc + receipt.gas_used.unwrap() * (receipt.effective_gas_price.unwrap()) + }); + + Ok(Self { + block: block.into(), + total_fees, + // issuance has no meaningful value in anvil's backend. just default to 0 + issuance: Default::default(), + }) + } +} + +/// Converts a regular block into the patched OtsBlock format +/// which includes the `transaction_count` field +impl From> for OtsBlock { + fn from(block: Block) -> Self { + let transaction_count = block.transactions.len(); + + Self { block, transaction_count } + } +} + +impl OtsBlockTransactions { + /// Fetches all receipts for the blocks's transactions, as required by the [`ots_getBlockTransactions`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_getblockdetails) endpoint spec, and returns the final response object. + pub async fn build( + mut block: Block, + backend: &Backend, + page: usize, + page_size: usize, + ) -> Result { + block.transactions = + block.transactions.into_iter().skip(page * page_size).take(page_size).collect(); + + let receipt_futs = block + .transactions + .iter() + .map(|tx| async { backend.transaction_receipt(tx.hash).await }); + + let receipts: Vec = join_all(receipt_futs) + .await + .into_iter() + .map(|r| match r { + Ok(Some(r)) => Ok(r), + _ => Err(BlockchainError::DataUnavailable), + }) + .collect::>()?; + + let fullblock: OtsBlock<_> = block.into(); + + Ok(Self { fullblock, receipts }) + } +} + +impl OtsSearchTransactions { + /// Constructs the final response object for both [`ots_searchTransactionsBefore` and + /// `ots_searchTransactionsAfter`](lrequires not only the transactions, but also the + /// corresponding receipts, which are fetched here before constructing the final) + pub async fn build( + hashes: Vec, + backend: &Backend, + first_page: bool, + last_page: bool, + ) -> Result { + let txs_futs = hashes.iter().map(|hash| async { backend.transaction_by_hash(*hash).await }); + + let txs: Vec = join_all(txs_futs) + .await + .into_iter() + .map(|t| match t { + Ok(Some(t)) => Ok(t), + _ => Err(BlockchainError::DataUnavailable), + }) + .collect::>()?; + + join_all(hashes.iter().map(|hash| async { + match backend.transaction_receipt(*hash).await { + Ok(Some(receipt)) => { + let timestamp = + backend.get_block(receipt.block_number.unwrap()).unwrap().header.timestamp; + Ok(OtsTransactionReceipt { receipt, timestamp }) + } + Ok(None) => Err(BlockchainError::DataUnavailable), + Err(e) => Err(e), + } + })) + .await + .into_iter() + .collect::>>() + .map(|receipts| Self { txs, receipts, first_page, last_page }) + } +} + +impl OtsInternalOperation { + /// Converts a batch of traces into a batch of internal operations, to comply with the spec for + /// [`ots_getInternalOperations`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_getinternaloperations) + pub fn batch_build(traces: Vec) -> Vec { + traces + .iter() + .filter_map(|trace| { + match (trace.action.clone(), trace.result.clone()) { + (Action::Call(Call { from, to, value, .. }), _) if !value.is_zero() => { + Some(Self { r#type: OtsInternalOperationType::Transfer, from, to, value }) + } + ( + Action::Create(Create { from, value, .. }), + Some(Res::Create(CreateResult { address, .. })), + ) => Some(Self { + r#type: OtsInternalOperationType::Create, + from, + to: address, + value, + }), + (Action::Suicide(Suicide { address, .. }), _) => { + // this assumes a suicide trace always has a parent trace + let (from, value) = + Self::find_suicide_caller(&traces, &trace.trace_address).unwrap(); + + Some(Self { + r#type: OtsInternalOperationType::SelfDestruct, + from, + to: address, + value, + }) + } + _ => None, + } + }) + .collect() + } + + /// finds the trace that parents a given trace_address + fn find_suicide_caller( + traces: &Vec, + suicide_address: &Vec, + ) -> Option<(Address, U256)> { + traces.iter().find(|t| t.trace_address == suicide_address[..suicide_address.len() - 1]).map( + |t| match t.action { + Action::Call(Call { from, value, .. }) => (from, value), + + Action::Create(Create { from, value, .. }) => (from, value), + + // we assume here a suicice trace can never be parented by another suicide trace + Action::Suicide(_) => Self::find_suicide_caller(traces, &t.trace_address).unwrap(), + + Action::Reward(_) => unreachable!(), + }, + ) + } +} + +impl OtsTrace { + /// Converts the list of traces for a transaction into the expected Otterscan format, as + /// specified in the [`ots_traceTransaction`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_tracetransaction) spec + pub fn batch_build(traces: Vec) -> Vec { + traces + .into_iter() + .filter_map(|trace| match trace.action { + Action::Call(call) => { + if let Ok(ots_type) = call.call_type.try_into() { + Some(OtsTrace { + r#type: ots_type, + depth: trace.trace_address.len(), + from: call.from, + to: call.to, + value: call.value, + input: call.input, + }) + } else { + None + } + } + Action::Create(_) => None, + Action::Suicide(_) => None, + Action::Reward(_) => None, + }) + .collect() + } +} + +impl TryFrom for OtsTraceType { + type Error = (); + + fn try_from(value: CallType) -> std::result::Result { + match value { + CallType::Call => Ok(OtsTraceType::Call), + CallType::StaticCall => Ok(OtsTraceType::StaticCall), + CallType::DelegateCall => Ok(OtsTraceType::DelegateCall), + _ => Err(()), + } + } +} diff --git a/anvil/tests/it/main.rs b/anvil/tests/it/main.rs index f66235f10993..cd99a9f15a01 100644 --- a/anvil/tests/it/main.rs +++ b/anvil/tests/it/main.rs @@ -12,6 +12,7 @@ mod logs; mod proof; mod pubsub; // mod revert; // TODO uncomment +mod otterscan; mod sign; mod traces; mod transaction; diff --git a/anvil/tests/it/otterscan.rs b/anvil/tests/it/otterscan.rs new file mode 100644 index 000000000000..f4c4913ca708 --- /dev/null +++ b/anvil/tests/it/otterscan.rs @@ -0,0 +1,438 @@ +//! tests for otterscan endpoints +use crate::abi::MulticallContract; +use anvil::{ + eth::otterscan::types::{ + OtsInternalOperation, OtsInternalOperationType, OtsTrace, OtsTraceType, + }, + spawn, NodeConfig, +}; +use ethers::{ + abi::Address, + prelude::{ContractFactory, ContractInstance, Middleware, SignerMiddleware}, + signers::Signer, + types::{BlockNumber, Bytes, TransactionRequest, U256}, + utils::get_contract_address, +}; +use ethers_solc::{project_util::TempProject, Artifact}; +use std::{collections::VecDeque, str::FromStr, sync::Arc}; + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_erigon_get_header_by_number() { + let (api, _handle) = spawn(NodeConfig::test()).await; + api.mine_one().await; + + let res0 = api.erigon_get_header_by_number(0.into()).await.unwrap().unwrap(); + let res1 = api.erigon_get_header_by_number(1.into()).await.unwrap().unwrap(); + + assert_eq!(res0.number, Some(0.into())); + assert_eq!(res1.number, Some(1.into())); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_api_level() { + let (api, _handle) = spawn(NodeConfig::test()).await; + + assert_eq!(api.ots_get_api_level().await.unwrap(), 8); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_internal_operations_contract_deploy() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let mut deploy_tx = MulticallContract::deploy(Arc::clone(&client), ()).unwrap().deployer.tx; + deploy_tx.set_nonce(0); + let contract_address = get_contract_address(sender, deploy_tx.nonce().unwrap()); + + let receipt = client.send_transaction(deploy_tx, None).await.unwrap().await.unwrap().unwrap(); + + let res = api.ots_get_internal_operations(receipt.transaction_hash).await.unwrap(); + + assert_eq!(res.len(), 1); + assert_eq!( + res[0], + OtsInternalOperation { + r#type: OtsInternalOperationType::Create, + from: sender, + to: contract_address, + value: 0.into() + } + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_has_code() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + api.mine_one().await; + + let mut deploy_tx = MulticallContract::deploy(Arc::clone(&client), ()).unwrap().deployer.tx; + deploy_tx.set_nonce(0); + + let pending_contract_address = get_contract_address(sender, deploy_tx.nonce().unwrap()); + + // no code in the address before deploying + assert!(!api + .ots_has_code(pending_contract_address, BlockNumber::Number(1.into())) + .await + .unwrap()); + + client.send_transaction(deploy_tx, None).await.unwrap(); + + let num = client.get_block_number().await.unwrap(); + // code is detected after deploying + assert!(api.ots_has_code(pending_contract_address, BlockNumber::Number(num)).await.unwrap()); + + // code is not detected for the previous block + assert!(!api + .ots_has_code(pending_contract_address, BlockNumber::Number(num - 1)) + .await + .unwrap()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_call_call_ots_trace_transaction() { + let prj = TempProject::dapptools().unwrap(); + prj.add_source( + "Contract", + r#" +pragma solidity 0.8.13; +contract Contract { + address payable private owner; + constructor() public { + owner = payable(msg.sender); + } + function run() payable public { + this.do_staticcall(); + this.do_call(); + } + + function do_staticcall() external view returns (bool) { + return true; + } + + function do_call() external { + owner.call{value: address(this).balance}(""); + address(this).delegatecall(abi.encodeWithSignature("do_delegatecall()")); + } + + function do_delegatecall() internal { + } +} +"#, + ) + .unwrap(); + + let mut compiled = prj.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + let contract = compiled.remove_first("Contract").unwrap(); + let (abi, bytecode, _) = contract.into_contract_bytecode().into_parts(); + + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.ws_provider().await; + let wallets = handle.dev_wallets().collect::>(); + let client = Arc::new(SignerMiddleware::new(provider, wallets[0].clone())); + + // deploy successfully + let factory = ContractFactory::new(abi.clone().unwrap(), bytecode.unwrap(), client); + let contract = factory.deploy(()).unwrap().send().await.unwrap(); + + let contract = ContractInstance::new( + contract.address(), + abi.unwrap(), + SignerMiddleware::new(handle.http_provider(), wallets[1].clone()), + ); + let call = contract.method::<_, ()>("run", ()).unwrap().value(1337); + let receipt = call.send().await.unwrap().await.unwrap().unwrap(); + + let res = api.ots_trace_transaction(receipt.transaction_hash).await.unwrap(); + + assert_eq!( + res, + vec![ + OtsTrace { + r#type: OtsTraceType::Call, + depth: 0, + from: wallets[1].address(), + to: contract.address(), + value: 1337.into(), + input: Bytes::from_str("0xc0406226").unwrap() + }, + OtsTrace { + r#type: OtsTraceType::StaticCall, + depth: 1, + from: contract.address(), + to: contract.address(), + value: U256::zero(), + input: Bytes::from_str("0x6a6758fe").unwrap() + }, + OtsTrace { + r#type: OtsTraceType::Call, + depth: 1, + from: contract.address(), + to: contract.address(), + value: U256::zero(), + input: Bytes::from_str("0x96385e39").unwrap() + }, + OtsTrace { + r#type: OtsTraceType::Call, + depth: 2, + from: contract.address(), + to: wallets[0].address(), + value: 1337.into(), + input: Bytes::from_str("0x").unwrap() + }, + OtsTrace { + r#type: OtsTraceType::DelegateCall, + depth: 2, + from: contract.address(), + to: contract.address(), + value: U256::zero(), + input: Bytes::from_str("0xa1325397").unwrap() + }, + ] + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_transaction_error() { + let prj = TempProject::dapptools().unwrap(); + prj.add_source( + "Contract", + r#" +pragma solidity 0.8.13; +contract Contract { + error CustomError(string msg); + + function trigger_revert() public { + revert CustomError("RevertStringFooBar"); + } +} +"#, + ) + .unwrap(); + + let mut compiled = prj.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + let contract = compiled.remove_first("Contract").unwrap(); + let (abi, bytecode, _) = contract.into_contract_bytecode().into_parts(); + + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.ws_provider().await; + + let wallet = handle.dev_wallets().next().unwrap(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + // deploy successfully + let factory = ContractFactory::new(abi.clone().unwrap(), bytecode.unwrap(), client); + let contract = factory.deploy(()).unwrap().send().await.unwrap(); + + let call = contract.method::<_, ()>("trigger_revert", ()).unwrap().gas(150_000u64); + let receipt = call.send().await.unwrap().await.unwrap().unwrap(); + + let block = api.block_by_number_full(BlockNumber::Latest).await.unwrap().unwrap(); + dbg!(block); + // let tx = block.transactions[0].hashVg + + let res = api.ots_get_transaction_error(receipt.transaction_hash).await.unwrap().unwrap(); + assert_eq!(res, Bytes::from_str("0x8d6ea8be00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012526576657274537472696e67466f6f4261720000000000000000000000000000").unwrap()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_block_details() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let tx = TransactionRequest::new().to(Address::random()).value(100u64); + let receipt = client.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap(); + + let result = api.ots_get_block_details(1.into()).await.unwrap(); + + assert_eq!(result.block.transaction_count, 1); + assert_eq!(result.block.block.transactions[0].hash, receipt.transaction_hash); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_block_details_by_hash() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let tx = TransactionRequest::new().to(Address::random()).value(100u64); + let receipt = client.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap(); + + let block_hash = receipt.block_hash.unwrap(); + let result = api.ots_get_block_details_by_hash(block_hash).await.unwrap(); + + assert_eq!(result.block.transaction_count, 1); + assert_eq!(result.block.block.transactions[0].hash, receipt.transaction_hash); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_block_transactions() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + // disable automine + api.anvil_set_auto_mine(false).await.unwrap(); + + let mut hashes = VecDeque::new(); + for i in 0..10 { + let tx = TransactionRequest::new().to(Address::random()).value(100u64).nonce(i); + let receipt = client.send_transaction(tx, None).await.unwrap(); + hashes.push_back(receipt.tx_hash()); + } + + dbg!(&hashes); + api.mine_one().await; + + let page_size = 3; + for page in 0..4 { + let result = api.ots_get_block_transactions(1, page, page_size).await.unwrap(); + dbg!(&result); + + assert!(result.receipts.len() <= page_size); + assert!(result.fullblock.block.transactions.len() <= page_size); + assert!(result.fullblock.transaction_count == result.receipts.len()); + + result.receipts.iter().enumerate().for_each(|(i, receipt)| { + let expected = hashes.pop_front(); + assert_eq!(expected, Some(receipt.transaction_hash)); + assert_eq!(expected, Some(result.fullblock.block.transactions[i].hash)); + }); + } + + assert!(hashes.is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_search_transactions_before() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let mut hashes = vec![]; + + for i in 0..7 { + let tx = TransactionRequest::new().to(Address::random()).value(100u64).nonce(i); + let receipt = client.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap(); + hashes.push(receipt.transaction_hash); + } + + let page_size = 2; + let mut block = 0; + for _ in 0..4 { + let result = api.ots_search_transactions_before(sender, block, page_size).await.unwrap(); + + assert!(result.txs.len() <= page_size); + + // check each individual hash + result.txs.iter().for_each(|tx| { + assert_eq!(hashes.pop(), Some(tx.hash)); + }); + + block = result.txs.last().unwrap().block_number.unwrap().as_u64() - 1; + } + + assert!(hashes.is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_search_transactions_after() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let mut hashes = VecDeque::new(); + + for i in 0..7 { + let tx = TransactionRequest::new().to(Address::random()).value(100u64).nonce(i); + let receipt = client.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap(); + hashes.push_front(receipt.transaction_hash); + } + + let page_size = 2; + let mut block = 0; + for _ in 0..4 { + let result = api.ots_search_transactions_after(sender, block, page_size).await.unwrap(); + + assert!(result.txs.len() <= page_size); + + // check each individual hash + result.txs.iter().for_each(|tx| { + assert_eq!(hashes.pop_back(), Some(tx.hash)); + }); + + block = result.txs.last().unwrap().block_number.unwrap().as_u64() + 1; + } + + assert!(hashes.is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_transaction_by_sender_and_nonce() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + api.mine_one().await; + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let tx1 = TransactionRequest::new().to(Address::random()).value(100u64); + let tx2 = TransactionRequest::new().to(Address::random()).value(100u64); + + let receipt1 = client.send_transaction(tx1, None).await.unwrap().await.unwrap().unwrap(); + let receipt2 = client.send_transaction(tx2, None).await.unwrap().await.unwrap().unwrap(); + + let result1 = api.ots_get_transaction_by_sender_and_nonce(sender, 0.into()).await.unwrap(); + let result2 = api.ots_get_transaction_by_sender_and_nonce(sender, 1.into()).await.unwrap(); + + assert_eq!(result1.unwrap().hash, receipt1.transaction_hash); + assert_eq!(result2.unwrap().hash, receipt2.transaction_hash); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_call_ots_get_contract_creator() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + api.mine_one().await; + + let wallet = handle.dev_wallets().next().unwrap(); + let sender = wallet.address(); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + let mut deploy_tx = MulticallContract::deploy(Arc::clone(&client), ()).unwrap().deployer.tx; + deploy_tx.set_nonce(0); + + let pending_contract_address = get_contract_address(sender, deploy_tx.nonce().unwrap()); + + let receipt = client.send_transaction(deploy_tx, None).await.unwrap().await.unwrap().unwrap(); + + let creator = api.ots_get_contract_creator(pending_contract_address).await.unwrap().unwrap(); + + assert_eq!(creator.creator, sender); + assert_eq!(creator.hash, receipt.transaction_hash); +}