Repository: gakonst/ethers-rs
Pull request #939 (2022-02-21 06:01:09)

feat(etherscan): account endpoints
## Motivation

https://github.com/gakonst/ethers-rs/issues/485

Add etherscan.io account endpoints

## Solution

Added them.

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [x] Updated the changelog


tomtau: FYI I checked forks and noticed @1wilkens had a WIP version  https://github.com/1wilkens/ethers-rs/commit/f2eb53732fca388b0af21a756828e8efab8106d0 -- feel free to review if this PR works for you!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31e0e9cd..16db8093 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+- Add Etherscan account API endpoints [939](https://github.com/gakonst/ethers-rs/pull/939)
 - Add FTM Mainet and testnet to parse method "try_from" from Chain.rs and add cronos mainet and testnet to "from_str"
 - Add FTM mainnet and testnet Multicall addresses [927](https://github.com/gakonst/ethers-rs/pull/927)
 - Add Cronos mainnet beta and testnet to the list of known chains
@@ -58,6 +59,8 @@
 
 ### Unreleased
 
+- Wrap `ethabi::Contract` into new type `LosslessAbi` and `abi: Option<Abi>` with `abi: Option<LosslessAbi>` in `ConfigurableContractArtifact`
+  [#952](https://github.com/gakonst/ethers-rs/pull/952)
 - Let `Project` take ownership of `ArtifactOutput` and change trait interface
   [#907](https://github.com/gakonst/ethers-rs/pull/907)
 - Total revamp of the `Project::compile` pipeline
diff --git a/Cargo.lock b/Cargo.lock
index b453f7d9..7b397336 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -516,7 +516,7 @@ dependencies = [
  "bs58",
  "coins-core",
  "digest 0.9.0",
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "hmac 0.11.0",
  "k256",
  "lazy_static",
@@ -533,7 +533,7 @@ checksum = "8f473ea37dfc9d2cb94fdde50c3d41f28c3f384b367573d66386fea38d76d466"
 dependencies = [
  "bitvec 0.17.4",
  "coins-bip32",
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "hex",
  "hmac 0.11.0",
  "pbkdf2 0.8.0",
@@ -1187,8 +1187,9 @@ dependencies = [
  "cfg-if 1.0.0",
  "dunce",
  "ethers-core",
+ "ethers-solc",
  "eyre",
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "hex",
  "once_cell",
  "proc-macro2",
@@ -1384,7 +1385,7 @@ dependencies = [
  "ethers-core",
  "fs_extra",
  "futures-util",
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "glob",
  "hex",
  "home",
@@ -1650,9 +1651,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
+checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
 dependencies = [
  "cfg-if 1.0.0",
  "js-sys",
@@ -2821,7 +2822,7 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
 dependencies = [
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
 ]
 
 [[package]]
@@ -2873,7 +2874,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
 dependencies = [
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "redox_syscall",
 ]
 
@@ -4063,7 +4064,7 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
 dependencies = [
- "getrandom 0.2.4",
+ "getrandom 0.2.5",
  "serde",
 ]
 
diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml
index 4c25310c..8a69ead6 100644
--- a/ethers-contract/ethers-contract-abigen/Cargo.toml
+++ b/ethers-contract/ethers-contract-abigen/Cargo.toml
@@ -42,3 +42,4 @@ rustls = ["reqwest/rustls-tls"]
 
 [dev-dependencies]
 tempfile = "3.2.0"
+ethers-solc = { path = "../../ethers-solc", default-features = false, features = ["project-util"] }
\ No newline at end of file
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index 813bf575..c9652f3d 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -76,7 +76,10 @@ pub struct Context {
     human_readable: bool,
 
     /// The contract name as an identifier.
-    contract_name: Ident,
+    contract_ident: Ident,
+
+    /// The contract name as string
+    contract_name: String,
 
     /// Manually specified method aliases.
     method_aliases: BTreeMap<String, MethodAlias>,
@@ -91,9 +94,9 @@ pub struct Context {
 impl Context {
     /// Expands the whole rust contract
     pub fn expand(&self) -> Result<ExpandedContract> {
-        let name = &self.contract_name;
+        let name = &self.contract_ident;
         let name_mod =
-            util::ident(&format!("{}_mod", self.contract_name.to_string().to_lowercase()));
+            util::ident(&format!("{}_mod", self.contract_ident.to_string().to_lowercase()));
 
         let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase()));
 
@@ -173,12 +176,16 @@ impl Context {
 
             internal_structs
         } else if abi_str.starts_with('{') {
-            abi_str = serde_json::to_string(&abi).context("fail to serialize abi to json")?;
-
-            serde_json::from_str::<RawAbi>(&abi_str)
-                .ok()
-                .map(InternalStructs::new)
-                .unwrap_or_default()
+            if let Ok(abi) = serde_json::from_str::<RawAbi>(&abi_str) {
+                if let Ok(s) = serde_json::to_string(&abi) {
+                    // need to update the `abi_str` here because we only want the `"abi": [...]`
+                    // part of the json object in the contract binding
+                    abi_str = s;
+                }
+                InternalStructs::new(abi)
+            } else {
+                InternalStructs::default()
+            }
         } else {
             serde_json::from_str::<RawAbi>(&abi_str)
                 .ok()
@@ -186,7 +193,7 @@ impl Context {
                 .unwrap_or_default()
         };
 
-        let contract_name = util::ident(&args.contract_name);
+        let contract_ident = util::ident(&args.contract_name);
 
         // NOTE: We only check for duplicate signatures here, since if there are
         //   duplicate aliases, the compiler will produce a warning because a
@@ -222,13 +229,19 @@ impl Context {
             abi_str: Literal::string(&abi_str),
             abi_parser,
             internal_structs,
-            contract_name,
+            contract_ident,
+            contract_name: args.contract_name,
             method_aliases,
             event_derives,
             event_aliases,
         })
     }
 
+    /// The initial name fo the contract
+    pub(crate) fn contract_name(&self) -> &str {
+        &self.contract_name
+    }
+
     /// The internal abi struct mapping table
     pub fn internal_structs(&self) -> &InternalStructs {
         &self.internal_structs
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs
index 05d787ff..07a008e7 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs
@@ -31,7 +31,7 @@ pub(crate) fn imports(name: &str) -> TokenStream {
 
 /// Generates the static `Abi` constants and the contract struct
 pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> TokenStream {
-    let name = &cx.contract_name;
+    let name = &cx.contract_ident;
     let abi = &cx.abi_str;
 
     let ethers_core = ethers_core_crate();
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
index c0d29bc2..96946af8 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
@@ -102,7 +102,7 @@ impl Context {
 
     /// The name ident of the events enum
     fn expand_event_enum_name(&self) -> Ident {
-        util::ident(&format!("{}Events", self.contract_name))
+        util::ident(&format!("{}Events", self.contract_ident))
     }
 
     /// Expands the `events` function that bundles all declared events of this contract
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index 42e34c28..212ebdb8 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -161,7 +161,7 @@ impl Context {
 
     /// The name ident of the calls enum
     fn expand_calls_enum_name(&self) -> Ident {
-        util::ident(&format!("{}Calls", self.contract_name))
+        util::ident(&format!("{}Calls", self.contract_ident))
     }
 
     /// Expands to the `name : type` pairs of the function's inputs
diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs
index a93f1aa3..ac83f68d 100644
--- a/ethers-contract/ethers-contract-abigen/src/lib.rs
+++ b/ethers-contract/ethers-contract-abigen/src/lib.rs
@@ -26,6 +26,7 @@ pub use ethers_core::types::Address;
 pub use source::Source;
 pub use util::parse_address;
 
+use crate::contract::ExpandedContract;
 use eyre::Result;
 use inflector::Inflector;
 use proc_macro2::TokenStream;
@@ -37,10 +38,13 @@ use std::{collections::HashMap, fs::File, io::Write, path::Path};
 /// [still not supported by Vyper](https://github.com/vyperlang/vyper/issues/1931), so you must adjust your ABIs and replace
 /// `constant` functions with `view` or `pure`.
 ///
+/// To generate bindings for _multiple_ contracts at once see also [`crate::MultiAbigen`].
+///
 /// # Example
 ///
-/// Running the command below will generate a file called `token.rs` containing the
-/// bindings inside, which exports an `ERC20Token` struct, along with all its events.
+/// Running the code below will generate a file called `token.rs` containing the
+/// bindings inside, which exports an `ERC20Token` struct, along with all its events. Put into a
+/// `build.rs` file this will generate the bindings during `cargo build`.
 ///
 /// ```no_run
 /// # use ethers_contract_abigen::Abigen;
@@ -84,7 +88,7 @@ impl Abigen {
         })
     }
 
-    /// Attemtps to load a new builder from an ABI JSON file at the specific
+    /// Attempts to load a new builder from an ABI JSON file at the specific
     /// path.
     pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
         let name = path
@@ -150,8 +154,15 @@ impl Abigen {
     pub fn generate(self) -> Result<ContractBindings> {
         let rustfmt = self.rustfmt;
         let name = self.contract_name.clone();
-        let tokens = Context::from_abigen(self)?.expand()?.into_tokens();
-        Ok(ContractBindings { tokens, rustfmt, name })
+        let (expanded, _) = self.expand()?;
+        Ok(ContractBindings { tokens: expanded.into_tokens(), rustfmt, name })
+    }
+
+    /// Expands the `Abigen` and returns the [`ExpandedContract`] that holds all tokens and the
+    /// [`Context`] that holds the state used during expansion.
+    pub fn expand(self) -> Result<(ExpandedContract, Context)> {
+        let ctx = Context::from_abigen(self)?;
+        Ok((ctx.expand()?, ctx))
     }
 }
 
@@ -235,6 +246,7 @@ impl ContractBindings {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use ethers_solc::project_util::TempProject;
 
     #[test]
     fn can_generate_structs() {
@@ -244,4 +256,42 @@ mod tests {
         let out = gen.tokens.to_string();
         assert!(out.contains("pub struct Stuff"));
     }
+
+    #[test]
+    fn can_compile_and_generate() {
+        let tmp = TempProject::dapptools().unwrap();
+
+        tmp.add_source(
+            "Greeter",
+            r#"
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+contract Greeter {
+
+    struct Inner {
+        bool a;
+    }
+
+    struct Stuff {
+        Inner inner;
+    }
+
+    function greet(Stuff calldata stuff) public view returns (Stuff memory) {
+        return stuff;
+    }
+}
+"#,
+        )
+        .unwrap();
+
+        let _ = tmp.compile().unwrap();
+
+        let abigen =
+            Abigen::from_file(tmp.artifacts_path().join("Greeter.sol/Greeter.json")).unwrap();
+        let gen = abigen.generate().unwrap();
+        let out = gen.tokens.to_string();
+        assert!(out.contains("pub struct Stuff"));
+        assert!(out.contains("pub struct Inner"));
+    }
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs
index 6b3c3662..bb9a12f7 100644
--- a/ethers-contract/ethers-contract-abigen/src/multi.rs
+++ b/ethers-contract/ethers-contract-abigen/src/multi.rs
@@ -2,9 +2,192 @@
 
 use eyre::Result;
 use inflector::Inflector;
-use std::{collections::BTreeMap, fs, io::Write, path::Path};
+use proc_macro2::TokenStream;
+use quote::quote;
+use std::{
+    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
+    fs,
+    io::Write,
+    path::Path,
+};
+
+use crate::{util, Abigen, Context, ContractBindings, ExpandedContract};
+
+/// Represents a collection of [`Abigen::expand()`]
+pub struct MultiExpansion {
+    // all expanded contracts collection from [`Abigen::expand()`]
+    contracts: Vec<(ExpandedContract, Context)>,
+}
+
+impl MultiExpansion {
+    /// Create a new instance that wraps the given `contracts`
+    pub fn new(contracts: Vec<(ExpandedContract, Context)>) -> Self {
+        Self { contracts }
+    }
+
+    /// Create a new instance by expanding all `Abigen` elements the given iterator yields
+    pub fn from_abigen(abigens: impl IntoIterator<Item = Abigen>) -> Result<Self> {
+        let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::<Result<_>>()?;
+        Ok(Self::new(contracts))
+    }
+
+    /// Expands all contracts into a single `TokenStream`
+    ///
+    /// This will deduplicate types into a separate `mod __shared_types` module, if any.
+    pub fn expand_inplace(self) -> TokenStream {
+        self.expand().expand_inplace()
+    }
+
+    /// Expands all contracts into separated [`TokenStream`]s
+    ///
+    /// If there was type deduplication, this returns a list of [`TokenStream`] containing the type
+    /// definitions of all shared types.
+    pub fn expand(self) -> MultiExpansionResult {
+        let mut expansions = self.contracts;
+        let mut shared_types = Vec::new();
+        // this keeps track of those contracts that need to be updated after a struct was
+        // extracted from the contract's module and moved to the shared module
+        let mut dirty_contracts = HashSet::new();
+
+        // merge all types if more than 1 contract
+        if expansions.len() > 1 {
+            // check for type conflicts across all contracts
+            let mut conflicts: HashMap<String, Vec<usize>> = HashMap::new();
+            for (idx, (_, ctx)) in expansions.iter().enumerate() {
+                for type_identifier in ctx.internal_structs().rust_type_names().keys() {
+                    conflicts
+                        .entry(type_identifier.clone())
+                        .or_insert_with(|| Vec::with_capacity(1))
+                        .push(idx);
+                }
+            }
+
+            // resolve type conflicts
+            for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
+                // extract the shared type once
+                shared_types.push(
+                    expansions[contracts[0]]
+                        .1
+                        .struct_definition(id)
+                        .expect("struct def succeeded previously"),
+                );
+
+                // remove the shared type from the contract's bindings
+                for contract in contracts.iter().copied() {
+                    expansions[contract].1.remove_struct(id);
+                    dirty_contracts.insert(contract);
+                }
+            }
 
-use crate::{util, Abigen, ContractBindings};
+            // regenerate all struct definitions that were hit
+            for contract in dirty_contracts.iter().copied() {
+                let (expanded, ctx) = &mut expansions[contract];
+                expanded.abi_structs = ctx.abi_structs().expect("struct def succeeded previously");
+            }
+        }
+
+        MultiExpansionResult { contracts: expansions, dirty_contracts, shared_types }
+    }
+}
+
+/// Represents an intermediary result of [`MultiExpansion::expand()`]
+pub struct MultiExpansionResult {
+    contracts: Vec<(ExpandedContract, Context)>,
+    /// contains the indices of contracts with structs that need to be updated
+    dirty_contracts: HashSet<usize>,
+    /// all type definitions of types that are shared by multiple contracts
+    shared_types: Vec<TokenStream>,
+}
+
+impl MultiExpansionResult {
+    /// Expands all contracts into a single [`TokenStream`]
+    pub fn expand_inplace(mut self) -> TokenStream {
+        let mut tokens = TokenStream::new();
+
+        let shared_types_module = quote! {__shared_types};
+        // the import path to the shared types
+        let shared_path = quote!(
+            pub use super::#shared_types_module::*;
+        );
+        self.add_shared_import_path(shared_path);
+
+        let Self { contracts, shared_types, .. } = self;
+
+        if !shared_types.is_empty() {
+            tokens.extend(quote! {
+                pub mod #shared_types_module {
+                    #( #shared_types )*
+                }
+            });
+        }
+
+        tokens.extend(contracts.into_iter().map(|(exp, _)| exp.into_tokens()));
+
+        tokens
+    }
+
+    /// Sets the path to the shared types module according to the value of `single_file`
+    ///
+    /// If `single_file` then it's expected that types will be written to `shared_types.rs`
+    fn set_shared_import_path(&mut self, single_file: bool) {
+        let shared_path = if single_file {
+            quote!(
+                pub use super::__shared_types::*;
+            )
+        } else {
+            quote!(
+                pub use super::super::shared_types::*;
+            )
+        };
+        self.add_shared_import_path(shared_path);
+    }
+
+    /// adds the `shared` import path to every `dirty` contract
+    fn add_shared_import_path(&mut self, shared: TokenStream) {
+        for contract in self.dirty_contracts.iter().copied() {
+            let (expanded, ..) = &mut self.contracts[contract];
+            expanded.imports.extend(shared.clone());
+        }
+    }
+
+    /// Converts this result into [`MultiBindingsInner`]
+    fn into_bindings(mut self, single_file: bool, rustfmt: bool) -> MultiBindingsInner {
+        self.set_shared_import_path(single_file);
+        let Self { contracts, shared_types, .. } = self;
+        let bindings = contracts
+            .into_iter()
+            .map(|(expanded, ctx)| ContractBindings {
+                tokens: expanded.into_tokens(),
+                rustfmt,
+                name: ctx.contract_name().to_string(),
+            })
+            .map(|v| (v.name.clone(), v))
+            .collect();
+
+        let shared_types = if !shared_types.is_empty() {
+            let shared_types = if single_file {
+                quote! {
+                    pub mod __shared_types {
+                        #( #shared_types )*
+                    }
+                }
+            } else {
+                quote! {
+                    #( #shared_types )*
+                }
+            };
+            Some(ContractBindings {
+                tokens: shared_types,
+                rustfmt,
+                name: "shared_types".to_string(),
+            })
+        } else {
+            None
+        };
+
+        MultiBindingsInner { bindings, shared_types }
+    }
+}
 
 /// Collects Abigen structs for a series of contracts, pending generation of
 /// the contract bindings.
@@ -86,16 +269,11 @@ impl MultiAbigen {
 
     /// Build the contract bindings and prepare for writing
     pub fn build(self) -> Result<MultiBindings> {
-        let bindings = self
-            .abigens
-            .into_iter()
-            .map(|v| v.generate())
-            .collect::<Result<Vec<_>>>()?
-            .into_iter()
-            .map(|v| (v.name.clone(), v))
-            .collect();
-
-        Ok(MultiBindings { bindings })
+        let rustfmt = self.abigens.iter().any(|gen| gen.rustfmt);
+        Ok(MultiBindings {
+            expansion: MultiExpansion::from_abigen(self.abigens)?.expand(),
+            rustfmt,
+        })
     }
 }
 
@@ -126,12 +304,188 @@ impl MultiAbigen {
 ///   * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
 ///     changed)
 pub struct MultiBindings {
+    expansion: MultiExpansionResult,
+    rustfmt: bool,
+}
+
+impl MultiBindings {
+    fn into_inner(self, single_file: bool) -> MultiBindingsInner {
+        self.expansion.into_bindings(single_file, self.rustfmt)
+    }
+
+    /// Generates all the bindings and writes them to the given module
+    ///
+    /// # Example
+    ///
+    /// Read all json abi files from the `./abi` directory
+    /// ```text
+    /// abi
+    /// ├── ERC20.json
+    /// ├── Contract1.json
+    /// ├── Contract2.json
+    /// ...
+    /// ```
+    ///
+    /// and write them to the `./src/contracts` location as
+    ///
+    /// ```text
+    /// src/contracts
+    /// ├── mod.rs
+    /// ├── er20.rs
+    /// ├── contract1.rs
+    /// ├── contract2.rs
+    /// ...
+    /// ```
+    ///
+    /// ```no_run
+    /// # use ethers_contract_abigen::MultiAbigen;
+    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
+    /// let bindings = gen.build().unwrap();
+    /// bindings.write_to_module("./src/contracts", false).unwrap();
+    /// ```
+    pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
+        self.into_inner(single_file).write_to_module(module, single_file)
+    }
+
+    /// Generates all the bindings and writes a library crate containing them
+    /// to the provided path
+    ///
+    /// # Example
+    ///
+    /// Read all json abi files from the `./abi` directory
+    /// ```text
+    /// abi
+    /// ├── ERC20.json
+    /// ├── Contract1.json
+    /// ├── Contract2.json
+    /// ├── Contract3/
+    ///     ├── Contract3.json
+    /// ...
+    /// ```
+    ///
+    /// and write them to the `./bindings` location as
+    ///
+    /// ```text
+    /// bindings
+    /// ├── Cargo.toml
+    /// ├── src/
+    ///     ├── lib.rs
+    ///     ├── er20.rs
+    ///     ├── contract1.rs
+    ///     ├── contract2.rs
+    /// ...
+    /// ```
+    ///
+    /// ```no_run
+    /// # use ethers_contract_abigen::MultiAbigen;
+    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
+    /// let bindings = gen.build().unwrap();
+    /// bindings.write_to_crate(
+    ///     "my-crate", "0.0.5", "./bindings", false
+    /// ).unwrap();
+    /// ```
+    pub fn write_to_crate(
+        self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+        lib: impl AsRef<Path>,
+        single_file: bool,
+    ) -> Result<()> {
+        self.into_inner(single_file).write_to_crate(name, version, lib, single_file)
+    }
+
+    /// This ensures that the already generated bindings crate matches the
+    /// output of a fresh new run. Run this in a rust test, to get notified in
+    /// CI if the newly generated bindings deviate from the already generated
+    /// ones, and it's time to generate them again. This could happen if the
+    /// ABI of a contract or the output that `ethers` generates changed.
+    ///
+    /// If this functions is run within a test during CI and fails, then it's
+    /// time to update all bindings.
+    ///
+    /// # Returns
+    ///
+    /// `Ok(())` if the freshly generated bindings match with the
+    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
+    /// with more information.
+    ///
+    /// # Example
+    ///
+    /// Check that the generated files are up to date
+    ///
+    /// ```no_run
+    /// # use ethers_contract_abigen::MultiAbigen;
+    /// #[test]
+    /// fn generated_bindings_are_fresh() {
+    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
+    ///  let abi_dir = project_root.join("abi");
+    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
+    ///  let bindings = gen.build().unwrap();
+    ///  bindings.ensure_consistent_crate(
+    ///     "my-crate", "0.0.1", project_root.join("src/contracts"), false
+    ///  ).expect("inconsistent bindings");
+    /// }
+    /// ```
+    pub fn ensure_consistent_crate(
+        self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+        crate_path: impl AsRef<Path>,
+        single_file: bool,
+    ) -> Result<()> {
+        self.into_inner(single_file).ensure_consistent_crate(name, version, crate_path, single_file)
+    }
+
+    /// This ensures that the already generated bindings module matches the
+    /// output of a fresh new run. Run this in a rust test, to get notified in
+    /// CI if the newly generated bindings deviate from the already generated
+    /// ones, and it's time to generate them again. This could happen if the
+    /// ABI of a contract or the output that `ethers` generates changed.
+    ///
+    /// If this functions is run within a test during CI and fails, then it's
+    /// time to update all bindings.
+    ///
+    /// # Returns
+    ///
+    /// `Ok(())` if the freshly generated bindings match with the
+    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
+    /// with more information.
+    ///
+    /// # Example
+    ///
+    /// Check that the generated files are up to date
+    ///
+    /// ```no_run
+    /// # use ethers_contract_abigen::MultiAbigen;
+    /// #[test]
+    /// fn generated_bindings_are_fresh() {
+    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
+    ///  let abi_dir = project_root.join("abi");
+    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
+    ///  let bindings = gen.build().unwrap();
+    ///  bindings.ensure_consistent_module(
+    ///     project_root.join("src/contracts"), false
+    ///  ).expect("inconsistent bindings");
+    /// }
+    /// ```
+    pub fn ensure_consistent_module(
+        self,
+        module: impl AsRef<Path>,
+        single_file: bool,
+    ) -> Result<()> {
+        self.into_inner(single_file).ensure_consistent_module(module, single_file)
+    }
+}
+
+struct MultiBindingsInner {
     /// Abigen objects to be written
     bindings: BTreeMap<String, ContractBindings>,
+    /// contains the content of the shared types if any
+    shared_types: Option<ContractBindings>,
 }
 
 // deref allows for inspection without modification
-impl std::ops::Deref for MultiBindings {
+impl std::ops::Deref for MultiBindingsInner {
     type Target = BTreeMap<String, ContractBindings>;
 
     fn deref(&self) -> &Self::Target {
@@ -139,8 +493,8 @@ impl std::ops::Deref for MultiBindings {
     }
 }
 
-impl MultiBindings {
-    /// Generat the contents of the `Cargo.toml` file for a lib
+impl MultiBindingsInner {
+    /// Generate the contents of the `Cargo.toml` file for a lib
     fn generate_cargo_toml(
         &self,
         name: impl AsRef<str>,
@@ -185,34 +539,14 @@ serde_json = "1.0.79"
         Ok(())
     }
 
-    /// Generate the shared prefix of the `lib.rs` or `mod.rs`
-    fn generate_prefix(
-        &self,
-        mut buf: impl Write,
-        is_crate: bool,
-        single_file: bool,
-    ) -> Result<()> {
-        writeln!(buf, "#![allow(clippy::all)]")?;
-        writeln!(
-            buf,
-            "//! This {} contains abigen! generated bindings for solidity contracts.",
-            if is_crate { "lib" } else { "module" }
-        )?;
-        writeln!(buf, "//! This is autogenerated code.")?;
-        writeln!(buf, "//! Do not manually edit these files.")?;
-        writeln!(
-            buf,
-            "//! {} may be overwritten by the codegen system at any time.",
-            if single_file && !is_crate { "This file" } else { "These files" }
-        )?;
-
-        Ok(())
-    }
-
     /// Append module declarations to the `lib.rs` or `mod.rs`
     fn append_module_names(&self, mut buf: impl Write) -> Result<()> {
-        // sorting here not necessary, as btreemap keys are ordered
-        for module in self.bindings.keys().map(|name| format!("pub mod {};", name.to_snake_case()))
+        let mut mod_names: BTreeSet<_> = self.bindings.keys().collect();
+        if let Some(ref shared) = self.shared_types {
+            mod_names.insert(&shared.name);
+        }
+
+        for module in mod_names.into_iter().map(|name| format!("pub mod {};", name.to_snake_case()))
         {
             writeln!(buf, "{}", module)?;
         }
@@ -223,14 +557,17 @@ serde_json = "1.0.79"
     /// Generate the contents of `lib.rs` or `mod.rs`
     fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> {
         let mut contents = vec![];
-        self.generate_prefix(&mut contents, is_crate, single_file)?;
+        generate_prefix(&mut contents, is_crate, single_file)?;
 
-        if !single_file {
-            self.append_module_names(&mut contents)?;
-        } else {
+        if single_file {
+            if let Some(ref shared) = self.shared_types {
+                shared.write(&mut contents)?;
+            }
             for binding in self.bindings.values() {
                 binding.write(&mut contents)?;
             }
+        } else {
+            self.append_module_names(&mut contents)?;
         }
 
         Ok(contents)
@@ -246,43 +583,16 @@ serde_json = "1.0.79"
 
     /// Write all contract bindings to their respective files
     fn write_bindings(&self, path: &Path) -> Result<()> {
+        if let Some(ref shared) = self.shared_types {
+            shared.write_module_in_dir(path)?;
+        }
         for binding in self.bindings.values() {
             binding.write_module_in_dir(path)?;
         }
         Ok(())
     }
 
-    /// Generates all the bindings and writes them to the given module
-    ///
-    /// # Example
-    ///
-    /// Read all json abi files from the `./abi` directory
-    /// ```text
-    /// abi
-    /// ├── ERC20.json
-    /// ├── Contract1.json
-    /// ├── Contract2.json
-    /// ...
-    /// ```
-    ///
-    /// and write them to the `./src/contracts` location as
-    ///
-    /// ```text
-    /// src/contracts
-    /// ├── mod.rs
-    /// ├── er20.rs
-    /// ├── contract1.rs
-    /// ├── contract2.rs
-    /// ...
-    /// ```
-    ///
-    /// ```no_run
-    /// # use ethers_contract_abigen::MultiAbigen;
-    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
-    /// let bindings = gen.build().unwrap();
-    /// bindings.write_to_module("./src/contracts", false).unwrap();
-    /// ```
-    pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
+    fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
         let module = module.as_ref();
         fs::create_dir_all(module)?;
 
@@ -294,44 +604,7 @@ serde_json = "1.0.79"
         Ok(())
     }
 
-    /// Generates all the bindings and writes a library crate containing them
-    /// to the provided path
-    ///
-    /// # Example
-    ///
-    /// Read all json abi files from the `./abi` directory
-    /// ```text
-    /// abi
-    /// ├── ERC20.json
-    /// ├── Contract1.json
-    /// ├── Contract2.json
-    /// ├── Contract3/
-    ///     ├── Contract3.json
-    /// ...
-    /// ```
-    ///
-    /// and write them to the `./bindings` location as
-    ///
-    /// ```text
-    /// bindings
-    /// ├── Cargo.toml
-    /// ├── src/
-    ///     ├── lib.rs
-    ///     ├── er20.rs
-    ///     ├── contract1.rs
-    ///     ├── contract2.rs
-    /// ...
-    /// ```
-    ///
-    /// ```no_run
-    /// # use ethers_contract_abigen::MultiAbigen;
-    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
-    /// let bindings = gen.build().unwrap();
-    /// bindings.write_to_crate(
-    ///     "my-crate", "0.0.5", "./bindings", false
-    /// ).unwrap();
-    /// ```
-    pub fn write_to_crate(
+    fn write_to_crate(
         self,
         name: impl AsRef<str>,
         version: impl AsRef<str>,
@@ -379,39 +652,7 @@ serde_json = "1.0.79"
         Ok(())
     }
 
-    /// This ensures that the already generated bindings crate matches the
-    /// output of a fresh new run. Run this in a rust test, to get notified in
-    /// CI if the newly generated bindings deviate from the already generated
-    /// ones, and it's time to generate them again. This could happen if the
-    /// ABI of a contract or the output that `ethers` generates changed.
-    ///
-    /// If this functions is run within a test during CI and fails, then it's
-    /// time to update all bindings.
-    ///
-    /// # Returns
-    ///
-    /// `Ok(())` if the freshly generated bindings match with the
-    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
-    /// with more information.
-    ///
-    /// # Example
-    ///
-    /// Check that the generated files are up to date
-    ///
-    /// ```no_run
-    /// # use ethers_contract_abigen::MultiAbigen;
-    /// #[test]
-    /// fn generated_bindings_are_fresh() {
-    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
-    ///  let abi_dir = project_root.join("abi");
-    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
-    ///  let bindings = gen.build().unwrap();
-    ///  bindings.ensure_consistent_crate(
-    ///     "my-crate", "0.0.1", project_root.join("src/contracts"), false
-    ///  ).expect("inconsistent bindings");
-    /// }
-    /// ```
-    pub fn ensure_consistent_crate(
+    fn ensure_consistent_crate(
         self,
         name: impl AsRef<str>,
         version: impl AsRef<str>,
@@ -428,48 +669,30 @@ serde_json = "1.0.79"
         Ok(())
     }
 
-    /// This ensures that the already generated bindings module matches the
-    /// output of a fresh new run. Run this in a rust test, to get notified in
-    /// CI if the newly generated bindings deviate from the already generated
-    /// ones, and it's time to generate them again. This could happen if the
-    /// ABI of a contract or the output that `ethers` generates changed.
-    ///
-    /// If this functions is run within a test during CI and fails, then it's
-    /// time to update all bindings.
-    ///
-    /// # Returns
-    ///
-    /// `Ok(())` if the freshly generated bindings match with the
-    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
-    /// with more information.
-    ///
-    /// # Example
-    ///
-    /// Check that the generated files are up to date
-    ///
-    /// ```no_run
-    /// # use ethers_contract_abigen::MultiAbigen;
-    /// #[test]
-    /// fn generated_bindings_are_fresh() {
-    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
-    ///  let abi_dir = project_root.join("abi");
-    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
-    ///  let bindings = gen.build().unwrap();
-    ///  bindings.ensure_consistent_module(
-    ///     project_root.join("src/contracts"), false
-    ///  ).expect("inconsistent bindings");
-    /// }
-    /// ```
-    pub fn ensure_consistent_module(
-        self,
-        module: impl AsRef<Path>,
-        single_file: bool,
-    ) -> Result<()> {
+    fn ensure_consistent_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
         self.ensure_consistent_bindings(module, false, single_file)?;
         Ok(())
     }
 }
 
+/// Generate the shared prefix of the `lib.rs` or `mod.rs`
+fn generate_prefix(mut buf: impl Write, is_crate: bool, single_file: bool) -> Result<()> {
+    writeln!(buf, "#![allow(clippy::all)]")?;
+    writeln!(
+        buf,
+        "//! This {} contains abigen! generated bindings for solidity contracts.",
+        if is_crate { "lib" } else { "module" }
+    )?;
+    writeln!(buf, "//! This is autogenerated code.")?;
+    writeln!(buf, "//! Do not manually edit these files.")?;
+    writeln!(
+        buf,
+        "//! {} may be overwritten by the codegen system at any time.",
+        if single_file && !is_crate { "This file" } else { "These files" }
+    )?;
+    Ok(())
+}
+
 fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> {
     eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display());
 
@@ -493,6 +716,7 @@ fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
 mod tests {
     use super::*;
 
+    use ethers_solc::project_util::TempProject;
     use std::{panic, path::PathBuf};
 
     struct Context {
@@ -718,7 +942,7 @@ mod tests {
     }
 
     #[test]
-    fn can_detect_incosistent_single_file_crate() {
+    fn can_detect_inconsistent_single_file_crate() {
         run_test(|context| {
             let Context { multi_gen, mod_root } = context;
 
@@ -754,4 +978,98 @@ mod tests {
             assert!(result, "Inconsistent bindings wrongly approved");
         })
     }
+
+    #[test]
+    fn does_not_generate_shared_types_if_empty() {
+        let gen = Abigen::new(
+            "Greeter",
+            r#"[
+                        struct Inner {bool a;}
+                        greet1() (uint256)
+                        greet2(Inner inner) (string)
+                    ]"#,
+        )
+        .unwrap();
+
+        let tokens = MultiExpansion::new(vec![gen.expand().unwrap()]).expand_inplace().to_string();
+        assert!(!tokens.contains("mod __shared_types"));
+    }
+
+    #[test]
+    fn can_deduplicate_types() {
+        let tmp = TempProject::dapptools().unwrap();
+
+        tmp.add_source(
+            "Greeter",
+            r#"
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+struct Inner {
+    bool a;
+}
+
+struct Stuff {
+    Inner inner;
+}
+
+contract Greeter1 {
+
+    function greet(Stuff calldata stuff) public pure returns (Stuff memory) {
+        return stuff;
+    }
+}
+
+contract Greeter2 {
+
+    function greet(Stuff calldata stuff) public pure returns (Stuff memory) {
+        return stuff;
+    }
+}
+"#,
+        )
+        .unwrap();
+
+        let _ = tmp.compile().unwrap();
+
+        let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap();
+        let bindings = gen.build().unwrap();
+        let single_file_dir = tmp.root().join("single_bindings");
+        bindings.write_to_module(&single_file_dir, true).unwrap();
+
+        let single_file_mod = single_file_dir.join("mod.rs");
+        assert!(single_file_mod.exists());
+        let content = fs::read_to_string(&single_file_mod).unwrap();
+        assert!(content.contains("mod __shared_types"));
+        assert!(content.contains("pub struct Inner"));
+        assert!(content.contains("pub struct Stuff"));
+
+        // multiple files
+        let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap();
+        let bindings = gen.build().unwrap();
+        let multi_file_dir = tmp.root().join("multi_bindings");
+        bindings.write_to_module(&multi_file_dir, false).unwrap();
+        let multi_file_mod = multi_file_dir.join("mod.rs");
+        assert!(multi_file_mod.exists());
+        let content = fs::read_to_string(&multi_file_mod).unwrap();
+        assert!(content.contains("pub mod shared_types"));
+
+        let greeter1 = multi_file_dir.join("greeter_1.rs");
+        assert!(greeter1.exists());
+        let content = fs::read_to_string(&greeter1).unwrap();
+        assert!(!content.contains("pub struct Inner"));
+        assert!(!content.contains("pub struct Stuff"));
+
+        let greeter2 = multi_file_dir.join("greeter_2.rs");
+        assert!(greeter2.exists());
+        let content = fs::read_to_string(&greeter2).unwrap();
+        assert!(!content.contains("pub struct Inner"));
+        assert!(!content.contains("pub struct Stuff"));
+
+        let shared_types = multi_file_dir.join("shared_types.rs");
+        assert!(shared_types.exists());
+        let content = fs::read_to_string(&shared_types).unwrap();
+        assert!(content.contains("pub struct Inner"));
+        assert!(content.contains("pub struct Stuff"));
+    }
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/rawabi.rs b/ethers-contract/ethers-contract-abigen/src/rawabi.rs
index 6c66b6f0..782e4d35 100644
--- a/ethers-contract/ethers-contract-abigen/src/rawabi.rs
+++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs
@@ -8,7 +8,8 @@ use serde::{
 };
 
 /// Contract ABI as a list of items where each item can be a function, constructor or event
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize)]
+#[serde(transparent)]
 pub struct RawAbi(Vec<Item>);
 
 impl IntoIterator for RawAbi {
@@ -83,6 +84,9 @@ pub struct Item {
     pub name: Option<String>,
     #[serde(default)]
     pub outputs: Vec<Component>,
+    // required to satisfy solidity events
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub anonymous: Option<bool>,
 }
 
 /// Either an input/output or a nested component of an input/output
@@ -96,11 +100,15 @@ pub struct Component {
     pub type_field: String,
     #[serde(default)]
     pub components: Vec<Component>,
+    /// Indexed flag. for solidity events
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub indexed: Option<bool>,
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use ethers_core::abi::Abi;
 
     #[test]
     fn can_parse_raw_abi() {
@@ -122,4 +130,14 @@ mod tests {
         let s = r#"[{"type":"function","name":"greet","inputs":[{"internalType":"struct Greeter.Stuff","name":"stuff","type":"tuple","components":[{"type":"bool"}]}],"outputs":[{"internalType":"struct Greeter.Stuff","name":"","type":"tuple","components":[{"type":"bool"}]}],"stateMutability":"view"}]"#;
         let _ = serde_json::from_str::<RawAbi>(s).unwrap();
     }
+
+    #[test]
+    fn can_ethabi_round_trip() {
+        let s = r#"[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint64","name":"number","type":"uint64"}],"name":"MyEvent","type":"event"},{"inputs":[],"name":"greet","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#;
+
+        let raw = serde_json::from_str::<RawAbi>(s).unwrap();
+        let abi = serde_json::from_str::<Abi>(s).unwrap();
+        let de = serde_json::to_string(&raw).unwrap();
+        assert_eq!(abi, serde_json::from_str::<Abi>(&de).unwrap());
+    }
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs
index d68df2a1..a3fc1819 100644
--- a/ethers-contract/ethers-contract-abigen/src/source.rs
+++ b/ethers-contract/ethers-contract-abigen/src/source.rs
@@ -259,7 +259,6 @@ fn get_etherscan_contract(address: Address, domain: &str) -> Result<String> {
     // NOTE: We do not retrieve the bytecode since deploying contracts with the
     //   same bytecode is unreliable as the libraries have already linked and
     //   probably don't reference anything when deploying on other networks.
-
     let api_key = {
         let key_res = match domain {
             "etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(),
@@ -276,6 +275,11 @@ fn get_etherscan_contract(address: Address, domain: &str) -> Result<String> {
     );
     let abi =
         util::http_get(&abi_url).context(format!("failed to retrieve ABI from {}", domain))?;
+
+    if abi.starts_with("Contract source code not verified") {
+        eyre::bail!("Contract source code not verified: {:?}", address);
+    }
+
     Ok(abi)
 }
 
diff --git a/ethers-contract/ethers-contract-derive/src/abigen.rs b/ethers-contract/ethers-contract-derive/src/abigen.rs
index 86b31a5e..f1c47e3b 100644
--- a/ethers-contract/ethers-contract-derive/src/abigen.rs
+++ b/ethers-contract/ethers-contract-derive/src/abigen.rs
@@ -5,13 +5,13 @@ use crate::spanned::{ParseInner, Spanned};
 use ethers_contract_abigen::Abigen;
 use ethers_core::abi::{Function, FunctionExt, Param, StateMutability};
 
-use ethers_contract_abigen::contract::{Context, ExpandedContract};
-use proc_macro2::{Span, TokenStream as TokenStream2};
-use quote::{quote, ToTokens};
-use std::{
-    collections::{HashMap, HashSet},
-    error::Error,
+use ethers_contract_abigen::{
+    contract::{Context, ExpandedContract},
+    multi::MultiExpansion,
 };
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::ToTokens;
+use std::{collections::HashSet, error::Error};
 use syn::{
     braced,
     ext::IdentExt,
@@ -28,7 +28,6 @@ pub(crate) struct Contracts {
 
 impl Contracts {
     pub(crate) fn expand(self) -> Result<TokenStream2, syn::Error> {
-        let mut tokens = TokenStream2::new();
         let mut expansions = Vec::with_capacity(self.inner.len());
 
         // expand all contracts
@@ -38,56 +37,14 @@ impl Contracts {
             expansions.push(contract);
         }
 
-        // merge all types if more than 1 contract
-        if expansions.len() > 1 {
-            // check for type conflicts
-            let mut conflicts: HashMap<String, Vec<usize>> = HashMap::new();
-            for (idx, (_, ctx)) in expansions.iter().enumerate() {
-                for type_identifier in ctx.internal_structs().rust_type_names().keys() {
-                    conflicts
-                        .entry(type_identifier.clone())
-                        .or_insert_with(|| Vec::with_capacity(1))
-                        .push(idx);
-                }
-            }
-
-            let mut shared_types = TokenStream2::new();
-            let shared_types_mdoule = quote!(__shared_types);
-            let mut dirty = HashSet::new();
-            // resolve type conflicts
-            for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
-                // extract the shared type once
-                shared_types.extend(expansions[contracts[0]].1.struct_definition(id).unwrap());
-                // remove the shared type
-                for contract in contracts.iter().copied() {
-                    expansions[contract].1.remove_struct(id);
-                    dirty.insert(contract);
-                }
-            }
-
-            // regenerate all struct definitions that were hit and adjust imports
-            for contract in dirty {
-                let (expanded, ctx) = &mut expansions[contract];
-                expanded.abi_structs = ctx.abi_structs().unwrap();
-                expanded.imports.extend(quote!( pub use super::#shared_types_mdoule::*;));
-            }
-            tokens.extend(quote! {
-                pub mod #shared_types_mdoule {
-                    #shared_types
-                }
-            });
-        }
-
-        tokens.extend(expansions.into_iter().map(|(exp, _)| exp.into_tokens()));
-        Ok(tokens)
+        // expand all contract expansions
+        Ok(MultiExpansion::new(expansions).expand_inplace())
     }
 
     fn expand_contract(
         contract: ContractArgs,
     ) -> Result<(ExpandedContract, Context), Box<dyn Error>> {
-        let contract = contract.into_builder()?;
-        let ctx = Context::from_abigen(contract)?;
-        Ok((ctx.expand()?, ctx))
+        Ok(contract.into_builder()?.expand()?)
     }
 }
 
diff --git a/ethers-etherscan/src/account.rs b/ethers-etherscan/src/account.rs
new file mode 100644
index 00000000..5aa9932a
--- /dev/null
+++ b/ethers-etherscan/src/account.rs
@@ -0,0 +1,729 @@
+use std::{
+    borrow::Cow,
+    collections::HashMap,
+    fmt::{Display, Error, Formatter},
+};
+
+use ethers_core::{
+    abi::Address,
+    types::{BlockNumber, Bytes, H256, U256, U64},
+};
+use serde::{Deserialize, Serialize};
+
+use crate::{Client, EtherscanError, Query, Response, Result};
+
+/// The raw response from the balance-related API endpoints
+#[derive(Debug, Serialize, Deserialize)]
+pub struct AccountBalance {
+    pub account: Address,
+    pub balance: String,
+}
+
+mod jsonstring {
+    use super::*;
+    use serde::{
+        de::{DeserializeOwned, Error as _},
+        ser::Error as _,
+        Deserializer, Serializer,
+    };
+
+    pub fn serialize<T, S>(
+        value: &GenesisOption<T>,
+        serializer: S,
+    ) -> std::result::Result<S::Ok, S::Error>
+    where
+        T: Serialize,
+        S: Serializer,
+    {
+        let json = match value {
+            GenesisOption::None => Cow::from(""),
+            GenesisOption::Genesis => Cow::from("GENESIS"),
+            GenesisOption::Some(value) => {
+                serde_json::to_string(value).map_err(S::Error::custom)?.into()
+            }
+        };
+        serializer.serialize_str(&json)
+    }
+
+    pub fn deserialize<'de, T, D>(
+        deserializer: D,
+    ) -> std::result::Result<GenesisOption<T>, D::Error>
+    where
+        T: DeserializeOwned,
+        D: Deserializer<'de>,
+    {
+        let json = Cow::<'de, str>::deserialize(deserializer)?;
+        if !json.is_empty() && !json.starts_with("GENESIS") {
+            let value =
+                serde_json::from_str(&format!("\"{}\"", &json)).map_err(D::Error::custom)?;
+            Ok(GenesisOption::Some(value))
+        } else if json.starts_with("GENESIS") {
+            Ok(GenesisOption::Genesis)
+        } else {
+            Ok(GenesisOption::None)
+        }
+    }
+}
+
+/// Possible values for some field responses
+#[derive(Debug)]
+pub enum GenesisOption<T> {
+    None,
+    Genesis,
+    Some(T),
+}
+
+impl<T> From<GenesisOption<T>> for Option<T> {
+    fn from(value: GenesisOption<T>) -> Self {
+        match value {
+            GenesisOption::Some(value) => Some(value),
+            _ => None,
+        }
+    }
+}
+
+impl<T> GenesisOption<T> {
+    pub fn is_genesis(&self) -> bool {
+        matches!(self, GenesisOption::Genesis)
+    }
+
+    pub fn value(&self) -> Option<&T> {
+        match self {
+            GenesisOption::Some(value) => Some(value),
+            _ => None,
+        }
+    }
+}
+
+/// The raw response from the transaction list API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NormalTransaction {
+    pub is_error: String,
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    #[serde(with = "jsonstring")]
+    pub hash: GenesisOption<H256>,
+    #[serde(with = "jsonstring")]
+    pub nonce: GenesisOption<U256>,
+    #[serde(with = "jsonstring")]
+    pub block_hash: GenesisOption<U256>,
+    pub transaction_index: Option<U64>,
+    #[serde(with = "jsonstring")]
+    pub from: GenesisOption<Address>,
+    pub to: Option<Address>,
+    pub value: U256,
+    pub gas: U256,
+    pub gas_price: Option<U256>,
+    #[serde(rename = "txreceipt_status")]
+    pub tx_receipt_status: String,
+    #[serde(with = "jsonstring")]
+    pub input: GenesisOption<Bytes>,
+    #[serde(with = "jsonstring")]
+    pub contract_address: GenesisOption<Address>,
+    pub gas_used: U256,
+    pub cumulative_gas_used: U256,
+    pub confirmations: U64,
+}
+
+/// The raw response from the internal transaction list API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InternalTransaction {
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    pub hash: H256,
+    pub from: Address,
+    #[serde(with = "jsonstring")]
+    pub to: GenesisOption<Address>,
+    pub value: U256,
+    #[serde(with = "jsonstring")]
+    pub contract_address: GenesisOption<Address>,
+    #[serde(with = "jsonstring")]
+    pub input: GenesisOption<Bytes>,
+    #[serde(rename = "type")]
+    pub result_type: String,
+    pub gas: U256,
+    pub gas_used: U256,
+    pub trace_id: String,
+    pub is_error: String,
+    pub err_code: String,
+}
+
+/// The raw response from the ERC20 transfer list API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ERC20TokenTransferEvent {
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    pub hash: H256,
+    pub nonce: U256,
+    pub block_hash: H256,
+    pub from: Address,
+    pub contract_address: Address,
+    pub to: Option<Address>,
+    pub value: U256,
+    pub token_name: String,
+    pub token_symbol: String,
+    pub token_decimal: String,
+    pub transaction_index: U64,
+    pub gas: U256,
+    pub gas_price: Option<U256>,
+    pub gas_used: U256,
+    pub cumulative_gas_used: U256,
+    /// deprecated
+    pub input: String,
+    pub confirmations: U64,
+}
+
+/// The raw response from the ERC721 transfer list API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ERC721TokenTransferEvent {
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    pub hash: H256,
+    pub nonce: U256,
+    pub block_hash: H256,
+    pub from: Address,
+    pub contract_address: Address,
+    pub to: Option<Address>,
+    #[serde(rename = "tokenID")]
+    pub token_id: String,
+    pub token_name: String,
+    pub token_symbol: String,
+    pub token_decimal: String,
+    pub transaction_index: U64,
+    pub gas: U256,
+    pub gas_price: Option<U256>,
+    pub gas_used: U256,
+    pub cumulative_gas_used: U256,
+    /// deprecated
+    pub input: String,
+    pub confirmations: U64,
+}
+
+/// The raw response from the mined blocks API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MinedBlock {
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    pub block_reward: String,
+}
+
+/// The pre-defined block parameter for balance API endpoints
+pub enum Tag {
+    Earliest,
+    Pending,
+    Latest,
+}
+
+impl Display for Tag {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
+        match self {
+            Tag::Earliest => write!(f, "earliest"),
+            Tag::Pending => write!(f, "pending"),
+            Tag::Latest => write!(f, "latest"),
+        }
+    }
+}
+
+impl Default for Tag {
+    fn default() -> Self {
+        Tag::Latest
+    }
+}
+
+/// The list sorting preference
+pub enum Sort {
+    Asc,
+    Desc,
+}
+
+impl Display for Sort {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
+        match self {
+            Sort::Asc => write!(f, "asc"),
+            Sort::Desc => write!(f, "desc"),
+        }
+    }
+}
+
+/// Common optional arguments for the transaction or event list API endpoints
+pub struct TxListParams {
+    start_block: u64,
+    end_block: u64,
+    page: u64,
+    offset: u64,
+    sort: Sort,
+}
+
+impl TxListParams {
+    pub fn new(start_block: u64, end_block: u64, page: u64, offset: u64, sort: Sort) -> Self {
+        Self { start_block, end_block, page, offset, sort }
+    }
+}
+
+impl Default for TxListParams {
+    fn default() -> Self {
+        Self { start_block: 0, end_block: 99999999, page: 0, offset: 10000, sort: Sort::Asc }
+    }
+}
+
+impl From<TxListParams> for HashMap<&'static str, String> {
+    fn from(tx_params: TxListParams) -> Self {
+        let mut params = HashMap::new();
+        params.insert("startBlock", tx_params.start_block.to_string());
+        params.insert("endBlock", tx_params.end_block.to_string());
+        params.insert("page", tx_params.page.to_string());
+        params.insert("offset", tx_params.offset.to_string());
+        params.insert("sort", tx_params.sort.to_string());
+        params
+    }
+}
+
+/// Options for querying internal transactions
+pub enum InternalTxQueryOption {
+    ByAddress(Address),
+    ByTransactionHash(H256),
+    ByBlockRange,
+}
+
+/// Options for querying ERC20 or ERC721 token transfers
+pub enum TokenQueryOption {
+    ByAddress(Address),
+    ByContract(Address),
+    ByAddressAndContract(Address, Address),
+}
+
+impl TokenQueryOption {
+    pub fn into_params(self, list_params: TxListParams) -> HashMap<&'static str, String> {
+        let mut params: HashMap<&'static str, String> = list_params.into();
+        match self {
+            TokenQueryOption::ByAddress(address) => {
+                params.insert("address", format!("{:?}", address));
+                params
+            }
+            TokenQueryOption::ByContract(contract) => {
+                params.insert("contractaddress", format!("{:?}", contract));
+                params
+            }
+            TokenQueryOption::ByAddressAndContract(address, contract) => {
+                params.insert("address", format!("{:?}", address));
+                params.insert("contractaddress", format!("{:?}", contract));
+                params
+            }
+        }
+    }
+}
+
+/// The pre-defined block type for retrieving mined blocks
+pub enum BlockType {
+    CanonicalBlocks,
+    Uncles,
+}
+
+impl Display for BlockType {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
+        match self {
+            BlockType::CanonicalBlocks => write!(f, "blocks"),
+            BlockType::Uncles => write!(f, "uncles"),
+        }
+    }
+}
+
+impl Default for BlockType {
+    fn default() -> Self {
+        BlockType::CanonicalBlocks
+    }
+}
+
+impl Client {
+    /// Returns the Ether balance of a given address.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::Client;
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let balance = client
+    ///         .get_ether_balance_single(&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(),
+    ///         None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_ether_balance_single(
+        &self,
+        address: &Address,
+        tag: Option<Tag>,
+    ) -> Result<AccountBalance> {
+        let tag_str = tag.unwrap_or_default().to_string();
+        let addr_str = format!("{:?}", address);
+        let query = self.create_query(
+            "account",
+            "balance",
+            HashMap::from([("address", &addr_str), ("tag", &tag_str)]),
+        );
+        let response: Response<String> = self.get_json(&query).await?;
+
+        match response.status.as_str() {
+            "0" => Err(EtherscanError::BalanceFailed),
+            "1" => Ok(AccountBalance { account: *address, balance: response.result }),
+            err => Err(EtherscanError::BadStatusCode(err.to_string())),
+        }
+    }
+
+    /// Returns the balance of the accounts from a list of addresses.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::Client;
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let balances = client
+    ///         .get_ether_balance_multi(&vec![&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()],
+    ///         None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_ether_balance_multi(
+        &self,
+        addresses: &[&Address],
+        tag: Option<Tag>,
+    ) -> Result<Vec<AccountBalance>> {
+        let tag_str = tag.unwrap_or_default().to_string();
+        let addrs = addresses.iter().map(|x| format!("{:?}", x)).collect::<Vec<String>>().join(",");
+        let query: Query<HashMap<&str, &str>> = self.create_query(
+            "account",
+            "balancemulti",
+            HashMap::from([("address", addrs.as_ref()), ("tag", tag_str.as_ref())]),
+        );
+        let response: Response<Vec<AccountBalance>> = self.get_json(&query).await?;
+
+        match response.status.as_str() {
+            "0" => Err(EtherscanError::BalanceFailed),
+            "1" => Ok(response.result),
+            err => Err(EtherscanError::BadStatusCode(err.to_string())),
+        }
+    }
+
+    /// Returns the list of transactions performed by an address, with optional pagination.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::Client;
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let txs = client
+    ///         .get_transactions(&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(),
+    ///         None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_transactions(
+        &self,
+        address: &Address,
+        params: Option<TxListParams>,
+    ) -> Result<Vec<NormalTransaction>> {
+        let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into();
+        tx_params.insert("address", format!("{:?}", address));
+        let query = self.create_query("account", "txlist", tx_params);
+        let response: Response<Vec<NormalTransaction>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+
+    /// Returns the list of internal transactions performed by an address or within a transaction,
+    /// with optional pagination.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::{Client, account::InternalTxQueryOption};
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let txs = client
+    ///         .get_internal_transactions(
+    ///             InternalTxQueryOption::ByAddress(
+    ///                 "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap()), None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_internal_transactions(
+        &self,
+        tx_query_option: InternalTxQueryOption,
+        params: Option<TxListParams>,
+    ) -> Result<Vec<InternalTransaction>> {
+        let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into();
+        match tx_query_option {
+            InternalTxQueryOption::ByAddress(address) => {
+                tx_params.insert("address", format!("{:?}", address));
+            }
+            InternalTxQueryOption::ByTransactionHash(tx_hash) => {
+                tx_params.insert("txhash", format!("{:?}", tx_hash));
+            }
+            _ => {}
+        }
+        let query = self.create_query("account", "txlistinternal", tx_params);
+        let response: Response<Vec<InternalTransaction>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+
+    /// Returns the list of ERC-20 tokens transferred by an address, with optional filtering by
+    /// token contract.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::{Client, account::TokenQueryOption};
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let txs = client
+    ///         .get_erc20_token_transfer_events(
+    ///             TokenQueryOption::ByAddress(
+    ///                 "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap()), None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_erc20_token_transfer_events(
+        &self,
+        event_query_option: TokenQueryOption,
+        params: Option<TxListParams>,
+    ) -> Result<Vec<ERC20TokenTransferEvent>> {
+        let params = event_query_option.into_params(params.unwrap_or_default());
+        let query = self.create_query("account", "tokentx", params);
+        let response: Response<Vec<ERC20TokenTransferEvent>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+
+    /// Returns the list of ERC-721 ( NFT ) tokens transferred by an address, with optional
+    /// filtering by token contract.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::{Client, account::TokenQueryOption};
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let txs = client
+    ///         .get_erc721_token_transfer_events(
+    ///             TokenQueryOption::ByAddressAndContract(
+    ///                 "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(),
+    ///                 "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(),
+    ///          ), None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_erc721_token_transfer_events(
+        &self,
+        event_query_option: TokenQueryOption,
+        params: Option<TxListParams>,
+    ) -> Result<Vec<ERC721TokenTransferEvent>> {
+        let params = event_query_option.into_params(params.unwrap_or_default());
+        let query = self.create_query("account", "tokennfttx", params);
+        let response: Response<Vec<ERC721TokenTransferEvent>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+
+    /// Returns the list of blocks mined by an address.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::Client;
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let blocks = client
+    ///         .get_mined_blocks(&"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(), None, None)
+    ///         .await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_mined_blocks(
+        &self,
+        address: &Address,
+        block_type: Option<BlockType>,
+        page_and_offset: Option<(u64, u64)>,
+    ) -> Result<Vec<MinedBlock>> {
+        let mut params = HashMap::new();
+        params.insert("address", format!("{:?}", address));
+        params.insert("blocktype", block_type.unwrap_or_default().to_string());
+        if let Some((page, offset)) = page_and_offset {
+            params.insert("page", page.to_string());
+            params.insert("offset", offset.to_string());
+        }
+        let query = self.create_query("account", "getminedblocks", params);
+        let response: Response<Vec<MinedBlock>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::time::Duration;
+
+    use serial_test::serial;
+
+    use crate::{tests::run_at_least_duration, Chain};
+
+    use super::*;
+
+    #[tokio::test]
+    #[serial]
+    async fn get_ether_balance_single_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let balance = client
+                .get_ether_balance_single(
+                    &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(),
+                    None,
+                )
+                .await;
+            assert!(balance.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_ether_balance_multi_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let balances = client
+                .get_ether_balance_multi(
+                    &vec![&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()],
+                    None,
+                )
+                .await;
+            assert!(balances.is_ok());
+            let balances = balances.unwrap();
+            assert!(balances.len() == 1);
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_transactions_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_transactions(
+                    &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(),
+                    None,
+                )
+                .await;
+            dbg!(&txs);
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_internal_transactions_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_internal_transactions(
+                    InternalTxQueryOption::ByAddress(
+                        "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap(),
+                    ),
+                    None,
+                )
+                .await;
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_internal_transactions_by_tx_hash_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_internal_transactions(
+                    InternalTxQueryOption::ByTransactionHash(
+                        "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170"
+                            .parse()
+                            .unwrap(),
+                    ),
+                    None,
+                )
+                .await;
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_erc20_transfer_events_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_erc20_token_transfer_events(
+                    TokenQueryOption::ByAddress(
+                        "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap(),
+                    ),
+                    None,
+                )
+                .await;
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_erc721_transfer_events_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_erc721_token_transfer_events(
+                    TokenQueryOption::ByAddressAndContract(
+                        "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(),
+                        "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(),
+                    ),
+                    None,
+                )
+                .await;
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
+    #[tokio::test]
+    #[serial]
+    async fn get_mined_blocks_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let blocks = client
+                .get_mined_blocks(
+                    &"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(),
+                    None,
+                    None,
+                )
+                .await;
+            assert!(blocks.is_ok());
+        })
+        .await
+    }
+}
diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs
index a3524b9b..6d7bf431 100644
--- a/ethers-etherscan/src/contract.rs
+++ b/ethers-etherscan/src/contract.rs
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 
 use ethers_core::abi::{Abi, Address};
 
-use crate::{Client, Response, Result};
+use crate::{Client, EtherscanError, Response, Result};
 
 /// Arguments for verifying contracts
 #[derive(Debug, Clone, Serialize)]
@@ -236,6 +236,9 @@ impl Client {
     pub async fn contract_abi(&self, address: Address) -> Result<Abi> {
         let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
         let resp: Response<String> = self.get_json(&query).await?;
+        if resp.result.starts_with("Contract source code not verified") {
+            return Err(EtherscanError::ContractCodeNotVerified(address))
+        }
         Ok(serde_json::from_str(&resp.result)?)
     }
 
@@ -268,7 +271,7 @@ mod tests {
     use serial_test::serial;
 
     use ethers_core::types::Chain;
-    use ethers_solc::{MinimalCombinedArtifacts, Project, ProjectPathsConfig};
+    use ethers_solc::{Project, ProjectPathsConfig};
 
     use crate::{contract::VerifyContract, tests::run_at_least_duration, Client};
 
diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs
index 8a53ca7a..ab6b6b0d 100644
--- a/ethers-etherscan/src/errors.rs
+++ b/ethers-etherscan/src/errors.rs
@@ -1,4 +1,4 @@
-use ethers_core::types::Chain;
+use ethers_core::types::{Address, Chain};
 use std::env::VarError;
 
 #[derive(Debug, thiserror::Error)]
@@ -7,6 +7,8 @@ pub enum EtherscanError {
     ChainNotSupported(Chain),
     #[error("contract execution call failed: {0}")]
     ExecutionFailed(String),
+    #[error("balance failed")]
+    BalanceFailed,
     #[error("tx receipt failed")]
     TransactionReceiptFailed,
     #[error("gas estimation failed")]
@@ -19,4 +21,6 @@ pub enum EtherscanError {
     Reqwest(#[from] reqwest::Error),
     #[error(transparent)]
     Serde(#[from] serde_json::Error),
+    #[error("Contract source code not verified: {0}")]
+    ContractCodeNotVerified(Address),
 }
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index 9d7b2615..6f2ebb9f 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -8,6 +8,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use errors::EtherscanError;
 use ethers_core::{abi::Address, types::Chain};
 
+pub mod account;
 pub mod contract;
 pub mod errors;
 pub mod gas;
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index 02285a00..d2ddf209 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -1139,6 +1139,11 @@ impl<P: JsonRpcClient> Provider<P> {
         // the call will return a Bytes array which we convert to an address
         let data = self.call(&ens::get_resolver(ens_addr, ens_name).into(), None).await?;
 
+        // otherwise, decode_bytes panics
+        if data.0.is_empty() {
+            return Err(ProviderError::EnsError(ens_name.to_owned()))
+        }
+
         let resolver_address: Address = decode_bytes(ParamType::Address, data);
         if resolver_address == Address::zero() {
             return Err(ProviderError::EnsError(ens_name.to_owned()))
diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index ae6b4162..dbd56b89 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -4,12 +4,11 @@ use crate::{
     artifacts::{
         output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection},
         CompactBytecode, CompactContract, CompactContractBytecode, CompactDeployedBytecode,
-        CompactEvm, DevDoc, Ewasm, GasEstimates, Metadata, Offsets, Settings, StorageLayout,
-        UserDoc,
+        CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi, Metadata, Offsets, Settings,
+        StorageLayout, UserDoc,
     },
     ArtifactOutput, Contract, SolcConfig, SolcError,
 };
-use ethers_core::abi::Abi;
 use serde::{Deserialize, Serialize};
 use std::{collections::BTreeMap, fs, path::Path};
 
@@ -21,7 +20,7 @@ use std::{collections::BTreeMap, fs, path::Path};
 pub struct ConfigurableContractArtifact {
     /// The Ethereum Contract ABI. If empty, it is represented as an empty
     /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html
-    pub abi: Option<Abi>,
+    pub abi: Option<LosslessAbi>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub bytecode: Option<CompactBytecode>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -74,7 +73,7 @@ impl ConfigurableContractArtifact {
 impl From<ConfigurableContractArtifact> for CompactContractBytecode {
     fn from(artifact: ConfigurableContractArtifact) -> Self {
         CompactContractBytecode {
-            abi: artifact.abi,
+            abi: artifact.abi.map(Into::into),
             bytecode: artifact.bytecode,
             deployed_bytecode: artifact.deployed_bytecode,
         }
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index d8f55081..0d5038a4 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -843,7 +843,7 @@ impl OutputContracts {
 pub struct Contract {
     /// The Ethereum Contract Metadata.
     /// See https://docs.soliditylang.org/en/develop/metadata.html
-    pub abi: Option<Abi>,
+    pub abi: Option<LosslessAbi>,
     #[serde(
         default,
         skip_serializing_if = "Option::is_none",
@@ -868,6 +868,48 @@ pub struct Contract {
     pub ir_optimized: Option<String>,
 }
 
+/// A helper type that ensures lossless (de)serialisation unlike [`ethabi::Contract`] which omits
+/// some information of (nested) components in a serde roundtrip. This is a problem for
+/// abienconderv2 structs because `ethabi::Contract`'s representation of those are [`ethabi::Param`]
+/// and the `kind` field of type [`ethabi::ParamType`] does not support deeply nested components as
+/// it's the case for structs. This is not easily fixable in ethabi as it would require a redesign
+/// of the overall `Param` and `ParamType` types. Instead, this type keeps a copy of the
+/// [`serde_json::Value`] when deserialized from the `solc` json compiler output and uses it to
+/// serialize the `abi` without loss.
+#[derive(Clone, Debug, PartialEq, Default)]
+pub struct LosslessAbi {
+    /// The complete abi as json value
+    pub abi_value: serde_json::Value,
+    /// The deserialised version of `abi_value`
+    pub abi: Abi,
+}
+
+impl From<LosslessAbi> for Abi {
+    fn from(abi: LosslessAbi) -> Self {
+        abi.abi
+    }
+}
+
+impl Serialize for LosslessAbi {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        self.abi_value.serialize(serializer)
+    }
+}
+
+impl<'de> Deserialize<'de> for LosslessAbi {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let abi_value = serde_json::Value::deserialize(deserializer)?;
+        let abi = serde_json::from_value(abi_value.clone()).map_err(serde::de::Error::custom)?;
+        Ok(Self { abi_value, abi })
+    }
+}
+
 /// Minimal representation of a contract with a present abi and bytecode.
 ///
 /// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole
@@ -933,7 +975,7 @@ impl From<Contract> for ContractBytecode {
             (None, None)
         };
 
-        Self { abi: c.abi, bytecode, deployed_bytecode }
+        Self { abi: c.abi.map(Into::into), bytecode, deployed_bytecode }
     }
 }
 
@@ -979,7 +1021,7 @@ impl From<Contract> for CompactContractBytecode {
             (None, None)
         };
 
-        Self { abi: c.abi, bytecode, deployed_bytecode }
+        Self { abi: c.abi.map(Into::into), bytecode, deployed_bytecode }
     }
 }
 
@@ -1300,7 +1342,7 @@ impl<'a> From<&'a Contract> for CompactContractRef<'a> {
             (None, None)
         };
 
-        Self { abi: c.abi.as_ref(), bin, bin_runtime }
+        Self { abi: c.abi.as_ref().map(|abi| &abi.abi), bin, bin_runtime }
     }
 }
 
diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs
index 722316dd..8657aac6 100644
--- a/ethers-solc/src/hh.rs
+++ b/ethers-solc/src/hh.rs
@@ -3,11 +3,10 @@
 use crate::{
     artifacts::{
         Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract,
-        ContractBytecode, DeployedBytecode, Offsets,
+        ContractBytecode, DeployedBytecode, LosslessAbi, Offsets,
     },
     ArtifactOutput,
 };
-use ethers_core::abi::Abi;
 use serde::{Deserialize, Serialize};
 use std::collections::btree_map::BTreeMap;
 
@@ -24,7 +23,7 @@ pub struct HardhatArtifact {
     /// The source name of this contract in the workspace like `contracts/Greeter.sol`
     pub source_name: String,
     /// The contract's ABI
-    pub abi: Abi,
+    pub abi: LosslessAbi,
     /// A "0x"-prefixed hex string of the unlinked deployment bytecode. If the contract is not
     /// deployable, this has the string "0x"
     pub bytecode: Option<BytecodeObject>,
@@ -44,7 +43,7 @@ pub struct HardhatArtifact {
 impl From<HardhatArtifact> for CompactContract {
     fn from(artifact: HardhatArtifact) -> Self {
         CompactContract {
-            abi: Some(artifact.abi),
+            abi: Some(artifact.abi.abi),
             bin: artifact.bytecode,
             bin_runtime: artifact.deployed_bytecode,
         }
@@ -65,7 +64,7 @@ impl From<HardhatArtifact> for ContractBytecode {
             bcode.into()
         });
 
-        ContractBytecode { abi: Some(artifact.abi), bytecode, deployed_bytecode }
+        ContractBytecode { abi: Some(artifact.abi.abi), bytecode, deployed_bytecode }
     }
 }
 
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 104865fd..20b77c96 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -807,13 +807,7 @@ fn parse_data(content: &str, file: &Path) -> SolData {
                 .map(|(cap, name)| {
                     SolDataUnit::new(name.as_str().to_owned(), cap.to_owned().into())
                 });
-            imports =
-                capture_outer_and_inner(content, &utils::RE_SOL_IMPORT, &["p1", "p2", "p3", "p4"])
-                    .iter()
-                    .map(|(cap, m)| {
-                        SolDataUnit::new(PathBuf::from(m.as_str()), cap.to_owned().into())
-                    })
-                    .collect();
+            imports = capture_imports(content);
         }
     };
     let license = content.lines().next().and_then(|line| {
@@ -846,10 +840,43 @@ fn capture_outer_and_inner<'a>(
         .collect()
 }
 
+fn capture_imports(content: &str) -> Vec<SolDataUnit<PathBuf>> {
+    capture_outer_and_inner(content, &utils::RE_SOL_IMPORT, &["p1", "p2", "p3", "p4"])
+        .iter()
+        .map(|(cap, m)| SolDataUnit::new(PathBuf::from(m.as_str()), cap.to_owned().into()))
+        .collect()
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
 
+    #[test]
+    fn can_capture_curly_imports() {
+        let content = r#"
+import { T } from "../Test.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import {DsTest} from "ds-test/test.sol";
+"#;
+
+        let captured_imports =
+            capture_imports(content).into_iter().map(|s| s.data).collect::<Vec<_>>();
+
+        let expected =
+            utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
+
+        assert_eq!(captured_imports, expected);
+
+        assert_eq!(
+            captured_imports,
+            vec![
+                PathBuf::from("../Test.sol"),
+                "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
+                "ds-test/test.sol".into(),
+            ]
+        );
+    }
+
     #[test]
     fn can_resolve_hardhat_dependency_graph() {
         let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index 556879a1..1aa57525 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -5,7 +5,7 @@ use ethers_core::{
     abi::{AbiDecode, AbiEncode, Address, Tokenizable},
     types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256},
 };
-use ethers_providers::Provider;
+use ethers_providers::{MockProvider, Provider};
 use ethers_solc::Solc;
 use std::{convert::TryFrom, sync::Arc};
 
@@ -472,3 +472,15 @@ async fn can_abiencoderv2_output() {
     let res = contract.default_person().call().await.unwrap();
     assert_eq!(res, person);
 }
+
+#[test]
+fn can_gen_multi_etherscan() {
+    abigen!(
+        MyContract, "etherscan:0xdAC17F958D2ee523a2206206994597C13D831ec7";
+        MyContract2, "etherscan:0x8418bb725b3ac45ec8fff3791dd8b4e0480cc2a2";
+    );
+
+    let provider = Arc::new(Provider::new(MockProvider::new()));
+    let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
+    let _contract = MyContract2::new(Address::default(), provider);
+}