Repository: gakonst/ethers-rs
Pull request #828 (2022-01-25 11:51:14)

fix/etherscan verification
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


mattsse: what could be nice to have is some roundtrip test:
1. get contract source code for a specific address
2. get metadata (hopefully compiler version and constructor arguments are included)
3. call verify


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10f1d6fb..85885018 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
 
 ### Unreleased
 
+- Add `eth_syncing` [848](https://github.com/gakonst/ethers-rs/pull/848)
+- Fix overflow and possible divide-by-zero in `estimate_priority_fee`
+- Add BSC mainnet and testnet to the list of known chains
+  [831](https://github.com/gakonst/ethers-rs/pull/831)
 - Returns error on invalid type conversion instead of panicking
   [691](https://github.com/gakonst/ethers-rs/pull/691/files)
 - Change types mapping for solidity `bytes` to rust `ethers::core::Bytes` and
@@ -20,6 +24,12 @@
   and no nonce is specified
 - Move `fill_transaction` implementation to the provider, to allow middleware
   to properly override its behavior.
+- Add informational messages to solc installation and compilation.
+- Significantly refactor `MultiAbigen` module generation. Now allows for lib
+  generation, and does not make unnecessary disk writes.
+  [#854](https://github.com/gakonst/ethers-rs/pull/852)
+- Refactor `ethers-contract-abigen` to use `eyre` instead of `anyhow` via
+  [#858](https://github.com/gakonst/ethers-rs/pull/858)
 
 ## ethers-contract-abigen
 
@@ -34,6 +44,8 @@
 
 ### Unreleased
 
+- Fix flatten replacement target location
+  [#846](https://github.com/gakonst/ethers-rs/pull/846)
 - Fix duplicate files during flattening
   [#813](https://github.com/gakonst/ethers-rs/pull/813)
 - Add ability to flatten file imports
@@ -99,6 +111,9 @@
 
 ### Unreleased
 
+- Add support for basic and bearer authentication in http and non-wasm websockets.
+  [829](https://github.com/gakonst/ethers-rs/pull/829)
+
 ### 0.5.3
 
 - Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435)
@@ -174,6 +189,7 @@
   [#568](https://github.com/gakonst/ethers-rs/pull/568)
 - Removes GasNow as a gas price oracle
   [#508](https://github.com/gakonst/ethers-rs/pull/508)
+- add initialize_nonce public function to initialize NonceMiddleManager
 
 ### 0.5.3
 
diff --git a/Cargo.lock b/Cargo.lock
index 84654357..71c6c318 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,9 +54,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.52"
+version = "1.0.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
 
 [[package]]
 name = "arrayref"
@@ -365,9 +365,9 @@ dependencies = [
 
 [[package]]
 name = "camino"
-version = "1.0.5"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b"
+checksum = "6f3132262930b0522068049f5870a856ab8affc80c70d08b6ecb785771a6fc23"
 dependencies = [
  "serde",
 ]
@@ -462,10 +462,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
 dependencies = [
  "bitflags",
- "textwrap",
+ "textwrap 0.11.0",
  "unicode-width",
 ]
 
+[[package]]
+name = "clap"
+version = "3.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08799f92c961c7a1cf0cc398a9073da99e21ce388b46372c37f3191f2f3eed3e"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap 0.14.2",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fd2078197a22f338bd4fbf7d6387eb6f0d6a3c69e6cbc09f5c93e97321fd92a"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "cmac"
 version = "0.6.0"
@@ -651,9 +681,9 @@ dependencies = [
 
 [[package]]
 name = "crc32fast"
-version = "1.3.0"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836"
+checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -666,7 +696,7 @@ checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10"
 dependencies = [
  "atty",
  "cast",
- "clap",
+ "clap 2.34.0",
  "criterion-plot",
  "csv",
  "futures",
@@ -698,9 +728,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
+checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-utils",
@@ -719,9 +749,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.5"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd"
+checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-utils",
@@ -732,9 +762,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
 dependencies = [
  "cfg-if 1.0.0",
  "lazy_static",
@@ -986,9 +1016,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "elliptic-curve"
-version = "0.11.9"
+version = "0.11.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a4641673db66b0492d99edd8fd1cf2e6eb4ab91de525d1d2d6cc99442ed15f5"
+checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6"
 dependencies = [
  "base16ct",
  "crypto-bigint",
@@ -1051,7 +1081,7 @@ dependencies = [
 [[package]]
 name = "ethabi"
 version = "16.0.0"
-source = "git+https://github.com/rust-ethereum/ethabi?branch=master#c622438f9fd2ac2f9b3c38ecb1ca760c069d9ec2"
+source = "git+https://github.com/rust-ethereum/ethabi?branch=master#6f18e11621d29b6cd978ef39b48fa572b823bb80"
 dependencies = [
  "ethereum-types",
  "hex",
@@ -1095,7 +1125,6 @@ dependencies = [
 name = "ethers"
 version = "0.6.0"
 dependencies = [
- "anyhow",
  "bytes",
  "ethers-addressbook",
  "ethers-contract",
@@ -1105,6 +1134,7 @@ dependencies = [
  "ethers-providers",
  "ethers-signers",
  "ethers-solc",
+ "eyre",
  "hex",
  "rand 0.8.4",
  "serde",
@@ -1149,10 +1179,10 @@ name = "ethers-contract-abigen"
 version = "0.6.0"
 dependencies = [
  "Inflector",
- "anyhow",
  "cfg-if 1.0.0",
  "dunce",
  "ethers-core",
+ "eyre",
  "getrandom 0.2.4",
  "hex",
  "once_cell",
@@ -1232,6 +1262,7 @@ name = "ethers-etherscan"
 version = "0.2.0"
 dependencies = [
  "ethers-core",
+ "ethers-solc",
  "reqwest",
  "serde",
  "serde-aux",
@@ -1274,6 +1305,7 @@ version = "0.6.0"
 dependencies = [
  "async-trait",
  "auto_impl",
+ "base64 0.13.0",
  "bytes",
  "ethers-core",
  "futures-channel",
@@ -1281,6 +1313,7 @@ dependencies = [
  "futures-timer",
  "futures-util",
  "hex",
+ "http",
  "parking_lot",
  "pin-project",
  "reqwest",
@@ -1386,6 +1419,16 @@ dependencies = [
  "wee_alloc",
 ]
 
+[[package]]
+name = "eyre"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc225d8f637923fe585089fcf03e705c222131232d2c1fb622e84ecf725d0eb8"
+dependencies = [
+ "indenter",
+ "once_cell",
+]
+
 [[package]]
 name = "fake-simd"
 version = "0.1.2"
@@ -1394,9 +1437,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
 [[package]]
 name = "fastrand"
-version = "1.6.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
 dependencies = [
  "instant",
 ]
@@ -1629,9 +1672,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
+checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
 dependencies = [
  "bytes",
  "fnv",
@@ -1660,12 +1703,9 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
 
 [[package]]
 name = "heck"
-version = "0.3.3"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
 
 [[package]]
 name = "hermit-abi"
@@ -1852,6 +1892,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "indenter"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+
 [[package]]
 name = "indexmap"
 version = "1.8.0"
@@ -1944,9 +1990,9 @@ checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
 
 [[package]]
 name = "lalrpop"
-version = "0.19.6"
+version = "0.19.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988"
+checksum = "852b75a095da6b69da8c5557731c3afd06525d4f655a4fc1c799e2ec8bc4dce4"
 dependencies = [
  "ascii-canvas",
  "atty",
@@ -1967,9 +2013,9 @@ dependencies = [
 
 [[package]]
 name = "lalrpop-util"
-version = "0.19.6"
+version = "0.19.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278"
+checksum = "d6d265705249fe209280676d8f68887859fa42e1d34f342fc05bd47726a5e188"
 dependencies = [
  "regex",
 ]
@@ -1982,9 +2028,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.112"
+version = "0.2.115"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
+checksum = "0a8d982fa7a96a000f6ec4cfe966de9703eccde29750df2bb8949da91b0e818d"
 
 [[package]]
 name = "libusb1-sys"
@@ -2189,6 +2235,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_threads"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "number_prefix"
 version = "0.4.0"
@@ -2235,9 +2290,9 @@ dependencies = [
 
 [[package]]
 name = "openssl-probe"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
@@ -2252,6 +2307,15 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "output_vt100"
 version = "0.1.2"
@@ -2263,9 +2327,9 @@ dependencies = [
 
 [[package]]
 name = "p256"
-version = "0.10.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0e0c5310031b5d4528ac6534bccc1446c289ac45c47b277d5aa91089c5f74fa"
+checksum = "19736d80675fbe9fe33426268150b951a3fb8f5cfca2a23a17c85ef3adb24e3b"
 dependencies = [
  "ecdsa",
  "elliptic-curve",
@@ -2544,9 +2608,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
 [[package]]
 name = "pretty_assertions"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc"
+checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50"
 dependencies = [
  "ansi_term",
  "ctor",
@@ -3119,9 +3183,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.4.2"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
+checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -3132,9 +3196,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.4.2"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
+checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -3157,9 +3221,9 @@ checksum = "930c0acf610d3fdb5e2ab6213019aaa04e227ebe9547b0649ba599b16d788bd7"
 
 [[package]]
 name = "serde"
-version = "1.0.133"
+version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
 dependencies = [
  "serde_derive",
 ]
@@ -3186,9 +3250,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.133"
+version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3197,9 +3261,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.74"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142"
+checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
 dependencies = [
  "itoa 1.0.1",
  "ryu",
@@ -3208,12 +3272,12 @@ dependencies = [
 
 [[package]]
 name = "serde_urlencoded"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
 dependencies = [
  "form_urlencoded",
- "itoa 0.4.8",
+ "itoa 1.0.1",
  "ryu",
  "serde",
 ]
@@ -3349,9 +3413,9 @@ dependencies = [
 
 [[package]]
 name = "siphasher"
-version = "0.3.7"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
+checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
 
 [[package]]
 name = "slab"
@@ -3361,15 +3425,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
 
 [[package]]
 name = "smallvec"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
 
 [[package]]
 name = "socket2"
-version = "0.4.2"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
 dependencies = [
  "libc",
  "winapi",
@@ -3378,7 +3442,7 @@ dependencies = [
 [[package]]
 name = "solang-parser"
 version = "0.1.1"
-source = "git+https://github.com/hyperledger-labs/solang#67f1325a5aabd513289106585f97bdfe2efd647d"
+source = "git+https://github.com/hyperledger-labs/solang#f3e8f6fcabab91c959833cf85e3f13d327b00e38"
 dependencies = [
  "lalrpop",
  "lalrpop-util",
@@ -3425,28 +3489,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "structopt"
-version = "0.3.25"
+name = "strsim"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c"
-dependencies = [
- "clap",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "subtle"
@@ -3456,11 +3502,12 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
 [[package]]
 name = "svm-rs"
-version = "0.2.1"
-source = "git+https://github.com/roynalnaruto/svm-rs#4dd8d3f93a6383ff660899899b4dc43c73a14f97"
+version = "0.2.2"
+source = "git+https://github.com/roynalnaruto/svm-rs#146021b95ef9f17bb3b084bc77fbb84f3d4c5ee2"
 dependencies = [
  "anyhow",
  "cfg-if 1.0.0",
+ "clap 3.0.13",
  "console 0.14.1",
  "dialoguer",
  "hex",
@@ -3474,7 +3521,6 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2 0.9.9",
- "structopt",
  "tempfile",
  "thiserror",
  "tokio",
@@ -3535,6 +3581,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "terminal_size"
 version = "0.1.17"
@@ -3554,6 +3609,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+
 [[package]]
 name = "thiserror"
 version = "1.0.30"
@@ -3576,20 +3637,21 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.1.3"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
 dependencies = [
  "once_cell",
 ]
 
 [[package]]
 name = "time"
-version = "0.3.5"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad"
+checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
 dependencies = [
  "libc",
+ "num_threads",
  "serde",
 ]
 
@@ -3629,9 +3691,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.15.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
+checksum = "95e99d373042c30406d317cfc5bfad7b5d604bdd31dab72cf8739abebaa64aee"
 dependencies = [
  "bytes",
  "libc",
@@ -3780,9 +3842,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77be66445c4eeebb934a7340f227bfe7b338173d3f8c00a60a5a58005c9faecf"
+checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22"
 dependencies = [
  "ansi_term",
  "sharded-slab",
@@ -3868,12 +3930,6 @@ dependencies = [
  "tinyvec",
 ]
 
-[[package]]
-name = "unicode-segmentation"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
-
 [[package]]
 name = "unicode-width"
 version = "0.1.9"
@@ -4225,9 +4281,9 @@ dependencies = [
 
 [[package]]
 name = "zeroize"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc222aec311c323c717f56060324f32b82da1ce1dd81d9a09aa6a9030bfe08db"
+checksum = "4062c749be08d90be727e9c5895371c3a0e49b90ba2b9592dc7afda95cc2b719"
 dependencies = [
  "zeroize_derive",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 0d16a9f3..852e86c9 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"] }
-anyhow = "1.0.39"
+eyre = "0.6.6"
 rand = "0.8.4"
 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 ef9b3a9a..4c25310c 100644
--- a/ethers-contract/ethers-contract-abigen/Cargo.toml
+++ b/ethers-contract/ethers-contract-abigen/Cargo.toml
@@ -12,7 +12,6 @@ keywords = ["ethereum", "web3", "celo", "ethers"]
 [dependencies]
 ethers-core = { version = "^0.6.0", path = "../../ethers-core", features = ["macros"] }
 
-anyhow = "1.0.37"
 Inflector = "0.11"
 proc-macro2 = "1.0"
 quote = "1.0"
@@ -26,6 +25,7 @@ once_cell = "1.8.0"
 cfg-if = "1.0.0"
 dunce = "1.0.2"
 walkdir = "2.3.2"
+eyre = "0.6.6"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 # NOTE: this enables wasm compatibility for getrandom indirectly
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index 42c1feba..813bf575 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -7,11 +7,11 @@ mod types;
 
 use super::{util, Abigen};
 use crate::{contract::structs::InternalStructs, rawabi::RawAbi};
-use anyhow::{anyhow, Context as _, Result};
 use ethers_core::{
     abi::{Abi, AbiParser},
     macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate},
 };
+use eyre::{eyre, Context as _, Result};
 
 use crate::contract::methods::MethodAlias;
 use proc_macro2::{Ident, Literal, TokenStream};
@@ -153,7 +153,7 @@ impl Context {
     pub fn from_abigen(args: Abigen) -> Result<Self> {
         // get the actual ABI string
         let mut abi_str =
-            args.abi_source.get().map_err(|e| anyhow!("failed to get ABI JSON: {}", e))?;
+            args.abi_source.get().map_err(|e| eyre!("failed to get ABI JSON: {}", e))?;
 
         let (abi, human_readable, abi_parser) = parse_abi(&abi_str)?;
 
@@ -199,7 +199,7 @@ impl Context {
             };
 
             if method_aliases.insert(signature.clone(), alias).is_some() {
-                return Err(anyhow!("duplicate method signature '{}' in method aliases", signature,))
+                eyre::bail!("duplicate method signature '{}' in method aliases", signature)
             }
         }
 
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
index d78787d4..c0d29bc2 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
@@ -1,9 +1,9 @@
 use super::{types, util, Context};
-use anyhow::Result;
 use ethers_core::{
     abi::{Event, EventExt, EventParam, ParamType, SolStruct},
     macros::{ethers_contract_crate, ethers_core_crate},
 };
+use eyre::Result;
 use inflector::Inflector;
 use proc_macro2::{Ident, Literal, TokenStream};
 use quote::quote;
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index 344d4eef..42e34c28 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -1,6 +1,6 @@
 use std::collections::{btree_map::Entry, BTreeMap, HashMap};
 
-use anyhow::{Context as _, Result};
+use eyre::{Context as _, Result};
 use inflector::Inflector;
 use proc_macro2::{Literal, TokenStream};
 use quote::quote;
@@ -428,33 +428,33 @@ impl Context {
                     0 => {
                         // this may happen if there are functions with different casing,
                         // like `INDEX`and `index`
-                        if overloaded_fun.name != first_fun.name {
-                            let overloaded_id = overloaded_fun.name.to_snake_case();
-                            let first_fun_id = first_fun.name.to_snake_case();
-                            if first_fun_id != overloaded_id {
-                                // no conflict
-                                overloaded_id
-                            } else {
-                                let overloaded_alias = MethodAlias {
-                                    function_name: util::safe_ident(&overloaded_fun.name),
-                                    struct_name: util::safe_ident(&overloaded_fun.name),
-                                };
-                                aliases.insert(overloaded_fun.abi_signature(), overloaded_alias);
-
-                                let first_fun_alias = MethodAlias {
-                                    function_name: util::safe_ident(&first_fun.name),
-                                    struct_name: util::safe_ident(&first_fun.name),
-                                };
-                                aliases.insert(first_fun.abi_signature(), first_fun_alias);
-                                continue
-                            }
+
+                        // this should not happen since functions with same
+                        // name and inputs are illegal
+                        eyre::ensure!(
+                            overloaded_fun.name != first_fun.name,
+                            "Function with same name and parameter types defined twice: {}",
+                            overloaded_fun.name
+                        );
+
+                        let overloaded_id = overloaded_fun.name.to_snake_case();
+                        let first_fun_id = first_fun.name.to_snake_case();
+                        if first_fun_id != overloaded_id {
+                            // no conflict
+                            overloaded_id
                         } else {
-                            // this should not happen since functions with same name and inputs are
-                            // illegal
-                            anyhow::bail!(
-                                "Function with same name and parameter types defined twice: {}",
-                                overloaded_fun.name
-                            );
+                            let overloaded_alias = MethodAlias {
+                                function_name: util::safe_ident(&overloaded_fun.name),
+                                struct_name: util::safe_ident(&overloaded_fun.name),
+                            };
+                            aliases.insert(overloaded_fun.abi_signature(), overloaded_alias);
+
+                            let first_fun_alias = MethodAlias {
+                                function_name: util::safe_ident(&first_fun.name),
+                                struct_name: util::safe_ident(&first_fun.name),
+                            };
+                            aliases.insert(first_fun.abi_signature(), first_fun_alias);
+                            continue
                         }
                     }
                     1 => {
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index ec5fa69a..305db7ee 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -1,7 +1,7 @@
 //! Methods for expanding structs
 use std::collections::{HashMap, VecDeque};
 
-use anyhow::{Context as _, Result};
+use eyre::{eyre, Result};
 use inflector::Inflector;
 use proc_macro2::TokenStream;
 use quote::quote;
@@ -55,17 +55,18 @@ impl Context {
 
     /// Generates the type definition for the name that matches the given identifier
     fn generate_internal_struct(&self, id: &str) -> Result<TokenStream> {
-        let sol_struct = self.internal_structs.structs.get(id).context("struct not found")?;
+        let sol_struct =
+            self.internal_structs.structs.get(id).ok_or_else(|| eyre!("struct not found"))?;
         let struct_name = self
             .internal_structs
             .rust_type_names
             .get(id)
-            .context(format!("No types found for {}", id))?;
+            .ok_or_else(|| eyre!("No types found for {}", id))?;
         let tuple = self
             .internal_structs
             .struct_tuples
             .get(id)
-            .context(format!("No types found for {}", id))?
+            .ok_or_else(|| eyre!("No types found for {}", id))?
             .clone();
         self.expand_internal_struct(struct_name, sol_struct, tuple)
     }
@@ -102,11 +103,7 @@ impl Context {
                     fields.push(quote! { pub #field_name: #ty });
                 }
                 FieldType::Mapping(_) => {
-                    return Err(anyhow::anyhow!(
-                        "Mapping types in struct `{}` are not supported {:?}",
-                        name,
-                        field
-                    ))
+                    eyre::bail!("Mapping types in struct `{}` are not supported {:?}", name, field)
                 }
             }
         }
@@ -137,7 +134,8 @@ impl Context {
     }
 
     fn generate_human_readable_struct(&self, name: &str) -> Result<TokenStream> {
-        let sol_struct = self.abi_parser.structs.get(name).context("struct not found")?;
+        let sol_struct =
+            self.abi_parser.structs.get(name).ok_or_else(|| eyre!("struct not found"))?;
         let mut fields = Vec::with_capacity(sol_struct.fields().len());
         let mut param_types = Vec::with_capacity(sol_struct.fields().len());
         for field in sol_struct.fields() {
@@ -157,18 +155,14 @@ impl Context {
                         .abi_parser
                         .struct_tuples
                         .get(name)
-                        .context(format!("No types found for {}", name))?
+                        .ok_or_else(|| eyre!("No types found for {}", name))?
                         .clone();
                     let tuple = ParamType::Tuple(tuple);
 
                     param_types.push(struct_ty.as_param(tuple));
                 }
                 FieldType::Mapping(_) => {
-                    return Err(anyhow::anyhow!(
-                        "Mapping types in struct `{}` are not supported {:?}",
-                        name,
-                        field
-                    ))
+                    eyre::bail!("Mapping types in struct `{}` are not supported {:?}", name, field)
                 }
             }
         }
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/types.rs b/ethers-contract/ethers-contract-abigen/src/contract/types.rs
index 4100c35f..acde39b3 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/types.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/types.rs
@@ -1,5 +1,5 @@
-use anyhow::{anyhow, Result};
 use ethers_core::{abi::ParamType, macros::ethers_core_crate};
+use eyre::{bail, Result};
 use proc_macro2::{Literal, TokenStream};
 use quote::quote;
 
@@ -16,7 +16,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result<TokenStream> {
             5..=8 => Ok(quote! { i64 }),
             9..=16 => Ok(quote! { i128 }),
             17..=32 => Ok(quote! { I256 }),
-            _ => Err(anyhow!("unsupported solidity type int{}", n)),
+            _ => bail!("unsupported solidity type int{}", n),
         },
         ParamType::Uint(n) => match n / 8 {
             1 => Ok(quote! { u8 }),
@@ -25,7 +25,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result<TokenStream> {
             5..=8 => Ok(quote! { u64 }),
             9..=16 => Ok(quote! { u128 }),
             17..=32 => Ok(quote! { #ethers_core::types::U256 }),
-            _ => Err(anyhow!("unsupported solidity type uint{}", n)),
+            _ => bail!("unsupported solidity type uint{}", n),
         },
         ParamType::Bool => Ok(quote! { bool }),
         ParamType::String => Ok(quote! { String }),
@@ -46,9 +46,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result<TokenStream> {
             Ok(quote! { [#inner; #size] })
         }
         ParamType::Tuple(members) => {
-            if members.is_empty() {
-                return Err(anyhow!("Tuple must have at least 1 member"))
-            }
+            eyre::ensure!(!members.is_empty(), "Tuple must have at least 1 member");
 
             let members = members.iter().map(expand).collect::<Result<Vec<_>, _>>()?;
             Ok(quote! { (#(#members,)*) })
diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs
index 5df72810..a9721b20 100644
--- a/ethers-contract/ethers-contract-abigen/src/lib.rs
+++ b/ethers-contract/ethers-contract-abigen/src/lib.rs
@@ -19,19 +19,17 @@ mod rustfmt;
 mod source;
 mod util;
 
+pub mod multi;
+pub use multi::MultiAbigen;
+
 pub use ethers_core::types::Address;
 pub use source::Source;
 pub use util::parse_address;
 
-use anyhow::Result;
+use eyre::Result;
 use inflector::Inflector;
 use proc_macro2::TokenStream;
-use std::{
-    collections::HashMap,
-    fs::{self, File},
-    io::Write,
-    path::Path,
-};
+use std::{collections::HashMap, fs::File, io::Write, path::Path};
 
 /// Builder struct for generating type-safe bindings from a contract's ABI
 ///
@@ -86,6 +84,19 @@ impl Abigen {
         })
     }
 
+    /// Attemtps 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
+            .as_ref()
+            .file_stem()
+            .ok_or_else(|| eyre::format_err!("Missing file stem in path"))?
+            .to_str()
+            .ok_or_else(|| eyre::format_err!("Unable to convert file stem to string"))?;
+
+        Self::new(name, std::fs::read_to_string(path.as_ref())?)
+    }
+
     /// Manually adds a solidity event alias to specify what the event struct
     /// and function name will be in Rust.
     #[must_use]
@@ -138,8 +149,9 @@ impl Abigen {
     /// Generates the contract bindings.
     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 })
+        Ok(ContractBindings { tokens, rustfmt, name })
     }
 }
 
@@ -150,6 +162,8 @@ pub struct ContractBindings {
     tokens: TokenStream,
     /// The output options used for serialization.
     rustfmt: bool,
+    /// The contract name
+    name: String,
 }
 
 impl ContractBindings {
@@ -172,6 +186,13 @@ impl ContractBindings {
         Ok(())
     }
 
+    /// Writes the bindings to a new Vec. Panics if unable to allocate
+    pub fn to_vec(&self) -> Vec<u8> {
+        let mut bindings = vec![];
+        self.write(&mut bindings).expect("allocations don't fail");
+        bindings
+    }
+
     /// Writes the bindings to the specified file.
     pub fn write_to_file<P>(&self, path: P) -> Result<()>
     where
@@ -181,289 +202,32 @@ impl ContractBindings {
         self.write(file)
     }
 
-    /// Converts the bindings into its underlying token stream. This allows it
-    /// to be used within a procedural macro.
-    pub fn into_tokens(self) -> TokenStream {
-        self.tokens
-    }
-}
-
-/// Generates bindings for a series of contracts
-///
-/// This type can be used to generate multiple `ContractBindings` and put them all in a single rust
-/// module, (eg. a `contracts` directory).
-///
-/// This can be used to
-/// 1) write all bindings directly into a new directory in the project's source directory, so that
-/// it is included in the repository. 2) write all bindings to the value of cargo's `OUT_DIR` in a
-/// build script and import the bindings as `include!(concat!(env!("OUT_DIR"), "/mod.rs"));`.
-///
-/// However, the main purpose of this generator is to create bindings for option `1)` and write all
-/// contracts to some `contracts`  module in `src`, like `src/contracts/mod.rs` __once__ via a build
-/// script or a test. After that it's recommend to remove the build script and replace it with an
-/// integration test (See `MultiAbigen::ensure_consistent_bindings`) that fails if the generated
-/// code is out of date. This has several advantages:
-///
-///   * No need for downstream users to compile the build script
-///   * No need for downstream users to run the whole `abigen!` generation steps
-///   * The generated code is more usable in an IDE
-///   * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
-///     changed)
-///
-/// See `MultiAbigen::ensure_consistent_bindings` for the recommended way to set this up to generate
-/// the bindings once via a test and then use the test to ensure consistency.
-#[derive(Debug, Clone)]
-pub struct MultiAbigen {
-    /// whether to write all contracts in a single file instead of separated modules
-    single_file: bool,
-
-    abigens: Vec<Abigen>,
-}
-
-impl MultiAbigen {
-    /// Create a new instance from a series of already resolved `Abigen`
-    pub fn from_abigen(abis: impl IntoIterator<Item = Abigen>) -> Self {
-        Self {
-            single_file: false,
-            abigens: abis.into_iter().map(|abi| abi.rustfmt(true)).collect(),
-        }
-    }
-
-    /// Create a new instance from a series (`contract name`, `abi_source`)
-    ///
-    /// See `Abigen::new`
-    pub fn new<I, Name, Source>(abis: I) -> Result<Self>
+    /// Writes the bindings to a `contract_name.rs` file in the specified
+    /// directory. The filename is the snake_case transformation of the contract
+    /// name.
+    pub fn write_module_in_dir<P>(&self, dir: P) -> Result<()>
     where
-        I: IntoIterator<Item = (Name, Source)>,
-        Name: AsRef<str>,
-        Source: AsRef<str>,
+        P: AsRef<Path>,
     {
-        let abis = abis
-            .into_iter()
-            .map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
-            .collect::<Result<Vec<_>>>()?;
-
-        Ok(Self::from_abigen(abis))
-    }
-
-    /// Reads all json files contained in the given `dir` and use the file name for the name of the
-    /// `ContractBindings`.
-    /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
-    ///
-    /// # Example
-    ///
-    /// ```text
-    /// abi
-    /// ├── ERC20.json
-    /// ├── Contract1.json
-    /// ├── Contract2.json
-    /// ...
-    /// ```
-    ///
-    /// ```no_run
-    /// # use ethers_contract_abigen::MultiAbigen;
-    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
-    /// ```
-    pub fn from_json_files(dir: impl AsRef<Path>) -> Result<Self> {
-        let mut abis = Vec::new();
-        for file in util::json_files(dir) {
-            if let Some(file_name) = file.file_stem().and_then(|s| s.to_str()) {
-                let content = fs::read_to_string(&file)?;
-                abis.push((file_name.to_string(), content));
-            }
-        }
-        Self::new(abis)
-    }
-
-    /// Write all bindings into a single rust file instead of separate modules
-    #[must_use]
-    pub fn single_file(mut self) -> Self {
-        self.single_file = true;
-        self
+        let file = dir.as_ref().join(self.module_filename());
+        self.write_to_file(file)
     }
 
-    /// 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();
-    /// gen.write_to_module("./src/contracts").unwrap();
-    /// ```
-    pub fn write_to_module(self, module: impl AsRef<Path>) -> Result<()> {
-        let module = module.as_ref();
-        fs::create_dir_all(module)?;
-
-        let mut contracts_mod =
-            b"/// This module contains all the autogenerated abigen! contract bindings\n".to_vec();
-
-        let mut modules = Vec::new();
-        for abi in self.abigens {
-            let name = abi.contract_name.to_snake_case();
-            let bindings = abi.generate()?;
-            if self.single_file {
-                // append to the mod file
-                bindings.write(&mut contracts_mod)?;
-            } else {
-                // create a contract rust file
-                let output = module.join(format!("{}.rs", name));
-                bindings.write_to_file(output)?;
-                modules.push(format!("pub mod {};", name));
-            }
-        }
-
-        if !modules.is_empty() {
-            modules.sort();
-            write!(contracts_mod, "{}", modules.join("\n"))?;
-        }
-
-        // write the mod file
-        fs::write(module.join("mod.rs"), contracts_mod)?;
-
-        Ok(())
+    /// Converts the bindings into its underlying token stream. This allows it
+    /// to be used within a procedural macro.
+    pub fn into_tokens(self) -> TokenStream {
+        self.tokens
     }
 
-    /// This ensures that the already generated contract bindings match 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.
-    ///
-    /// So if this functions is run within a test during CI and fails, then it's time to update all
-    /// bindings.
-    ///
-    /// Returns `true` if the freshly generated bindings match with the existing bindings, `false`
-    /// otherwise
-    ///
-    /// # 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();
-    ///  assert!(gen.ensure_consistent_bindings(project_root.join("src/contracts")));
-    /// }
-    ///
-    /// gen.write_to_module("./src/contracts").unwrap();
-    /// ```
-    #[cfg(test)]
-    pub fn ensure_consistent_bindings(self, module: impl AsRef<Path>) -> bool {
-        let module = module.as_ref();
-        let dir = tempfile::tempdir().expect("Failed to create temp dir");
-        let temp_module = dir.path().join("contracts");
-        self.write_to_module(&temp_module).expect("Failed to generate bindings");
-
-        for file in fs::read_dir(&temp_module).unwrap() {
-            let fresh_file = file.unwrap();
-            let fresh_file_path = fresh_file.path();
-            let file_name = fresh_file_path.file_name().and_then(|p| p.to_str()).unwrap();
-            assert!(file_name.ends_with(".rs"), "Expected rust file");
-
-            let existing_bindings_file = module.join(file_name);
-
-            if !existing_bindings_file.is_file() {
-                // file does not already exist
-                return false
-            }
-
-            // read the existing file
-            let existing_contract_bindings = fs::read_to_string(existing_bindings_file).unwrap();
-
-            let fresh_bindings = fs::read_to_string(fresh_file.path()).unwrap();
-
-            if existing_contract_bindings != fresh_bindings {
-                return false
-            }
-        }
-        true
+    /// Generate the default module name (snake case of the contract name)
+    pub fn module_name(&self) -> String {
+        self.name.to_snake_case()
     }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn can_generate_multi_abi() {
-        let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
-
-        let tempdir = tempfile::tempdir().unwrap();
-        let mod_root = tempdir.path().join("contracts");
-
-        let console = Abigen::new(
-            "Console",
-            crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
-        )
-        .unwrap();
-
-        let simple_storage = Abigen::new(
-            "SimpleStorage",
-            crate_root
-                .join("../tests/solidity-contracts/simplestorage_abi.json")
-                .display()
-                .to_string(),
-        )
-        .unwrap();
-
-        let human_readable = Abigen::new(
-            "HrContract",
-            r#"[
-        struct Foo { uint256 x; }
-        function foo(Foo memory x)
-        function bar(uint256 x, uint256 y, address addr)
-        yeet(uint256,uint256,address)
-    ]"#,
-        )
-        .unwrap();
-
-        let mut multi_gen = MultiAbigen::from_abigen([console, simple_storage, human_readable]);
-
-        multi_gen.clone().write_to_module(&mod_root).unwrap();
-        assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root));
-
-        // add another contract
-        multi_gen.abigens.push(
-            Abigen::new(
-                "AdditionalContract",
-                r#"[
-        getValue() (uint256)
-        getValue(uint256 otherValue) (uint256)
-        getValue(uint256 otherValue, address addr) (uint256)
-    ]"#,
-            )
-            .unwrap(),
-        );
-
-        // ensure inconsistent bindings are detected
-        assert!(!multi_gen.clone().ensure_consistent_bindings(&mod_root));
 
-        // update with new contract
-        multi_gen.clone().write_to_module(&mod_root).unwrap();
-        assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root));
+    /// Generate the default filename of the module
+    pub fn module_filename(&self) -> String {
+        let mut name = self.module_name();
+        name.extend([".rs"]);
+        name
     }
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs
new file mode 100644
index 00000000..013c930e
--- /dev/null
+++ b/ethers-contract/ethers-contract-abigen/src/multi.rs
@@ -0,0 +1,754 @@
+//! TODO
+
+use eyre::Result;
+use inflector::Inflector;
+use std::{collections::BTreeMap, fs, io::Write, path::Path};
+
+use crate::{util, Abigen, ContractBindings};
+
+/// Collects Abigen structs for a series of contracts, pending generation of
+/// the contract bindings.
+#[derive(Debug, Clone)]
+pub struct MultiAbigen {
+    /// Abigen objects to be written
+    abigens: Vec<Abigen>,
+}
+
+impl std::ops::Deref for MultiAbigen {
+    type Target = Vec<Abigen>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.abigens
+    }
+}
+
+impl From<Vec<Abigen>> for MultiAbigen {
+    fn from(abigens: Vec<Abigen>) -> Self {
+        Self { abigens }
+    }
+}
+
+impl std::iter::FromIterator<Abigen> for MultiAbigen {
+    fn from_iter<I: IntoIterator<Item = Abigen>>(iter: I) -> Self {
+        iter.into_iter().collect::<Vec<_>>().into()
+    }
+}
+
+impl MultiAbigen {
+    /// Create a new instance from a series (`contract name`, `abi_source`)
+    ///
+    /// See `Abigen::new`
+    pub fn new<I, Name, Source>(abis: I) -> Result<Self>
+    where
+        I: IntoIterator<Item = (Name, Source)>,
+        Name: AsRef<str>,
+        Source: AsRef<str>,
+    {
+        let abis = abis
+            .into_iter()
+            .map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
+            .collect::<Result<Vec<_>>>()?;
+
+        Ok(Self::from_abigens(abis))
+    }
+
+    /// Create a new instance from a series of already resolved `Abigen`
+    pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> Self {
+        abis.into_iter().collect()
+    }
+
+    /// Reads all json files contained in the given `dir` and use the file name for the name of the
+    /// `ContractBindings`.
+    /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
+    ///
+    /// # Example
+    ///
+    /// ```text
+    /// abi
+    /// ├── ERC20.json
+    /// ├── Contract1.json
+    /// ├── Contract2.json
+    /// ...
+    /// ```
+    ///
+    /// ```no_run
+    /// # use ethers_contract_abigen::MultiAbigen;
+    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
+    /// ```
+    pub fn from_json_files(root: impl AsRef<Path>) -> Result<Self> {
+        util::json_files(root.as_ref()).into_iter().map(Abigen::from_file).collect()
+    }
+
+    /// Add another Abigen to the module or lib
+    pub fn push(&mut self, abigen: Abigen) {
+        self.abigens.push(abigen)
+    }
+
+    /// 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 })
+    }
+}
+
+/// Output of the [`MultiAbigen`] build process. `MultiBindings` wraps a group
+/// of built contract bindings that have yet to be written to disk.
+///
+/// `MultiBindings` enables the user to
+/// 1. Write a collection of bindings to a rust module
+/// 2. Write a collection of bindings to a rust lib
+/// 3. Ensure that a collection of bindings matches an on-disk module or lib.
+///
+/// Generally we recommend writing the bindings to a module folder within your
+/// rust project. Users seeking to create "official" bindings for some project
+/// may instead write an entire library to publish via crates.io.
+///
+/// Rather than using `MultiAbigen` in a build script, we recommend committing
+/// the generated files, and replacing the build script with an integration
+/// test. To enable this, we have provided
+/// `MultiBindings::ensure_consistent_bindings` and
+/// `MultiBindings::ensure_consistent_crate`. These functions generate the
+/// expected module or library in memory, and check that the on-disk files
+/// match the expected files. We recommend running these inside CI.
+///
+/// This has several advantages:
+///   * No need for downstream users to compile the build script
+///   * No need for downstream users to run the whole `abigen!` generation steps
+///   * The generated code is more usable in an IDE
+///   * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
+///     changed)
+pub struct MultiBindings {
+    /// Abigen objects to be written
+    bindings: BTreeMap<String, ContractBindings>,
+}
+
+// deref allows for inspection without modification
+impl std::ops::Deref for MultiBindings {
+    type Target = BTreeMap<String, ContractBindings>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.bindings
+    }
+}
+
+impl MultiBindings {
+    /// Generat the contents of the `Cargo.toml` file for a lib
+    fn generate_cargo_toml(
+        &self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+    ) -> Result<Vec<u8>> {
+        let mut toml = vec![];
+
+        writeln!(toml, "[package]")?;
+        writeln!(toml, r#"name = "{}""#, name.as_ref())?;
+        writeln!(toml, r#"version = "{}""#, version.as_ref())?;
+        writeln!(toml, r#"edition = "2021""#)?;
+        writeln!(toml)?;
+        writeln!(toml, "# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html")?;
+        writeln!(toml)?;
+        writeln!(toml, "[dependencies]")?;
+        writeln!(
+            toml,
+            r#"ethers = {{ git = "https://github.com/gakonst/ethers-rs", default-features = false }}"#
+        )?;
+        Ok(toml)
+    }
+
+    /// Write the contents of `Cargo.toml` to disk
+    fn write_cargo_toml(
+        &self,
+        lib: &Path,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+    ) -> Result<()> {
+        let contents = self.generate_cargo_toml(name, version)?;
+
+        let mut file = fs::OpenOptions::new()
+            .read(true)
+            .write(true)
+            .create_new(true)
+            .open(lib.join("Cargo.toml"))?;
+        file.write_all(&contents)?;
+
+        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()))
+        {
+            writeln!(buf, "{}", module)?;
+        }
+
+        Ok(())
+    }
+
+    /// 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)?;
+
+        if !single_file {
+            self.append_module_names(&mut contents)?;
+        } else {
+            for binding in self.bindings.values() {
+                binding.write(&mut contents)?;
+            }
+        }
+
+        Ok(contents)
+    }
+
+    /// Write the `lib.rs` or `mod.rs` to disk
+    fn write_super_file(&self, path: &Path, is_crate: bool, single_file: bool) -> Result<()> {
+        let filename = if is_crate { "lib.rs" } else { "mod.rs" };
+        let contents = self.generate_super_contents(is_crate, single_file)?;
+        fs::write(path.join(filename), contents)?;
+        Ok(())
+    }
+
+    /// Write all contract bindings to their respective files
+    fn write_bindings(&self, path: &Path) -> Result<()> {
+        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<()> {
+        let module = module.as_ref();
+        fs::create_dir_all(module)?;
+
+        self.write_super_file(module, false, single_file)?;
+
+        if !single_file {
+            self.write_bindings(module)?;
+        }
+        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(
+        self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+        lib: impl AsRef<Path>,
+        single_file: bool,
+    ) -> Result<()> {
+        let lib = lib.as_ref();
+        let src = lib.join("src");
+        fs::create_dir_all(&src)?;
+
+        self.write_cargo_toml(lib, name, version)?;
+        self.write_super_file(&src, true, single_file)?;
+
+        if !single_file {
+            self.write_bindings(&src)?;
+        }
+
+        Ok(())
+    }
+
+    /// Ensures the contents of the bindings directory are correct
+    ///
+    /// Does this by first generating the `lib.rs` or `mod.rs`, then the
+    /// contents of each binding file in turn.
+    fn ensure_consistent_bindings(
+        self,
+        dir: impl AsRef<Path>,
+        is_crate: bool,
+        single_file: bool,
+    ) -> Result<()> {
+        let dir = dir.as_ref();
+        let super_name = if is_crate { "lib.rs" } else { "mod.rs" };
+
+        let super_contents = self.generate_super_contents(is_crate, single_file)?;
+        check_file_in_dir(dir, super_name, &super_contents)?;
+
+        // If it is single file, we skip checking anything but the super
+        // contents
+        if !single_file {
+            for binding in self.bindings.values() {
+                check_binding_in_dir(dir, binding)?;
+            }
+        }
+
+        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(
+        self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+        crate_path: impl AsRef<Path>,
+        single_file: bool,
+    ) -> Result<()> {
+        let crate_path = crate_path.as_ref();
+
+        // additionally check the contents of the cargo
+        let cargo_contents = self.generate_cargo_toml(name, version)?;
+        check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
+
+        self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?;
+        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<()> {
+        self.ensure_consistent_bindings(module, false, single_file)?;
+        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());
+
+    let file_path = dir.join(file_name);
+    eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display());
+
+    let contents = fs::read(file_path).expect("Unable to read file");
+    eyre::ensure!(contents == expected_contents, "file contents do not match");
+    Ok(())
+}
+
+fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
+    let name = binding.module_filename();
+    let contents = binding.to_vec();
+
+    check_file_in_dir(dir, &name, &contents)?;
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use std::{panic, path::PathBuf};
+
+    struct Context {
+        multi_gen: MultiAbigen,
+        mod_root: PathBuf,
+    }
+
+    fn run_test<T>(test: T)
+    where
+        T: FnOnce(&Context) -> () + panic::UnwindSafe,
+    {
+        let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")).to_owned();
+        let console = Abigen::new(
+            "Console",
+            crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
+        )
+        .unwrap();
+
+        let simple_storage = Abigen::new(
+            "SimpleStorage",
+            crate_root
+                .join("../tests/solidity-contracts/simplestorage_abi.json")
+                .display()
+                .to_string(),
+        )
+        .unwrap();
+
+        let human_readable = Abigen::new(
+            "HrContract",
+            r#"[
+        struct Foo { uint256 x; }
+        function foo(Foo memory x)
+        function bar(uint256 x, uint256 y, address addr)
+        yeet(uint256,uint256,address)
+    ]"#,
+        )
+        .unwrap();
+
+        let multi_gen = MultiAbigen::from_abigens([console, simple_storage, human_readable]);
+
+        let mod_root = tempfile::tempdir().unwrap().path().join("contracts");
+        let context = Context { multi_gen, mod_root };
+
+        let result = panic::catch_unwind(|| test(&context));
+
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn can_generate_multi_file_module() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = false;
+
+            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .ensure_consistent_module(&mod_root, single_file)
+                .expect("Inconsistent bindings");
+        })
+    }
+
+    #[test]
+    fn can_generate_single_file_module() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = true;
+
+            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .ensure_consistent_module(&mod_root, single_file)
+                .expect("Inconsistent bindings");
+        })
+    }
+
+    #[test]
+    fn can_generate_multi_file_crate() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = false;
+            let name = "a-name";
+            let version = "290.3782.3";
+
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .write_to_crate(name, version, &mod_root, single_file)
+                .unwrap();
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .expect("Inconsistent bindings");
+        })
+    }
+
+    #[test]
+    fn can_generate_single_file_crate() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = true;
+            let name = "a-name";
+            let version = "290.3782.3";
+
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .write_to_crate(name, version, &mod_root, single_file)
+                .unwrap();
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .expect("Inconsistent bindings");
+        })
+    }
+
+    #[test]
+    fn can_detect_incosistent_multi_file_module() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = false;
+
+            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
+
+            let mut cloned = multi_gen.clone();
+            cloned.push(
+                Abigen::new(
+                    "AdditionalContract",
+                    r#"[
+                        getValue() (uint256)
+                    ]"#,
+                )
+                .unwrap(),
+            );
+
+            let result =
+                cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
+
+            // ensure inconsistent bindings are detected
+            assert!(result, "Inconsistent bindings wrongly approved");
+        })
+    }
+
+    #[test]
+    fn can_detect_incosistent_single_file_module() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = true;
+
+            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
+
+            let mut cloned = multi_gen.clone();
+            cloned.push(
+                Abigen::new(
+                    "AdditionalContract",
+                    r#"[
+                        getValue() (uint256)
+                    ]"#,
+                )
+                .unwrap(),
+            );
+
+            let result =
+                cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
+
+            // ensure inconsistent bindings are detected
+            assert!(result, "Inconsistent bindings wrongly approved");
+        })
+    }
+
+    #[test]
+    fn can_detect_incosistent_multi_file_crate() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = false;
+            let name = "a-name";
+            let version = "290.3782.3";
+
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .write_to_crate(name, version, &mod_root, single_file)
+                .unwrap();
+
+            let mut cloned = multi_gen.clone();
+            cloned.push(
+                Abigen::new(
+                    "AdditionalContract",
+                    r#"[
+                            getValue() (uint256)
+                        ]"#,
+                )
+                .unwrap(),
+            );
+
+            let result = cloned
+                .build()
+                .unwrap()
+                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .is_err();
+
+            // ensure inconsistent bindings are detected
+            assert!(result, "Inconsistent bindings wrongly approved");
+        })
+    }
+
+    #[test]
+    fn can_detect_incosistent_single_file_crate() {
+        run_test(|context| {
+            let Context { multi_gen, mod_root } = context;
+
+            let single_file = true;
+            let name = "a-name";
+            let version = "290.3782.3";
+
+            multi_gen
+                .clone()
+                .build()
+                .unwrap()
+                .write_to_crate(name, version, &mod_root, single_file)
+                .unwrap();
+
+            let mut cloned = multi_gen.clone();
+            cloned.push(
+                Abigen::new(
+                    "AdditionalContract",
+                    r#"[
+                            getValue() (uint256)
+                        ]"#,
+                )
+                .unwrap(),
+            );
+
+            let result = cloned
+                .build()
+                .unwrap()
+                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .is_err();
+
+            // ensure inconsistent bindings are detected
+            assert!(result, "Inconsistent bindings wrongly approved");
+        })
+    }
+}
diff --git a/ethers-contract/ethers-contract-abigen/src/rustfmt.rs b/ethers-contract/ethers-contract-abigen/src/rustfmt.rs
index c64b7f3e..c9bb546d 100644
--- a/ethers-contract/ethers-contract-abigen/src/rustfmt.rs
+++ b/ethers-contract/ethers-contract-abigen/src/rustfmt.rs
@@ -1,6 +1,6 @@
 //! This module implements basic `rustfmt` code formatting.
 
-use anyhow::{anyhow, Result};
+use eyre::{eyre, Result};
 use std::{
     io::Write,
     process::{Command, Stdio},
@@ -18,18 +18,18 @@ where
         let stdin = rustfmt
             .stdin
             .as_mut()
-            .ok_or_else(|| anyhow!("stdin was not created for `rustfmt` child process"))?;
+            .ok_or_else(|| eyre!("stdin was not created for `rustfmt` child process"))?;
         stdin.write_all(source.as_ref().as_bytes())?;
     }
 
     let output = rustfmt.wait_with_output()?;
-    if !output.status.success() {
-        return Err(anyhow!(
-            "`rustfmt` exited with code {}:\n{}",
-            output.status,
-            String::from_utf8_lossy(&output.stderr),
-        ))
-    }
+
+    eyre::ensure!(
+        output.status.success(),
+        "`rustfmt` exited with code {}:\n{}",
+        output.status,
+        String::from_utf8_lossy(&output.stderr),
+    );
 
     let stdout = String::from_utf8(output.stdout)?;
     Ok(stdout)
diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs
index 1b8ab08b..d68df2a1 100644
--- a/ethers-contract/ethers-contract-abigen/src/source.rs
+++ b/ethers-contract/ethers-contract-abigen/src/source.rs
@@ -3,8 +3,8 @@ use super::util;
 use ethers_core::types::Address;
 
 use crate::util::resolve_path;
-use anyhow::{anyhow, Context, Error, Result};
 use cfg_if::cfg_if;
+use eyre::{eyre, Context, Error, Result};
 use std::{env, fs, path::Path, str::FromStr};
 use url::Url;
 
@@ -88,10 +88,10 @@ impl Source {
                     format!("{}", root.display())
                 };
                 let base = Url::parse(&root)
-                    .map_err(|_| anyhow!("root path '{}' is not absolute", root))?;
+                    .map_err(|_| eyre!("root path '{}' is not absolute", root))?;
             } else {
                 let base = Url::from_directory_path(root)
-                    .map_err(|_| anyhow!("root path '{}' is not absolute", root.display()))?;
+                    .map_err(|_| eyre!("root path '{}' is not absolute", root.display()))?;
             }
         }
         let url = base.join(source)?;
@@ -103,19 +103,19 @@ impl Source {
                     url.path()
                         .rsplit('/')
                         .next()
-                        .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
+                        .ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
                 ),
                 Some("polygonscan.com") => Source::polygonscan(
                     url.path()
                         .rsplit('/')
                         .next()
-                        .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
+                        .ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
                 ),
                 Some("snowtrace.io") => Source::snowtrace(
                     url.path()
                         .rsplit('/')
                         .next()
-                        .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
+                        .ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
                 ),
                 _ => Ok(Source::Http(url)),
             },
@@ -123,7 +123,7 @@ impl Source {
             "polygonscan" => Source::polygonscan(url.path()),
             "snowtrace" => Source::snowtrace(url.path()),
             "npm" => Ok(Source::npm(url.path())),
-            _ => Err(anyhow!("unsupported URL '{}'", url)),
+            _ => Err(eyre!("unsupported URL '{}'", url)),
         }
     }
 
@@ -232,7 +232,7 @@ fn get_local_contract(path: impl AsRef<str>) -> Result<String> {
             contract_path = dunce::canonicalize(&path)?;
         }
         if !contract_path.exists() {
-            anyhow::bail!("Unable to find local contract \"{}\"", path.display())
+            eyre::bail!("Unable to find local contract \"{}\"", path.display())
         }
         contract_path
     } else {
diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs
index 7a779990..4c9b6657 100644
--- a/ethers-contract/ethers-contract-abigen/src/util.rs
+++ b/ethers-contract/ethers-contract-abigen/src/util.rs
@@ -1,7 +1,7 @@
 use ethers_core::types::Address;
 use std::path::PathBuf;
 
-use anyhow::{anyhow, Result};
+use eyre::Result;
 use inflector::Inflector;
 use proc_macro2::{Ident, Literal, Span, TokenStream};
 use quote::quote;
@@ -76,9 +76,7 @@ where
     S: AsRef<str>,
 {
     let address_str = address_str.as_ref();
-    if !address_str.starts_with("0x") {
-        return Err(anyhow!("address must start with '0x'"))
-    }
+    eyre::ensure!(address_str.starts_with("0x"), "address must start with '0x'");
     Ok(address_str[2..].parse()?)
 }
 
@@ -89,7 +87,7 @@ pub fn http_get(_url: &str) -> Result<String> {
         if #[cfg(feature = "reqwest")]{
             Ok(reqwest::blocking::get(_url)?.text()?)
         } else {
-            Err(anyhow!("HTTP is unsupported"))
+            eyre::bail!("HTTP is unsupported")
         }
     }
 }
@@ -110,7 +108,7 @@ pub fn resolve_path(raw: &str) -> Result<PathBuf> {
                 unprocessed = rest;
             }
             None => {
-                anyhow::bail!("Unable to parse a variable from \"{}\"", tail)
+                eyre::bail!("Unable to parse a variable from \"{}\"", tail)
             }
         }
     }
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index be15fd84..788ffe0a 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -17,7 +17,7 @@ rlp-derive = { version = "0.1.0", default-features = false }
 
 # crypto
 ecdsa = { version = "0.13.4", default-features = false, features = ["std"] }
-elliptic-curve = { version = "0.11.9", default-features = false }
+elliptic-curve = { version = "0.11.12", default-features = false }
 generic-array = { version = "0.14.5", default-features = false }
 k256 = { version = "0.10.2", default-features = false, features = ["keccak256", "ecdsa"] }
 rand = { version = "0.8.4", default-features = false }
diff --git a/ethers-core/src/abi/codec.rs b/ethers-core/src/abi/codec.rs
index f4de400c..fef30f18 100644
--- a/ethers-core/src/abi/codec.rs
+++ b/ethers-core/src/abi/codec.rs
@@ -2,7 +2,7 @@ use crate::{
     abi::{
         AbiArrayType, AbiError, AbiType, Detokenize, Token, Tokenizable, TokenizableItem, Tokenize,
     },
-    types::{Address, H256, U128, U256},
+    types::{Address, Bytes, H256, U128, U256},
 };
 
 /// Trait for ABI encoding
@@ -40,6 +40,7 @@ macro_rules! impl_abi_codec {
 
 impl_abi_codec!(
     Vec<u8>,
+    Bytes,
     Address,
     bool,
     String,
@@ -228,4 +229,11 @@ mod tests {
         .collect::<String>()
         };
     }
+
+    #[test]
+    fn bytes_codec() {
+        let bytes: Bytes = std::iter::repeat_with(random::<u8>).take(10).collect::<Vec<_>>().into();
+        let v = vec![bytes];
+        assert_codec(v);
+    }
 }
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index 6bcbdd56..c31c45ba 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -32,6 +32,8 @@ pub enum Chain {
     Moonriver = 1285,
     Optimism = 10,
     OptimismKovan = 69,
+    BinanceSmartChain = 56,
+    BinanceSmartChainTestnet = 97,
 }
 
 impl fmt::Display for Chain {
@@ -79,6 +81,8 @@ impl TryFrom<u64> for Chain {
             1285 => Chain::Moonriver,
             10 => Chain::Optimism,
             69 => Chain::OptimismKovan,
+            56 => Chain::BinanceSmartChain,
+            97 => Chain::BinanceSmartChainTestnet,
             _ => return Err(ParseChainError(chain.to_string())),
         })
     }
@@ -106,7 +110,26 @@ impl FromStr for Chain {
             "optimism-kovan" => Chain::OptimismKovan,
             "fantom" => Chain::Fantom,
             "fantom-testnet" => Chain::FantomTestnet,
+            "bsc" => Chain::BinanceSmartChain,
+            "bsc-testnet" => Chain::BinanceSmartChainTestnet,
             _ => return Err(ParseChainError(chain.to_owned())),
         })
     }
 }
+
+impl Chain {
+    /// Helper function for checking if a chainid corresponds to a legacy chainid
+    /// without eip1559
+    pub fn is_legacy(&self) -> bool {
+        // TODO: Add other chains which do not support EIP1559.
+        matches!(
+            self,
+            Chain::Optimism |
+                Chain::OptimismKovan |
+                Chain::Fantom |
+                Chain::FantomTestnet |
+                Chain::BinanceSmartChain |
+                Chain::BinanceSmartChainTestnet
+        )
+    }
+}
diff --git a/ethers-core/src/types/ens.rs b/ethers-core/src/types/ens.rs
index 35784a8b..9b68ac4e 100644
--- a/ethers-core/src/types/ens.rs
+++ b/ethers-core/src/types/ens.rs
@@ -1,5 +1,7 @@
+use std::cmp::Ordering;
+
 use crate::types::Address;
-use rlp::{Encodable, RlpStream};
+use rlp::{Decodable, Encodable, RlpStream};
 use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer};
 
 /// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name
@@ -29,6 +31,25 @@ impl Encodable for NameOrAddress {
     }
 }
 
+impl Decodable for NameOrAddress {
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        // An address (H160) is 20 bytes, so let's only accept 20 byte rlp string encodings.
+        if !rlp.is_data() {
+            return Err(rlp::DecoderError::RlpExpectedToBeData)
+        }
+
+        // the data needs to be 20 bytes long
+        match 20.cmp(&rlp.size()) {
+            Ordering::Less => Err(rlp::DecoderError::RlpIsTooShort),
+            Ordering::Greater => Err(rlp::DecoderError::RlpIsTooBig),
+            Ordering::Equal => {
+                let rlp_data = rlp.data()?;
+                Ok(NameOrAddress::Address(Address::from_slice(rlp_data)))
+            }
+        }
+    }
+}
+
 impl From<&str> for NameOrAddress {
     fn from(s: &str) -> Self {
         NameOrAddress::Name(s.to_owned())
@@ -71,6 +92,8 @@ impl<'de> Deserialize<'de> for NameOrAddress {
 
 #[cfg(test)]
 mod tests {
+    use rlp::Rlp;
+
     use super::*;
 
     #[test]
@@ -103,6 +126,20 @@ mod tests {
         assert_eq!(rlp.as_raw(), expected.as_raw());
     }
 
+    #[test]
+    fn rlp_address_deserialized() {
+        let addr = "3dd6f334b732d23b51dfbee2070b40bbd1a97a8f".parse().unwrap();
+        let expected = NameOrAddress::Address(addr);
+
+        let mut rlp = RlpStream::new();
+        rlp.append(&addr);
+        let rlp_bytes = &rlp.out().freeze()[..];
+        let data = Rlp::new(rlp_bytes);
+        let name = NameOrAddress::decode(&data).unwrap();
+
+        assert_eq!(name, expected);
+    }
+
     #[test]
     fn serde_name_not_serialized() {
         let name = NameOrAddress::Name("ens.eth".to_string());
diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs
index 2868be31..38d1d705 100644
--- a/ethers-core/src/types/transaction/eip1559.rs
+++ b/ethers-core/src/types/transaction/eip1559.rs
@@ -3,7 +3,7 @@ use crate::{
     types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
     utils::keccak256,
 };
-use rlp::RlpStream;
+use rlp::{Decodable, DecoderError, RlpStream};
 
 /// EIP-1559 transactions have 9 fields
 const NUM_TX_FIELDS: usize = 9;
@@ -57,6 +57,10 @@ pub struct Eip1559TransactionRequest {
     /// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and
     /// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user.
     pub max_fee_per_gas: Option<U256>,
+
+    #[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
+    /// Chain ID (None for mainnet)
+    pub chain_id: Option<U64>,
 }
 
 impl Eip1559TransactionRequest {
@@ -130,25 +134,34 @@ impl Eip1559TransactionRequest {
         self
     }
 
+    /// Sets the `chain_id` field in the transaction to the provided value
+    #[must_use]
+    pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
+        self.chain_id = Some(chain_id.into());
+        self
+    }
+
     /// Hashes the transaction's data with the provided chain id
-    pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
-        keccak256(self.rlp(chain_id).as_ref()).into()
+    pub fn sighash(&self) -> H256 {
+        keccak256(self.rlp().as_ref()).into()
     }
 
     /// Gets the unsigned transaction's RLP encoding
-    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+    pub fn rlp(&self) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_list(NUM_TX_FIELDS);
-        self.rlp_base(chain_id, &mut rlp);
+        self.rlp_base(&mut rlp);
         rlp.out().freeze().into()
     }
 
     /// Produces the RLP encoding of the transaction with the provided signature
-    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+    pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_unbounded_list();
-        let chain_id = chain_id.into();
-        self.rlp_base(chain_id, &mut rlp);
+        self.rlp_base(&mut rlp);
+
+        // if the chain_id is none we assume mainnet and choose one
+        let chain_id = self.chain_id.unwrap_or_else(U64::one);
 
         // append the signature
         let v = normalize_v(signature.v, chain_id);
@@ -159,8 +172,8 @@ impl Eip1559TransactionRequest {
         rlp.out().freeze().into()
     }
 
-    pub(crate) fn rlp_base<T: Into<U64>>(&self, chain_id: T, rlp: &mut RlpStream) {
-        rlp.append(&chain_id.into());
+    pub(crate) fn rlp_base(&self, rlp: &mut RlpStream) {
+        rlp_opt(rlp, &self.chain_id);
         rlp_opt(rlp, &self.nonce);
         rlp_opt(rlp, &self.max_priority_fee_per_gas);
         rlp_opt(rlp, &self.max_fee_per_gas);
@@ -170,6 +183,44 @@ impl Eip1559TransactionRequest {
         rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
         rlp.append(&self.access_list);
     }
+
+    /// Decodes fields of the request starting at the RLP offset passed. Increments the offset for
+    /// each element parsed.
+    #[inline]
+    fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> {
+        self.chain_id = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.nonce = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.max_fee_per_gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.to = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.value = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        self.data = match data.len() {
+            0 => None,
+            _ => Some(Bytes::from(data.to_vec())),
+        };
+        *offset += 1;
+        self.access_list = rlp.val_at(*offset)?;
+        *offset += 1;
+        Ok(())
+    }
+}
+
+impl Decodable for Eip1559TransactionRequest {
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        let mut txn = Eip1559TransactionRequest::new();
+        let mut offset = 0;
+        txn.decode_base_rlp(rlp, &mut offset)?;
+        Ok(txn)
+    }
 }
 
 impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
@@ -188,6 +239,7 @@ impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
             gateway_fee_recipient: None,
             #[cfg(feature = "celo")]
             gateway_fee: None,
+            chain_id: tx.chain_id,
         }
     }
 }
diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs
index 60db1fb6..fe211df7 100644
--- a/ethers-core/src/types/transaction/eip2718.rs
+++ b/ethers-core/src/types/transaction/eip2718.rs
@@ -162,6 +162,23 @@ impl TypedTransaction {
         };
     }
 
+    pub fn chain_id(&self) -> Option<U64> {
+        match self {
+            Legacy(inner) => inner.chain_id,
+            Eip2930(inner) => inner.tx.chain_id,
+            Eip1559(inner) => inner.chain_id,
+        }
+    }
+
+    pub fn set_chain_id<T: Into<U64>>(&mut self, chain_id: T) {
+        let chain_id = chain_id.into();
+        match self {
+            Legacy(inner) => inner.chain_id = Some(chain_id),
+            Eip2930(inner) => inner.tx.chain_id = Some(chain_id),
+            Eip1559(inner) => inner.chain_id = Some(chain_id),
+        };
+    }
+
     pub fn data(&self) -> Option<&Bytes> {
         match self {
             Legacy(inner) => inner.data.as_ref(),
@@ -194,7 +211,7 @@ impl TypedTransaction {
         };
     }
 
-    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+    pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
         let mut encoded = vec![];
         match self {
             Legacy(ref tx) => {
@@ -202,44 +219,71 @@ impl TypedTransaction {
             }
             Eip2930(inner) => {
                 encoded.extend_from_slice(&[0x1]);
-                encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
+                encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
             }
             Eip1559(inner) => {
                 encoded.extend_from_slice(&[0x2]);
-                encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
+                encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
             }
         };
         encoded.into()
     }
 
-    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
-        let chain_id = chain_id.into();
+    pub fn rlp(&self) -> Bytes {
         let mut encoded = vec![];
         match self {
             Legacy(inner) => {
-                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+                encoded.extend_from_slice(inner.rlp().as_ref());
             }
             Eip2930(inner) => {
                 encoded.extend_from_slice(&[0x1]);
-                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+                encoded.extend_from_slice(inner.rlp().as_ref());
             }
             Eip1559(inner) => {
                 encoded.extend_from_slice(&[0x2]);
-                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+                encoded.extend_from_slice(inner.rlp().as_ref());
             }
         };
 
         encoded.into()
     }
 
-    /// Hashes the transaction's data with the provided chain id
-    /// Does not double-RLP encode
-    pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
-        let encoded = self.rlp(chain_id);
+    /// Hashes the transaction's data. Does not double-RLP encode
+    pub fn sighash(&self) -> H256 {
+        let encoded = self.rlp();
         keccak256(encoded).into()
     }
 }
 
+/// Get a TypedTransaction directly from an rlp encoded byte stream
+impl rlp::Decodable for TypedTransaction {
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        let tx_type: Option<U64> = match rlp.is_data() {
+            true => Some(rlp.data().unwrap().into()),
+            false => None,
+        };
+        let rest = rlp::Rlp::new(
+            rlp.as_raw().get(1..).ok_or(rlp::DecoderError::Custom("no transaction payload"))?,
+        );
+
+        match tx_type {
+            Some(x) if x == U64::from(1) => {
+                // EIP-2930 (0x01)
+                Ok(Self::Eip2930(Eip2930TransactionRequest::decode(&rest)?))
+            }
+            Some(x) if x == U64::from(2) => {
+                // EIP-1559 (0x02)
+                Ok(Self::Eip1559(Eip1559TransactionRequest::decode(&rest)?))
+            }
+            _ => {
+                // Legacy (0x00)
+                // use the original rlp
+                Ok(Self::Legacy(TransactionRequest::decode(rlp)?))
+            }
+        }
+    }
+}
+
 impl From<TransactionRequest> for TypedTransaction {
     fn from(src: TransactionRequest) -> TypedTransaction {
         TypedTransaction::Legacy(src)
@@ -260,8 +304,11 @@ impl From<Eip1559TransactionRequest> for TypedTransaction {
 
 #[cfg(test)]
 mod tests {
+    use rlp::Decodable;
+
     use super::*;
     use crate::types::{Address, U256};
+    use std::str::FromStr;
 
     #[test]
     fn serde_legacy_tx() {
@@ -276,4 +323,102 @@ mod tests {
         let de: TransactionRequest = serde_json::from_str(&serialized).unwrap();
         assert_eq!(tx, TypedTransaction::Legacy(de));
     }
+
+    #[test]
+    fn test_typed_tx_without_access_list() {
+        let tx: Eip1559TransactionRequest = serde_json::from_str(
+            r#"{
+            "gas": "0x186a0",
+            "maxFeePerGas": "0x77359400",
+            "maxPriorityFeePerGas": "0x77359400",
+            "data": "0x5544",
+            "nonce": "0x2",
+            "to": "0x96216849c49358B10257cb55b28eA603c874b05E",
+            "value": "0x5af3107a4000",
+            "type": "0x2",
+            "chainId": "0x539",
+            "accessList": [],
+            "v": "0x1",
+            "r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e",
+            "s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74"
+        }"#,
+        )
+        .unwrap();
+
+        let envelope = TypedTransaction::Eip1559(tx);
+
+        let expected =
+            H256::from_str("0xa1ea3121940930f7e7b54506d80717f14c5163807951624c36354202a8bffda6")
+                .unwrap();
+        let actual = envelope.sighash();
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_typed_tx() {
+        let tx: Eip1559TransactionRequest = serde_json::from_str(
+            r#"{
+            "gas": "0x186a0",
+            "maxFeePerGas": "0x77359400",
+            "maxPriorityFeePerGas": "0x77359400",
+            "data": "0x5544",
+            "nonce": "0x2",
+            "to": "0x96216849c49358B10257cb55b28eA603c874b05E",
+            "value": "0x5af3107a4000",
+            "type": "0x2",
+            "accessList": [
+                {
+                    "address": "0x0000000000000000000000000000000000000001",
+                    "storageKeys": [
+                        "0x0100000000000000000000000000000000000000000000000000000000000000"
+                    ]
+                }
+            ],
+            "chainId": "0x539",
+            "v": "0x1",
+            "r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e",
+            "s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74"
+        }"#,
+        )
+        .unwrap();
+
+        let envelope = TypedTransaction::Eip1559(tx);
+
+        let expected =
+            H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d")
+                .unwrap();
+        let actual = envelope.sighash();
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_typed_tx_decode() {
+        // this is the same transaction as the above test
+        let typed_tx_hex = hex::decode("02f86b8205390284773594008477359400830186a09496216849c49358b10257cb55b28ea603c874b05e865af3107a4000825544f838f7940000000000000000000000000000000000000001e1a00100000000000000000000000000000000000000000000000000000000000000").unwrap();
+        let tx_rlp = rlp::Rlp::new(typed_tx_hex.as_slice());
+        let actual_tx = TypedTransaction::decode(&tx_rlp).unwrap();
+
+        let expected =
+            H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d")
+                .unwrap();
+        let actual = actual_tx.sighash();
+        assert_eq!(expected, actual);
+    }
+
+    #[cfg(not(feature = "celo"))]
+    #[test]
+    fn test_eip155_decode() {
+        let tx = TransactionRequest::new()
+            .nonce(9)
+            .to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
+            .value(1000000000000000000u64)
+            .gas_price(20000000000u64)
+            .gas(21000)
+            .chain_id(1);
+
+        let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
+        let expected_rlp = rlp::Rlp::new(expected_hex.as_slice());
+        let decoded_transaction = TypedTransaction::decode(&expected_rlp).unwrap();
+        assert_eq!(tx.sighash(), decoded_transaction.sighash());
+    }
 }
diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs
index c9b6fb4e..5e78b75f 100644
--- a/ethers-core/src/types/transaction/eip2930.rs
+++ b/ethers-core/src/types/transaction/eip2930.rs
@@ -1,8 +1,8 @@
 use super::{normalize_v, request::TransactionRequest};
 use crate::types::{Address, Bytes, Signature, H256, U256, U64};
 
-use rlp::RlpStream;
-use rlp_derive::{RlpEncodable, RlpEncodableWrapper};
+use rlp::{Decodable, DecoderError, RlpStream};
+use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
 use serde::{Deserialize, Serialize};
 
 const NUM_EIP2930_FIELDS: usize = 8;
@@ -10,7 +10,17 @@ const NUM_EIP2930_FIELDS: usize = 8;
 /// Access list
 // NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output
 // https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodableWrapper)]
+#[derive(
+    Debug,
+    Default,
+    Clone,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    RlpEncodableWrapper,
+    RlpDecodableWrapper,
+)]
 pub struct AccessList(pub Vec<AccessListItem>);
 
 #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -38,7 +48,9 @@ impl TransactionRequest {
 }
 
 /// Access list item
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable)]
+#[derive(
+    Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable, RlpDecodable,
+)]
 #[serde(rename_all = "camelCase")]
 pub struct AccessListItem {
     /// Accessed address
@@ -60,10 +72,12 @@ impl Eip2930TransactionRequest {
         Self { tx, access_list }
     }
 
-    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+    pub fn rlp(&self) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_list(NUM_EIP2930_FIELDS);
-        rlp.append(&chain_id.into());
+
+        let chain_id = self.tx.chain_id.unwrap_or_else(U64::one);
+        rlp.append(&chain_id);
         self.tx.rlp_base(&mut rlp);
         // append the access list in addition to the base rlp encoding
         rlp.append(&self.access_list);
@@ -72,11 +86,11 @@ impl Eip2930TransactionRequest {
     }
 
     /// Produces the RLP encoding of the transaction with the provided signature
-    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+    pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_list(NUM_EIP2930_FIELDS + 3);
 
-        let chain_id = chain_id.into();
+        let chain_id = self.tx.chain_id.unwrap_or_else(U64::one);
         rlp.append(&chain_id);
         self.tx.rlp_base(&mut rlp);
         // append the access list in addition to the base rlp encoding
@@ -89,10 +103,50 @@ impl Eip2930TransactionRequest {
         rlp.append(&signature.s);
         rlp.out().freeze().into()
     }
+
+    /// Decodes fields based on the RLP offset passed
+    fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> {
+        self.tx.chain_id = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.tx.nonce = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.tx.gas_price = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.tx.gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+
+        self.tx.gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.tx.to = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.tx.value = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        self.tx.data = match data.len() {
+            0 => None,
+            _ => Some(Bytes::from(data.to_vec())),
+        };
+        *offset += 1;
+        self.access_list = rlp.val_at(*offset)?;
+        *offset += 1;
+
+        Ok(())
+    }
+}
+
+/// Get a Eip2930TransactionRequest from a rlp encoded byte stream
+impl Decodable for Eip2930TransactionRequest {
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        let mut new_tx = Self::new(TransactionRequest::new(), AccessList::default());
+        let mut offset = 0;
+        new_tx.decode_base_rlp(rlp, &mut offset)?;
+        Ok(new_tx)
+    }
 }
 
 #[cfg(test)]
 mod tests {
+
     use super::*;
     use crate::types::{transaction::eip2718::TypedTransaction, U256};
 
@@ -110,14 +164,14 @@ mod tests {
             .with_access_list(vec![])
             .into();
 
-        let hash = tx.sighash(1);
+        let hash = tx.sighash();
         let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
         assert_eq!(
             hash,
             "49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3".parse().unwrap()
         );
 
-        let enc = rlp::encode(&tx.rlp_signed(1, &sig).as_ref());
+        let enc = rlp::encode(&tx.rlp_signed(&sig).as_ref());
         let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
         assert_eq!(hex::encode(&enc), expected);
     }
diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs
index baf9ce17..b4a25e97 100644
--- a/ethers-core/src/types/transaction/mod.rs
+++ b/ethers-core/src/types/transaction/mod.rs
@@ -33,3 +33,44 @@ pub(crate) fn normalize_v(v: u64, chain_id: crate::types::U64) -> u64 {
         v
     }
 }
+
+/// extracts the chainid from the signature v value based on EIP-155
+pub(crate) fn extract_chain_id(v: u64) -> Option<crate::types::U64> {
+    // https://eips.ethereum.org/EIPS/eip-155
+    // if chainid is available, v = {0, 1} + CHAIN_ID * 2 + 35
+    if v >= 35 {
+        return Some(crate::types::U64::from((v - 35) >> 1))
+    }
+    None
+}
+
+/// Decodes the signature portion of the RLP encoding based on the RLP offset passed.
+/// Increments the offset for each element parsed.
+#[inline]
+fn decode_signature(
+    rlp: &rlp::Rlp,
+    offset: &mut usize,
+) -> Result<super::Signature, rlp::DecoderError> {
+    let sig = super::Signature {
+        v: rlp.val_at(*offset)?,
+        r: rlp.val_at(*offset + 1)?,
+        s: rlp.val_at(*offset + 2)?,
+    };
+    *offset += 3;
+    Ok(sig)
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::types::{transaction::rlp_opt, U64};
+    use rlp::RlpStream;
+
+    #[test]
+    fn test_rlp_opt_none() {
+        let mut stream = RlpStream::new_list(1);
+        let empty_chainid: Option<U64> = None;
+        rlp_opt(&mut stream, &empty_chainid);
+        let out = stream.out();
+        assert_eq!(out, vec![0xc1, 0x80]);
+    }
+}
diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs
index 371b120f..12f579cc 100644
--- a/ethers-core/src/types/transaction/request.rs
+++ b/ethers-core/src/types/transaction/request.rs
@@ -1,11 +1,11 @@
 //! Transaction types
-use super::{rlp_opt, NUM_TX_FIELDS};
+use super::{extract_chain_id, rlp_opt, NUM_TX_FIELDS};
 use crate::{
     types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
     utils::keccak256,
 };
 
-use rlp::RlpStream;
+use rlp::{Decodable, RlpStream};
 use serde::{Deserialize, Serialize};
 
 /// Parameters for sending a transaction
@@ -41,6 +41,10 @@ pub struct TransactionRequest {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub nonce: Option<U256>,
 
+    /// Chain ID (None for mainnet)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub chain_id: Option<U64>,
+
     /////////////////  Celo-specific transaction fields /////////////////
     /// The currency fees are paid in (None for native currency)
     #[cfg(feature = "celo")]
@@ -123,29 +127,49 @@ impl TransactionRequest {
         self
     }
 
+    /// Sets the `chain_id` field in the transaction to the provided value
+    #[must_use]
+    pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
+        self.chain_id = Some(chain_id.into());
+        self
+    }
+
     /// Hashes the transaction's data with the provided chain id
-    pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
-        keccak256(self.rlp(chain_id).as_ref()).into()
+    pub fn sighash(&self) -> H256 {
+        match self.chain_id {
+            Some(_) => keccak256(self.rlp().as_ref()).into(),
+            None => keccak256(self.rlp_unsigned().as_ref()).into(),
+        }
     }
 
-    /// Gets the unsigned transaction's RLP encoding
-    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+    /// Gets the transaction's RLP encoding, prepared with the chain_id and extra fields for
+    /// signing. Assumes the chainid exists.
+    pub fn rlp(&self) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_list(NUM_TX_FIELDS);
         self.rlp_base(&mut rlp);
 
         // Only hash the 3 extra fields when preparing the
         // data to sign if chain_id is present
-        rlp.append(&chain_id.into());
+        rlp_opt(&mut rlp, &self.chain_id);
         rlp.append(&0u8);
         rlp.append(&0u8);
         rlp.out().freeze().into()
     }
 
+    /// Gets the unsigned transaction's RLP encoding
+    pub fn rlp_unsigned(&self) -> Bytes {
+        let mut rlp = RlpStream::new();
+        rlp.begin_list(NUM_TX_FIELDS - 3);
+        self.rlp_base(&mut rlp);
+        rlp.out().freeze().into()
+    }
+
     /// Produces the RLP encoding of the transaction with the provided signature
     pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
         let mut rlp = RlpStream::new();
         rlp.begin_list(NUM_TX_FIELDS);
+
         self.rlp_base(&mut rlp);
 
         // append the signature
@@ -167,6 +191,86 @@ impl TransactionRequest {
         rlp_opt(rlp, &self.value);
         rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
     }
+
+    /// Decodes the unsigned rlp, returning the transaction request and incrementing the counter
+    /// passed as we are traversing the rlp list.
+    fn decode_unsigned_rlp_base(
+        rlp: &rlp::Rlp,
+        offset: &mut usize,
+    ) -> Result<Self, rlp::DecoderError> {
+        let mut txn = TransactionRequest::new();
+        txn.nonce = Some(rlp.at(*offset)?.as_val()?);
+        *offset += 1;
+        txn.gas_price = Some(rlp.at(*offset)?.as_val()?);
+        *offset += 1;
+        txn.gas = Some(rlp.at(*offset)?.as_val()?);
+        *offset += 1;
+
+        #[cfg(feature = "celo")]
+        {
+            txn.fee_currency = Some(rlp.at(*offset)?.as_val()?);
+            *offset += 1;
+            txn.gateway_fee_recipient = Some(rlp.at(*offset)?.as_val()?);
+            *offset += 1;
+            txn.gateway_fee = Some(rlp.at(*offset)?.as_val()?);
+            *offset += 1;
+        }
+
+        txn.to = Some(rlp.at(*offset)?.as_val()?);
+        *offset += 1;
+        txn.value = Some(rlp.at(*offset)?.as_val()?);
+        *offset += 1;
+
+        // finally we need to extract the data which will be encoded as another rlp
+        let txndata = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        txn.data = match txndata.len() {
+            0 => None,
+            _ => Some(Bytes::from(txndata.to_vec())),
+        };
+        *offset += 1;
+        Ok(txn)
+    }
+
+    /// Decodes RLP into a transaction.
+    pub fn decode_unsigned_rlp(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        let mut offset = 0;
+        let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?;
+
+        // If a signed transaction is passed to this method, the chainid would be set to the v value
+        // of the signature.
+        if let Ok(chainid) = rlp.at(offset)?.as_val() {
+            txn.chain_id = Some(chainid);
+        }
+
+        // parse the last two elements so we return an error if a signed transaction is passed
+        let _first_zero: u8 = rlp.at(offset + 1)?.as_val()?;
+        let _second_zero: u8 = rlp.at(offset + 2)?.as_val()?;
+        Ok(txn)
+    }
+
+    /// Decodes the given RLP into a transaction, attempting to decode its signature as well.
+    pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), rlp::DecoderError> {
+        let mut offset = 0;
+        let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?;
+
+        let v = rlp.at(offset)?.as_val()?;
+        // populate chainid from v
+        txn.chain_id = extract_chain_id(v);
+        offset += 1;
+        let r = rlp.at(offset)?.as_val()?;
+        offset += 1;
+        let s = rlp.at(offset)?.as_val()?;
+
+        let sig = Signature { r, s, v };
+        Ok((txn, sig))
+    }
+}
+
+impl Decodable for TransactionRequest {
+    /// Decodes the given RLP into a transaction request, ignoring the signature if populated
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
+        TransactionRequest::decode_unsigned_rlp(rlp)
+    }
 }
 
 // Separate impl block for the celo-specific fields
