Repository: gakonst/ethers-rs
Pull request #1479 (2022-07-14 12:38:52)

fix: parse constructor as function
<!--
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
Closes https://github.com/foundry-rs/foundry/issues/2176

`abi-encode constructor(...)` failed to parse the function

while this is not entirely correct to parse constructor as `Function` it severely simplifies things 

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/ethers-core/src/abi/human_readable/lexer.rs b/ethers-core/src/abi/human_readable/lexer.rs
index 5e807fb9..3c9c6f24 100644
--- a/ethers-core/src/abi/human_readable/lexer.rs
+++ b/ethers-core/src/abi/human_readable/lexer.rs
@@ -1,4 +1,4 @@
-use ethabi::{Event, EventParam, Function, Param, ParamType, StateMutability};
+use ethabi::{Constructor, Event, EventParam, Function, Param, ParamType, StateMutability};
 use std::{fmt, iter::Peekable, str::CharIndices};
 use unicode_xid::UnicodeXID;
 
@@ -320,6 +320,18 @@ impl<'input> HumanReadableParser<'input> {
         Self::new(input).take_function()
     }
 
+    /// Parses a [Constructor] from a human readable form
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_core::abi::HumanReadableParser;
+    /// let mut constructor = HumanReadableParser::parse_constructor("constructor(address author, string oldValue, string newValue)").unwrap();
+    /// ```
+    pub fn parse_constructor(input: &'input str) -> Result<Constructor, LexerError> {
+        Self::new(input).take_constructor()
+    }
+
     /// Parses an [Event] from a human readable form
     ///
     /// # Example
@@ -332,6 +344,15 @@ impl<'input> HumanReadableParser<'input> {
         Self::new(input).take_event()
     }
 
