Repository: gakonst/ethers-rs
Pull request #1161 (2022-04-21 12:23:33)

feat: add debug info bindings
<!--
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
Add bindings for missing `debug` info
Ref https://github.com/foundry-rs/foundry/issues/1375
<!--
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

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/README.md b/README.md
index 5309b18e..2d4fdcde 100644
--- a/README.md
+++ b/README.md
@@ -31,11 +31,12 @@ ethers = { git = "https://github.com/gakonst/ethers-rs" }
 
 Tests require the following installed:
 
-1. [`solc`](https://solidity.readthedocs.io/en/latest/installing-solidity.html). We also recommend using [solc-select](https://github.com/crytic/solc-select) for more flexibility.
+1. [`solc`](https://solidity.readthedocs.io/en/latest/installing-solidity.html) (>=0.8.10). We also recommend using [solc-select](https://github.com/crytic/solc-select) for more flexibility.
 2. [`ganache-cli`](https://github.com/trufflesuite/ganache-cli#installation)
+3. [`geth`](https://github.com/ethereum/go-ethereum)
 
 In addition, it is recommended that you set the `ETHERSCAN_API_KEY` environment variable
-for [the abigen via Etherscan](https://github.com/gakonst/ethers-rs/blob/master/ethers/tests/major_contracts.rs) tests.
+for [the abigen via Etherscan](https://github.com/gakonst/ethers-rs/blob/master/ethers-contract/tests/abigen.rs) tests.
 You can get one [here](https://etherscan.io/apis).
 
 ### EVM-compatible chains support
diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs
index de909145..1202c0e5 100644
--- a/ethers-contract/src/contract.rs
+++ b/ethers-contract/src/contract.rs
@@ -261,13 +261,11 @@ impl<M: Middleware> Contract<M> {
     ///
     /// Clones `self` internally
     #[must_use]
-    pub fn connect(&self, client: Arc<M>) -> Self
+    pub fn connect<N>(&self, client: Arc<N>) -> Contract<N>
     where
-        M: Clone,
+        N: Clone,
     {
-        let mut this = self.clone();
-        this.client = client;
-        this
+        Contract { base_contract: self.base_contract.clone(), client, address: self.address }
     }
 
     /// Returns the contract's address
diff --git a/ethers-core/src/utils/anvil.rs b/ethers-core/src/utils/anvil.rs
new file mode 100644
index 00000000..fe75514a
--- /dev/null
+++ b/ethers-core/src/utils/anvil.rs
@@ -0,0 +1,217 @@
+use crate::{
+    types::Address,
+    utils::{secret_key_to_address, unused_port},
+};
+use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
+use std::{
+    io::{BufRead, BufReader},
+    process::{Child, Command},
+    time::{Duration, Instant},
+};
+
+/// How long we will wait for anvil to indicate that it is ready.
+const ANVIL_STARTUP_TIMEOUT_MILLIS: u64 = 5_000;
+
+/// An anvil CLI instance. Will close the instance when dropped.
+///
+/// Construct this using [`Anvil`](crate::utils::Anvil)
+pub struct AnvilInstance {
+    pid: Child,
+    private_keys: Vec<K256SecretKey>,
+    addresses: Vec<Address>,
+    port: u16,
+}
+
+impl AnvilInstance {
+    /// Returns the private keys used to instantiate this instance
+    pub fn keys(&self) -> &[K256SecretKey] {
+        &self.private_keys
+    }
+
+    /// Returns the addresses used to instantiate this instance
+    pub fn addresses(&self) -> &[Address] {
+        &self.addresses
+    }
+
+    /// Returns the port of this instance
+    pub fn port(&self) -> u16 {
+        self.port
+    }
+
+    /// Returns the HTTP endpoint of this instance
+    pub fn endpoint(&self) -> String {
+        format!("http://localhost:{}", self.port)
+    }
+
+    /// Returns the Websocket endpoint of this instance
+    pub fn ws_endpoint(&self) -> String {
+        format!("ws://localhost:{}", self.port)
+    }
+}
+
+impl Drop for AnvilInstance {
+    fn drop(&mut self) {
+        let _ = self.pid.kill().expect("could not kill anvil");
+    }
+}
+
+/// Builder for launching `anvil`.
+///
+/// # Panics
+///
+/// If `spawn` is called without `anvil` being available in the user's $PATH
+///
+/// # Example
+///
+/// ```no_run
+/// use ethers_core::utils::Anvil;
+///
+/// let port = 8545u16;
+/// let url = format!("http://localhost:{}", port).to_string();
+///
+/// let anvil = Anvil::new()
+///     .port(port)
+///     .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
+///     .spawn();
+///
+/// drop(anvil); // this will kill the instance
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct Anvil {
+    port: Option<u16>,
+    block_time: Option<u64>,
+    mnemonic: Option<String>,
+    fork: Option<String>,
+    args: Vec<String>,
+}
+
+impl Anvil {
+    /// Creates an empty Anvil builder.
+    /// The default port is 8545. The mnemonic is chosen randomly.
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Sets the port which will be used when the `anvil` instance is launched.
+    #[must_use]
+    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
+        self.port = Some(port.into());
+        self
+    }
+
+    /// Sets the mnemonic which will be used when the `anvil` instance is launched.
+    #[must_use]
+    pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
+        self.mnemonic = Some(mnemonic.into());
+        self
+    }
+
+    /// Sets the block-time which will be used when the `anvil` instance is launched.
+    #[must_use]
+    pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
+        self.block_time = Some(block_time.into());
+        self
+    }
+
+    /// Sets the `fork` argument to fork from another currently running Ethereum client
+    /// at a given block. Input should be the HTTP location and port of the other client,
+    /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from
+    /// using an @ sign: `http://localhost:8545@1599200`
+    #[must_use]
+    pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
+        self.fork = Some(fork.into());
+        self
+    }
+
+    /// Adds an argument to pass to the `anvil`.
+    #[must_use]
+    pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
+        self.args.push(arg.into());
+        self
+    }
+
+    /// Adds multiple arguments to pass to the `anvil`.
+    #[must_use]
+    pub fn args<I, S>(mut self, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: Into<String>,
+    {
+        for arg in args {
+            self = self.arg(arg);
+        }
+        self
+    }
+
+    /// Consumes the builder and spawns `anvil` with stdout redirected
+    /// to /dev/null.
+    pub fn spawn(self) -> AnvilInstance {
+        let mut cmd = Command::new("anvil");
+        cmd.stdout(std::process::Stdio::piped());
+        let port = if let Some(port) = self.port { port } else { unused_port() };
+        cmd.arg("-p").arg(port.to_string());
+
+        if let Some(mnemonic) = self.mnemonic {
+            cmd.arg("-m").arg(mnemonic);
+        }
+
+        if let Some(block_time) = self.block_time {
+            cmd.arg("-b").arg(block_time.to_string());
+        }
+
+        if let Some(fork) = self.fork {
+            cmd.arg("-f").arg(fork);
+        }
+
+        cmd.args(self.args);
+
+        let mut child = cmd.spawn().expect("couldnt start anvil");
+
+        let stdout = child.stdout.expect("Unable to get stdout for anvil child process");
+
+        let start = Instant::now();
+        let mut reader = BufReader::new(stdout);
+
+        let mut private_keys = Vec::new();
+        let mut addresses = Vec::new();
+        let mut is_private_key = false;
+        loop {
+            if start + Duration::from_millis(ANVIL_STARTUP_TIMEOUT_MILLIS) <= Instant::now() {
+                panic!("Timed out waiting for anvil to start. Is anvil installed?")
+            }
+
+            let mut line = String::new();
+            reader.read_line(&mut line).expect("Failed to read line from anvil process");
+            if line.contains("Listening on") {
+                break
+            }
+
+            if line.starts_with("Private Keys") {
+                is_private_key = true;
+            }
+
+            if is_private_key && line.starts_with('(') {
+                let key_str = &line[6..line.len() - 1];
+                let key_hex = hex::decode(key_str).expect("could not parse as hex");
+                let key = K256SecretKey::from_be_bytes(&key_hex).expect("did not get private key");
+                addresses.push(secret_key_to_address(&SigningKey::from(&key)));
+                private_keys.push(key);
+            }
+        }
+
+        child.stdout = Some(reader.into_inner());
+
+        AnvilInstance { pid: child, private_keys, addresses, port }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    #[ignore]
+    fn can_launch_anvil() {
+        let _ = Anvil::new().spawn();
+    }
+}
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 6dce364b..000a5335 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -10,6 +10,12 @@ mod geth;
 #[cfg(not(target_arch = "wasm32"))]
 pub use geth::{Geth, GethInstance};
 
+/// Utilities for launching an anvil instance
+#[cfg(not(target_arch = "wasm32"))]
+mod anvil;
+#[cfg(not(target_arch = "wasm32"))]
+pub use anvil::{Anvil, AnvilInstance};
+
 /// Moonbeam utils
 pub mod moonbeam;
 
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index c60bd8e3..5cbe8e61 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -97,6 +97,8 @@ impl CompilerInput {
     pub fn sanitized(mut self, version: &Version) -> Self {
         static PRE_V0_6_0: once_cell::sync::Lazy<VersionReq> =
             once_cell::sync::Lazy::new(|| VersionReq::parse("<0.6.0").unwrap());
+        static PRE_V0_8_10: once_cell::sync::Lazy<VersionReq> =
+            once_cell::sync::Lazy::new(|| VersionReq::parse("<0.8.10").unwrap());
 
         if PRE_V0_6_0.matches(version) {
             if let Some(ref mut meta) = self.settings.metadata {
@@ -104,7 +106,18 @@ impl CompilerInput {
                 // missing in <https://docs.soliditylang.org/en/v0.5.17/using-the-compiler.html#compiler-api>
                 meta.bytecode_hash.take();
             }
+            // introduced in <https://docs.soliditylang.org/en/v0.6.0/using-the-compiler.html#compiler-api>
+            let _ = self.settings.debug.take();
         }
+
+        if PRE_V0_8_10.matches(version) {
+            if let Some(ref mut debug) = self.settings.debug {
+                // introduced in <https://docs.soliditylang.org/en/v0.8.10/using-the-compiler.html#compiler-api>
+                // <https://github.com/ethereum/solidity/releases/tag/v0.8.10>
+                debug.debug_info.clear();
+            }
+        }
+
         self
     }
 
@@ -194,6 +207,8 @@ pub struct Settings {
     /// false by default.
     #[serde(rename = "viaIR", default, skip_serializing_if = "Option::is_none")]
     pub via_ir: Option<bool>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub debug: Option<DebuggingSettings>,
     #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
     pub libraries: BTreeMap<String, BTreeMap<String, String>>,
 }
@@ -308,6 +323,7 @@ impl Default for Settings {
             output_selection: OutputSelection::default_output_selection(),
             evm_version: Some(EvmVersion::default()),
             via_ir: None,
+            debug: None,
             libraries: Default::default(),
             remappings: Default::default(),
         }
@@ -483,6 +499,80 @@ impl FromStr for EvmVersion {
         }
     }
 }
+
+/// Debugging settings for solc
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DebuggingSettings {
+    #[serde(
+        default,
+        with = "serde_helpers::display_from_str_opt",
+        skip_serializing_if = "Option::is_none"
+    )]
+    pub revert_strings: Option<RevertStrings>,
+    ///How much extra debug information to include in comments in the produced EVM assembly and
+    /// Yul code.
+    /// Available components are:
+    // - `location`: Annotations of the form `@src <index>:<start>:<end>` indicating the location of
+    //   the corresponding element in the original Solidity file, where:
+    //     - `<index>` is the file index matching the `@use-src` annotation,
+    //     - `<start>` is the index of the first byte at that location,
+    //     - `<end>` is the index of the first byte after that location.
+    // - `snippet`: A single-line code snippet from the location indicated by `@src`. The snippet is
+    //   quoted and follows the corresponding `@src` annotation.
+    // - `*`: Wildcard value that can be used to request everything.
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub debug_info: Vec<String>,
+}
+
+/// How to treat revert (and require) reason strings.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+pub enum RevertStrings {
+    /// "default" does not inject compiler-generated revert strings and keeps user-supplied ones.
+    Default,
+    /// "strip" removes all revert strings (if possible, i.e. if literals are used) keeping
+    /// side-effects
+    Strip,
+    /// "debug" injects strings for compiler-generated internal reverts, implemented for ABI
+    /// encoders V1 and V2 for now.
+    Debug,
+    /// "verboseDebug" even appends further information to user-supplied revert strings (not yet
+    /// implemented)
+    VerboseDebug,
+}
+
+impl fmt::Display for RevertStrings {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let string = match self {
+            RevertStrings::Default => "default",
+            RevertStrings::Strip => "strip",
+            RevertStrings::Debug => "debug",
+            RevertStrings::VerboseDebug => "verboseDebug",
+        };
+        write!(f, "{}", string)
+    }
+}
+
+impl FromStr for RevertStrings {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "default" => Ok(RevertStrings::Default),
+            "strip" => Ok(RevertStrings::Strip),
+            "debug" => Ok(RevertStrings::Debug),
+            "verboseDebug" | "verbosedebug" => Ok(RevertStrings::VerboseDebug),
+            s => Err(format!("Unknown evm version: {}", s)),
+        }
+    }
+}
+
+impl Default for RevertStrings {
+    fn default() -> Self {
+        RevertStrings::Default
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SettingsMetadata {
     /// Use only literal content and not URLs (false by default)


Test patch:
diff --git a/tests/major_contracts.rs b/tests/major_contracts.rs
deleted file mode 100644
index d72a3ad0..00000000
--- a/tests/major_contracts.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-// // This test exists to ensure that the abigen macro works "reasonably" well with popular
-// contracts use ethers::contract::abigen;
-//
-// abigen!(
-//     KeepBonding,
-//     "etherscan:0x7137701e90C6a80B0dA36922cd83942b32A8fc95"
-// );
-// abigen!(cDAI, "etherscan:0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643");
-// abigen!(
-//     Comptroller,
-//     "etherscan:0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b"
-// );
-//
-// // https://github.com/vyperlang/vyper/issues/1931
-// // abigen!(
-// //     Curve,
-// //     "etherscan:0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56"
-// // );
-// abigen!(
-//     UmaAdmin,
-//     "etherscan:0x4E6CCB1dA3C7844887F9A5aF4e8450d9fd90317A"
-// );
-//
-// // e.g. aave's `initialize` methods exist multiple times, so we should rename it
-// abigen!(
-//     AavePoolCore,
-//     "etherscan:0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3",
-//     methods {
-//         initialize(address,bytes) as initialize_proxy;
-//     }
-// );
-//
-// // The DyDxLimitOrders contract uses Abi Encoder v2 with nested tuples
-// abigen!(
-//     DyDxLimitOrders,
-//     "etherscan:0xDEf136D9884528e1EB302f39457af0E4d3AD24EB"
-// );