Repository: gakonst/ethers-rs
Pull request #945 (2022-02-22 03:13:40)

chore(deps): bump tokio-tungstenite from 0.16.1 to 0.17.1
Bumps [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) from 0.16.1 to 0.17.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md">tokio-tungstenite's changelog</a>.</em></p>
<blockquote>
<h1>0.17.1</h1>
<ul>
<li>Update the <code>tungstenite</code> dependency (fixes a panic in <code>tungstenite</code> and MSRV), see <a href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0172"><code>tungstenite</code>'s changelog for more details</a>.</li>
</ul>
<h1>0.17.0</h1>
<ul>
<li>Update the dependencies, please refer to the <a href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0170"><code>tungstenite</code> changelog</a> for the actual changes.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/snapview/tokio-tungstenite/commit/b25e863fbcf11841dfed4f39f7419c186974904d"><code>b25e863</code></a> Bump version, update <code>tungstenite</code> and changelog</li>
<li><a href="https://github.com/snapview/tokio-tungstenite/commit/92e65c14a2f6bf1ceb972d6dd04ed910ebf0b3e4"><code>92e65c1</code></a> Bump version</li>
<li><a href="https://github.com/snapview/tokio-tungstenite/commit/e4b48e850d407b646657f2be6dd9ba48887ad6d2"><code>e4b48e8</code></a> Update changelog</li>
<li><a href="https://github.com/snapview/tokio-tungstenite/commit/cbc40db190906c9d0e4f4f4c8bd0f786f89ec75d"><code>cbc40db</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/snapview/tokio-tungstenite/issues/210">#210</a> from dnaka91/update-deps</li>
<li><a href="https://github.com/snapview/tokio-tungstenite/commit/082255891efa0b5a5882ef209320c6019be0d53f"><code>0822558</code></a> Bump up dependency versions</li>
<li>See full diff in <a href="https://github.com/snapview/tokio-tungstenite/compare/v0.16.1...v0.17.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tokio-tungstenite&package-manager=cargo&previous-version=0.16.1&new-version=0.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


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 dc7cc164..eab70a36 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",
@@ -1429,9 +1430,9 @@ dependencies = [
 
 [[package]]
 name = "eyre"
-version = "0.6.6"
+version = "0.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc225d8f637923fe585089fcf03e705c222131232d2c1fb622e84ecf725d0eb8"
+checksum = "9289ed2c0440a6536e65119725cf91fc2c6b5e513bfd2e36e1134d7cca6ca12f"
 dependencies = [
  "indenter",
  "once_cell",
@@ -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",
 ]
 
@@ -3253,9 +3254,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7"
+checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d"
 dependencies = [
  "serde",
 ]
@@ -3355,15 +3356,13 @@ dependencies = [
 
 [[package]]
 name = "sha-1"
-version = "0.9.8"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
 dependencies = [
- "block-buffer 0.9.0",
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest 0.9.0",
- "opaque-debug 0.3.0",
+ "digest 0.10.3",
 ]
 
 [[package]]
@@ -3814,9 +3813,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-tungstenite"
-version = "0.16.1"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72"
+checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae"
 dependencies = [
  "futures-util",
  "log",
@@ -3968,9 +3967,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
 [[package]]
 name = "tungstenite"
-version = "0.16.0"
+version = "0.17.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1"
+checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
 dependencies = [
  "base64 0.13.0",
  "byteorder",
@@ -4063,7 +4062,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/Cargo.toml b/Cargo.toml
index e9916114..10846f5d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -97,7 +97,7 @@ ethers-providers = { version = "^0.6.0", default-features = false, path = "./eth
 
 [target.'cfg(target_family = "unix")'.dev-dependencies]
 ethers-providers = { version = "^0.6.0", default-features = false, path = "./ethers-providers", features = ["ws", "ipc"] }
-eyre = "0.6.6"
+eyre = "0.6.7"
 rand = "0.8.5"
 serde = { version = "1.0.124", features = ["derive"] }
 serde_json = "1.0.64"
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..4d54aea2 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
@@ -342,7 +342,7 @@ impl Context {
         let mut all_functions = HashMap::new();
         for function in self.abi.functions() {
             all_functions
-                .entry(function.name.to_lowercase())
+                .entry(util::safe_snake_case_ident(&function.name))
                 .or_insert_with(Vec::new)
                 .push(function);
         }
@@ -558,7 +558,7 @@ fn expand_function_name(function: &Function, alias: Option<&MethodAlias>) -> Ide
     if let Some(alias) = alias {
         alias.function_name.clone()
     } else {
-        util::safe_ident(&function.name.to_snake_case())
+        util::safe_ident(&util::safe_snake_case(&function.name))
     }
 }
 
@@ -567,7 +567,7 @@ fn expand_call_struct_name(function: &Function, alias: Option<&MethodAlias>) ->
     let name = if let Some(alias) = alias {
         format!("{}Call", alias.struct_name)
     } else {
-        format!("{}Call", function.name.to_pascal_case())
+        format!("{}Call", util::safe_pascal_case(&function.name))
     };
     util::ident(&name)
 }
@@ -577,7 +577,7 @@ fn expand_call_struct_variant_name(function: &Function, alias: Option<&MethodAli
     if let Some(alias) = alias {
         alias.struct_name.clone()
     } else {
-        util::safe_ident(&function.name.to_pascal_case())
+        util::safe_ident(&util::safe_pascal_case(&function.name))
     }
 }
 
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 305db7ee..d57c8ea5 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -91,23 +91,43 @@ impl Context {
         tuple: ParamType,
     ) -> Result<TokenStream> {
         let mut fields = Vec::with_capacity(sol_struct.fields().len());
+
+        // determines whether we have enough info to create named fields
+        let is_tuple = sol_struct.has_nameless_field();
+
         for field in sol_struct.fields() {
-            let field_name = util::ident(&field.name().to_snake_case());
-            match field.r#type() {
-                FieldType::Elementary(ty) => {
-                    let ty = types::expand(ty)?;
-                    fields.push(quote! { pub #field_name: #ty });
-                }
-                FieldType::Struct(struct_ty) => {
-                    let ty = expand_struct_type(struct_ty);
-                    fields.push(quote! { pub #field_name: #ty });
-                }
+            let ty = match field.r#type() {
+                FieldType::Elementary(ty) => types::expand(ty)?,
+                FieldType::Struct(struct_ty) => expand_struct_type(struct_ty),
                 FieldType::Mapping(_) => {
                     eyre::bail!("Mapping types in struct `{}` are not supported {:?}", name, field)
                 }
+            };
+
+            if is_tuple {
+                fields.push(ty);
+            } else {
+                let field_name = util::ident(&field.name().to_snake_case());
+                fields.push(quote! { pub #field_name: #ty });
             }
         }
 
+        let name = util::ident(name);
+
+        let struct_def = if is_tuple {
+            quote! {
+                pub struct #name(
+                    #( #fields ),*
+                );
+            }
+        } else {
+            quote! {
+                pub struct #name {
+                    #( #fields ),*
+                }
+            }
+        };
+
         let sig = if let ParamType::Tuple(ref tokens) = tuple {
             tokens.iter().map(|kind| kind.to_string()).collect::<Vec<_>>().join(",")
         } else {
@@ -118,8 +138,6 @@ impl Context {
 
         let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature));
 
-        let name = util::ident(name);
-
         // use the same derives as for events
         let derives = util::expand_derives(&self.event_derives);
 
@@ -127,9 +145,7 @@ impl Context {
         Ok(quote! {
             #abi_signature_doc
             #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #ethers_contract::EthAbiCodec, #derives)]
-            pub struct #name {
-                #( #fields ),*
-            }
+            #struct_def
         })
     }
 
diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs
index a9721b20..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))
     }
 }
 
@@ -231,3 +242,56 @@ impl ContractBindings {
         name
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use ethers_solc::project_util::TempProject;
+
+    #[test]
+    fn can_generate_structs() {
+        let greeter = include_str!("../../tests/solidity-contracts/greeter_with_struct.json");
+        let abigen = Abigen::new("Greeter", greeter).unwrap();
+        let gen = abigen.generate().unwrap();
+        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 6afd6270..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 {
@@ -502,7 +726,7 @@ mod tests {
 
     fn run_test<T>(test: T)
     where
-        T: FnOnce(&Context) -> () + panic::UnwindSafe,
+        T: FnOnce(&Context) + panic::UnwindSafe,
     {
         let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")).to_owned();
         let console = Abigen::new(
@@ -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 ce9c62bb..782e4d35 100644
--- a/ethers-contract/ethers-contract-abigen/src/rawabi.rs
+++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs
@@ -8,6 +8,8 @@ use serde::{
 };
 
 /// Contract ABI as a list of items where each item can be a function, constructor or event
+#[derive(Debug, Clone, Serialize)]
+#[serde(transparent)]
 pub struct RawAbi(Vec<Item>);
 
 impl IntoIterator for RawAbi {
@@ -82,23 +84,31 @@ 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
+/// Either an input/output or a nested component of an input/output
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct Component {
     #[serde(rename = "internalType", default, skip_serializing_if = "Option::is_none")]
     pub internal_type: Option<String>,
+    #[serde(default)]
     pub name: String,
     #[serde(rename = "type")]
     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() {
@@ -112,4 +122,22 @@ mod tests {
             include_str!("../../tests/solidity-contracts/verifier_abi_hardhat.json");
         let _ = serde_json::from_str::<RawAbi>(VERIFIER_ABI).unwrap();
     }
+
+    /// due to ethabi's limitations some may be stripped when ethers-solc generates the abi, such as
+    /// the name of the component
+    #[test]
+    fn can_parse_ethers_solc_generated_abi() {
+        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..346a4b94 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,17 @@ 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);
+    }
+    if abi.starts_with('{') && abi.contains("Max rate limit reached") {
+        eyre::bail!(
+            "Max rate limit reached, please use etherscan API Key for higher rate limit: {:?}",
+            address
+        );
+    }
+
     Ok(abi)
 }
 
diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs
index 4c9b6657..d9c812e0 100644
--- a/ethers-contract/ethers-contract-abigen/src/util.rs
+++ b/ethers-contract/ethers-contract-abigen/src/util.rs
@@ -21,6 +21,25 @@ pub fn safe_ident(name: &str) -> Ident {
     syn::parse_str::<SynIdent>(name).unwrap_or_else(|_| ident(&format!("{}_", name)))
 }
 
+///  Converts a `&str` to `snake_case` `String` while respecting identifier rules
+pub fn safe_snake_case(ident: &str) -> String {
+    safe_identifier_name(ident.to_snake_case())
+}
+
+///  Converts a `&str` to `PascalCase` `String` while respecting identifier rules
+pub fn safe_pascal_case(ident: &str) -> String {
+    safe_identifier_name(ident.to_pascal_case())
+}
+
+/// respects identifier rules, such as, an identifier must not start with a numeric char
+fn safe_identifier_name(name: String) -> String {
+    if name.starts_with(|c: char| c.is_numeric()) {
+        format!("_{}", name)
+    } else {
+        name
+    }
+}
+
 /// Expands an identifier as snakecase and preserve any leading or trailing underscores
 pub fn safe_snake_case_ident(name: &str) -> Ident {
     let i = name.to_snake_case();
@@ -190,7 +209,7 @@ mod tests {
     #[test]
     fn parse_address_missing_prefix() {
         assert!(
-            !parse_address("0000000000000000000000000000000000000000").is_ok(),
+            parse_address("0000000000000000000000000000000000000000").is_err(),
             "parsing address not starting with 0x should fail"
         );
     }
@@ -198,7 +217,7 @@ mod tests {
     #[test]
     fn parse_address_address_too_short() {
         assert!(
-            !parse_address("0x00000000000000").is_ok(),
+            parse_address("0x00000000000000").is_err(),
             "parsing address not starting with 0x should fail"
         );
     }
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-core/src/abi/struct_def.rs b/ethers-core/src/abi/struct_def.rs
index 713fafad..d9545aca 100644
--- a/ethers-core/src/abi/struct_def.rs
+++ b/ethers-core/src/abi/struct_def.rs
@@ -280,6 +280,11 @@ impl SolStruct {
         &self.fields
     }
 
+    /// Returns `true` if a field with an empty name exists
+    pub fn has_nameless_field(&self) -> bool {
+        self.fields.iter().any(|f| f.name.is_empty())
+    }
+
     /// If the struct only consists of elementary fields, this will return `ParamType::Tuple` with
     /// all those fields
     pub fn as_tuple(&self) -> Option<ParamType> {
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/Cargo.toml b/ethers-providers/Cargo.toml
index f119895e..35d1dec3 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -44,7 +44,7 @@ bytes = { version  = "1.1.0", default-features = false, optional = true }
 # tokio
 tokio-util = { version = "0.7.0", default-features = false, features = ["io"], optional = true }
 tokio = { version = "1.5", default-features = false, optional = true }
-tokio-tungstenite = { version = "0.16.1", default-features = false, features = ["connect"], optional = true }
+tokio-tungstenite = { version = "0.17.1", default-features = false, features = ["connect"], optional = true }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 ws_stream_wasm = "0.7"
diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs
index 05298a25..0e1a8bef 100644
--- a/ethers-providers/src/pending_transaction.rs
+++ b/ethers-providers/src/pending_transaction.rs
@@ -85,6 +85,11 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
         self.provider.clone()
     }
 
+    /// Returns the transaction hash of the pending transaction
+    pub fn tx_hash(&self) -> TxHash {
+        self.tx_hash
+    }
+
     /// Sets the number of confirmations for the pending transaction to resolve
     /// to a receipt
     #[must_use]
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-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs
index fc1ae882..ba27729b 100644
--- a/ethers-providers/src/transports/ws.rs
+++ b/ethers-providers/src/transports/ws.rs
@@ -366,6 +366,7 @@ where
     async fn handle(&mut self, resp: Message) -> Result<(), ClientError> {
         match resp {
             Message::Text(inner) => self.handle_text(inner).await,
+            Message::Frame(_) => Ok(()), // Server is allowed to send Raw frames
             Message::Ping(inner) => self.handle_ping(inner).await,
             Message::Pong(_) => Ok(()), // Server is allowed to send unsolicited pongs.
             Message::Close(Some(frame)) => Err(ClientError::WsClosed(frame)),
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 12744c49..6216120b 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -27,7 +27,7 @@ rand = { version = "0.8.5", default-features = false }
 yubihsm = { version = "0.40.0", features = ["secp256k1", "http", "usb"], optional = true }
 futures-util = "^0.3"
 futures-executor = "^0.3"
-semver = "1.0.5"
+semver = "1.0.6"
 trezor-client = { version = "0.0.4", optional = true, default-features = false, features = ["f_ethereum"] }
 
 # aws
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index cfe4c78e..a45505a9 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -17,7 +17,7 @@ keywords = ["ethereum", "web3", "solc", "solidity", "ethers"]
 ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false }
 serde_json = "1.0.68"
 serde = { version = "1.0.130", features = ["derive"] }
-semver = { version = "1.0.5", features = ["serde"] }
+semver = { version = "1.0.6", features = ["serde"] }
 walkdir = "2.3.2"
 tokio = { version = "1.15.0", default-features = false, features = ["process", "io-util", "fs", "time"], optional = true }
 futures-util = { version = "^0.3", optional = true }
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 9ad98b5e..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 }
     }
 }
 
@@ -1592,6 +1634,7 @@ pub enum BytecodeObject {
     #[serde(deserialize_with = "serde_helpers::deserialize_bytes")]
     Bytecode(Bytes),
     /// Bytecode as hex string that's not fully linked yet and contains library placeholders
+    #[serde(with = "serde_helpers::string_bytes")]
     Unlinked(String),
 }
 
@@ -1612,6 +1655,13 @@ impl BytecodeObject {
             BytecodeObject::Unlinked(_) => None,
         }
     }
+    /// Returns a reference to the underlying `String` if the object is unlinked
+    pub fn as_str(&self) -> Option<&str> {
+        match self {
+            BytecodeObject::Bytecode(_) => None,
+            BytecodeObject::Unlinked(s) => Some(s.as_str()),
+        }
+    }
 
     /// Returns the unlinked `String` if the object is unlinked or empty
     pub fn into_unlinked(self) -> Option<String> {
@@ -1715,10 +1765,10 @@ impl BytecodeObject {
     }
 }
 
-// Returns a not deployable bytecode by default as "0x"
+// Returns a not deployable bytecode by default as empty
 impl Default for BytecodeObject {
     fn default() -> Self {
-        BytecodeObject::Unlinked("0x".to_string())
+        BytecodeObject::Unlinked("".to_string())
     }
 }
 
diff --git a/ethers-solc/src/artifacts/serde_helpers.rs b/ethers-solc/src/artifacts/serde_helpers.rs
index 6aba852e..d2a6c151 100644
--- a/ethers-solc/src/artifacts/serde_helpers.rs
+++ b/ethers-solc/src/artifacts/serde_helpers.rs
@@ -77,6 +77,34 @@ pub mod json_string_opt {
     }
 }
 
+/// serde support for string
+pub mod string_bytes {
+    use serde::{Deserialize, Deserializer, Serializer};
+
+    pub fn serialize<S>(value: &String, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        if value.starts_with("0x") {
+            serializer.serialize_str(value.as_str())
+        } else {
+            serializer.serialize_str(&format!("0x{}", value))
+        }
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+        if let Some(rem) = value.strip_prefix("0x") {
+            Ok(rem.to_string())
+        } else {
+            Ok(value)
+        }
+    }
+}
+
 pub mod display_from_str_opt {
     use serde::{de, Deserialize, Deserializer, Serializer};
     use std::{fmt, str::FromStr};
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 9a7077b5..bff8bdea 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -278,8 +278,11 @@ impl ProjectPathsConfig {
 
         for import in imports.iter() {
             let import_path = self.resolve_import(target_dir, import.data())?;
-            let import_content = self.flatten_node(&import_path, graph, imported, true, true)?;
-            let import_content = import_content.trim().as_bytes().to_owned();
+            let import_content = self
+                .flatten_node(&import_path, graph, imported, true, true)?
+                .trim()
+                .as_bytes()
+                .to_owned();
             let import_content_len = import_content.len() as isize;
             let (start, end) = import.loc_by_offset(offset);
             content.splice(start..end, import_content);
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 c8e1ca60..20b77c96 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -807,9 +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 = utils::find_import_paths(content)
-                .map(|m| SolDataUnit::new(PathBuf::from(m.as_str()), m.to_owned().into()))
-                .collect();
+            imports = capture_imports(content);
         }
     };
     let license = content.lines().next().and_then(|line| {
@@ -842,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");
diff --git a/examples/ethers-wasm/yarn.lock b/examples/ethers-wasm/yarn.lock
index 6b711d2f..929fc451 100644
--- a/examples/ethers-wasm/yarn.lock
+++ b/examples/ethers-wasm/yarn.lock
@@ -4076,9 +4076,9 @@ urix@^0.1.0:
   integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
 
 url-parse@^1.4.3, url-parse@^1.5.1:
-  version "1.5.7"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
-  integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+  integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
   dependencies:
     querystringify "^2.1.1"
     requires-port "^1.0.0"


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index 556879a1..4afcec6f 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};
 
@@ -374,6 +374,23 @@ fn can_handle_unique_underscore_functions() {
     let _contract_call = ConsoleLogCalls::Log2(call);
 }
 
+#[test]
+fn can_handle_underscore_numeric() {
+    abigen!(
+        Test,
+        r#"[
+            _100pct(string)
+        ]"#
+    );
+    let call = _100PctCall("message".to_string());
+
+    let provider = Arc::new(Provider::new(MockProvider::new()));
+    let contract = Test::new(Address::default(), Arc::clone(&provider));
+    // NOTE: this seems to be weird behaviour of `Inflector::to_snake_case` which turns "100pct" ->
+    // "10_0pct"
+    let _call = contract._10_0pct("hello".to_string());
+}
+
 #[test]
 fn can_handle_duplicates_with_same_name() {
     abigen!(
@@ -427,6 +444,23 @@ fn can_generate_nested_types() {
     assert_eq!(call, decoded_call);
 }
 
+#[test]
+fn can_handle_different_calls() {
+    abigen!(
+        Test,
+        r#"[
+        function fooBar()
+        function FOO_BAR()
+    ]"#,
+    );
+
+    let (client, _mock) = Provider::mocked();
+    let contract = Test::new(Address::default(), Arc::new(client));
+
+    let _ = contract.fooBar();
+    let _ = contract.FOO_BAR();
+}
+
 #[test]
 fn can_handle_case_sensitive_calls() {
     abigen!(
@@ -472,3 +506,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);
+}
diff --git a/ethers-contract/tests/solidity-contracts/greeter_with_struct.json b/ethers-contract/tests/solidity-contracts/greeter_with_struct.json
new file mode 100644
index 00000000..4c80ddc1
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/greeter_with_struct.json
@@ -0,0 +1,43 @@
+{
+  "abi": [
+    {
+      "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"
+    }
+  ],
+  "bytecode": {
+    "object": "0x608060405234801561001057600080fd5b50610242806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635581701b14610030575b600080fd5b61004a60048036038101906100459190610199565b610060565b60405161005791906101f1565b60405180910390f35b610068610070565b819050919050565b60405180602001604052806000151581525090565b6000604051905090565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6100e282610099565b810181811067ffffffffffffffff82111715610101576101006100aa565b5b80604052505050565b6000610114610085565b905061012082826100d9565b919050565b60008115159050919050565b61013a81610125565b811461014557600080fd5b50565b60008135905061015781610131565b92915050565b60006020828403121561017357610172610094565b5b61017d602061010a565b9050600061018d84828501610148565b60008301525092915050565b6000602082840312156101af576101ae61008f565b5b60006101bd8482850161015d565b91505092915050565b6101cf81610125565b82525050565b6020820160008201516101eb60008501826101c6565b50505050565b600060208201905061020660008301846101d5565b9291505056fea2646970667358221220890202b0964477379a457ab3725a21d7c14581e4596552e32a54e23f1c6564e064736f6c634300080c0033",
+    "sourceMap": "58:158:0:-:0;;;;;;;;;;;;;;;;;;;",
+    "linkReferences": {}
+  },
+  "deployedBytecode": {
+    "object": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c80635581701b14610030575b600080fd5b61004a60048036038101906100459190610199565b610060565b60405161005791906101f1565b60405180910390f35b610068610070565b819050919050565b60405180602001604052806000151581525090565b6000604051905090565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6100e282610099565b810181811067ffffffffffffffff82111715610101576101006100aa565b5b80604052505050565b6000610114610085565b905061012082826100d9565b919050565b60008115159050919050565b61013a81610125565b811461014557600080fd5b50565b60008135905061015781610131565b92915050565b60006020828403121561017357610172610094565b5b61017d602061010a565b9050600061018d84828501610148565b60008301525092915050565b6000602082840312156101af576101ae61008f565b5b60006101bd8482850161015d565b91505092915050565b6101cf81610125565b82525050565b6020820160008201516101eb60008501826101c6565b50505050565b600060208201905061020660008301846101d5565b9291505056fea2646970667358221220890202b0964477379a457ab3725a21d7c14581e4596552e32a54e23f1c6564e064736f6c634300080c0033",
+    "sourceMap": "58:158:0:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;115:99;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;171:12;;:::i;:::-;202:5;195:12;;115:99;;;:::o;-1:-1:-1:-;;;;;;;;;;;;;;;;:::o;7:75:1:-;40:6;73:2;67:9;57:19;;7:75;:::o;88:117::-;197:1;194;187:12;334:117;443:1;440;433:12;457:102;498:6;549:2;545:7;540:2;533:5;529:14;525:28;515:38;;457:102;;;:::o;565:180::-;613:77;610:1;603:88;710:4;707:1;700:15;734:4;731:1;724:15;751:281;834:27;856:4;834:27;:::i;:::-;826:6;822:40;964:6;952:10;949:22;928:18;916:10;913:34;910:62;907:88;;;975:18;;:::i;:::-;907:88;1015:10;1011:2;1004:22;794:238;751:281;;:::o;1038:129::-;1072:6;1099:20;;:::i;:::-;1089:30;;1128:33;1156:4;1148:6;1128:33;:::i;:::-;1038:129;;;:::o;1296:90::-;1330:7;1373:5;1366:13;1359:21;1348:32;;1296:90;;;:::o;1392:116::-;1462:21;1477:5;1462:21;:::i;:::-;1455:5;1452:32;1442:60;;1498:1;1495;1488:12;1442:60;1392:116;:::o;1514:133::-;1557:5;1595:6;1582:20;1573:29;;1611:30;1635:5;1611:30;:::i;:::-;1514:133;;;;:::o;1681:405::-;1750:5;1794:4;1782:9;1777:3;1773:19;1769:30;1766:117;;;1802:79;;:::i;:::-;1766:117;1901:21;1917:4;1901:21;:::i;:::-;1892:30;;1981:1;2021:46;2063:3;2054:6;2043:9;2039:22;2021:46;:::i;:::-;2014:4;2007:5;2003:16;1996:72;1932:147;1681:405;;;;:::o;2092:369::-;2171:6;2220:2;2208:9;2199:7;2195:23;2191:32;2188:119;;;2226:79;;:::i;:::-;2188:119;2346:1;2371:73;2436:7;2427:6;2416:9;2412:22;2371:73;:::i;:::-;2361:83;;2317:137;2092:369;;;;:::o;2467:99::-;2538:21;2553:5;2538:21;:::i;:::-;2533:3;2526:34;2467:99;;:::o;2624:317::-;2761:4;2756:3;2752:14;2848:4;2841:5;2837:16;2831:23;2867:57;2918:4;2913:3;2909:14;2895:12;2867:57;:::i;:::-;2776:158;2730:211;2624:317;;:::o;2947:302::-;3080:4;3118:2;3107:9;3103:18;3095:26;;3131:111;3239:1;3228:9;3224:17;3215:6;3131:111;:::i;:::-;2947:302;;;;:::o",
+    "linkReferences": {}
+  }
+}
\ No newline at end of file
diff --git a/ethers-solc/test-data/flatten-sample/contracts/Bar.sol b/ethers-solc/test-data/test-flatten-duplicates/contracts/Bar.sol
similarity index 52%
rename from ethers-solc/test-data/flatten-sample/contracts/Bar.sol
rename to ethers-solc/test-data/test-flatten-duplicates/contracts/Bar.sol
index 5cb19aba..5e9034a5 100644
--- a/ethers-solc/test-data/flatten-sample/contracts/Bar.sol
+++ b/ethers-solc/test-data/test-flatten-duplicates/contracts/Bar.sol
@@ -1,4 +1,4 @@
-//SPDX-License-Identifier: Unlicense
+//SPDX-License-Identifier: UNLICENSED
 pragma solidity >=0.6.0;
 
 contract Bar {}
diff --git a/ethers-solc/test-data/flatten-sample/contracts/Foo.sol b/ethers-solc/test-data/test-flatten-duplicates/contracts/Foo.sol
similarity index 66%
rename from ethers-solc/test-data/flatten-sample/contracts/Foo.sol
rename to ethers-solc/test-data/test-flatten-duplicates/contracts/Foo.sol
index f9cf562d..1f7086de 100644
--- a/ethers-solc/test-data/flatten-sample/contracts/Foo.sol
+++ b/ethers-solc/test-data/test-flatten-duplicates/contracts/Foo.sol
@@ -1,4 +1,4 @@
-//SPDX-License-Identifier: Unlicense
+//SPDX-License-Identifier: UNLICENSED
 pragma solidity >=0.6.0;
 
 import { Bar } from './Bar.sol';
diff --git a/ethers-solc/test-data/flatten-sample/contracts/FooBar.sol b/ethers-solc/test-data/test-flatten-duplicates/contracts/FooBar.sol
similarity index 74%
rename from ethers-solc/test-data/flatten-sample/contracts/FooBar.sol
rename to ethers-solc/test-data/test-flatten-duplicates/contracts/FooBar.sol
index eda3b10b..58ec146b 100644
--- a/ethers-solc/test-data/flatten-sample/contracts/FooBar.sol
+++ b/ethers-solc/test-data/test-flatten-duplicates/contracts/FooBar.sol
@@ -1,4 +1,4 @@
-//SPDX-License-Identifier: Unlicense
+//SPDX-License-Identifier: UNLICENSED
 pragma solidity >=0.6.0;
 
 import { Bar } from './Bar.sol';
diff --git a/ethers-solc/test-data/test-flatten-solang-failure/contracts/Contract.sol b/ethers-solc/test-data/test-flatten-solang-failure/contracts/Contract.sol
new file mode 100644
index 00000000..c0be0077
--- /dev/null
+++ b/ethers-solc/test-data/test-flatten-solang-failure/contracts/Contract.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.10;
+
+import { Lib } from "./Lib.sol";
+
+// Intentionally erroneous code
+contract Contract {
+    failure();
+}
diff --git a/ethers-solc/test-data/test-flatten-solang-failure/contracts/Lib.sol b/ethers-solc/test-data/test-flatten-solang-failure/contracts/Lib.sol
new file mode 100644
index 00000000..407fb855
--- /dev/null
+++ b/ethers-solc/test-data/test-flatten-solang-failure/contracts/Lib.sol
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.10;
+
+library Lib {}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 2a32d16f..4777c041 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -375,6 +375,7 @@ fn can_flatten_file() {
     assert!(result.is_ok());
 
     let result = result.unwrap();
+    assert!(!result.contains("import"));
     assert!(result.contains("contract Foo"));
     assert!(result.contains("contract Bar"));
 }
@@ -393,6 +394,7 @@ fn can_flatten_file_with_external_lib() {
     assert!(result.is_ok());
 
     let result = result.unwrap();
+    assert!(!result.contains("import"));
     assert!(result.contains("library console"));
     assert!(result.contains("contract Greeter"));
 }
@@ -409,6 +411,7 @@ fn can_flatten_file_in_dapp_sample() {
     assert!(result.is_ok());
 
     let result = result.unwrap();
+    assert!(!result.contains("import"));
     assert!(result.contains("contract DSTest"));
     assert!(result.contains("contract Dapp"));
     assert!(result.contains("contract DappTest"));
@@ -416,7 +419,7 @@ fn can_flatten_file_in_dapp_sample() {
 
 #[test]
 fn can_flatten_file_with_duplicates() {
-    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/flatten-sample");
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-flatten-duplicates");
     let paths = ProjectPathsConfig::builder().sources(root.join("contracts"));
     let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
 
@@ -426,10 +429,44 @@ fn can_flatten_file_with_duplicates() {
     assert!(result.is_ok());
 
     let result = result.unwrap();
-    assert_eq!(result.matches("contract Foo {").count(), 1);
-    assert_eq!(result.matches("contract Bar {").count(), 1);
-    assert_eq!(result.matches("contract FooBar {").count(), 1);
-    assert_eq!(result.matches(';').count(), 1);
+    assert_eq!(
+        result,
+        r#"//SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.6.0;
+
+contract Bar {}
+contract Foo {}
+
+contract FooBar {}
+"#
+    );
+}
+
+#[test]
+fn can_flatten_on_solang_failure() {
+    let root =
+        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-flatten-solang-failure");
+    let paths = ProjectPathsConfig::builder().sources(&root.join("contracts"));
+    let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
+
+    let target = root.join("contracts/Contract.sol");
+
+    let result = project.flatten(&target);
+    assert!(result.is_ok());
+
+    let result = result.unwrap();
+    assert_eq!(
+        result,
+        r#"// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.10;
+
+library Lib {}
+// Intentionally erroneous code
+contract Contract {
+    failure();
+}
+"#
+    );
 }
 
 #[test]
@@ -459,7 +496,7 @@ fn can_detect_type_error() {
 fn can_compile_single_files() {
     let tmp = TempProject::dapptools().unwrap();
 
-    let foo = tmp
+    let f = tmp
         .add_contract(
             "examples/Foo",
             r#"
@@ -470,7 +507,7 @@ fn can_compile_single_files() {
         )
         .unwrap();
 
-    let compiled = tmp.project().compile_file(foo.clone()).unwrap();
+    let compiled = tmp.project().compile_file(f.clone()).unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(compiled.find("Foo").is_some());
 
@@ -485,8 +522,43 @@ fn can_compile_single_files() {
         )
         .unwrap();
 
-    let compiled = tmp.project().compile_files(vec![foo, bar]).unwrap();
+    let compiled = tmp.project().compile_files(vec![f, bar]).unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(compiled.find("Foo").is_some());
     assert!(compiled.find("Bar").is_some());
 }
+
+#[test]
+fn consistent_bytecode() {
+    let tmp = TempProject::dapptools().unwrap();
+
+    tmp.add_source(
+        "LinkTest",
+        r#"
+// SPDX-License-Identifier: MIT
+library LibTest {
+    function foobar(uint256 a) public view returns (uint256) {
+    	return a * 100;
+    }
+}
+contract LinkTest {
+    function foo() public returns (uint256) {
+        return LibTest.foobar(1);
+    }
+}
+"#,
+    )
+    .unwrap();
+
+    let compiled = tmp.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let contract = compiled.find("LinkTest").unwrap();
+    let bytecode = &contract.bytecode.as_ref().unwrap().object;
+    assert!(bytecode.is_unlinked());
+    let s = bytecode.as_str().unwrap();
+    assert!(!s.starts_with("0x"));
+
+    let s = serde_json::to_string(&bytecode).unwrap();
+    assert_eq!(bytecode.clone(), serde_json::from_str(&s).unwrap());
+}