@@ -207,8 +311,46 @@ impl TransactionRequest {
 #[cfg(test)]
 #[cfg(not(feature = "celo"))]
 mod tests {
-    use super::*;
+    use crate::types::Signature;
+    use rlp::{Decodable, Rlp};
+
+    use super::{Address, TransactionRequest, U256, U64};
 
+    #[test]
+    fn encode_decode_rlp() {
+        let tx = TransactionRequest::new()
+            .nonce(3)
+            .gas_price(1)
+            .gas(25000)
+            .to("b94f5374fce5edbc8e2a8697c15331677e6ebf0b".parse::<Address>().unwrap())
+            .value(10)
+            .data(vec![0x55, 0x44])
+            .chain_id(1);
+
+        // turn the rlp bytes encoding into a rlp stream and check that the decoding returns the
+        // same struct
+        let rlp_bytes = &tx.rlp().to_vec()[..];
+        let got_rlp = Rlp::new(rlp_bytes);
+        let txn_request = TransactionRequest::decode(&got_rlp).unwrap();
+
+        // We compare the sighash rather than the specific struct
+        assert_eq!(tx.sighash(), txn_request.sighash());
+    }
+
+    #[test]
+    // test data from https://github.com/ethereum/go-ethereum/blob/b1e72f7ea998ad662166bcf23705ca59cf81e925/core/types/transaction_test.go#L40
+    fn empty_sighash_check() {
+        let tx = TransactionRequest::new()
+            .nonce(0)
+            .to("095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::<Address>().unwrap())
+            .value(0)
+            .gas(0)
+            .gas_price(0);
+
+        let expected_sighash = "c775b99e7ad12f50d819fcd602390467e28141316969f4b57f0626f74fe3b386";
+        let got_sighash = hex::encode(tx.sighash().as_bytes());
+        assert_eq!(expected_sighash, got_sighash);
+    }
     #[test]
     fn decode_unsigned_transaction() {
         let _res: TransactionRequest = serde_json::from_str(
@@ -226,4 +368,81 @@ mod tests {
         )
         .unwrap();
     }
+
+    #[test]
+    fn decode_known_rlp_goerli() {
+        let tx = TransactionRequest::new()
+            .nonce(70272)
+            .from("fab2b4b677a4e104759d378ea25504862150256e".parse::<Address>().unwrap())
+            .to("d1f23226fb4d2b7d2f3bcdd99381b038de705a64".parse::<Address>().unwrap())
+            .value(0)
+            .gas_price(1940000007)
+            .gas(21000);
+
+        let expected_rlp = hex::decode("f866830112808473a20d0782520894d1f23226fb4d2b7d2f3bcdd99381b038de705a6480801ca04bc89d41c954168afb4cbd01fe2e0f9fe12e3aa4665eefcee8c4a208df044b5da05d410fd85a2e31870ea6d6af53fafc8e3c1ae1859717c863cac5cff40fee8da4").unwrap();
+        let (got_tx, _signature) =
+            TransactionRequest::decode_signed_rlp(&Rlp::new(&expected_rlp)).unwrap();
+
+        // intialization of TransactionRequests using new() uses the Default trait, so we just
+        // compare the sighash and signed encoding instead.
+        assert_eq!(got_tx.sighash(), tx.sighash());
+    }
+
+    #[test]
+    fn test_eip155_encode() {
+        let tx = TransactionRequest::new()
+            .nonce(9)
+            .to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
+            .value(1000000000000000000u64)
+            .gas_price(20000000000u64)
+            .gas(21000)
+            .chain_id(1);
+
+        let expected_rlp = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
+        assert_eq!(expected_rlp, tx.rlp().to_vec());
+
+        let expected_sighash =
+            hex::decode("daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53")
+                .unwrap();
+
+        assert_eq!(expected_sighash, tx.sighash().as_bytes().to_vec());
+    }
+
+    #[test]
+    fn test_eip155_decode() {
+        let tx = TransactionRequest::new()
+            .nonce(9)
+            .to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
+            .value(1000000000000000000u64)
+            .gas_price(20000000000u64)
+            .gas(21000)
+            .chain_id(1);
+
+        let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
+        let expected_rlp = rlp::Rlp::new(expected_hex.as_slice());
+        let decoded_transaction = TransactionRequest::decode(&expected_rlp).unwrap();
+        assert_eq!(tx, decoded_transaction);
+    }
+
+    #[test]
+    fn test_eip155_decode_signed() {
+        let expected_signed_bytes = hex::decode("f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83").unwrap();
+        let expected_signed_rlp = rlp::Rlp::new(expected_signed_bytes.as_slice());
+        let (decoded_tx, decoded_sig) =
+            TransactionRequest::decode_signed_rlp(&expected_signed_rlp).unwrap();
+
+        let expected_sig = Signature {
+            v: 37,
+            r: U256::from_dec_str(
+                "18515461264373351373200002665853028612451056578545711640558177340181847433846",
+            )
+            .unwrap(),
+            s: U256::from_dec_str(
+                "46948507304638947509940763649030358759909902576025900602547168820602576006531",
+            )
+            .unwrap(),
+        };
+        assert_eq!(expected_sig, decoded_sig);
+        assert_eq!(decoded_tx.chain_id, Some(U64::from(1)));
+    }
 }
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index d5eb190b..a13f1f5a 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -1,10 +1,10 @@
 //! Transaction types
-use super::{eip2930::AccessList, normalize_v, rlp_opt};
+use super::{decode_signature, eip2930::AccessList, normalize_v, rlp_opt};
 use crate::{
     types::{Address, Bloom, Bytes, Log, H256, U256, U64},
     utils::keccak256,
 };
-use rlp::RlpStream;
+use rlp::{Decodable, DecoderError, RlpStream};
 use serde::{Deserialize, Serialize};
 
 /// Details of a signed transaction
@@ -201,6 +201,152 @@ impl Transaction {
             _ => rlp_bytes,
         }
     }
+
+    /// Decodes the Celo-specific metadata starting at the RLP offset passed.
+    /// Increments the offset for each element parsed.
+    #[cfg(feature = "celo")]
+    #[inline]
+    fn decode_celo_metadata(
+        &mut self,
+        rlp: &rlp::Rlp,
+        offset: &mut usize,
+    ) -> Result<(), DecoderError> {
+        self.fee_currency = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gateway_fee_recipient = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gateway_fee = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        Ok(())
+    }
+
+    /// Decodes fields of the type 2 transaction response starting at the RLP offset passed.
+    /// Increments the offset for each element parsed.
+    #[inline]
+    fn decode_base_eip1559(
+        &mut self,
+        rlp: &rlp::Rlp,
+        offset: &mut usize,
+    ) -> Result<(), DecoderError> {
+        self.chain_id = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.nonce = rlp.val_at(*offset)?;
+        *offset += 1;
+        self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.max_fee_per_gas = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gas = rlp.val_at(*offset)?;
+        *offset += 1;
+        self.to = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.value = rlp.val_at(*offset)?;
+        *offset += 1;
+        let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        self.input = Bytes::from(input.to_vec());
+        *offset += 1;
+        self.access_list = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        Ok(())
+    }
+
+    /// Decodes fields of the type 1 transaction response based on the RLP offset passed.
+    /// Increments the offset for each element parsed.
+    fn decode_base_eip2930(
+        &mut self,
+        rlp: &rlp::Rlp,
+        offset: &mut usize,
+    ) -> Result<(), DecoderError> {
+        self.chain_id = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.nonce = rlp.val_at(*offset)?;
+        *offset += 1;
+        self.gas_price = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gas = rlp.val_at(*offset)?;
+        *offset += 1;
+
+        #[cfg(feature = "celo")]
+        self.decode_celo_metadata(rlp, offset)?;
+
+        self.gas = rlp.val_at(*offset)?;
+        *offset += 1;
+        self.to = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.value = rlp.val_at(*offset)?;
+        *offset += 1;
+        let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        self.input = Bytes::from(input.to_vec());
+        *offset += 1;
+        self.access_list = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+
+        Ok(())
+    }
+
+    /// Decodes a legacy transaction starting at the RLP offset passed.
+    /// Increments the offset for each element parsed.
+    #[inline]
+    fn decode_base_legacy(
+        &mut self,
+        rlp: &rlp::Rlp,
+        offset: &mut usize,
+    ) -> Result<(), DecoderError> {
+        println!("are we a list {}", rlp.is_list());
+        self.nonce = rlp.val_at(*offset)?;
+        *offset += 1;
+        self.gas_price = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.gas = rlp.val_at(*offset)?;
+        *offset += 1;
+
+        #[cfg(feature = "celo")]
+        self.decode_celo_metadata(rlp, offset)?;
+
+        self.to = Some(rlp.val_at(*offset)?);
+        *offset += 1;
+        self.value = rlp.val_at(*offset)?;
+        *offset += 1;
+        let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
+        self.input = Bytes::from(input.to_vec());
+        *offset += 1;
+        Ok(())
+    }
+}
+
+/// Get a TransactionReceipt directly from an rlp encoded byte stream
+impl Decodable for Transaction {
+    fn decode(rlp: &rlp::Rlp) -> Result<Self, DecoderError> {
+        let mut txn = Self::default();
+        // we can get the type from the first value
+        let mut offset = 0;
+        txn.transaction_type = Some(rlp.data().unwrap().into());
+        let rest = rlp::Rlp::new(
+            rlp.as_raw().get(1..).ok_or(DecoderError::Custom("no transaction payload"))?,
+        );
+
+        match txn.transaction_type {
+            Some(x) if x == U64::from(1) => {
+                // EIP-2930 (0x01)
+                txn.decode_base_eip2930(&rest, &mut offset)?;
+            }
+            Some(x) if x == U64::from(2) => {
+                // EIP-1559 (0x02)
+                txn.decode_base_eip1559(&rest, &mut offset)?;
+            }
+            _ => {
+                // Legacy (0x00)
+                txn.decode_base_legacy(&rest, &mut offset)?;
+            }
+        }
+
+        let sig = decode_signature(&rest, &mut offset)?;
+        txn.r = sig.r;
+        txn.s = sig.s;
+        txn.v = sig.v.into();
+
+        Ok(txn)
+    }
 }
 
 /// "Receipt" of an executed transaction: details of its execution.
@@ -401,4 +547,94 @@ mod tests {
             )
         );
     }
