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;