Repository: gakonst/ethers-rs
Pull request #2137 (2023-02-10 19:29:07)

feat(examples): add basic event filtering example
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

Improve the `events` section of `examples` by adding a simple filtering example.

First-time contributor and also new to Rust, so any feedback is greatly appreciated!  I'm happy to add more examples, as I find them very helpful for familiarizing others with the project as well as being great exercises for learning Rust.

## Solution

Adds a basic event filtering example that also includes parsing log topics.

## PR Checklist

-   [ ] Added Tests
-   [ ] Added Documentation
-   [x] Updated the changelog
-   [ ] Breaking changes



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5510068b..8ca82ecb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -114,6 +114,7 @@
   Added `twos_complement` function for I256.
 - [#1934](https://github.com/gakonst/ethers-rs/pull/1934) Allow 16 calls in multicall.
 - [#1941](https://github.com/gakonst/ethers-rs/pull/1941) Add `add_calls` and `call_array` for `Multicall`.
+- Added basic event log filtering example.
 
 ## ethers-contract-abigen
 
diff --git a/Cargo.lock b/Cargo.lock
index 2fee682e..e1ffa773 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4027,9 +4027,9 @@ dependencies = [
 
 [[package]]
 name = "solang-parser"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e9653f278d9531b60f042c29732bb835d519943f4167b1e5684c7e820dd9fec"
+checksum = "58feb14d4002859db0b744b0915840bae403b8a5675efded3e8adddb6beb85b2"
 dependencies = [
  "itertools",
  "lalrpop",
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index 95492294..99e83c67 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -56,7 +56,11 @@ impl ExpandedContract {
            // export all the created data types
             pub use #module::*;
 
-            #[allow(clippy::too_many_arguments, non_camel_case_types)]
+            #[allow(
+                clippy::too_many_arguments,
+                non_camel_case_types,
+                clippy::upper_case_acronyms
+            )]
             pub mod #module {
                 #imports
                 #contract
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs
index c48e306d..b53b08d6 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs
@@ -62,7 +62,7 @@ where
 }
 
 pub(crate) fn imports(name: &str) -> TokenStream {
-    let doc_str = format!("{name} was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs");
+    let doc_str = format!("{name} was auto-generated with ethers-rs Abigen. More information at: <https://github.com/gakonst/ethers-rs>");
 
     let ethers_core = ethers_core_crate();
     let ethers_providers = ethers_providers_crate();
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 4002e7b6..b2b3dbeb 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -596,7 +596,7 @@ fn struct_type_projections(name: &str) -> Vec<String> {
 fn expand_struct_type(struct_ty: &StructFieldType) -> TokenStream {
     match struct_ty {
         StructFieldType::Type(ty) => {
-            let ty = util::ident(ty.name());
+            let ty = util::ident(&ty.name().to_pascal_case());
             quote! {#ty}
         }
         StructFieldType::Array(ty) => {
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index e423b1a6..1d713548 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -280,10 +280,7 @@ impl Chain {
             ArbitrumTestnet => {
                 ("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io")
             }
-            ArbitrumGoerli => (
-                "https://goerli-rollup-explorer.arbitrum.io/api",
-                "https://goerli-rollup-explorer.arbitrum.io",
-            ),
+            ArbitrumGoerli => ("https://api-goerli.arbiscan.io/api", "https://goerli.arbiscan.io"),
             ArbitrumNova => ("https://api-nova.arbiscan.io/api", "https://nova.arbiscan.io/"),
             Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"),
             CronosTestnet => {
diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs
index f10920ba..f6cc587d 100644
--- a/ethers-etherscan/src/errors.rs
+++ b/ethers-etherscan/src/errors.rs
@@ -41,6 +41,8 @@ pub enum EtherscanError {
     BlockedByCloudflare,
     #[error("The Requested prompted a cloudflare captcha security challenge to review the security of your connection before proceeding.")]
     CloudFlareSecurityChallenge,
+    #[error("Received `Page not found` response. API server is likely down")]
+    PageNotFound,
 }
 
 /// etherscan/polyscan is protected by cloudflare, which can lead to html responses like `Sorry, you have been blocked` See also <https://community.cloudflare.com/t/sorry-you-have-been-blocked/110790>
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index b3fa1c6e..ee8437b6 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -207,7 +207,9 @@ impl Client {
         let res = res.as_ref();
         let res: ResponseData<T> = serde_json::from_str(res).map_err(|err| {
             error!(target: "etherscan", ?res, "Failed to deserialize response: {}", err);
-            if is_blocked_by_cloudflare_response(res) {
+            if res == "Page not found" {
+                EtherscanError::PageNotFound
+            } else if is_blocked_by_cloudflare_response(res) {
                 EtherscanError::BlockedByCloudflare
             } else if is_cloudflare_security_challenge(res) {
                 EtherscanError::CloudFlareSecurityChallenge
diff --git a/ethers-middleware/src/transformer/ds_proxy/factory.rs b/ethers-middleware/src/transformer/ds_proxy/factory.rs
index 122c7bf1..71d35043 100644
--- a/ethers-middleware/src/transformer/ds_proxy/factory.rs
+++ b/ethers-middleware/src/transformer/ds_proxy/factory.rs
@@ -42,7 +42,7 @@ mod dsproxyfactory_mod {
         types::*,
     };
     use ethers_providers::Middleware;
-    #[doc = "DsProxyFactory was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"]
+    #[doc = "DsProxyFactory was auto-generated with ethers-rs Abigen. More information at: <https://github.com/gakonst/ethers-rs>"]
     use std::sync::Arc;
 
     pub static DSPROXYFACTORY_ABI: Lazy<Abi> = Lazy::new(|| {
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 7b432680..4a7cf974 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -18,7 +18,7 @@ keywords = ["ethereum", "web3", "solc", "solidity", "ethers"]
 [dependencies]
 ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false }
 serde_json = "1.0.68"
-serde = { version = "1.0.130", features = ["derive"] }
+serde = { version = "1.0.130", features = ["derive", "rc"] }
 semver = { version = "1.0.16", features = ["serde"] }
 walkdir = "2.3.2"
 tokio = { version = "1.18", default-features = false, features = ["rt"] }
@@ -37,7 +37,7 @@ tempfile = { version = "3.3.0", optional = true }
 fs_extra = { version = "1.3.0", optional = true }
 sha2 = { version = "0.10.6", default-features = false, optional = true }
 dunce = "1.0.3"
-solang-parser = { default-features = false, version = "=0.2.1" }
+solang-parser = { default-features = false, version = "=0.2.2" }
 rayon = "1.6.0"
 rand = { version = "0.8.5", optional = true }
 path-slash = "0.2.1"
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 94806f9c..35bedb92 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -11,6 +11,7 @@ use std::{
     fmt, fs,
     path::{Path, PathBuf},
     str::FromStr,
+    sync::Arc,
 };
 use tracing::warn;
 use yansi::Paint;
@@ -1018,6 +1019,8 @@ pub struct ModelCheckerSettings {
     pub timeout: Option<u32>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub targets: Option<Vec<ModelCheckerTarget>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub invariants: Option<Vec<ModelCheckerInvariant>>,
 }
 
 /// Which model checker engine to run.
@@ -1104,6 +1107,36 @@ impl FromStr for ModelCheckerTarget {
     }
 }
 
+/// Which model checker invariants to check.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum ModelCheckerInvariant {
+    Contract,
+    Reentrancy,
+}
+
+impl fmt::Display for ModelCheckerInvariant {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let string = match self {
+            ModelCheckerInvariant::Contract => "contract",
+            ModelCheckerInvariant::Reentrancy => "reentrancy",
+        };
+        write!(f, "{string}")
+    }
+}
+
+impl FromStr for ModelCheckerInvariant {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "contract" => Ok(ModelCheckerInvariant::Contract),
+            "reentrancy" => Ok(ModelCheckerInvariant::Reentrancy),
+            s => Err(format!("Unknown model checker invariant: {s}")),
+        }
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Compiler {
     pub version: String,
@@ -1163,16 +1196,29 @@ pub struct DocLibraries {
     pub libs: BTreeMap<String, serde_json::Value>,
 }
 
+/// Content of a solidity file
+///
+/// This contains the actual source code of a file
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
 pub struct Source {
-    pub content: String,
+    /// Content of the file
+    ///
+    /// This is an `Arc` because it may be cloned. If the [Graph](crate::resolver::Graph) of the
+    /// project contains multiple conflicting versions then the same [Source] may be required by
+    /// conflicting versions and needs to be duplicated.
+    pub content: Arc<String>,
 }
 
 impl Source {
-    /// Reads the file content
+    /// Creates a new instance of [Source] with the given content.
+    pub fn new(content: impl Into<String>) -> Self {
+        Self { content: Arc::new(content.into()) }
+    }
+
+    /// Reads the file's content
     pub fn read(file: impl AsRef<Path>) -> Result<Self, SolcIoError> {
         let file = file.as_ref();
-        Ok(Self { content: fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))? })
+        Ok(Self::new(fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))?))
     }
 
     /// Recursively finds all source files under the given dir path and reads them all
@@ -1222,7 +1268,7 @@ impl Source {
     /// Generate a non-cryptographically secure checksum of the file's content
     pub fn content_hash(&self) -> String {
         let mut hasher = md5::Md5::new();
-        hasher.update(&self.content);
+        hasher.update(self);
         let result = hasher.finalize();
         hex::encode(result)
     }
@@ -1238,11 +1284,9 @@ impl Source {
     /// async version of `Self::read`
     pub async fn async_read(file: impl AsRef<Path>) -> Result<Self, SolcIoError> {
         let file = file.as_ref();
-        Ok(Self {
-            content: tokio::fs::read_to_string(file)
-                .await
-                .map_err(|err| SolcIoError::new(err, file))?,
-        })
+        Ok(Self::new(
+            tokio::fs::read_to_string(file).await.map_err(|err| SolcIoError::new(err, file))?,
+        ))
     }
 
     /// Finds all source files under the given dir path and reads them all
@@ -1274,6 +1318,12 @@ impl AsRef<str> for Source {
     }
 }
 
+impl AsRef<[u8]> for Source {
+    fn as_ref(&self) -> &[u8] {
+        self.content.as_bytes()
+    }
+}
+
 /// Output type `solc` produces
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
 pub struct CompilerOutput {
diff --git a/ethers-solc/src/buildinfo.rs b/ethers-solc/src/buildinfo.rs
index 97507872..27eaffa0 100644
--- a/ethers-solc/src/buildinfo.rs
+++ b/ethers-solc/src/buildinfo.rs
@@ -104,7 +104,7 @@ mod tests {
     fn build_info_serde() {
         let inputs = CompilerInput::with_sources(BTreeMap::from([(
             PathBuf::from("input.sol"),
-            Source { content: "".to_string() },
+            Source::new(""),
         )]));
         let output = CompilerOutput::default();
         let v: Version = "0.8.4+commit.c7e474f2".parse().unwrap();
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index a00123c9..25d5307f 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -909,6 +909,6 @@ mod tests {
     ///// helpers
 
     fn source(version: &str) -> Source {
-        Source { content: format!("pragma solidity {version};\n") }
+        Source::new(format!("pragma solidity {version};\n"))
     }
 }
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 732b5d1a..08b1b0f1 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -455,17 +455,20 @@ impl Graph {
             all_nodes: &mut HashMap<usize, (PathBuf, Source)>,
             sources: &mut Sources,
             edges: &[Vec<usize>],
-            num_input_files: usize,
+            processed_sources: &mut HashSet<usize>,
         ) {
+            // iterate over all dependencies not processed yet
             for dep in edges[idx].iter().copied() {
-                // we only process nodes that were added as part of the resolve step because input
-                // nodes are handled separately
-                if dep >= num_input_files {
-                    // library import
-                    if let Some((path, source)) = all_nodes.remove(&dep) {
-                        sources.insert(path, source);
-                        insert_imports(dep, all_nodes, sources, edges, num_input_files);
-                    }
+                // keep track of processed dependencies, if the dep was already in the set we have
+                // processed it already
+                if !processed_sources.insert(dep) {
+                    continue
+                }
+
+                // library import
+                if let Some((path, source)) = all_nodes.get(&dep).cloned() {
+                    sources.insert(path, source);
+                    insert_imports(dep, all_nodes, sources, edges, processed_sources);
                 }
             }
         }
@@ -480,17 +483,21 @@ impl Graph {
         // determine the `Sources` set for each solc version
         for (version, input_node_indices) in versioned_nodes {
             let mut sources = Sources::new();
+
+            // all input nodes will be processed
+            let mut processed_sources = input_node_indices.iter().copied().collect();
+
             // we only process input nodes (from sources, tests for example)
             for idx in input_node_indices {
                 // insert the input node in the sources set and remove it from the available set
-                let (path, source) = all_nodes.remove(&idx).expect("node is preset. qed");
+                let (path, source) = all_nodes.get(&idx).cloned().expect("node is preset. qed");
                 sources.insert(path, source);
                 insert_imports(
                     idx,
                     &mut all_nodes,
                     &mut sources,
                     &edges.edges,
-                    edges.num_input_files,
+                    &mut processed_sources,
                 );
             }
             versioned_sources.insert(version, sources);
diff --git a/examples/README.md b/examples/README.md
index bb25a09d..c37836b0 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -22,7 +22,7 @@
     - [x] Events with meta
     - [ ] Methods
 - [ ] Events
-  - [ ] Logs and filtering
+  - [x] Logs and filtering
   - [ ] Solidity topics
 - [ ] Middleware
   - [x] Builder
diff --git a/examples/events/examples/filtering.rs b/examples/events/examples/filtering.rs
new file mode 100644
index 00000000..5791934e
--- /dev/null
+++ b/examples/events/examples/filtering.rs
@@ -0,0 +1,48 @@
+use ethers::{
+    core::types::{Address, Filter, H160, H256, U256},
+    providers::{Http, Middleware, Provider},
+};
+use eyre::Result;
+use std::sync::Arc;
+use tokio;
+
+const HTTP_URL: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
+const V3FACTORY_ADDRESS: &str = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
+const DAI_ADDRESS: &str = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
+const USDC_ADDRESS: &str = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
+const USDT_ADDRESS: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
+
+/// This example demonstrates filtering and parsing event logs by fetching all Uniswap V3 pools
+/// where both tokens are in the set [USDC, USDT, DAI].
+///
+/// V3 factory reference: https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/IUniswapV3Factory.sol
+#[tokio::main]
+async fn main() -> Result<()> {
+    let provider = Provider::<Http>::try_from(HTTP_URL)?;
+    let client = Arc::new(provider);
+    let token_topics = vec![
+        H256::from(USDC_ADDRESS.parse::<H160>()?),
+        H256::from(USDT_ADDRESS.parse::<H160>()?),
+        H256::from(DAI_ADDRESS.parse::<H160>()?),
+    ];
+    let filter = Filter::new()
+        .address(V3FACTORY_ADDRESS.parse::<Address>()?)
+        .event("PoolCreated(address,address,uint24,int24,address)")
+        .topic1(token_topics.to_vec())
+        .topic2(token_topics.to_vec())
+        .from_block(0);
+    let logs = client.get_logs(&filter).await?;
+    println!("{} pools found!", logs.iter().len());
+    for log in logs.iter() {
+        let token0 = Address::from(log.topics[1]);
+        let token1 = Address::from(log.topics[2]);
+        let fee_tier = U256::from_big_endian(&log.topics[3].as_bytes()[29..32]);
+        let tick_spacing = U256::from_big_endian(&log.data[29..32]);
+        let pool = Address::from(&log.data[44..64].try_into()?);
+        println!(
+            "pool = {}, token0 = {}, token1 = {}, fee = {}, spacing = {}",
+            pool, token0, token1, fee_tier, tick_spacing,
+        );
+    }
+    Ok(())
+}


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 45aa9079..3cc8c97e 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -1610,6 +1610,7 @@ fn can_compile_model_checker_sample() {
         engine: Some(CHC),
         targets: None,
         timeout: Some(10000),
+        invariants: None,
     });
     let compiled = project.compile().unwrap();
 
@@ -1860,7 +1861,7 @@ fn can_parse_doc() {
 
     let contract = r#"
 // SPDX-License-Identifier: GPL-3.0-only
-pragma solidity ^0.8.0;
+pragma solidity 0.8.17;
 
 /// @title Not an ERC20.
 /// @author Notadev