+
+    #[test]
+    fn rlp_london_goerli() {
+        let tx = Transaction {
+            hash: H256::from_str(
+                "5e2fc091e15119c97722e9b63d5d32b043d077d834f377b91f80d32872c78109",
+            )
+            .unwrap(),
+            nonce: 65.into(),
+            block_hash: Some(
+                H256::from_str("f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c")
+                    .unwrap(),
+            ),
+            block_number: Some(6203173.into()),
+            transaction_index: Some(10.into()),
+            from: Address::from_str("e66b278fa9fbb181522f6916ec2f6d66ab846e04").unwrap(),
+            to: Some(Address::from_str("11d7c2ab0d4aa26b7d8502f6a7ef6844908495c2").unwrap()),
+            value: 0.into(),
+            gas_price: Some(1500000007.into()),
+            gas: 106703.into(),
+            input: hex::decode("e5225381").unwrap().into(),
+            v: 1.into(),
+            r: U256::from_str_radix(
+                "12010114865104992543118914714169554862963471200433926679648874237672573604889",
+                10,
+            )
+            .unwrap(),
+            s: U256::from_str_radix(
+                "22830728216401371437656932733690354795366167672037272747970692473382669718804",
+                10,
+            )
+            .unwrap(),
+            transaction_type: Some(2.into()),
+            access_list: Some(AccessList::default()),
+            max_priority_fee_per_gas: Some(1500000000.into()),
+            max_fee_per_gas: Some(1500000009.into()),
+            chain_id: Some(5.into()),
+        };
+        assert_eq!(
+            tx.rlp(),
+            Bytes::from(
+                hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap()
+            )
+        );
+    }
+
+    #[test]
+    fn decode_rlp_london_goerli() {
+        let tx = Transaction {
+            hash: H256::from_str(
+                "5e2fc091e15119c97722e9b63d5d32b043d077d834f377b91f80d32872c78109",
+            )
+            .unwrap(),
+            nonce: 65.into(),
+            block_hash: Some(
+                H256::from_str("f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c")
+                    .unwrap(),
+            ),
+            block_number: Some(6203173.into()),
+            transaction_index: Some(10.into()),
+            from: Address::from_str("e66b278fa9fbb181522f6916ec2f6d66ab846e04").unwrap(),
+            to: Some(Address::from_str("11d7c2ab0d4aa26b7d8502f6a7ef6844908495c2").unwrap()),
+            value: 0.into(),
+            gas_price: Some(1500000007.into()),
+            gas: 106703.into(),
+            input: hex::decode("e5225381").unwrap().into(),
+            v: 1.into(),
+            r: U256::from_str_radix(
+                "12010114865104992543118914714169554862963471200433926679648874237672573604889",
+                10,
+            )
+            .unwrap(),
+            s: U256::from_str_radix(
+                "22830728216401371437656932733690354795366167672037272747970692473382669718804",
+                10,
+            )
+            .unwrap(),
+            transaction_type: Some(2.into()),
+            access_list: Some(AccessList::default()),
+            max_priority_fee_per_gas: Some(1500000000.into()),
+            max_fee_per_gas: Some(1500000009.into()),
+            chain_id: Some(5.into()),
+        };
+
+        let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap();
+        let decoded_transaction = Transaction::decode(&rlp::Rlp::new(&rlp_bytes)).unwrap();
+
+        // we compare hash because the hash depends on the rlp encoding
+        assert_eq!(decoded_transaction.hash(), tx.hash());
+    }
 }
diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs
index 3c4fb1d1..dfea1017 100644
--- a/ethers-core/src/utils/ganache.rs
+++ b/ethers-core/src/utils/ganache.rs
@@ -183,7 +183,7 @@ impl Ganache {
 
             let mut line = String::new();
             reader.read_line(&mut line).expect("Failed to read line from ganache process");
-            if line.starts_with("Listening on") {
+            if line.contains("Listening on") {
                 break
             }
 
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index c5db9d48..cbbc8f9b 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -25,11 +25,11 @@ pub use rlp;
 /// Re-export hex
 pub use hex;
 
-use crate::types::{Address, Bytes, U256};
+use crate::types::{Address, Bytes, I256, U256};
 use elliptic_curve::sec1::ToEncodedPoint;
 use ethabi::ethereum_types::FromDecStrErr;
 use k256::{ecdsa::SigningKey, PublicKey as K256PublicKey};
-use std::{convert::TryInto, ops::Neg};
+use std::convert::{TryFrom, TryInto};
 use thiserror::Error;
 
 #[derive(Error, Debug)]
@@ -335,15 +335,13 @@ fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
     let mut rewards_copy = rewards.clone();
     rewards_copy.rotate_left(1);
 
-    let mut percentage_change: Vec<i64> = rewards
+    let mut percentage_change: Vec<I256> = rewards
         .iter()
         .zip(rewards_copy.iter())
         .map(|(a, b)| {
-            if b > a {
-                ((b - a).low_u32() as i64 * 100) / (a.low_u32() as i64)
-            } else {
-                (((a - b).low_u32() as i64 * 100) / (a.low_u32() as i64)).neg()
-            }
+            let a = I256::try_from(*a).expect("priority fee overflow");
+            let b = I256::try_from(*b).expect("priority fee overflow");
+            ((b - a) * 100.into()) / a
         })
         .collect();
     percentage_change.pop();
@@ -354,7 +352,7 @@ fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
 
     // If we encountered a big change in fees at a certain position, then consider only
     // the values >= it.
-    let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE &&
+    let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE.into() &&
         (max_change_index >= (rewards.len() / 2))
     {
         rewards[max_change_index..].to_vec()
@@ -682,5 +680,11 @@ mod tests {
 
         // The median should be taken because none of the changes are big enough to ignore values.
         assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into());
+
+        // Ensure fee estimation doesn't panic when overflowing a u32. This had been a divide by
+        // zero.
+        let overflow = U256::from(u32::MAX) + 1;
+        let rewards_overflow: Vec<Vec<U256>> = vec![vec![overflow], vec![overflow]];
+        assert_eq!(estimate_priority_fee(rewards_overflow), overflow);
     }
 }
diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index e1f6efa2..aa6880bf 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -15,6 +15,7 @@ keywords = ["ethereum", "web3", "etherscan", "ethers"]
 
 [dependencies]
 ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false }
+ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false }
 reqwest = { version = "0.11.9", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-etherscan/resources/IERC20.sol b/ethers-etherscan/resources/IERC20.sol
new file mode 100644
index 00000000..7d38ff38
--- /dev/null
+++ b/ethers-etherscan/resources/IERC20.sol
@@ -0,0 +1,15 @@
+interface IERC20 {
+    function totalSupply() external view returns(uint);
+
+    function balanceOf(address account) external view returns(uint);
+
+    function transfer(address recipient, uint amount) external returns(bool);
+
+    function allowance(address owner, address spender) external view returns(uint);
+
+    function approve(address spender, uint amount) external returns(bool);
+
+    function transferFrom(address sender, address recipient, uint amount) external returns(bool);
+    event Transfer(address indexed from, address indexed to, uint value);
+    event Approval(address indexed owner, address indexed spender, uint value);
+}
diff --git a/ethers-etherscan/resources/UniswapExchange.sol b/ethers-etherscan/resources/UniswapExchange.sol
index 4b8ac067..dd13149e 100644
--- a/ethers-etherscan/resources/UniswapExchange.sol
+++ b/ethers-etherscan/resources/UniswapExchange.sol
@@ -1,23 +1,10 @@
 /**
- *Submitted for verification at Etherscan.io on 2021-10-03
-*/
+ * Submitted for verification at Etherscan.io on 2021-10-03
+ */
 
 pragma solidity ^0.5.17;
-interface IERC20 {
-    function totalSupply() external view returns(uint);
 
-    function balanceOf(address account) external view returns(uint);
-
-    function transfer(address recipient, uint amount) external returns(bool);
-
-    function allowance(address owner, address spender) external view returns(uint);
-
-    function approve(address spender, uint amount) external returns(bool);
-
-    function transferFrom(address sender, address recipient, uint amount) external returns(bool);
-    event Transfer(address indexed from, address indexed to, uint value);
-    event Approval(address indexed owner, address indexed spender, uint value);
-}
+import "./IERC20.sol";
 
 library Address {
     function isContract(address account) internal view returns(bool) {
diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs
index 45eab419..a7cb2186 100644
--- a/ethers-etherscan/src/contract.rs
+++ b/ethers-etherscan/src/contract.rs
@@ -201,7 +201,7 @@ impl Client {
         contract: &VerifyContract,
     ) -> Result<Response<String>> {
         let body = self.create_query("contract", "verifysourcecode", contract);
-        Ok(self.post_form(&body).await?)
+        self.post_form(&body).await
     }
 
     /// Check Source Code Verification Status with receipt received from
@@ -215,7 +215,7 @@ impl Client {
             "checkverifystatus",
             HashMap::from([("guid", guid.as_ref())]),
         );
-        Ok(self.post_form(&body).await?)
+        self.post_form(&body).await
     }
 
     /// Returns the contract ABI of a verified contract
@@ -262,11 +262,12 @@ impl Client {
 
 #[cfg(test)]
 mod tests {
-    use std::time::Duration;
+    use std::{path::PathBuf, time::Duration};
 
     use serial_test::serial;
 
     use ethers_core::types::Chain;
+    use ethers_solc::{MinimalCombinedArtifacts, Project, ProjectPathsConfig};
 
     use crate::{contract::VerifyContract, tests::run_at_least_duration, Client};
 
@@ -302,26 +303,32 @@ mod tests {
 
     #[tokio::test]
     #[serial]
-    #[ignore]
-    async fn can_verify_contract() {
+    async fn can_flatten_and_verify_contract() {
         run_at_least_duration(Duration::from_millis(250), async {
-            // TODO this needs further investigation
+            let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
+            let paths = ProjectPathsConfig::builder()
+                .sources(&root)
+                .build()
+                .expect("failed to resolve project paths");
+            let project = Project::<MinimalCombinedArtifacts>::builder()
+                .paths(paths)
+                .build()
+                .expect("failed to build the project");
 
-            // https://etherscan.io/address/0x9e744c9115b74834c0f33f4097f40c02a9ac5c33#code
-            let contract = include_str!("../resources/UniswapExchange.sol");
             let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33".parse().unwrap();
             let compiler_version = "v0.5.17+commit.d19bba13";
             let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000";
+            let contract = project.flatten(&root.join("UniswapExchange.sol")).expect("failed to flatten contract");
 
             let client = Client::new_from_env(Chain::Mainnet).unwrap();
 
             let contract =
-                VerifyContract::new(address, contract.to_string(), compiler_version.to_string())
+                VerifyContract::new(address, contract, compiler_version.to_string())
                     .constructor_arguments(Some(constructor_args))
                     .optimization(true)
                     .runs(200);
-
             let _resp = client.submit_contract_verification(&contract).await;
-        }).await
+        })
+        .await
     }
 }
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index 341b855b..2eaa1702 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -73,6 +73,13 @@ impl Client {
                 Url::parse("https://api-testnet.ftmscan.com"),
                 Url::parse("https://testnet.ftmscan.com"),
             ),
+            Chain::BinanceSmartChain => {
+                (Url::parse("https://api.bscscan.com/api"), Url::parse("https://bscscan.com"))
+            }
+            Chain::BinanceSmartChainTestnet => (
+                Url::parse("https://api-testnet.bscscan.com/api"),
+                Url::parse("https://testnet.bscscan.com"),
+            ),
             chain => return Err(EtherscanError::ChainNotSupported(chain)),
         };
 
@@ -98,7 +105,9 @@ impl Client {
             Chain::Optimism |
             Chain::OptimismKovan |
             Chain::Fantom |
-            Chain::FantomTestnet => std::env::var("ETHERSCAN_API_KEY")?,
+            Chain::FantomTestnet |
+            Chain::BinanceSmartChain |
+            Chain::BinanceSmartChainTestnet => std::env::var("ETHERSCAN_API_KEY")?,
 
             Chain::XDai | Chain::Sepolia => String::default(),
             Chain::Moonbeam | Chain::MoonbeamDev | Chain::Moonriver => {
diff --git a/ethers-middleware/src/nonce_manager.rs b/ethers-middleware/src/nonce_manager.rs
index 1ff15405..f9e221a0 100644
--- a/ethers-middleware/src/nonce_manager.rs
+++ b/ethers-middleware/src/nonce_manager.rs
@@ -30,6 +30,24 @@ where
         nonce.into()
     }
 
+    pub async fn initialize_nonce(
+        &self,
+        block: Option<BlockId>,
+    ) -> Result<U256, NonceManagerError<M>> {
+        // initialize the nonce the first time the manager is called
+        if !self.initialized.load(Ordering::SeqCst) {
+            let nonce = self
+                .inner
+                .get_transaction_count(self.address, block)
+                .await
+                .map_err(FromErr::from)?;
+            self.nonce.store(nonce.as_u64(), Ordering::SeqCst);
+            self.initialized.store(true, Ordering::SeqCst);
+        }
+        // return current nonce
+        Ok(self.nonce.load(Ordering::SeqCst).into())
+    }
+
     async fn get_transaction_count_with_manager(
         &self,
         block: Option<BlockId>,
diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs
index 7234e94f..542df8ca 100644
--- a/ethers-middleware/src/signer.rs
+++ b/ethers-middleware/src/signer.rs
@@ -94,6 +94,9 @@ pub enum SignerMiddlewareError<M: Middleware, S: Signer> {
     /// Thrown if a signature is requested from a different address
     #[error("specified from address is not signer")]
     WrongSigner,
+    /// Thrown if the signer's chain_id is different than the chain_id of the transaction
+    #[error("specified chain_id is different than the signer's chain_id")]
+    DifferentChainID,
 }
 
 // Helper functions for locally signing transactions
@@ -113,11 +116,25 @@ where
         &self,
         tx: TypedTransaction,
     ) -> Result<Bytes, SignerMiddlewareError<M, S>> {
+        // compare chain_id and use signer's chain_id if the tranasaction's chain_id is None,
+        // return an error if they are not consistent
+        let chain_id = self.signer.chain_id();
+        match tx.chain_id() {
+            Some(id) if id.as_u64() != chain_id => {
+                return Err(SignerMiddlewareError::DifferentChainID)
+            }
+            None => {
+                let mut tx = tx.clone();
+                tx.set_chain_id(chain_id);
+            }
+            _ => {}
+        }
+
         let signature =
             self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?;
 
         // Return the raw rlp-encoded signed transaction
-        Ok(tx.rlp_signed(self.signer.chain_id(), &signature))
+        Ok(tx.rlp_signed(&signature))
     }
 
     /// Returns the client's address
@@ -190,6 +207,12 @@ where
         };
         tx.set_from(from);
 
+        // get the signer's chain_id if the transaction does not set it
+        let chain_id = self.signer.chain_id();
+        if tx.chain_id().is_none() {
+            tx.set_chain_id(chain_id);
+        }
+
         let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
         tx.set_nonce(nonce);
         self.inner()
@@ -266,6 +289,7 @@ mod tests {
             nonce: Some(0.into()),
             gas_price: Some(21_000_000_000u128.into()),
             data: None,
+            chain_id: None,
         }
         .into();
         let chain_id = 1u64;
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index b68d0652..1da8e8fa 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -24,6 +24,8 @@ serde_json = { version = "1.0.64", default-features = false }
 thiserror = { version = "1.0.30", default-features = false }
 url = { version = "2.2.2", default-features = false }
 auto_impl = { version = "0.5.0", default-features = false }
+http = { version = "0.2" }
+base64 = "0.13"
 
 # required for implementing stream on the filters
 futures-core = { version = "0.3.16", default-features = false }
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index 696fa8c4..632d1a43 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -79,6 +79,15 @@ where
     }
 }
 
