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 ## ethers-core
### Unreleased ### 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. - 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) - 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 - Remove rust_decimals dependency for ethers-core

View File

@ -4,22 +4,22 @@ middleware functionalities that you need.
## Available Middleware ## Available Middleware
- [`Signer`](./signer/struct.SignerMiddleware.html): Signs transactions locally, - [`Signer`](./signer/struct.SignerMiddleware.html): Signs transactions locally,
with a private key or a hardware wallet with a private key or a hardware wallet
- [`Nonce Manager`](./nonce_manager/struct.NonceManagerMiddleware.html): Manages - [`Nonce Manager`](./nonce_manager/struct.NonceManagerMiddleware.html): Manages
nonces locally, allowing the rapid broadcast of transactions without having to nonces locally, allowing the rapid broadcast of transactions without having to
wait for them to be submitted wait for them to be submitted
- [`Gas Escalator`](./gas_escalator/struct.GasEscalatorMiddleware.html): Bumps - [`Gas Escalator`](./gas_escalator/struct.GasEscalatorMiddleware.html): Bumps
transaction gas prices in the background transaction gas prices in the background
- [`Gas Oracle`](./gas_oracle/struct.GasOracleMiddleware.html): Allows getting - [`Gas Oracle`](./gas_oracle/struct.GasOracleMiddleware.html): Allows getting
your gas price estimates from places other than `eth_gasPrice`. your gas price estimates from places other than `eth_gasPrice`.
- [`Transformer`](./transformer/trait.Transformer.html): Allows intercepting and - [`Transformer`](./transformer/trait.Transformer.html): Allows intercepting and
transforming a transaction to be broadcasted via a proxy wallet, e.g. transforming a transaction to be broadcasted via a proxy wallet, e.g.
[`DSProxy`](./transformer/struct.DsProxy.html). [`DSProxy`](./transformer/struct.DsProxy.html).
## Example of a middleware stack ## Example of a middleware stack
```no_run ```rust no_run
use ethers_providers::{Provider, Http}; use ethers_providers::{Provider, Http};
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{ use ethers_middleware::{
@ -53,3 +53,43 @@ let provider = NonceManagerMiddleware::new(provider, address);
// ... do something with the provider // ... 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 /// before the chain tip
pub mod timelag; pub mod timelag;
pub use timelag::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();
}
}