Repository: gakonst/ethers-rs
Pull request #1890 (2022-11-24 21:20:30)

Feat middleware stack builder
## Motivation
The ability to stack different `Middleware`s  is a powerful feature of this library, since it promotes "Single Responsibility", "Open Closed Principle" and other good stuff. 
I think that readability and composability can be further improved. For such reason I propose another approach to build stacked `Middleware`s using a builder struct.

## Solution
I provided
- A `ProviderBuilder` module under  the `ethers-middleware` lib
- Extensive documentation
- Unit tests

It works as follows 
```rust
fn example() {
    let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
    let signer = key.parse::<LocalWallet>().unwrap();
    let address = signer.address();
    let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
 
    let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
 
    ProviderBuilder::from(provider)
        .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
        .wrap_into(|p| SignerMiddleware::new(p, signer))
        .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
        .wrap_into(|p| NonceManagerMiddleware::new(p, address)) // Outermost layer
        .build();
}
```

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [x] Updated the changelog


prestwich: would this be better hardcoding common middleware?

````
ProviderBuilder::from(provider)
    .with_signer(signer)
    .nonce_manager(addr)
    .wrap_into(|p| SomeCustomMiddleware(p, other_arg))
    .build();
prestwich: should this be implemented as provided functions on the Middleware trait?
prestwich: direction is very good. ergonomics I'm not so sure about? 🤔 
0xMelkor: Probably this could be part of the Middleware trait.
For what concerns ergonomics I can improve it. Do you have ideas?
prestwich: > Probably this could be part of the Middleware trait.

I think this would be really cool :)
0xMelkor: > > Probably this could be part of the Middleware trait.
> 
> I think this would be really cool :)

The idea is interesting, but the `Middleware` trait (defined in `ethers-providers`) would need to reference the `ethers-middleware` module. The project structure doesn't allow this at the moment. 

We have at least 2 chances:
1. Update the workspace structure to link `ethers-providers` -> `ethers-middleware`. Probably not the best choice.
2. Keep the builder inside the `ethers-middleware` package

What do you think?
prestwich: > The idea is interesting, but the Middleware trait (defined in ethers-providers) would need to reference the ethers-middleware module. The project structure doesn't allow this at the moment.

well what I mean is that there's now no reason to have this be an independent struct at all. it could be provided methods on the `Middleware` trait itself 

@gakonst side note, if we're going to keep middleware pattern in 2.0, we should design a builder that prevents misuse like multiple signers, or combining known-incompatible middleware🤔 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59f25388..b8c22654 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
 ## ethers-core
 
 ### Unreleased
-
+-   `MiddlewareBuilder` trait to instantiate a `Provider` as `Middleware` layers.
 -   An `Event` builder can be instantiated specifying the event filter type, without the need to instantiate a contract.
 -   Add 'ethers_core::types::OpCode' and use in 'ethers_core::types::VMOperation' [1857](https://github.com/gakonst/ethers-rs/issues/1857)
 -   Remove rust_decimals dependency for ethers-core
diff --git a/Cargo.lock b/Cargo.lock
index db879812..3ecf6d76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -121,7 +121,7 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi",
 ]
@@ -1125,17 +1125,38 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
-version = "0.9.3"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
 dependencies = [
- "atty",
  "humantime",
+ "is-terminal",
  "log",
  "regex",
  "termcolor",
 ]
 
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
 [[package]]
 name = "eth-keystore"
 version = "0.5.0"