+/// Structure used in eth_syncing RPC
+#[derive(Clone, Debug)]
+pub enum SyncingStatus {
+    /// When client is synced to highest block, eth_syncing with return string "false"
+    IsFalse,
+    /// When client is still syncing past blocks we get IsSyncing information.
+    IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
+}
+
 /// A middleware allows customizing requests send and received from an ethereum node.
 ///
 /// Writing a middleware is as simple as:
@@ -168,6 +177,7 @@ pub trait Middleware: Sync + Send + Debug {
     /// 4. Estimate gas usage _with_ access lists
     /// 5. Enable access lists IFF they are cheaper
     /// 6. Poll and set legacy or 1559 gas prices
+    /// 7. Set the chain_id with the provider's, if not already set
     ///
     /// It does NOT set the nonce by default.
     /// It MAY override the gas amount set by the user, if access lists are
@@ -223,7 +233,6 @@ pub trait Middleware: Sync + Send + Debug {
         }
 
         let gas_price = original.gas_price().expect("filled");
-        let chain_id = self.get_chainid().await?.low_u64();
         let sign_futs: Vec<_> = (0..escalations)
             .map(|i| {
                 let new_price = policy(gas_price, i);
@@ -234,7 +243,7 @@ pub trait Middleware: Sync + Send + Debug {
             .map(|req| async move {
                 self.sign_transaction(&req, self.default_sender().unwrap_or_default())
                     .await
-                    .map(|sig| req.rlp_signed(chain_id, &sig))
+                    .map(|sig| req.rlp_signed(&sig))
             })
             .collect();
 
@@ -303,6 +312,10 @@ pub trait Middleware: Sync + Send + Debug {
         self.inner().call(tx, block).await.map_err(FromErr::from)
     }
 
+    async fn syncing(&self) -> Result<SyncingStatus, Self::Error> {
+        self.inner().syncing().await.map_err(FromErr::from)
+    }
+
     async fn get_chainid(&self) -> Result<U256, Self::Error> {
         self.inner().get_chainid().await.map_err(FromErr::from)
     }
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index d5a9b23c..ec32b303 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -3,7 +3,7 @@ use crate::{
     pubsub::{PubsubClient, SubscriptionStream},
     stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
     FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, MockProvider,
-    PendingTransaction, QuorumProvider,
+    PendingTransaction, QuorumProvider, SyncingStatus,
 };
 
 #[cfg(feature = "celo")]
@@ -23,7 +23,7 @@ use ethers_core::{
     utils,
 };
 use hex::FromHex;
-use serde::{de::DeserializeOwned, Serialize};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use thiserror::Error;
 use url::{ParseError, Url};
 
@@ -497,6 +497,30 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
         self.request("eth_chainId", ()).await
     }
 
+    /// Return current client syncing status. If IsFalse sync is over.
+    async fn syncing(&self) -> Result<SyncingStatus, Self::Error> {
+        #[derive(Debug, Serialize, Deserialize)]
+        #[serde(untagged)]
+        pub enum SyncingStatusIntermediate {
+            /// When client is synced to highest block, eth_syncing with return string "false"
+            IsFalse(bool),
+            /// When client is still syncing past blocks we get IsSyncing information.
+            IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
+        }
+        let intermediate: SyncingStatusIntermediate = self.request("eth_syncing", ()).await?;
+        match intermediate {
+            SyncingStatusIntermediate::IsFalse(false) => Ok(SyncingStatus::IsFalse),
+            SyncingStatusIntermediate::IsFalse(true) => Err(ProviderError::CustomError(
+                "eth_syncing returned `true` that is undefined value.".to_owned(),
+            )),
+            SyncingStatusIntermediate::IsSyncing {
+                starting_block,
+                current_block,
+                highest_block,
+            } => Ok(SyncingStatus::IsSyncing { starting_block, current_block, highest_block }),
+        }
+    }
+
     /// Returns the network version.
     async fn get_net_version(&self) -> Result<U64, ProviderError> {
         self.request("net_version", ()).await
@@ -1109,7 +1133,7 @@ impl TryFrom<String> for Provider<HttpProvider> {
 /// use ethers_core::utils::Ganache;
 /// use std::convert::TryFrom;
 ///
-/// # #[tokio::main]
+/// # #[tokio::main(flavor = "current_thread")]
 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let ganache = Ganache::new().spawn();
 /// let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
diff --git a/ethers-providers/src/transports/common.rs b/ethers-providers/src/transports/common.rs
index 29917b76..6162bde6 100644
--- a/ethers-providers/src/transports/common.rs
+++ b/ethers-providers/src/transports/common.rs
@@ -94,6 +94,35 @@ impl ResponseData<serde_json::Value> {
     }
 }
 
+/// Basic or bearer authentication in http or websocket transport
+///
+/// Use to inject username and password or an auth token into requests
+#[derive(Clone, Debug)]
+pub enum Authorization {
+    Basic(String),
+    Bearer(String),
+}
+
+impl Authorization {
+    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
+        let auth_secret = base64::encode(username.into() + ":" + &password.into());
+        Self::Basic(auth_secret)
+    }
+
+    pub fn bearer(token: impl Into<String>) -> Self {
+        Self::Bearer(token.into())
+    }
+}
+
+impl fmt::Display for Authorization {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Authorization::Basic(auth_secret) => write!(f, "Basic {}", auth_secret),
+            Authorization::Bearer(token) => write!(f, "Bearer {}", token),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/ethers-providers/src/transports/http.rs b/ethers-providers/src/transports/http.rs
index 3db8d2f2..87fc57f9 100644
--- a/ethers-providers/src/transports/http.rs
+++ b/ethers-providers/src/transports/http.rs
@@ -2,7 +2,7 @@
 use crate::{provider::ProviderError, JsonRpcClient};
 
 use async_trait::async_trait;
-use reqwest::{Client, Error as ReqwestError};
+use reqwest::{header::HeaderValue, Client, Error as ReqwestError};
 use serde::{de::DeserializeOwned, Serialize};
 use std::{
     str::FromStr,
@@ -11,7 +11,7 @@ use std::{
 use thiserror::Error;
 use url::Url;
 
-use super::common::{JsonRpcError, Request, Response};
+use super::common::{Authorization, JsonRpcError, Request, Response};
 
 /// A low-level JSON-RPC Client over HTTP.
 ///
@@ -69,7 +69,6 @@ impl JsonRpcClient for Provider {
         params: T,
     ) -> Result<R, ClientError> {
         let next_id = self.id.fetch_add(1, Ordering::SeqCst);
-
         let payload = Request::new(next_id, method, params);
 
         let res = self.client.post(self.url.as_ref()).json(&payload).send().await?;
@@ -94,7 +93,49 @@ impl Provider {
     /// let provider = Http::new(url);
     /// ```
     pub fn new(url: impl Into<Url>) -> Self {
-        Self { id: AtomicU64::new(0), client: Client::new(), url: url.into() }
+        Self::new_with_client(url, Client::new())
+    }
+
+    /// Initializes a new HTTP Client with authentication
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_providers::{Authorization, Http};
+    /// use url::Url;
+    ///
+    /// let url = Url::parse("http://localhost:8545").unwrap();
+    /// let provider = Http::new_with_auth(url, Authorization::basic("admin", "good_password"));
+    /// ```
+    pub fn new_with_auth(
+        url: impl Into<Url>,
+        auth: Authorization,
+    ) -> Result<Self, HttpClientError> {
+        let mut auth_value = HeaderValue::from_str(&auth.to_string())?;
+        auth_value.set_sensitive(true);
+
+        let mut headers = reqwest::header::HeaderMap::new();
+        headers.insert(reqwest::header::AUTHORIZATION, auth_value);
+
+        let client = Client::builder().default_headers(headers).build()?;
+
+        Ok(Self::new_with_client(url, client))
+    }
+
+    /// Allows to customize the provider by providing your own http client
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_providers::Http;
+    /// use url::Url;
+    ///
+    /// let url = Url::parse("http://localhost:8545").unwrap();
+    /// let client = reqwest::Client::builder().build().unwrap();
+    /// let provider = Http::new_with_client(url, client);
+    /// ```
+    pub fn new_with_client(url: impl Into<Url>, client: reqwest::Client) -> Self {
+        Self { id: AtomicU64::new(0), client, url: url.into() }
     }
 }
 
@@ -112,3 +153,15 @@ impl Clone for Provider {
         Self { id: AtomicU64::new(0), client: self.client.clone(), url: self.url.clone() }
     }
 }
+
+#[derive(Error, Debug)]
+/// Error thrown when dealing with Http clients
+pub enum HttpClientError {
+    /// Thrown if unable to build headers for client
+    #[error(transparent)]
+    InvalidHeader(#[from] http::header::InvalidHeaderValue),
+
+    /// Thrown if unable to build client
+    #[error(transparent)]
+    ClientBuild(#[from] reqwest::Error),
+}
diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs
index 1c714eac..b7a6c64e 100644
--- a/ethers-providers/src/transports/mod.rs
+++ b/ethers-providers/src/transports/mod.rs
@@ -1,4 +1,5 @@
 mod common;
+pub use common::Authorization;
 
 // only used with WS
 #[cfg(feature = "ws")]
@@ -24,7 +25,7 @@ mod ipc;
 pub use ipc::Ipc;
 
 mod http;
-pub use http::{ClientError as HttpClientError, Provider as Http};
+pub use self::http::{ClientError as HttpClientError, Provider as Http};
 
 #[cfg(feature = "ws")]
 mod ws;
diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs
index 44df77d1..fc1ae882 100644
--- a/ethers-providers/src/transports/ws.rs
+++ b/ethers-providers/src/transports/ws.rs
@@ -59,7 +59,11 @@ if_not_wasm! {
     type Message = tungstenite::protocol::Message;
     type WsError = tungstenite::Error;
     type WsStreamItem = Result<Message, WsError>;
+    use super::Authorization;
     use tracing::{debug, error, warn};
+    use http::Request as HttpRequest;
+    use http::Uri;
+    use std::str::FromStr;
 }
 
 type Pending = oneshot::Sender<Result<serde_json::Value, JsonRpcError>>;
@@ -140,6 +144,22 @@ impl Ws {
         Ok(Self::new(ws))
     }
 
+    /// Initializes a new WebSocket Client with authentication
+    #[cfg(not(target_arch = "wasm32"))]
+    pub async fn connect_with_auth(
+        uri: impl AsRef<str> + Unpin,
+        auth: Authorization,
+    ) -> Result<Self, ClientError> {
+        let mut request: HttpRequest<()> =
+            HttpRequest::builder().method("GET").uri(Uri::from_str(uri.as_ref())?).body(())?;
+
+        let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?;
+        auth_value.set_sensitive(true);
+
+        request.headers_mut().insert(http::header::AUTHORIZATION, auth_value);
+        Self::connect(request).await
+    }
+
     fn send(&self, msg: Instruction) -> Result<(), ClientError> {
         self.instructions.unbounded_send(msg).map_err(to_client_error)
     }
@@ -442,6 +462,21 @@ pub enum ClientError {
     /// Something caused the websocket to close
     #[error("WebSocket connection closed unexpectedly")]
     UnexpectedClose,
+
+    /// Could not create an auth header for websocket handshake
+    #[error(transparent)]
+    #[cfg(not(target_arch = "wasm32"))]
+    WsAuth(#[from] http::header::InvalidHeaderValue),
+
+    /// Unable to create a valid Uri
+    #[error(transparent)]
+    #[cfg(not(target_arch = "wasm32"))]
+    UriError(#[from] http::uri::InvalidUri),
+
+    /// Unable to create a valid Request
+    #[error(transparent)]
+    #[cfg(not(target_arch = "wasm32"))]
+    RequestError(#[from] http::Error),
 }
 
 impl From<ClientError> for ProviderError {
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index fa52aeb7..b34bf8e2 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -21,7 +21,7 @@ coins-bip39 = "0.6.0"
 coins-ledger = { version = "0.6.0", default-features = false, optional = true }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
 async-trait = { version = "0.1.50", default-features = false }
-elliptic-curve = { version = "0.11.9", default-features = false }
+elliptic-curve = { version = "0.11.12", default-features = false }
 sha2 = { version = "0.9.8", default-features = false }
 rand = { version = "0.8.4", default-features = false }
 yubihsm = { version = "0.40.0", features = ["secp256k1", "http", "usb"], optional = true }
@@ -45,7 +45,7 @@ home = "0.5.3"
 ethers-contract = { version = "^0.6.0", path = "../ethers-contract", features = ["eip712", "abigen"]}
 ethers-derive-eip712 = { version = "0.2.0", path = "../ethers-core/ethers-derive-eip712" }
 serde_json = { version = "1.0.64" }
-tracing-subscriber = "0.3.6"
+tracing-subscriber = "0.3.7"
 yubihsm = { version = "0.40.0", features = ["secp256k1", "usb", "mockhsm"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs
index abf33a70..798cb2db 100644
--- a/ethers-signers/src/aws/mod.rs
+++ b/ethers-signers/src/aws/mod.rs
@@ -177,7 +177,7 @@ impl<'a> AwsSigner<'a> {
     where
         T: AsRef<str>,
     {
-        Ok(request_get_pubkey(self.kms, key_id).await.map(utils::decode_pubkey)??)
+        request_get_pubkey(self.kms, key_id).await.map(utils::decode_pubkey)?
     }
 
     /// Fetch the pubkey associated with this signer's key ID
@@ -194,7 +194,7 @@ impl<'a> AwsSigner<'a> {
     where
         T: AsRef<str>,
     {
-        Ok(request_sign_digest(self.kms, key_id, digest).await.map(utils::decode_signature)??)
+        request_sign_digest(self.kms, key_id, digest).await.map(utils::decode_signature)?
     }
 
     /// Sign a digest with this signer's key
@@ -235,7 +235,7 @@ impl<'a> super::Signer for AwsSigner<'a> {
 
     #[instrument(err)]
     async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> {
-        let sighash = tx.sighash(self.chain_id);
+        let sighash = tx.sighash();
         self.sign_digest_with_eip155(sighash).await
     }
 
diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs
index 558d0995..346231fc 100644
--- a/ethers-signers/src/ledger/app.rs
+++ b/ethers-signers/src/ledger/app.rs
@@ -118,7 +118,7 @@ impl LedgerEthereum {
     /// Signs an Ethereum transaction (requires confirmation on the ledger)
     pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
         let mut payload = Self::path_to_bytes(&self.derivation);
-        payload.extend_from_slice(tx.rlp(self.chain_id).as_ref());
+        payload.extend_from_slice(tx.rlp().as_ref());
         self.sign_payload(INS::SIGN, payload).await
     }
 
diff --git a/ethers-signers/src/trezor/app.rs b/ethers-signers/src/trezor/app.rs
index 6282aac0..d67d6b22 100644
--- a/ethers-signers/src/trezor/app.rs
+++ b/ethers-signers/src/trezor/app.rs
@@ -134,7 +134,7 @@ impl TrezorEthereum {
 
     /// Get the account which corresponds to our derivation path
     pub async fn get_address(&self) -> Result<Address, TrezorError> {
-        Ok(self.get_address_with_path(&self.derivation).await?)
+        self.get_address_with_path(&self.derivation).await
     }
 
     /// Gets the account which corresponds to the provided derivation path
diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs
index 6e8dcf10..65e8e7d1 100644
--- a/ethers-signers/src/wallet/mod.rs
+++ b/ethers-signers/src/wallet/mod.rs
@@ -18,7 +18,7 @@ use ethers_core::{
     },
     types::{
         transaction::{eip2718::TypedTransaction, eip712::Eip712},
-        Address, Signature, H256, U256,
+        Address, Signature, H256, U256, U64,
     },
     utils::hash_message,
 };
@@ -83,7 +83,23 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
     }
 
     async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> {
-        Ok(self.sign_transaction_sync(tx))
+        let chain_id = tx.chain_id();
+        match chain_id {
+            Some(id) => {
+                if U64::from(self.chain_id) != id {
+                    return Err(WalletError::InvalidTransactionError(
+                        "transaction chain_id does not match the signer".to_string(),
+                    ))
+                }
+                Ok(self.sign_transaction_sync(tx))
+            }
+            None => {
+                // in the case we don't have a chain_id, let's use the signer chain id instead
+                let mut tx_with_chain = tx.clone();
+                tx_with_chain.set_chain_id(self.chain_id);
+                Ok(self.sign_transaction_sync(&tx_with_chain))
+            }
+        }
     }
 
     async fn sign_typed_data<T: Eip712 + Send + Sync>(
@@ -119,7 +135,7 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
 impl<D: DigestSigner<Sha256Proxy, RecoverableSignature>> Wallet<D> {
     /// Synchronously signs the provided transaction.
     pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Signature {
-        let sighash = tx.sighash(self.chain_id);
+        let sighash = tx.sighash();
         self.sign_hash(sighash, true)
     }
 
diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs
index a8b2e936..4998fd9c 100644
--- a/ethers-signers/src/wallet/private_key.rs
+++ b/ethers-signers/src/wallet/private_key.rs
@@ -46,6 +46,8 @@ pub enum WalletError {
     /// Error type from Eip712Error message
     #[error("error encoding eip712 struct: {0:?}")]
     Eip712Error(String),
+    #[error("invalid transaction: {0:?}")]
+    InvalidTransactionError(String),
 }
 
 impl Clone for Wallet<SigningKey> {
@@ -142,8 +144,8 @@ impl FromStr for Wallet<SigningKey> {
 #[cfg(not(target_arch = "wasm32"))]
 mod tests {
     use super::*;
-    use crate::Signer;
-    use ethers_core::types::Address;
+    use crate::{Signer, TypedTransaction};
+    use ethers_core::types::{Address, U64};
     use tempfile::tempdir;
 
     #[tokio::test]
@@ -195,7 +197,7 @@ mod tests {
         use ethers_core::types::TransactionRequest;
         // retrieved test vector from:
         // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
-        let tx = TransactionRequest {
+        let tx: TypedTransaction = TransactionRequest {
             from: None,
             to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
             value: Some(1_000_000_000.into()),
@@ -203,16 +205,15 @@ mod tests {
             nonce: Some(0.into()),
             gas_price: Some(21_000_000_000u128.into()),
             data: None,
+            chain_id: Some(U64::one()),
         }
         .into();
-        let chain_id = 1u64;
-
         let wallet: Wallet<SigningKey> =
             "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
-        let wallet = wallet.with_chain_id(chain_id);
+        let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64());
 
         let sig = wallet.sign_transaction(&tx).await.unwrap();
-        let sighash = tx.sighash(chain_id);
+        let sighash = tx.sighash();
         assert!(sig.verify(sighash, wallet.address).is_ok());
     }
 
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 042ae8aa..4371f8c8 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -50,7 +50,7 @@ getrandom = { version = "0.2", features = ["js"] }
 
 [dev-dependencies]
 criterion = { version = "0.3", features = ["async_tokio"] }
-pretty_assertions = "1.0.0"
+pretty_assertions = "1.1.0"
 tempfile = "3.3.0"
 tokio = { version = "1.15.0", features = ["full"] }
 
diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs
index e671b9fc..3df38461 100644
--- a/ethers-solc/src/artifacts.rs
+++ b/ethers-solc/src/artifacts.rs
@@ -752,6 +752,7 @@ impl<'a> fmt::Display for OutputDiagnostics<'a> {
 
 /// Represents a compiled solidity contract
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
 pub struct Contract {
     /// The Ethereum Contract Metadata.
     /// See https://docs.soliditylang.org/en/develop/metadata.html
@@ -764,7 +765,7 @@ pub struct Contract {
     pub devdoc: DevDoc,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub ir: Option<String>,
-    #[serde(default, rename = "storageLayout", skip_serializing_if = "StorageLayout::is_empty")]
+    #[serde(default, skip_serializing_if = "StorageLayout::is_empty")]
     pub storage_layout: StorageLayout,
     /// EVM-related outputs
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -772,6 +773,8 @@ pub struct Contract {
     /// Ewasm related outputs
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub ewasm: Option<Ewasm>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ir_optimized: Option<String>,
 }
 
 /// Minimal representation of a contract with a present abi and bytecode.
@@ -814,6 +817,21 @@ impl ContractBytecode {
             deployed_bytecode: self.deployed_bytecode.unwrap(),
         }
     }
+
+    /// Looks for all link references in deployment and runtime bytecodes
+    pub fn all_link_references(&self) -> BTreeMap<String, BTreeMap<String, Vec<Offsets>>> {
+        let mut links = BTreeMap::new();
+        if let Some(bcode) = &self.bytecode {
+            links.extend(bcode.link_references.clone());
+        }
+
+        if let Some(d_bcode) = &self.deployed_bytecode {
+            if let Some(bcode) = &d_bcode.bytecode {
+                links.extend(bcode.link_references.clone());
+            }
+        }
+        links
+    }
 }
 
 impl From<Contract> for ContractBytecode {
@@ -828,6 +846,80 @@ impl From<Contract> for ContractBytecode {
     }
 }
 
+/// Minimal representation of a contract with a present abi and bytecode.
+///
+/// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole
+/// `Bytecode` object.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct CompactContractBytecode {
+    /// 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>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bytecode: Option<CompactBytecode>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub deployed_bytecode: Option<CompactDeployedBytecode>,
+}
+
+impl CompactContractBytecode {
+    /// Looks for all link references in deployment and runtime bytecodes
+    pub fn all_link_references(&self) -> BTreeMap<String, BTreeMap<String, Vec<Offsets>>> {
+        let mut links = BTreeMap::new();
+        if let Some(bcode) = &self.bytecode {
+            links.extend(bcode.link_references.clone());
+        }
+
+        if let Some(d_bcode) = &self.deployed_bytecode {
+            if let Some(bcode) = &d_bcode.bytecode {
+                links.extend(bcode.link_references.clone());
+            }
+        }
+        links
+    }
+}
+
+impl From<Contract> for CompactContractBytecode {
+    fn from(c: Contract) -> Self {
+        let (bytecode, deployed_bytecode) = if let Some(evm) = c.evm {
+            let (maybe_bcode, maybe_runtime) = match (evm.bytecode, evm.deployed_bytecode) {
+                (Some(bcode), Some(dbcode)) => (Some(bcode.into()), Some(dbcode.into())),
+                (None, Some(dbcode)) => (None, Some(dbcode.into())),
+                (Some(bcode), None) => (Some(bcode.into()), None),
+                (None, None) => (None, None),
+            };
+            (maybe_bcode, maybe_runtime)
+        } else {
+            (None, None)
+        };
+
+        Self { abi: c.abi, bytecode, deployed_bytecode }
+    }
+}
+
+impl From<ContractBytecode> for CompactContractBytecode {
+    fn from(c: ContractBytecode) -> Self {
+        let (maybe_bcode, maybe_runtime) = match (c.bytecode, c.deployed_bytecode) {
+            (Some(bcode), Some(dbcode)) => (Some(bcode.into()), Some(dbcode.into())),
+            (None, Some(dbcode)) => (None, Some(dbcode.into())),
+            (Some(bcode), None) => (Some(bcode.into()), None),
+            (None, None) => (None, None),
+        };
+        Self { abi: c.abi, bytecode: maybe_bcode, deployed_bytecode: maybe_runtime }
+    }
+}
+
+impl From<CompactContractBytecode> for ContractBytecode {
+    fn from(c: CompactContractBytecode) -> Self {
+        let (maybe_bcode, maybe_runtime) = match (c.bytecode, c.deployed_bytecode) {
+            (Some(bcode), Some(dbcode)) => (Some(bcode.into()), Some(dbcode.into())),
+            (None, Some(dbcode)) => (None, Some(dbcode.into())),
+            (Some(bcode), None) => (Some(bcode.into()), None),
+            (None, None) => (None, None),
+        };
+        Self { abi: c.abi, bytecode: maybe_bcode, deployed_bytecode: maybe_runtime }
+    }
+}
+
 /// Minimal representation of a contract with a present abi and bytecode.
 ///
 /// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole
@@ -971,6 +1063,13 @@ impl From<ContractBytecode> for CompactContract {
     }
 }
 
+impl From<CompactContractBytecode> for CompactContract {
+    fn from(c: CompactContractBytecode) -> Self {
+        let c: ContractBytecode = c.into();
+        c.into()
+    }
+}
+
 impl From<ContractBytecodeSome> for CompactContract {
     fn from(c: ContractBytecodeSome) -> Self {
         Self {
@@ -1183,6 +1282,88 @@ pub struct Bytecode {
     pub link_references: BTreeMap<String, BTreeMap<String, Vec<Offsets>>>,
 }
 
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct CompactBytecode {
+    /// The bytecode as a hex string.
+    pub object: BytecodeObject,
+    /// The source mapping as a string. See the source mapping definition.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub source_map: Option<String>,
+    /// If given, this is an unlinked object.
+    #[serde(default)]
+    pub link_references: BTreeMap<String, BTreeMap<String, Vec<Offsets>>>,
+}
+
+impl CompactBytecode {
+    /// Tries to link the bytecode object with the `file` and `library` name.
+    /// Replaces all library placeholders with the given address.
+    ///
+    /// Returns true if the bytecode object is fully linked, false otherwise
+    /// This is a noop if the bytecode object is already fully linked.
+    pub fn link(
+        &mut self,
+        file: impl AsRef<str>,
+        library: impl AsRef<str>,
+        address: Address,
+    ) -> bool {
+        if !self.object.is_unlinked() {
+            return true
+        }
+
+        let file = file.as_ref();
+        let library = library.as_ref();
+        if let Some((key, mut contracts)) = self.link_references.remove_entry(file) {
+            if contracts.remove(library).is_some() {
+                self.object.link(file, library, address);
+            }
+            if !contracts.is_empty() {
+                self.link_references.insert(key, contracts);
+            }
+            if self.link_references.is_empty() {
+                return self.object.resolve().is_some()
+            }
+        }
+        false
+    }
+}
+
+impl From<Bytecode> for CompactBytecode {
+    fn from(bcode: Bytecode) -> CompactBytecode {
+        CompactBytecode {
+            object: bcode.object,
+            source_map: bcode.source_map,
+            link_references: bcode.link_references,
+        }
+    }
+}
+
+impl From<CompactBytecode> for Bytecode {
+    fn from(bcode: CompactBytecode) -> Bytecode {
+        Bytecode {
+            object: bcode.object,
+            source_map: bcode.source_map,
+            link_references: bcode.link_references,
+            function_debug_data: Default::default(),
+            opcodes: Default::default(),
+            generated_sources: Default::default(),
+        }
+    }
+}
+
+impl From<BytecodeObject> for Bytecode {
+    fn from(object: BytecodeObject) -> Bytecode {
+        Bytecode {
+            object,
+            function_debug_data: Default::default(),
+            opcodes: Default::default(),
+            source_map: Default::default(),
+            generated_sources: Default::default(),
+            link_references: Default::default(),
+        }
+    }
+}
+
 impl Bytecode {
     /// Returns the parsed source map
     ///
@@ -1453,6 +1634,43 @@ impl DeployedBytecode {
     }
 }
 
+impl From<Bytecode> for DeployedBytecode {
+    fn from(bcode: Bytecode) -> DeployedBytecode {
+        DeployedBytecode { bytecode: Some(bcode), immutable_references: Default::default() }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct CompactDeployedBytecode {
+    #[serde(flatten)]
+    pub bytecode: Option<CompactBytecode>,
+    #[serde(
+        default,
+        rename = "immutableReferences",
+        skip_serializing_if = "::std::collections::BTreeMap::is_empty"
+    )]
+    pub immutable_references: BTreeMap<String, Vec<Offsets>>,
+}
+
+impl From<DeployedBytecode> for CompactDeployedBytecode {
+    fn from(bcode: DeployedBytecode) -> CompactDeployedBytecode {
+        CompactDeployedBytecode {
+            bytecode: bcode.bytecode.map(|d_bcode| d_bcode.into()),
+            immutable_references: bcode.immutable_references,
+        }
+    }
+}
+
+impl From<CompactDeployedBytecode> for DeployedBytecode {
+    fn from(bcode: CompactDeployedBytecode) -> DeployedBytecode {
+        DeployedBytecode {
+            bytecode: bcode.bytecode.map(|d_bcode| d_bcode.into()),
+            immutable_references: bcode.immutable_references,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
 pub struct GasEstimates {
     pub creation: Creation,
@@ -1515,7 +1733,7 @@ pub struct Error {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub source_location: Option<SourceLocation>,
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
-    pub secondary_source_locations: Vec<SourceLocation>,
+    pub secondary_source_locations: Vec<SecondarySourceLocation>,
     pub r#type: String,
     pub component: String,
     pub severity: Severity,
@@ -1627,6 +1845,13 @@ pub struct SourceLocation {
     pub file: String,
     pub start: i32,
     pub end: i32,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct SecondarySourceLocation {
+    pub file: Option<String>,
+    pub start: Option<i32>,
+    pub end: Option<i32>,
     pub message: Option<String>,
 }
 
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index 027c1587..0fa81b8b 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -262,7 +262,7 @@ impl SolFilesCache {
     pub async fn async_write(&self, path: impl AsRef<Path>) -> Result<()> {
         let path = path.as_ref();
         let content = serde_json::to_vec_pretty(self)?;
-        Ok(tokio::fs::write(path, content).await.map_err(|err| SolcError::io(err, path))?)
+        tokio::fs::write(path, content).await.map_err(|err| SolcError::io(err, path))
     }
 }
 
diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs
index 0acbfc9d..5153591b 100644
--- a/ethers-solc/src/compile.rs
+++ b/ethers-solc/src/compile.rs
@@ -5,6 +5,7 @@ use crate::{
 };
 use semver::{Version, VersionReq};
 use serde::{de::DeserializeOwned, Serialize};
+
 use std::{
     fmt,
     fmt::Formatter,
@@ -47,6 +48,32 @@ use std::sync::Mutex;
 #[allow(unused)]
 static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
 
+#[cfg(all(feature = "svm", feature = "async"))]
+#[allow(clippy::large_enum_variant)]
+pub enum RuntimeOrHandle {
+    Runtime(tokio::runtime::Runtime),
+    Handle(tokio::runtime::Handle),
+}
+
+#[cfg(all(feature = "svm", feature = "async"))]
+impl Default for RuntimeOrHandle {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(all(feature = "svm", feature = "async"))]
+impl RuntimeOrHandle {
+    pub fn new() -> RuntimeOrHandle {
+        match tokio::runtime::Handle::try_current() {
+            Ok(handle) => RuntimeOrHandle::Handle(handle),
+            Err(_) => RuntimeOrHandle::Runtime(
+                tokio::runtime::Runtime::new().expect("Failed to start runtime"),
+            ),
+        }
+    }
+}
+
 /// take the lock in tests, we use this to enforce that
 /// a test does not run while a compiler version is being installed
 ///
@@ -65,17 +92,34 @@ pub(crate) fn take_solc_installer_lock() -> std::sync::MutexGuard<'static, ()> {
 /// The boolean value marks whether there was an error.
 pub static RELEASES: Lazy<(svm::Releases, Vec<Version>, bool)> = Lazy::new(|| {
     // Try to download the releases, if it fails default to empty
-    match tokio::runtime::Runtime::new()
-        .expect("could not create tokio rt to get remote releases")
+    let releases_result = match RuntimeOrHandle::new() {
+        RuntimeOrHandle::Runtime(runtime) =>
         // we do not degrade startup performance if the consumer has a weak network?
         // use a 3 sec timeout for the request which should still be fine for slower connections
-        .block_on(async {
-            tokio::time::timeout(
-                std::time::Duration::from_millis(3000),
-                svm::all_releases(svm::platform()),
-            )
-            .await
-        }) {
+        {
+            runtime.block_on(async {
+                tokio::time::timeout(
+                    std::time::Duration::from_millis(3000),
+                    svm::all_releases(svm::platform()),
+                )
+                .await
+            })
+        }
+        RuntimeOrHandle::Handle(handle) =>
+        // we do not degrade startup performance if the consumer has a weak network?
+        // use a 3 sec timeout for the request which should still be fine for slower connections
+        {
+            handle.block_on(async {
+                tokio::time::timeout(
+                    std::time::Duration::from_millis(3000),
+                    svm::all_releases(svm::platform()),
+                )
+                .await
+            })
+        }
+    };
+
+    match releases_result {
         Ok(Ok(releases)) => {
             let mut sorted_releases = releases.releases.keys().cloned().collect::<Vec<Version>>();
             sorted_releases.sort();
@@ -365,14 +409,19 @@ impl Solc {
     #[cfg(feature = "svm")]
     pub async fn install(version: &Version) -> std::result::Result<(), svm::SolcVmError> {
         tracing::trace!("installing solc version \"{}\"", version);
-        svm::install(version).await
+        println!("installing solc version \"{}\"", version);
+        let result = svm::install(version).await;
+        println!("installation complete");
+        result
     }
 
     /// Blocking version of `Self::install`
     #[cfg(all(feature = "svm", feature = "async"))]
     pub fn blocking_install(version: &Version) -> std::result::Result<(), svm::SolcVmError> {
         tracing::trace!("blocking installing solc version \"{}\"", version);
+        println!("installing solc version \"{}\"", version);
         tokio::runtime::Runtime::new().unwrap().block_on(svm::install(version))?;
+        println!("installation completed");
         Ok(())
     }
 
@@ -439,7 +488,6 @@ impl Solc {
 
     pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
         let mut cmd = Command::new(&self.solc);
-
         let mut child = cmd
             .args(&self.args)
             .arg("--standard-json")
@@ -449,7 +497,6 @@ impl Solc {
             .spawn()
             .map_err(|err| SolcError::io(err, &self.solc))?;
         let stdin = child.stdin.take().unwrap();
-
         serde_json::to_writer(stdin, input)?;
         compile_output(child.wait_with_output().map_err(|err| SolcError::io(err, &self.solc))?)
     }
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index ba2e26dd..80208792 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -1,5 +1,5 @@
 use crate::{
-    artifacts::{CompactContract, CompactContractRef, Contract, Settings},
+    artifacts::{CompactContract, CompactContractBytecode, Contract, Settings},
     cache::SOLIDITY_FILES_CACHE_FILENAME,
     error::{Result, SolcError, SolcIoError},
     hh::HardhatArtifact,
@@ -128,14 +128,69 @@ impl ProjectPathsConfig {
 
     /// Attempts to find the path to the real solidity file that's imported via the given `import`
     /// path by applying the configured remappings and checking the library dirs
+    ///
+    /// # Example
+    ///
+    /// Following `@aave` dependency in the `lib` folder `node_modules`
+    ///
+    /// ```text
+    /// <root>/node_modules/@aave
+    /// ├── aave-token
+    /// │   ├── contracts
+    /// │   │   ├── open-zeppelin
+    /// │   │   ├── token
+    /// ├── governance-v2
+    ///     ├── contracts
+    ///         ├── interfaces
+    /// ```
+    ///
+    /// has this remapping: `@aave/=@aave/` (name:path) so contracts can be imported as
+    ///
+    /// ```solidity
+    /// import "@aave/governance-v2/contracts/governance/Executor.sol";
+    /// ```
+    ///
+    /// So that `Executor.sol` can be found by checking each `lib` folder (`node_modules`) with
+    /// applied remappings. Applying remapping works by checking if the import path of an import
+    /// statement starts with the name of a remapping and replacing it with the remapping's `path`.
+    ///
+    /// There are some caveats though, dapptools style remappings usually include the `src` folder
+    /// `ds-test/=lib/ds-test/src/` so that imports look like `import "ds-test/test.sol";` (note the
+    /// missing `src` in the import path).
+    ///
+    /// For hardhat/npm style that's not always the case, most notably for [openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) if installed via npm.
+    /// The remapping is detected as `'@openzeppelin/=node_modules/@openzeppelin/contracts/'`, which
+    /// includes the source directory `contracts`, however it's common to see import paths like:
+    ///
+    /// `import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`
+    ///
+    /// instead of
+    ///
+    /// `import "@openzeppelin/token/ERC20/IERC20.sol";`
+    ///
+    /// There is no strict rule behind this, but because [`Remappings::find_many`] returns
+    /// `'@openzeppelin/=node_modules/@openzeppelin/contracts/'` we should handle the case if the
+    /// remapping path ends with `contracts` and the import path starts with `<remapping
+    /// name>/contracts`. Otherwise we can end up with a resolved path that has a duplicate
+    /// `contracts` segment: `@openzeppelin/contracts/contracts/token/ERC20/IERC20.sol` we check
+    /// for this edge case here so that both styles work out of the box.
     pub fn resolve_library_import(&self, import: &Path) -> Option<PathBuf> {
         // if the import path starts with the name of the remapping then we get the resolved path by
         // removing the name and adding the remainder to the path of the remapping
-        if let Some(path) = self
-            .remappings
-            .iter()
-            .find_map(|r| import.strip_prefix(&r.name).ok().map(|p| Path::new(&r.path).join(p)))
-        {
+        if let Some(path) = self.remappings.iter().find_map(|r| {
+            import.strip_prefix(&r.name).ok().map(|stripped_import| {
+                let lib_path = Path::new(&r.path).join(stripped_import);
+
+                // we handle the edge case where the path of a remapping ends with "contracts"
+                // (`<name>/=.../contracts`) and the stripped import also starts with `contracts`
+                if let Ok(adjusted_import) = stripped_import.strip_prefix("contracts/") {
+                    if r.path.ends_with("contracts/") && !lib_path.exists() {
+                        return Path::new(&r.path).join(adjusted_import)
+                    }
+                }
+                lib_path
+            })
+        }) {
             Some(self.root.join(path))
         } else {
             utils::resolve_library(&self.libraries, import)
@@ -448,14 +503,20 @@ pub trait Artifact {
     /// Returns the artifact's `Abi` and bytecode
     fn into_inner(self) -> (Option<Abi>, Option<Bytes>);
 
-    /// Turns the artifact into a container type for abi, bytecode and deployed bytecode
+    /// Turns the artifact into a container type for abi, compact bytecode and deployed bytecode
     fn into_compact_contract(self) -> CompactContract;
 
+    /// Turns the artifact into a container type for abi, full bytecode and deployed bytecode
+    fn into_contract_bytecode(self) -> CompactContractBytecode;
+
     /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
     fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>);
 }
 
-impl<T: Into<CompactContract>> Artifact for T {
+impl<T> Artifact for T
+where
+    T: Into<CompactContractBytecode> + Into<CompactContract>,
+{
     fn into_inner(self) -> (Option<Abi>, Option<Bytes>) {
         let artifact = self.into_compact_contract();
         (artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
@@ -465,6 +526,10 @@ impl<T: Into<CompactContract>> Artifact for T {
         self.into()
     }
 
+    fn into_contract_bytecode(self) -> CompactContractBytecode {
+        self.into()
+    }
+
     fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
         self.into_compact_contract().into_parts()
     }
@@ -570,7 +635,7 @@ pub trait ArtifactOutput {
 pub struct MinimalCombinedArtifacts;
 
 impl ArtifactOutput for MinimalCombinedArtifacts {
-    type Artifact = CompactContract;
+    type Artifact = CompactContractBytecode;
 
     fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
         fs::create_dir_all(&layout.artifacts)
@@ -588,7 +653,30 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
                         ))
                     })?;
                 }
-                let min = CompactContractRef::from(contract);
+
+                if let Some(iropt) = &contract.ir_optimized {
+                    fs::write(&file.with_extension("iropt"), iropt)
+                        .map_err(|err| SolcError::io(err, file.with_extension("iropt")))?
+                }
+
+                if let Some(ir) = &contract.ir {
+                    fs::write(&file.with_extension("ir"), ir)
+                        .map_err(|err| SolcError::io(err, file.with_extension("ir")))?
+                }
+
+                if let Some(ewasm) = &contract.ewasm {
+                    fs::write(&file.with_extension("ewasm"), serde_json::to_vec_pretty(&ewasm)?)
+                        .map_err(|err| SolcError::io(err, file.with_extension("ewasm")))?;
+                }
+
+                if let Some(evm) = &contract.evm {
+                    if let Some(asm) = &evm.assembly {
+                        fs::write(&file.with_extension("asm"), asm)
+                            .map_err(|err| SolcError::io(err, file.with_extension("asm")))?
+                    }
+                }
+
+                let min = CompactContractBytecode::from(contract.clone());
                 fs::write(&file, serde_json::to_vec_pretty(&min)?)
                     .map_err(|err| SolcError::io(err, file))?
             }
@@ -607,7 +695,7 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
 pub struct MinimalCombinedArtifactsHardhatFallback;
 
 impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
-    type Artifact = CompactContract;
+    type Artifact = CompactContractBytecode;
 
     fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
         MinimalCombinedArtifacts::on_output(output, layout)
@@ -623,7 +711,7 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
             tracing::trace!("Fallback to hardhat artifact deserialization");
             let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
             tracing::trace!("successfully deserialized hardhat artifact");
-            Ok(artifact.into_compact_contract())
+            Ok(artifact.into_contract_bytecode())
         }
     }
 
diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs
index dd79f75a..8a7e3d78 100644
--- a/ethers-solc/src/hh.rs
+++ b/ethers-solc/src/hh.rs
@@ -1,7 +1,10 @@
 //! Hardhat support
 
 use crate::{
-    artifacts::{BytecodeObject, CompactContract, Contract, Offsets},
+    artifacts::{
+        Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract,
+        ContractBytecode, DeployedBytecode, Offsets,
+    },
     error::{Result, SolcError},
     ArtifactOutput, CompilerOutput, ProjectPathsConfig,
 };
@@ -49,6 +52,32 @@ impl From<HardhatArtifact> for CompactContract {
     }
 }
 
+impl From<HardhatArtifact> for ContractBytecode {
+    fn from(artifact: HardhatArtifact) -> Self {
+        let bytecode: Option<Bytecode> = artifact.bytecode.as_ref().map(|t| {
+            let mut bcode: Bytecode = t.clone().into();
+            bcode.link_references = artifact.link_references.clone();
+            bcode
+        });
+
+        let deployed_bytecode: Option<DeployedBytecode> = artifact.bytecode.as_ref().map(|t| {
+            let mut bcode: Bytecode = t.clone().into();
+            bcode.link_references = artifact.deployed_link_references.clone();
+            bcode.into()
+        });
+
+        ContractBytecode { abi: Some(artifact.abi), bytecode, deployed_bytecode }
+    }
+}
+
+impl From<HardhatArtifact> for CompactContractBytecode {
+    fn from(artifact: HardhatArtifact) -> Self {
+        let c: ContractBytecode = artifact.into();
+
+        c.into()
+    }
+}
+
 /// Hardhat style artifacts handler
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
 pub struct HardhatArtifacts;
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 1bcae07c..92e4eb83 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -918,11 +918,11 @@ impl<T: ArtifactOutput + 'static> ProjectCompileOutput<T> {
     ///
     /// ```no_run
     /// use std::collections::BTreeMap;
-    /// use ethers_solc::artifacts::CompactContract;
+    /// use ethers_solc::artifacts::CompactContractBytecode;
     /// use ethers_solc::Project;
     ///
     /// let project = Project::builder().build().unwrap();
-    /// let contracts: BTreeMap<String, CompactContract> = project.compile().unwrap().into_artifacts().collect();
+    /// let contracts: BTreeMap<String, CompactContractBytecode> = project.compile().unwrap().into_artifacts().collect();
     /// ```
     pub fn into_artifacts(mut self) -> Box<dyn Iterator<Item = (String, T::Artifact)>> {
         let artifacts = self.artifacts.into_iter().filter_map(|(path, art)| {
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index 6492a018..681bf66b 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -491,7 +491,7 @@ fn last_nested_source_dir(root: &Path, dir: &Path) -> PathBuf {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::utils::tempdir;
+    use crate::{utils::tempdir, ProjectPathsConfig};
 
     #[test]
     fn relative_remapping() {
@@ -538,6 +538,9 @@ mod tests {
 
     fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
         for path in paths {
+            if let Some(parent) = Path::new(path).parent() {
+                std::fs::create_dir_all(tmp.join(parent)).unwrap();
+            }
             if path.ends_with(".sol") {
                 let path = tmp.join(path);
                 touch(&path).unwrap();
@@ -569,35 +572,52 @@ mod tests {
         assert_eq!(remappings[0].path, format!("{}/src/", path));
     }
 
+    #[test]
+    fn can_resolve_oz_remappings() {
+        let tmp_dir = tempdir("node_modules").unwrap();
+        let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
+        let paths = [
+            "node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
+            "node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
+            "node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
+            "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
+        ];
+        mkdir_or_touch(tmp_dir.path(), &paths[..]);
+        let remappings = Remapping::find_many(&tmp_dir_node_modules);
+
+        let mut paths = ProjectPathsConfig::hardhat(tmp_dir.path()).unwrap();
+        paths.remappings = remappings;
+
+        let resolved = paths
+            .resolve_library_import(Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"))
+            .unwrap();
+        assert!(resolved.exists());
+
+        // adjust remappings
+        paths.remappings[0].name = "@openzeppelin/contracts/".to_string();
+
+        let resolved = paths
+            .resolve_library_import(Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"))
+            .unwrap();
+        assert!(resolved.exists());
+    }
+
     #[test]
     fn recursive_remappings() {
         let tmp_dir = tempdir("lib").unwrap();
         let tmp_dir_path = tmp_dir.path();
         let paths = [
-            "repo1/src/",
             "repo1/src/contract.sol",
-            "repo1/lib/",
-            "repo1/lib/ds-test/src/",
             "repo1/lib/ds-test/src/test.sol",
-            "repo1/lib/ds-math/src/",
             "repo1/lib/ds-math/src/contract.sol",
-            "repo1/lib/ds-math/lib/ds-test/src/",
             "repo1/lib/ds-math/lib/ds-test/src/test.sol",
-            "repo1/lib/guni-lev/src",
             "repo1/lib/guni-lev/src/contract.sol",
-            "repo1/lib/solmate/src/auth",
             "repo1/lib/solmate/src/auth/contract.sol",
-            "repo1/lib/solmate/src/tokens",
             "repo1/lib/solmate/src/tokens/contract.sol",
-            "repo1/lib/solmate/lib/ds-test/src/",
             "repo1/lib/solmate/lib/ds-test/src/test.sol",
-            "repo1/lib/solmate/lib/ds-test/demo/",
             "repo1/lib/solmate/lib/ds-test/demo/demo.sol",
-            "repo1/lib/openzeppelin-contracts/contracts/access",
             "repo1/lib/openzeppelin-contracts/contracts/access/AccessControl.sol",
-            "repo1/lib/ds-token/lib/ds-stop/src",
             "repo1/lib/ds-token/lib/ds-stop/src/contract.sol",
-            "repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src",
             "repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src/contract.sol",
         ];
         mkdir_or_touch(tmp_dir_path, &paths[..]);
@@ -692,12 +712,8 @@ mod tests {
             "ds-test/demo",
             "ds-test/demo/demo.sol",
             "ds-test/src/test.sol",
-            "openzeppelin/src",
-            "openzeppelin/src/interfaces",
             "openzeppelin/src/interfaces/c.sol",
-            "openzeppelin/src/token/ERC/",
             "openzeppelin/src/token/ERC/c.sol",
-            "standards/src/interfaces",
             "standards/src/interfaces/iweth.sol",
             "uniswapv2/src",
         ];
@@ -730,24 +746,17 @@ mod tests {
         let tmp_dir = tempdir("node_modules").unwrap();
         let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
         let paths = [
-            "node_modules/@aave/aave-token/contracts/token/",
             "node_modules/@aave/aave-token/contracts/token/AaveToken.sol",
-            "node_modules/@aave/governance-v2/contracts/governance/",
             "node_modules/@aave/governance-v2/contracts/governance/Executor.sol",
             "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/",
             "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol",
-            "node_modules/@ensdomains/ens/contracts/",
             "node_modules/@ensdomains/ens/contracts/contract.sol",
             "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/",
             "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/
             ModifierDefinitions.sol",
-            "node_modules/@openzeppelin/contracts/tokens",
             "node_modules/@openzeppelin/contracts/tokens/contract.sol",
-            "node_modules/@openzeppelin/contracts/access",
             "node_modules/@openzeppelin/contracts/access/contract.sol",
-            "node_modules/eth-gas-reporter/mock/contracts",
             "node_modules/eth-gas-reporter/mock/contracts/ConvertLib.sol",
-            "node_modules/eth-gas-reporter/mock/test/",
             "node_modules/eth-gas-reporter/mock/test/TestMetacoin.sol",
         ];
         mkdir_or_touch(tmp_dir.path(), &paths[..]);
diff --git a/ethers-solc/src/resolver.rs b/ethers-solc/src/resolver.rs
index e2c8403d..0bc10f86 100644
--- a/ethers-solc/src/resolver.rs
+++ b/ethers-solc/src/resolver.rs
@@ -485,7 +485,8 @@ impl<T> SolDataUnit<T> {
     pub fn loc_by_offset(&self, offset: isize) -> (usize, usize) {
         (
             offset.saturating_add(self.loc.start as isize) as usize,
-            offset.saturating_add(self.loc.end as isize) as usize,
+            // make the end location exclusive
+            offset.saturating_add(self.loc.end as isize + 1) as usize,
         )
     }
 }
@@ -517,7 +518,7 @@ fn parse_data(content: &str) -> SolData {
     let mut version = None;
     let mut imports = Vec::<SolDataUnit<PathBuf>>::new();
     match solang_parser::parse(content, 0) {
-        Ok(units) => {
+        Ok((units, _)) => {
             for unit in units.0 {
                 match unit {
                     SourceUnitPart::PragmaDirective(loc, _, pragma, value) => {
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index fc1960b4..f6672a5f 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -2,10 +2,7 @@
 
 use std::path::{Component, Path, PathBuf};
 
-use crate::{
-    error::{self, SolcError},
-    ProjectPathsConfig, SolcIoError,
-};
+use crate::{error::SolcError, SolcIoError};
 use once_cell::sync::Lazy;
 use regex::{Match, Regex};
 use semver::Version;
@@ -85,37 +82,6 @@ pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
     dunce::canonicalize(&path).map_err(|err| SolcIoError::new(err, path))
 }
 
-/// Try to resolve import to a local file or library path
-pub fn resolve_import_component(
-    import: &Path,
-    node_dir: &Path,
-    paths: &ProjectPathsConfig,
-) -> error::Result<PathBuf> {
-    let component = match import.components().next() {
-        Some(inner) => inner,
-        None => {
-            return Err(SolcError::msg(format!(
-                "failed to resolve import at \"{:?}\"",
-                import.display()
-            )))
-        }
-    };
-
-    if component == Component::CurDir || component == Component::ParentDir {
-        // if the import is relative we assume it's already part of the processed input file set
-        canonicalize(node_dir.join(import)).map_err(|err| err.into())
-    } else {
-        // resolve library file
-        match paths.resolve_library_import(import.as_ref()) {
-            Some(lib) => Ok(lib),
-            None => Err(SolcError::msg(format!(
-                "failed to resolve library import \"{:?}\"",
-                import.display()
-            ))),
-        }
-    }
-}
-
 /// Returns the path to the library if the source path is in fact determined to be a library path,
 /// and it exists.
 /// Note: this does not handle relative imports or remappings.
diff --git a/examples/abigen.rs b/examples/abigen.rs
index e3c8bc1a..5880d115 100644
--- a/examples/abigen.rs
+++ b/examples/abigen.rs
@@ -1,6 +1,6 @@
 use ethers::{contract::Abigen, solc::Solc};
 
-fn main() -> anyhow::Result<()> {
+fn main() -> eyre::Result<()> {
     let mut args = std::env::args();
     args.next().unwrap(); // skip program name
 
diff --git a/examples/contract_human_readable.rs b/examples/contract_human_readable.rs
index 41b98288..674c2e66 100644
--- a/examples/contract_human_readable.rs
+++ b/examples/contract_human_readable.rs
@@ -1,9 +1,9 @@
-use anyhow::Result;
 use ethers::{
     prelude::*,
     solc::{Project, ProjectPathsConfig},
     utils::Ganache,
 };
+use eyre::Result;
 use std::{convert::TryFrom, path::PathBuf, sync::Arc, time::Duration};
 
 // Generate the type-safe contract bindings by providing the ABI
diff --git a/examples/contract_with_abi.rs b/examples/contract_with_abi.rs
index 83af2d9f..c151649e 100644
--- a/examples/contract_with_abi.rs
+++ b/examples/contract_with_abi.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::{prelude::*, utils::Ganache};
+use eyre::Result;
 use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
 
 // Generate the type-safe contract bindings by providing the ABI
diff --git a/examples/ens.rs b/examples/ens.rs
index 4afbe4d5..894c466e 100644
--- a/examples/ens.rs
+++ b/examples/ens.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::{prelude::*, utils::Ganache};
+use eyre::Result;
 use std::convert::TryFrom;
 
 #[tokio::main]
@@ -20,7 +20,7 @@ async fn main() -> Result<()> {
         .send_transaction(tx, None)
         .await?
         .await?
-        .ok_or_else(|| anyhow::format_err!("tx dropped from mempool"))?;
+        .ok_or_else(|| eyre::format_err!("tx dropped from mempool"))?;
     let tx = provider.get_transaction(receipt.transaction_hash).await?;
 
     println!("{}", serde_json::to_string(&tx)?);
diff --git a/examples/ipc.rs b/examples/ipc.rs
index 9f747635..a4c9e4d7 100644
--- a/examples/ipc.rs
+++ b/examples/ipc.rs
@@ -1,6 +1,6 @@
 #[tokio::main]
 #[cfg(feature = "ipc")]
-async fn main() -> anyhow::Result<()> {
+async fn main() -> eyre::Result<()> {
     use ethers::prelude::*;
 
     let provider = Provider::connect_ipc("~/.ethereum/geth.ipc")
diff --git a/examples/local_signer.rs b/examples/local_signer.rs
index d8eb415b..3ddcda82 100644
--- a/examples/local_signer.rs
+++ b/examples/local_signer.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::{prelude::*, utils::Ganache};
+use eyre::Result;
 use std::convert::TryFrom;
 
 #[tokio::main]
@@ -22,8 +22,7 @@ async fn main() -> Result<()> {
     let pending_tx = client.send_transaction(tx, None).await?;
 
     // get the mined tx
-    let receipt =
-        pending_tx.await?.ok_or_else(|| anyhow::format_err!("tx dropped from mempool"))?;
+    let receipt = pending_tx.await?.ok_or_else(|| eyre::format_err!("tx dropped from mempool"))?;
     let tx = client.get_transaction(receipt.transaction_hash).await?;
 
     println!("Sent tx: {}\n", serde_json::to_string(&tx)?);
diff --git a/examples/mnemonic.rs b/examples/mnemonic.rs
index 0685a165..2c14b69d 100644
--- a/examples/mnemonic.rs
+++ b/examples/mnemonic.rs
@@ -1,6 +1,6 @@
 use ethers::signers::{coins_bip39::English, MnemonicBuilder};
 
-fn main() -> anyhow::Result<()> {
+fn main() -> eyre::Result<()> {
     let phrase = "work man father plunge mystery proud hollow address reunion sauce theory bonus";
     let index = 0u32;
     let password = "TREZOR123";
diff --git a/examples/moonbeam_with_abi.rs b/examples/moonbeam_with_abi.rs
index c455d5f3..266c56c9 100644
--- a/examples/moonbeam_with_abi.rs
+++ b/examples/moonbeam_with_abi.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::prelude::*;
+use eyre::Result;
 use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
 
 abigen!(
diff --git a/examples/quorum.rs b/examples/quorum.rs
index a4581ecc..63cd59bd 100644
--- a/examples/quorum.rs
+++ b/examples/quorum.rs
@@ -5,7 +5,7 @@ use ethers::{prelude::*, utils::Ganache};
 use std::{str::FromStr, time::Duration};
 
 #[tokio::main]
-async fn main() -> anyhow::Result<()> {
+async fn main() -> eyre::Result<()> {
     let ganache = Ganache::new().spawn();
 
     // create a quorum provider with some providers
diff --git a/examples/sign.rs b/examples/sign.rs
index a385b048..a3c2d79d 100644
--- a/examples/sign.rs
+++ b/examples/sign.rs
@@ -1,5 +1,5 @@
-// use the anyhow crate for easy idiomatic error handling
-use anyhow::Result;
+// use the eyre crate for easy idiomatic error handling
+use eyre::Result;
 // use the ethers_core rand for rng
 use ethers_core::rand::thread_rng;
 // use the ethers_signers crate to manage LocalWallet and Signer
diff --git a/examples/transfer_eth.rs b/examples/transfer_eth.rs
index c0680c71..9aa249d3 100644
--- a/examples/transfer_eth.rs
+++ b/examples/transfer_eth.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::{prelude::*, utils::Ganache};
+use eyre::Result;
 use std::convert::TryFrom;
 
 #[tokio::main]
diff --git a/examples/uniswapv2.rs b/examples/uniswapv2.rs
index 91ae6b2b..c2c51655 100644
--- a/examples/uniswapv2.rs
+++ b/examples/uniswapv2.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
 use ethers::prelude::*;
+use eyre::Result;
 use std::sync::Arc;
 
 // Generate the type-safe contract bindings by providing the ABI
diff --git a/examples/watch_blocks.rs b/examples/watch_blocks.rs
index 8ee2cffb..7e075857 100644
--- a/examples/watch_blocks.rs
+++ b/examples/watch_blocks.rs
@@ -2,7 +2,7 @@ use ethers::{prelude::*, utils::Ganache};
 use std::time::Duration;
 
 #[tokio::main]
-async fn main() -> anyhow::Result<()> {
+async fn main() -> eyre::Result<()> {
     let ganache = Ganache::new().block_time(1u64).spawn();
     let ws = Ws::connect(ganache.ws_endpoint()).await?;
     let provider = Provider::new(ws).interval(Duration::from_millis(2000));


Test patch:
diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs
index 58c0aa83..a18879d3 100644
--- a/ethers-contract/tests/common/derive.rs
+++ b/ethers-contract/tests/common/derive.rs
@@ -3,7 +3,7 @@ use ethers_contract::{
 };
 use ethers_core::{
     abi::{AbiDecode, AbiEncode, RawLog, Tokenizable},
-    types::{Address, H160, H256, I256, U128, U256},
+    types::{Address, Bytes, H160, H256, I256, U128, U256},
 };
 
 fn assert_tokenizeable<T: Tokenizable>() {}
@@ -560,3 +560,15 @@ fn can_derive_abi_codec_two_field() {
 
     assert_eq!(decoded_wrapped, tuple);
 }
+
+#[test]
+fn can_derive_ethcall_for_bytes() {
+    #[derive(Clone, Debug, Default, Eq, PartialEq, EthCall, EthDisplay)]
+    #[ethcall(name = "batch", abi = "batch(bytes[],bool)")]
+    pub struct BatchCall {
+        pub calls: Vec<Bytes>,
+        pub revert_on_fail: bool,
+    }
+
+    assert_ethcall::<BatchCall>();
+}
diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs
index e61759b0..30b3fa03 100644
--- a/ethers-middleware/tests/nonce_manager.rs
+++ b/ethers-middleware/tests/nonce_manager.rs
@@ -36,7 +36,10 @@ async fn nonce_manager() {
     let mut tx_hashes = Vec::new();
     for _ in 0..10 {
         let tx = provider
-            .send_transaction(Eip1559TransactionRequest::new().to(address).value(100u64), None)
+            .send_transaction(
+                Eip1559TransactionRequest::new().to(address).value(100u64).chain_id(chain_id),
+                None,
+            )
             .await
             .unwrap();
         tx_hashes.push(*tx);
diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs
index 5d4cd67b..e5cf3570 100644
--- a/ethers-middleware/tests/signer.rs
+++ b/ethers-middleware/tests/signer.rs
@@ -39,7 +39,7 @@ async fn send_eth() {
     let provider = SignerMiddleware::new(provider, wallet);
 
     // craft the transaction
-    let tx = TransactionRequest::new().to(wallet2.address()).value(10000);
+    let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id);
 
     let balance_before = provider.get_balance(provider.address(), None).await.unwrap();
 
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index c50fd3eb..e095b01e 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -376,4 +376,28 @@ fn can_flatten_file_with_duplicates() {
     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);
+}
+
+#[test]
+fn can_detect_type_error() {
+    let project = TempProject::<MinimalCombinedArtifacts>::dapptools().unwrap();
+
+    project
+        .add_source(
+            "Contract",
+            r#"
+    pragma solidity ^0.8.10;
+
+   contract Contract {
+        function xyz() public {
+            require(address(0), "Error");
+        }
+   }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(compiled.has_compiler_errors());
 }