diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 67e47d23..a1b585e4 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -42,9 +42,9 @@ The `status` options are: | [`ETH`](#eth-namespace) | [`eth_call`](#eth_call) | `SUPPORTED` | Executes a new message call immediately without creating a transaction on the block chain | | [`ETH`](#eth-namespace) | [`eth_sendRawTransaction`](#eth_sendrawtransaction) | `SUPPORTED` | Creates new message call transaction or a contract creation for signed transactions | | [`ETH`](#eth-namespace) | [`eth_getCode`](#eth_getcode) | `SUPPORTED` | Returns code at a given address | -| [`ETH`](#eth-namespace) | [`eth_getFilterChanges`](#`eth_getFilterChanges) | `SUPPORTED` | Polling method for a filter, which returns an array of logs, block hashes, or transaction hashes, depending on the filter type, which occurred since last poll | -| `ETH` | `eth_getFilterLogs` | `NOT IMPLEMENTED`
[GitHub Issue #41](https://github.com/matter-labs/era-test-node/issues/41) | Returns an array of all logs matching filter with given id | -| `ETH` | `eth_getLogs` | `NOT IMPLEMENTED`
[GitHub Issue #40](https://github.com/matter-labs/era-test-node/issues/40) | Returns an array of all logs matching a given filter object | +| [`ETH`](#eth-namespace) | [`eth_getFilterChanges`](#`eth_getfilterchanges) | `SUPPORTED` | Polling method for a filter, which returns an array of logs, block hashes, or transaction hashes, depending on the filter type, which occurred since last poll | +| [`ETH`](#eth-namespace) | [`eth_getFilterLogs`](#eth_getfilterlogs) | `SUPPORTED` | Returns an array of all logs matching filter with given id | +| [`ETH`](#eth-namespace) | [`eth_getLogs`](#eth_getlogs) | `SUPPORTED` | Returns an array of all logs matching a given filter object | | `ETH` | `eth_getProof` | `NOT IMPLEMENTED` | Returns the details for the account at the specified address and block number, the account's Merkle proof, and the storage values for the specified storage keys with their Merkle-proofs | | `ETH` | `eth_getStorageAt` | `NOT IMPLEMENTED`
[GitHub Issue #45](https://github.com/matter-labs/era-test-node/issues/45) | Returns the value from a storage position at a given address | | `ETH` | `eth_getTransactionByBlockHashAndIndex` | `NOT IMPLEMENTED`
[GitHub Issue #46](https://github.com/matter-labs/era-test-node/issues/46) | Returns information about a transaction by block hash and transaction index position | @@ -737,6 +737,68 @@ curl --request POST \ }' ``` + +### `eth_getFilterLogs` + +[source](src/node.rs) + +Returns an array of all logs matching filter with given id + +#### Arguments + ++ `id: U256` + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "1", + "method": "eth_getFilterLogs", + "params": ["0x1"] +}' +``` + +### `eth_getLogs` + +[source](src/node.rs) + +Returns an array of all logs matching a filter + +#### Arguments + ++ `filter: Filter` + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "1", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0xa", + "toBlock": "0xff", + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "topics": [] + }] +}' +``` + ### `eth_getCode` [source](src/node.rs) diff --git a/src/filters.rs b/src/filters.rs index 48fdb876..b490eab9 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -35,7 +35,22 @@ pub struct LogFilter { } impl LogFilter { - fn matches(&self, log: &Log, latest_block_number: U64) -> bool { + pub fn new( + from_block: BlockNumber, + to_block: BlockNumber, + addresses: Vec, + topics: [Option>; 4], + ) -> Self { + Self { + from_block, + to_block, + addresses, + topics, + updates: Default::default(), + } + } + + pub fn matches(&self, log: &Log, latest_block_number: U64) -> bool { let from = utils::to_real_block_number(self.from_block, latest_block_number); let to = utils::to_real_block_number(self.to_block, latest_block_number); @@ -185,6 +200,10 @@ impl EthFilters { Ok(changes) } + pub fn get_filter(&self, id: U256) -> Option<&FilterType> { + self.filters.get(&id) + } + /// Notify available filters of a newly produced block pub fn notify_new_block(&mut self, hash: H256) { self.filters.iter_mut().for_each(|(_, filter)| { diff --git a/src/node.rs b/src/node.rs index 9dba2a19..516ccc4a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2,7 +2,7 @@ use crate::{ bootloader_debug::BootloaderDebug, console_log::ConsoleLogHandler, - filters::EthFilters, + filters::{EthFilters, FilterType, LogFilter}, fork::{ForkDetails, ForkSource, ForkStorage}, formatter, system_contracts::{self, Options, SystemContracts}, @@ -15,6 +15,7 @@ use clap::Parser; use colored::Colorize; use core::fmt::Display; use futures::FutureExt; +use itertools::Itertools; use jsonrpc_core::BoxFuture; use std::{ cmp::{self}, @@ -1956,18 +1957,108 @@ impl EthNamespaceT for Ok(result).into_boxed_future() } + /// Returns an array of all logs matching a given filter. + /// + /// # Arguments + /// + /// * `filter`: The filter options - + /// fromBlock - Integer block number, or the string "latest", "earliest" or "pending". + /// toBlock - Integer block number, or the string "latest", "earliest" or "pending". + /// address - Contract address or a list of addresses from which the logs should originate. + /// topics - [H256] topics. Topics are order-dependent. Each topic can also be an array with "or" options. + /// See `new_filter` documention for how to specify topics. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `jsonrpc_core::Result` that resolves to an array of logs. fn get_logs( &self, - _filter: Filter, + filter: Filter, ) -> jsonrpc_core::BoxFuture>> { - not_implemented("get_logs") + let reader = match self.inner.read() { + Ok(r) => r, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + }; + let from_block = filter + .from_block + .unwrap_or(zksync_types::api::BlockNumber::Earliest); + let to_block = filter + .to_block + .unwrap_or(zksync_types::api::BlockNumber::Latest); + let addresses = filter.address.unwrap_or_default().0; + let mut topics: [Option>; 4] = Default::default(); + + if let Some(filter_topics) = filter.topics { + filter_topics + .into_iter() + .take(4) + .enumerate() + .for_each(|(i, maybe_topic_set)| { + if let Some(topic_set) = maybe_topic_set { + topics[i] = Some(topic_set.0.into_iter().collect()); + } + }) + } + + let log_filter = LogFilter::new(from_block, to_block, addresses, topics); + + let latest_block_number = U64::from(reader.current_miniblock); + let logs = reader + .tx_results + .values() + .flat_map(|tx_result| { + tx_result + .receipt + .logs + .iter() + .filter(|log| log_filter.matches(log, latest_block_number)) + .cloned() + }) + .collect_vec(); + + Ok(logs).into_boxed_future() } + /// Returns an array of all logs matching filter with given id. + /// + /// # Arguments + /// + /// * `id`: The filter id + /// + /// # Returns + /// + /// A `BoxFuture` containing a `jsonrpc_core::Result` that resolves to an array of logs. fn get_filter_logs( &self, - _filter_index: U256, + id: U256, ) -> jsonrpc_core::BoxFuture> { - not_implemented("get_filter_logs") + let reader = match self.inner.read() { + Ok(r) => r, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + }; + + let latest_block_number = U64::from(reader.current_miniblock); + let logs = match reader.filters.get_filter(id) { + Some(FilterType::Log(f)) => reader + .tx_results + .values() + .flat_map(|tx_result| { + tx_result + .receipt + .logs + .iter() + .filter(|log| f.matches(log, latest_block_number)) + .cloned() + }) + .collect_vec(), + _ => return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed(), + }; + + Ok(FilterChanges::Logs(logs)).into_boxed_future() } /// Polling method for a filter, which returns an array of logs, block hashes, or transaction hashes, @@ -2224,10 +2315,10 @@ mod tests { cache::CacheConfig, http_fork_source::HttpForkSource, node::InMemoryNode, - testing::{self, ForkBlockConfig, MockServer}, + testing::{self, ForkBlockConfig, LogBuilder, MockServer}, }; use zksync_types::api::BlockNumber; - use zksync_web3_decl::types::SyncState; + use zksync_web3_decl::types::{SyncState, ValueOrArray}; use super::*; @@ -2987,4 +3078,153 @@ mod tests { changes => panic!("expected no changes in the second call, got {:?}", changes), } } + + #[tokio::test] + async fn test_get_filter_logs_returns_matching_logs_for_valid_id() { + let node = InMemoryNode::::default(); + + // populate tx receipts with 2 tx each having logs + { + let mut writer = node.inner.write().unwrap(); + writer.tx_results.insert( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build()], + ..Default::default() + }, + }, + ); + writer.tx_results.insert( + H256::repeat_byte(0x2), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![ + LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build(), + LogBuilder::new() + .set_address(H160::repeat_byte(0xa2)) + .build(), + ], + ..Default::default() + }, + }, + ); + } + + let filter_id = node + .new_filter(Filter { + address: Some(ValueOrArray(vec![H160::repeat_byte(0xa1)])), + ..Default::default() + }) + .await + .expect("failed creating filter"); + + match node + .get_filter_logs(filter_id) + .await + .expect("failed getting filter changes") + { + FilterChanges::Logs(result) => assert_eq!(2, result.len()), + changes => panic!("unexpected filter changes: {:?}", changes), + } + } + + #[tokio::test] + async fn test_get_filter_logs_returns_error_for_invalid_id() { + let node = InMemoryNode::::default(); + + // populate tx receipts with 2 tx each having logs + { + let mut writer = node.inner.write().unwrap(); + writer.tx_results.insert( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build()], + ..Default::default() + }, + }, + ); + } + + let invalid_filter_id = U256::from(100); + let result = node.get_filter_logs(invalid_filter_id).await; + + assert!(result.is_err(), "expected an error for invalid filter id"); + } + + #[tokio::test] + async fn test_get_logs_returns_matching_logs() { + let node = InMemoryNode::::default(); + + // populate tx receipts with 2 tx each having logs + { + let mut writer = node.inner.write().unwrap(); + writer.tx_results.insert( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build()], + ..Default::default() + }, + }, + ); + writer.tx_results.insert( + H256::repeat_byte(0x2), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![ + LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build(), + LogBuilder::new() + .set_address(H160::repeat_byte(0xa2)) + .build(), + ], + ..Default::default() + }, + }, + ); + } + + let result = node + .get_logs(Filter { + address: Some(ValueOrArray(vec![H160::repeat_byte(0xa2)])), + ..Default::default() + }) + .await + .expect("failed getting filter changes"); + assert_eq!(1, result.len()); + + let result = node + .get_logs(Filter { + address: Some(ValueOrArray(vec![H160::repeat_byte(0xa1)])), + ..Default::default() + }) + .await + .expect("failed getting filter changes"); + assert_eq!(2, result.len()); + + let result = node + .get_logs(Filter { + address: Some(ValueOrArray(vec![H160::repeat_byte(0x11)])), + ..Default::default() + }) + .await + .expect("failed getting filter changes"); + assert_eq!(0, result.len()); + } } diff --git a/src/testing.rs b/src/testing.rs index 94c32f32..ac0d4b62 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -5,7 +5,7 @@ #![cfg(test)] -use crate::node::InMemoryNode; +use crate::node::{InMemoryNode, TxExecutionInfo}; use crate::{fork::ForkSource, node::compute_hash}; use httptest::{ @@ -15,8 +15,10 @@ use httptest::{ }; use itertools::Itertools; use std::str::FromStr; +use vm::vm::{VmPartialExecutionResult, VmTxExecutionResult}; use zksync_basic_types::{H160, U64}; use zksync_types::api::Log; +use zksync_types::tx::tx_execution_info::TxExecutionStatus; use zksync_types::{fee::Fee, l2::L2Tx, Address, L2ChainId, Nonce, PackedEthSignature, H256, U256}; /// Configuration for the [MockServer]'s initial block. @@ -438,6 +440,37 @@ impl LogBuilder { } } +/// Returns a default instance for a successful [TxExecutionInfo] +pub fn default_tx_execution_info() -> TxExecutionInfo { + TxExecutionInfo { + tx: L2Tx { + execute: zksync_types::Execute { + contract_address: Default::default(), + calldata: Default::default(), + value: Default::default(), + factory_deps: Default::default(), + }, + common_data: Default::default(), + received_timestamp_ms: Default::default(), + }, + batch_number: Default::default(), + miniblock_number: Default::default(), + result: VmTxExecutionResult { + status: TxExecutionStatus::Success, + result: VmPartialExecutionResult { + logs: Default::default(), + revert_reason: Default::default(), + contracts_used: Default::default(), + cycles_used: Default::default(), + computational_gas_used: Default::default(), + }, + call_traces: Default::default(), + gas_refunded: Default::default(), + operator_suggested_refund: Default::default(), + }, + } +} + mod test { use super::*; use crate::http_fork_source::HttpForkSource;