Feat middleware stack builder (#1890)

* ProviderBuilder struct + docs

* README

* Docs typos

* Removed useless Arc
Rename module

* Unit tests

* cargo +nightly fmt

* CHANGELOG

* CHANGELOG typo

* ci: fix docs job

* review: Removed Option<M>. Builder functions directly consume self.

* feat: `with_signer` builder function

* feat: `nonce_manager` builder function

* feat: `gas_oracle` builder function

* Unit tests

* Docs

* fix: cargo +nightly fmt

* Update ethers-middleware/src/builder.rs

Typo

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>

* Builder functions provided as a trait
Update docs
Update tests

* cargo +nightly fmt

* CHANGELOG

Co-authored-by: Andrea Simeoni <>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Andrea Simeoni 2022-11-28 21:16:21 +01:00 committed by GitHub
parent ed47eaadad
commit 16ab3d5820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 219 additions and 14 deletions

View File

@ -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

View File

@ -19,7 +19,7 @@ middleware functionalities that you need.
## 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
}
```

View File

@ -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 {}

View File

@ -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;

View File

@ -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();
}
}