+    /// Returns the next `Constructor` and consumes the underlying tokens
+    pub fn take_constructor(&mut self) -> Result<Constructor, LexerError> {
+        self.take_next_exact(Token::Constructor)?;
+        self.take_open_parenthesis()?;
+        let inputs = self.take_function_params()?;
+        self.take_close_parenthesis()?;
+        Ok(Constructor { inputs })
+    }
+    /// Returns the next `Function` and consumes the underlying tokens
     pub fn take_function(&mut self) -> Result<Function, LexerError> {
         let name = self.take_identifier(Token::Function)?;
 
@@ -812,6 +833,30 @@ pub enum DataLocation {
 mod tests {
     use super::*;
 
+    #[test]
+    fn parse_constructor() {
+        let f = Constructor {
+            inputs: vec![
+                Param { name: "author".to_string(), kind: ParamType::Address, internal_type: None },
+                Param {
+                    name: "oldValue".to_string(),
+                    kind: ParamType::String,
+                    internal_type: None,
+                },
+                Param {
+                    name: "newValue".to_string(),
+                    kind: ParamType::String,
+                    internal_type: None,
+                },
+            ],
+        };
+        let parsed = HumanReadableParser::parse_constructor(
+            "constructor(address author, string oldValue, string newValue)",
+        )
+        .unwrap();
+        assert_eq!(f, parsed);
+    }
+
     #[test]
     fn test_parse_function() {
         #[allow(deprecated)]
diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/transports/retry.rs
index 5c496045..72b823de 100644
--- a/ethers-providers/src/transports/retry.rs
+++ b/ethers-providers/src/transports/retry.rs
@@ -5,7 +5,6 @@ use super::{common::JsonRpcError, http::ClientError};
 use crate::{provider::ProviderError, JsonRpcClient};
 
 use std::{
-    clone::Clone,
     fmt::Debug,
     sync::atomic::{AtomicU32, Ordering},
     time::Duration,
@@ -135,10 +134,23 @@ where
         A: std::fmt::Debug + Serialize + Send + Sync,
         R: DeserializeOwned,
     {
-        let ahead_in_queue = self.requests_enqueued.fetch_add(1, Ordering::SeqCst) as u64;
+        // Helper type that caches the `params` value across several retries
+        // This is necessary because the wrapper provider is supposed to skip he `params` if it's of
+        // size 0, see `crate::transports::common::Request`
+        enum RetryParams<Params> {
+            Value(Params),
+            Zst(()),
+        }
 
-        let params =
-            serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
+        let params = if std::mem::size_of::<A>() == 0 {
+            RetryParams::Zst(())
+        } else {
+            let params =
+                serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
+            RetryParams::Value(params)
+        };
+
+        let ahead_in_queue = self.requests_enqueued.fetch_add(1, Ordering::SeqCst) as u64;
 
         let mut retry_number: u32 = 0;
 
@@ -148,7 +160,11 @@ where
             // hack to not hold `R` across an await in the sleep future and prevent requiring
             // R: Send + Sync
             {
-                match self.inner.request(method, params.clone()).await {
+                let resp = match params {
+                    RetryParams::Value(ref params) => self.inner.request(method, params).await,
+                    RetryParams::Zst(unit) => self.inner.request(method, unit).await,
+                };
+                match resp {
                     Ok(ret) => {
                         self.requests_enqueued.fetch_sub(1, Ordering::SeqCst);
                         return Ok(ret)
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index cc4d98f0..75dc4d9a 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -119,13 +119,25 @@ impl ProjectPathsConfig {
         Ok(Source::read_all_from(&self.scripts)?)
     }
 
-    /// Returns the combined set solidity file paths for `Self::sources` and `Self::tests`
+    /// Returns true if the there is at least one solidity file in this config.
+    ///
+    /// See also, `Self::input_files()`
+    pub fn has_input_files(&self) -> bool {
+        self.input_files_iter().next().is_some()
+    }
+
+    /// Returns an iterator that yields all solidity file paths for `Self::sources`, `Self::tests`
+    /// and `Self::scripts`
+    pub fn input_files_iter(&self) -> impl Iterator<Item = PathBuf> + '_ {
+        utils::source_files_iter(&self.sources)
+            .chain(utils::source_files_iter(&self.tests))
+            .chain(utils::source_files_iter(&self.scripts))
+    }
+
+    /// Returns the combined set solidity file paths for `Self::sources`, `Self::tests` and
+    /// `Self::scripts`
     pub fn input_files(&self) -> Vec<PathBuf> {
-        utils::source_files(&self.sources)
-            .into_iter()
-            .chain(utils::source_files(&self.tests))
-            .chain(utils::source_files(&self.scripts))
-            .collect()
+        self.input_files_iter().collect()
     }
 
     /// Returns the combined set of `Self::read_sources` + `Self::read_tests` + `Self::read_scripts`
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index eedea95f..487055cf 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -72,6 +72,22 @@ pub fn find_version_pragma(contract: &str) -> Option<Match> {
     RE_SOL_PRAGMA_VERSION.captures(contract)?.name("version")
 }
 
+/// Returns an iterator that yields all solidity/yul files funder under the given root path or the
+/// `root` itself, if it is a sol/yul file
+///
+/// This also follows symlinks.
+pub fn source_files_iter(root: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
+    WalkDir::new(root)
+        .follow_links(true)
+        .into_iter()
+        .filter_map(Result::ok)
+        .filter(|e| e.file_type().is_file())
+        .filter(|e| {
+            e.path().extension().map(|ext| (ext == "sol") || (ext == "yul")).unwrap_or_default()
+        })
+        .map(|e| e.path().into())
+}
+
 /// Returns a list of absolute paths to all the solidity files under the root, or the file itself,
 /// if the path is a solidity file.
 ///
@@ -86,16 +102,7 @@ pub fn find_version_pragma(contract: &str) -> Option<Match> {
 /// let sources = utils::source_files("./contracts");
 /// ```
 pub fn source_files(root: impl AsRef<Path>) -> Vec<PathBuf> {
-    WalkDir::new(root)
-        .follow_links(true)
-        .into_iter()
-        .filter_map(Result::ok)
-        .filter(|e| e.file_type().is_file())
-        .filter(|e| {
-            e.path().extension().map(|ext| (ext == "sol") || (ext == "yul")).unwrap_or_default()
-        })
-        .map(|e| e.path().into())
-        .collect()
+    source_files_iter(root).collect()
 }
 
 /// Returns a list of _unique_ paths to all folders under `root` that contain at least one solidity


Test patch:
diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs
index 666de8e7..b1da9db7 100644
--- a/ethers-providers/tests/provider.rs
+++ b/ethers-providers/tests/provider.rs
@@ -91,6 +91,28 @@ mod eth_tests {
         let (_max_fee_per_gas, _max_priority_fee_per_gas) =
             provider.estimate_eip1559_fees(None).await.unwrap();
     }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_hardhat_compatibility() {
+        use ethers_providers::RetryClient;
+
+        async fn send_zst_requests<M: Middleware>(provider: M) {
+            let _ = provider.get_chainid().await.unwrap();
+            let _ = provider.get_block_number().await.unwrap();
+            let _ = provider.get_gas_price().await.unwrap();
+            let _ = provider.get_accounts().await.unwrap();
+            let _ = provider.get_net_version().await.unwrap();
+        }
+
+        let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
+        send_zst_requests(provider).await;
+
+        let provider =
+            Provider::<RetryClient<Http>>::new_client("http://localhost:8545", 10, 200).unwrap();
+
+        send_zst_requests(provider).await;
+    }
 }
 
 #[cfg(feature = "celo")]