@@ -1208,7 +1229,7 @@ dependencies = [
 
 [[package]]
 name = "ethers"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "bytes",
  "ethers-addressbook",
@@ -1229,7 +1250,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-addressbook"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "ethers-core",
  "once_cell",
@@ -1239,7 +1260,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-contract"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "ethers-contract-abigen",
  "ethers-contract-derive",
@@ -1260,7 +1281,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-contract-abigen"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "Inflector",
  "cfg-if 1.0.0",
@@ -1285,7 +1306,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-contract-derive"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "ethers-contract-abigen",
  "ethers-core",
@@ -1298,7 +1319,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-core"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "arrayvec 0.7.2",
  "bincode",
@@ -1329,7 +1350,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-derive-eip712"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "ethers-contract-derive",
  "ethers-core",
@@ -1341,7 +1362,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-etherscan"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "ethers-core",
  "ethers-solc",
@@ -1361,7 +1382,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-middleware"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "async-trait",
  "auto_impl 0.5.0",
@@ -1390,7 +1411,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-providers"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "async-trait",
  "auto_impl 1.0.1",
@@ -1427,7 +1448,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-signers"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "async-trait",
  "coins-bip32",
@@ -1460,7 +1481,7 @@ dependencies = [
 
 [[package]]
 name = "ethers-solc"
-version = "1.0.0"
+version = "1.0.2"
 dependencies = [
  "cfg-if 1.0.0",
  "criterion",
@@ -1859,6 +1880,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -2099,12 +2129,34 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7d367024b3f3414d8e01f437f704f41a9f64ab36f9067fa73e526ad4c763c87"
+dependencies = [
+ "libc",
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
 
+[[package]]
+name = "is-terminal"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "itertools"
 version = "0.10.3"
@@ -2203,9 +2255,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.121"
+version = "0.2.137"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
 
 [[package]]
 name = "libusb1-sys"
@@ -2219,6 +2271,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
+
 [[package]]
 name = "lock_api"
 version = "0.4.6"
@@ -2317,7 +2375,7 @@ dependencies = [
  "libc",
  "log",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -2392,7 +2450,7 @@ version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
 ]
 
@@ -2610,7 +2668,7 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -3294,6 +3352,20 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.36.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "rustls"
 version = "0.20.2"
@@ -4569,43 +4641,100 @@ version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
 [[package]]
 name = "winreg"
 version = "0.10.1"
diff --git a/Cargo.toml b/Cargo.toml
index d2eea1dd..c02d04f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2021"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-addressbook/Cargo.toml b/ethers-addressbook/Cargo.toml
index 3b7a5aab..0d443d87 100644
--- a/ethers-addressbook/Cargo.toml
+++ b/ethers-addressbook/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-addressbook"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2021"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml
index 2e0f6dd3..02f6bac3 100644
--- a/ethers-contract/Cargo.toml
+++ b/ethers-contract/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-contract"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml
index ec96bbae..f69e2f69 100644
--- a/ethers-contract/ethers-contract-abigen/Cargo.toml
+++ b/ethers-contract/ethers-contract-abigen/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-contract-abigen"
-version = "1.0.0"
+version = "1.0.2"
 authors = [
     "Nicholas Rodrigues Lordello <nlordell@gmail.com>",
     "Georgios Konstantopoulos <me@gakonst.com>",
diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml
index aa2b1ec0..85da947d 100644
--- a/ethers-contract/ethers-contract-derive/Cargo.toml
+++ b/ethers-contract/ethers-contract-derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-contract-derive"
-version = "1.0.0"
+version = "1.0.2"
 authors = [
     "Nicholas Rodrigues Lordello <nlordell@gmail.com>",
     "Georgios Konstantopoulos <me@gakonst.com>",
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index e92e2929..8edd188c 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "ethers-core"
 license = "MIT OR Apache-2.0"
-version = "1.0.0"
+version = "1.0.2"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
 edition = "2018"
diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml
index d8fe773a..305bfcfd 100644
--- a/ethers-core/ethers-derive-eip712/Cargo.toml
+++ b/ethers-core/ethers-derive-eip712/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-derive-eip712"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 description = "Custom derive macro for EIP-712 typed data"
 license = "MIT OR Apache-2.0"
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 21da3047..f3e7d7bc 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -41,6 +41,11 @@ use std::{
 };
 use thiserror::Error;
 
+/// I256 overflows for numbers wider than 77 units.
+const OVERFLOW_I256_UNITS: usize = 77;
+/// U256 overflows for numbers wider than 78 units.
+const OVERFLOW_U256_UNITS: usize = 78;
+
 /// Re-export of serde-json
 #[doc(hidden)]
 pub mod __serde_json {
@@ -158,29 +163,35 @@ where
     T: Into<ParseUnits>,
     K: TryInto<Units, Error = ConversionError>,
 {
-    let units = units.try_into()?;
-    match amount.into() {
+    let units: usize = units.try_into()?.into();
+    let amount = amount.into();
+
+    match amount {
+        // 2**256 ~= 1.16e77
+        ParseUnits::U256(_) if units >= OVERFLOW_U256_UNITS => {
+            return Err(ConversionError::ParseOverflow)
+        }
+        // 2**255 ~= 5.79e76
+        ParseUnits::I256(_) if units >= OVERFLOW_I256_UNITS => {
+            return Err(ConversionError::ParseOverflow)
+        }
+        _ => {}
+    };
+    let exp10 = U256::exp10(units);
+
+    // `decimals` are formatted twice because U256 does not support alignment (`:0>width`).
+    match amount {
         ParseUnits::U256(amount) => {
-            let amount_decimals = amount % U256::from(10_u128.pow(units.as_num()));
-            let amount_integer = amount / U256::from(10_u128.pow(units.as_num()));
-            Ok(format!(
-                "{}.{:0width$}",
-                amount_integer,
-                amount_decimals.as_u128(),
-                width = units.as_num() as usize
-            ))
+            let integer = amount / exp10;
+            let decimals = (amount % exp10).to_string();
+            Ok(format!("{integer}.{decimals:0>units$}"))
         }
         ParseUnits::I256(amount) => {
+            let exp10 = I256::from_raw(exp10);
             let sign = if amount.is_negative() { "-" } else { "" };
-            let amount_decimals = amount % I256::from(10_u128.pow(units.as_num()));
-            let amount_integer = amount / I256::from(10_u128.pow(units.as_num()));
-            Ok(format!(
-                "{}{}.{:0width$}",
-                sign,
-                amount_integer.twos_complement(),
-                amount_decimals.twos_complement().as_u128(),
-                width = units.as_num() as usize
-            ))
+            let integer = (amount / exp10).twos_complement();
+            let decimals = ((amount % exp10).twos_complement()).to_string();
+            Ok(format!("{sign}{integer}.{decimals:0>units$}"))
         }
     }
 }
@@ -546,7 +557,7 @@ mod tests {
             format_units(U256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap();
         assert_eq!(eth, "1.005633240123456789");
 
-        let eth = format_units(255u8, 4).unwrap();
+        let eth = format_units(u8::MAX, 4).unwrap();
         assert_eq!(eth, "0.0255");
 
         let eth = format_units(u16::MAX, "ether").unwrap();
@@ -562,6 +573,15 @@ mod tests {
 
         let eth = format_units(u128::MAX, 36).unwrap();
         assert_eq!(eth, "340.282366920938463463374607431768211455");
+
+        let eth = format_units(U256::MAX, 77).unwrap();
+        assert_eq!(
+            eth,
+            "1.15792089237316195423570985008687907853269984665640564039457584007913129639935"
+        );
+
+        let err = format_units(U256::MAX, 78).unwrap_err();
+        assert!(matches!(err, ConversionError::ParseOverflow));
     }
 
     #[test]
@@ -599,6 +619,15 @@ mod tests {
 
         let eth = format_units(i128::MIN, 36).unwrap();
         assert_eq!(eth, "-170.141183460469231731687303715884105728");
+
+        let eth = format_units(I256::MIN, 76).unwrap();
+        assert_eq!(
+            eth,
+            "-5.7896044618658097711785492504343953926634992332820282019728792003956564819968"
+        );
+
+        let err = format_units(I256::MIN, 77).unwrap_err();
+        assert!(matches!(err, ConversionError::ParseOverflow));
     }
 
     #[test]
diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index 84347f42..52283992 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-etherscan"
-version = "1.0.0"
+version = "1.0.2"
 rust-version = "1.62"
 authors = [
     "Matthias Seitz <matthias.seitz@outlook.de>",
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 1b4b3c16..b53788bf 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-middleware"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-middleware/README.md b/ethers-middleware/README.md
index 3d24264f..186c571c 100644
--- a/ethers-middleware/README.md
+++ b/ethers-middleware/README.md
@@ -4,22 +4,22 @@ middleware functionalities that you need.
 
 ## Available Middleware
 
--   [`Signer`](./signer/struct.SignerMiddleware.html): Signs transactions locally,
-    with a private key or a hardware wallet
--   [`Nonce Manager`](./nonce_manager/struct.NonceManagerMiddleware.html): Manages
-    nonces locally, allowing the rapid broadcast of transactions without having to
-    wait for them to be submitted
--   [`Gas Escalator`](./gas_escalator/struct.GasEscalatorMiddleware.html): Bumps
-    transaction gas prices in the background
--   [`Gas Oracle`](./gas_oracle/struct.GasOracleMiddleware.html): Allows getting
-    your gas price estimates from places other than `eth_gasPrice`.
--   [`Transformer`](./transformer/trait.Transformer.html): Allows intercepting and
-    transforming a transaction to be broadcasted via a proxy wallet, e.g.
-    [`DSProxy`](./transformer/struct.DsProxy.html).
+- [`Signer`](./signer/struct.SignerMiddleware.html): Signs transactions locally,
+  with a private key or a hardware wallet
+- [`Nonce Manager`](./nonce_manager/struct.NonceManagerMiddleware.html): Manages
+  nonces locally, allowing the rapid broadcast of transactions without having to
+  wait for them to be submitted
+- [`Gas Escalator`](./gas_escalator/struct.GasEscalatorMiddleware.html): Bumps
+  transaction gas prices in the background
+- [`Gas Oracle`](./gas_oracle/struct.GasOracleMiddleware.html): Allows getting
+  your gas price estimates from places other than `eth_gasPrice`.
+- [`Transformer`](./transformer/trait.Transformer.html): Allows intercepting and
+  transforming a transaction to be broadcasted via a proxy wallet, e.g.
+  [`DSProxy`](./transformer/struct.DsProxy.html).
 
 ## Example of a middleware stack
 
-```no_run
+```rust no_run
 use ethers_providers::{Provider, Http};
 use ethers_signers::{LocalWallet, Signer};
 use ethers_middleware::{
@@ -53,3 +53,43 @@ let provider = NonceManagerMiddleware::new(provider, address);
 
 // ... do something with the provider
 ```
+## Example of a middleware stack using a builder
+
+Each [`Middleware`](ethers_providers::Middleware) implements the trait [MiddlewareBuilder](crate::MiddlewareBuilder) to help composition of a [`Middleware`](ethers_providers::Middleware) stack. As usual the composition acts in a wrapping fashion. Adding a new layer results in wrapping its predecessor.
+Builder can be used as follows:
+```rust
+use ethers_providers::{Middleware, Provider, Http};
+use std::sync::Arc;
+use std::convert::TryFrom;
+use ethers_signers::{LocalWallet, Signer};
+use ethers_middleware::{*,gas_escalator::*,gas_oracle::*};
+
+fn builder_example() {
+    let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
+    let signer = key.parse::<LocalWallet>().unwrap();
+    let address = signer.address();
+    let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
+    let gas_oracle = EthGasStation::new(None);
+
+    let provider = Provider::<Http>::try_from("http://localhost:8545")
+        .unwrap()
+        .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) 
+        .gas_oracle(gas_oracle)
+        .with_signer(signer)
+        .nonce_manager(address); // Outermost layer
+}
+
+fn builder_example_raw_wrap() {
+    let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
+    let signer = key.parse::<LocalWallet>().unwrap();
+    let address = signer.address();
+    let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
+
+    let provider = Provider::<Http>::try_from("http://localhost:8545")
+        .unwrap()
+        .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
+        .wrap_into(|p| SignerMiddleware::new(p, signer))
+        .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
+        .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
+}
+```
diff --git a/ethers-middleware/src/builder.rs b/ethers-middleware/src/builder.rs
new file mode 100644
index 00000000..c9801962
--- /dev/null
+++ b/ethers-middleware/src/builder.rs
@@ -0,0 +1,91 @@
+use crate::{
+    gas_oracle::{GasOracle, GasOracleMiddleware},
+    NonceManagerMiddleware, SignerMiddleware,
+};
+use ethers_core::types::Address;
+use ethers_providers::Middleware;
+use ethers_signers::Signer;
+
+/// A builder trait to compose different [`Middleware`](ethers_providers::Middleware) layers
+/// and then build a composed [`Provider`](ethers_providers::Provider) architecture.
+/// [`Middleware`](ethers_providers::Middleware) composition acts in a wrapping fashion. Adding a
+/// new layer results in wrapping its predecessor.
+///
+/// ```rust
+/// use ethers_providers::{Middleware, Provider, Http};
+/// use std::sync::Arc;
+/// use std::convert::TryFrom;
+/// use ethers_signers::{LocalWallet, Signer};
+/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*};
+///
+/// fn builder_example() {
+///     let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
+///     let signer = key.parse::<LocalWallet>().unwrap();
+///     let address = signer.address();
+///     let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
+///     let gas_oracle = EthGasStation::new(None);
+///
+///     let provider = Provider::<Http>::try_from("http://localhost:8545")
+///         .unwrap()
+///         .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
+///         .gas_oracle(gas_oracle)
+///         .with_signer(signer)
+///         .nonce_manager(address); // Outermost layer
+/// }
+///
+/// fn builder_example_raw_wrap() {
+///     let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
+///     let signer = key.parse::<LocalWallet>().unwrap();
+///     let address = signer.address();
+///     let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
+///
+///     let provider = Provider::<Http>::try_from("http://localhost:8545")
+///         .unwrap()
+///         .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
+///         .wrap_into(|p| SignerMiddleware::new(p, signer))
+///         .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
+///         .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
+/// }
+/// ```
+pub trait MiddlewareBuilder: Middleware + Sized + 'static {
+    /// Wraps `self` inside a new [`Middleware`](ethers_providers::Middleware).
+    ///
+    /// `f` Consumes `self`, must be used to return a new
+    /// [`Middleware`](ethers_providers::Middleware) around `self`.
+    fn wrap_into<F, T>(self, f: F) -> T
+    where
+        F: FnOnce(Self) -> T,
+        T: Middleware,
+    {
+        f(self)
+    }
+
+    /// Wraps `self` inside a [`SignerMiddleware`](crate::SignerMiddleware).
+    ///
+    /// [`Signer`] ethers_signers::Signer
+    fn with_signer<S>(self, s: S) -> SignerMiddleware<Self, S>
+    where
+        S: Signer,
+    {
+        SignerMiddleware::new(self, s)
+    }
+
+    /// Wraps `self` inside a [`NonceManagerMiddleware`](crate::NonceManagerMiddleware).
+    ///
+    /// [`Address`] ethers_core::types::Address
+    fn nonce_manager(self, address: Address) -> NonceManagerMiddleware<Self> {
+        NonceManagerMiddleware::new(self, address)
+    }
+
+    /// Wraps `self` inside a [`GasOracleMiddleware`](crate::gas_oracle::GasOracleMiddleware).
+    ///
+    /// [`Address`] ethers_core::types::Address
+    fn gas_oracle<G>(self, gas_oracle: G) -> GasOracleMiddleware<Self, G>
+    where
+        G: GasOracle,
+    {
+        GasOracleMiddleware::new(self, gas_oracle)
+    }
+}
+
+impl<M> MiddlewareBuilder for M where M: Middleware + Sized + 'static {}
diff --git a/ethers-middleware/src/lib.rs b/ethers-middleware/src/lib.rs
index 33e90ef2..8593a10b 100644
--- a/ethers-middleware/src/lib.rs
+++ b/ethers-middleware/src/lib.rs
@@ -36,3 +36,8 @@ pub use policy::PolicyMiddleware;
 /// before the chain tip
 pub mod timelag;
 pub use timelag::TimeLag;
+
+/// The [MiddlewareBuilder](crate::MiddlewareBuilder) provides a way to compose many
+/// [`Middleware`](ethers_providers::Middleware) in a concise way
+pub mod builder;
+pub use builder::MiddlewareBuilder;
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 7c25401e..9e482923 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-providers"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 7e17df8e..18cd5657 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-signers"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 rust-version = "1.62"
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index a804559f..26de0f69 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ethers-solc"
-version = "1.0.0"
+version = "1.0.2"
 edition = "2018"
 rust-version = "1.62"
 authors = [
@@ -56,7 +56,7 @@ getrandom = { version = "0.2", features = ["js"] }
 
 [dev-dependencies]
 criterion = { version = "0.3", features = ["async_tokio"] }
-env_logger = "*"
+env_logger = "0.10.0"
 tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] }
 rand = "0.8.5"
 pretty_assertions = "1.3.0"
diff --git a/examples/subscribe_events_by_type.rs b/examples/subscribe_events_by_type.rs
index 9a3aa56f..bc917ccb 100644
--- a/examples/subscribe_events_by_type.rs
+++ b/examples/subscribe_events_by_type.rs
@@ -27,14 +27,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
     // refine the event filter
     let event = Contract::event_of_type::<AnswerUpdatedFilter>(&client)
         .from_block(16022082)
-        .to_block(16022282)
         .address(ValueOrArray::Array(vec![
             PRICE_FEED_1.parse()?,
             PRICE_FEED_2.parse()?,
             PRICE_FEED_3.parse()?,
         ]));
 
-    let mut stream = event.subscribe_with_meta().await?;
+    let mut stream = event.subscribe_with_meta().await?.take(2);
 
     // Note that `log` has type AnswerUpdatedFilter
     while let Some(Ok((log, meta))) = stream.next().await {
diff --git a/examples/subscribe_logs.rs b/examples/subscribe_logs.rs
index 9cd57e98..c85e3728 100644
--- a/examples/subscribe_logs.rs
+++ b/examples/subscribe_logs.rs
@@ -17,7 +17,7 @@ async fn main() -> Result<()> {
     let erc20_transfer_filter =
         Filter::new().from_block(last_block - 25).event("Transfer(address,address,uint256)");
 
-    let mut stream = client.subscribe_logs(&erc20_transfer_filter).await?;
+    let mut stream = client.subscribe_logs(&erc20_transfer_filter).await?.take(2);
 
     while let Some(log) = stream.next().await {
         println!(


Test patch:
diff --git a/ethers-middleware/tests/builder.rs b/ethers-middleware/tests/builder.rs
new file mode 100644
index 00000000..7c988bb8
--- /dev/null
+++ b/ethers-middleware/tests/builder.rs
@@ -0,0 +1,69 @@
+#![cfg(not(target_arch = "wasm32"))]
+#[cfg(not(feature = "celo"))]
+mod tests {
+    use ethers_core::{rand::thread_rng, types::U64};
+    use ethers_middleware::{
+        builder::MiddlewareBuilder,
+        gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
+        gas_oracle::{EthGasStation, GasOracleMiddleware},
+        nonce_manager::NonceManagerMiddleware,
+        signer::SignerMiddleware,
+    };
+    use ethers_providers::{Middleware, Provider};
+    use ethers_signers::{LocalWallet, Signer};
+
+    #[tokio::test]
+    async fn build_raw_middleware_stack() {
+        let (provider, mock) = Provider::mocked();
+
+        let signer = LocalWallet::new(&mut thread_rng());
+        let address = signer.address();
+        let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
+
+        let provider = provider
+            .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
+            .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
+            .wrap_into(|p| SignerMiddleware::new(p, signer))
+            .wrap_into(|p| NonceManagerMiddleware::new(p, address));
+
+        // push a response
+        mock.push(U64::from(12u64)).unwrap();
+        let block: U64 = provider.get_block_number().await.unwrap();
+        assert_eq!(block.as_u64(), 12);
+
+        provider.get_block_number().await.unwrap_err();
+
+        // 2 calls were made
+        mock.assert_request("eth_blockNumber", ()).unwrap();
+        mock.assert_request("eth_blockNumber", ()).unwrap();
+        mock.assert_request("eth_blockNumber", ()).unwrap_err();
+    }
+
+    #[tokio::test]
+    async fn build_declarative_middleware_stack() {
+        let (provider, mock) = Provider::mocked();
+
+        let signer = LocalWallet::new(&mut thread_rng());
+        let address = signer.address();
+        let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
+        let gas_oracle = EthGasStation::new(None);
+
+        let provider = provider
+            .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
+            .gas_oracle(gas_oracle)
+            .with_signer(signer)
+            .nonce_manager(address);
+
+        // push a response
+        mock.push(U64::from(12u64)).unwrap();
+        let block: U64 = provider.get_block_number().await.unwrap();
+        assert_eq!(block.as_u64(), 12);
+
+        provider.get_block_number().await.unwrap_err();
+
+        // 2 calls were made
+        mock.assert_request("eth_blockNumber", ()).unwrap();
+        mock.assert_request("eth_blockNumber", ()).unwrap();
+        mock.assert_request("eth_blockNumber", ()).unwrap_err();
+    }
+}