fix(middleware): oracles, tests (#1944)

* fix(middleware): oracles, tests

* fix: use util

* fix: make blocknative api key optional

* make response fields public

* import orders

* docs: oracle trait

* fix: make response fns public

* chore: clippy

* fix: doc tests
This commit is contained in:
DaniPopes 2022-12-18 12:45:47 +01:00 committed by GitHub
parent 022f082cee
commit bb4af1c134
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 753 additions and 653 deletions

View File

@ -23,13 +23,13 @@ use ethers_providers::{Middleware, Provider, Http};
use std::sync::Arc; use std::sync::Arc;
use std::convert::TryFrom; use std::convert::TryFrom;
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{*,gas_oracle::*}; use ethers_middleware::{gas_oracle::{GasOracle, GasNow}, MiddlewareBuilder};
fn builder_example() { fn builder_example() {
let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
let signer = key.parse::<LocalWallet>().unwrap(); let signer = key.parse::<LocalWallet>().unwrap();
let address = signer.address(); let address = signer.address();
let gas_oracle = EthGasStation::new(None); let gas_oracle = GasNow::new();
let provider = Provider::<Http>::try_from("http://localhost:8545") let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap() .unwrap()
@ -58,7 +58,7 @@ fn builder_example_wrap_into() {
.unwrap() .unwrap()
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.wrap_into(|p| SignerMiddleware::new(p, signer)) .wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
.wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
} }
``` ```
@ -72,7 +72,7 @@ use ethers_providers::{Provider, Http};
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{ use ethers_middleware::{
gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency}, gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency},
gas_oracle::{GasOracleMiddleware, EthGasStation, GasCategory}, gas_oracle::{GasOracleMiddleware, GasCategory, GasNow},
signer::SignerMiddleware, signer::SignerMiddleware,
nonce_manager::NonceManagerMiddleware, nonce_manager::NonceManagerMiddleware,
}; };
@ -91,8 +91,8 @@ let signer = LocalWallet::new(&mut rand::thread_rng());
let address = signer.address(); let address = signer.address();
let provider = SignerMiddleware::new(provider, signer); let provider = SignerMiddleware::new(provider, signer);
// Use EthGasStation as the gas oracle // Use GasNow as the gas oracle
let gas_oracle = EthGasStation::new(None); let gas_oracle = GasNow::new();
let provider = GasOracleMiddleware::new(provider, gas_oracle); let provider = GasOracleMiddleware::new(provider, gas_oracle);
// Manage nonces locally // Manage nonces locally

View File

@ -16,14 +16,14 @@ use ethers_signers::Signer;
/// use std::sync::Arc; /// use std::sync::Arc;
/// use std::convert::TryFrom; /// use std::convert::TryFrom;
/// use ethers_signers::{LocalWallet, Signer}; /// use ethers_signers::{LocalWallet, Signer};
/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*}; /// use ethers_middleware::{*, gas_escalator::*, gas_oracle::*};
/// ///
/// fn builder_example() { /// fn builder_example() {
/// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; /// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
/// let signer = key.parse::<LocalWallet>().unwrap(); /// let signer = key.parse::<LocalWallet>().unwrap();
/// let address = signer.address(); /// let address = signer.address();
/// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>); /// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
/// let gas_oracle = EthGasStation::new(None); /// let gas_oracle = GasNow::new();
/// ///
/// let provider = Provider::<Http>::try_from("http://localhost:8545") /// let provider = Provider::<Http>::try_from("http://localhost:8545")
/// .unwrap() /// .unwrap()
@ -43,7 +43,7 @@ use ethers_signers::Signer;
/// .unwrap() /// .unwrap()
/// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) /// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
/// .wrap_into(|p| SignerMiddleware::new(p, signer)) /// .wrap_into(|p| SignerMiddleware::new(p, signer))
/// .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) /// .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
/// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer /// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
/// } /// }
/// ``` /// ```

View File

@ -46,7 +46,7 @@ pub enum Frequency {
/// use ethers_providers::{Provider, Http}; /// use ethers_providers::{Provider, Http};
/// use ethers_middleware::{ /// use ethers_middleware::{
/// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware}, /// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware},
/// gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware}, /// gas_oracle::{GasNow, GasCategory, GasOracleMiddleware},
/// }; /// };
/// use std::{convert::TryFrom, time::Duration, sync::Arc}; /// use std::{convert::TryFrom, time::Duration, sync::Arc};
/// ///
@ -60,7 +60,7 @@ pub enum Frequency {
/// }; /// };
/// ///
/// // ... proceed to wrap it in other middleware /// // ... proceed to wrap it in other middleware
/// let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); /// let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
/// let provider = GasOracleMiddleware::new(provider, gas_oracle); /// let provider = GasOracleMiddleware::new(provider, gas_oracle);
/// ``` /// ```
pub struct GasEscalatorMiddleware<M, E> { pub struct GasEscalatorMiddleware<M, E> {

View File

@ -1,13 +1,128 @@
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use reqwest::{header::AUTHORIZATION, Client}; use reqwest::{header::AUTHORIZATION, Client};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, convert::TryInto}; use std::collections::HashMap;
use url::Url; use url::Url;
const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices"; const URL: &str = "https://api.blocknative.com/gasprices/blockprices";
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
/// that implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[must_use]
pub struct BlockNative {
client: Client,
url: Url,
api_key: Option<String>,
gas_category: GasCategory,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Response {
pub system: String,
pub network: String,
pub unit: String,
pub max_price: u64,
pub block_prices: Vec<BlockPrice>,
pub estimated_base_fees: Option<Vec<HashMap<String, Vec<BaseFeeEstimate>>>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockPrice {
pub block_number: u64,
pub estimated_transaction_count: u64,
pub base_fee_per_gas: f64,
pub estimated_prices: Vec<GasEstimate>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GasEstimate {
pub confidence: u64,
pub price: u64,
pub max_priority_fee_per_gas: f64,
pub max_fee_per_gas: f64,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BaseFeeEstimate {
pub confidence: u64,
pub base_fee: f64,
}
impl Response {
#[inline]
pub fn estimate_from_category(&self, gas_category: &GasCategory) -> Result<GasEstimate> {
let confidence = gas_category_to_confidence(gas_category);
let price = self
.block_prices
.first()
.ok_or(GasOracleError::InvalidResponse)?
.estimated_prices
.iter()
.find(|p| p.confidence == confidence)
.ok_or(GasOracleError::GasCategoryNotSupported)?;
Ok(*price)
}
}
impl Default for BlockNative {
fn default() -> Self {
Self::new(None)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for BlockNative {
async fn fetch(&self) -> Result<U256> {
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
Ok(U256::from(estimate.price) * GWEI_TO_WEI_U256)
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
let max = from_gwei_f64(estimate.max_fee_per_gas);
let prio = from_gwei_f64(estimate.max_priority_fee_per_gas);
Ok((max, prio))
}
}
impl BlockNative {
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
pub fn new(api_key: Option<String>) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: Option<String>) -> Self {
let url = Url::parse(URL).unwrap();
Self { client, api_key, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response, GasOracleError> {
let mut request = self.client.get(self.url.clone());
if let Some(api_key) = self.api_key.as_ref() {
request = request.header(AUTHORIZATION, api_key);
}
let response = request.send().await?.error_for_status()?.json().await?;
Ok(response)
}
}
#[inline]
fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 { fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
match gas_category { match gas_category {
GasCategory::SafeLow => 80, GasCategory::SafeLow => 80,
@ -16,124 +131,3 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
GasCategory::Fastest => 99, GasCategory::Fastest => 99,
} }
} }
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)]
pub struct BlockNative {
client: Client,
url: Url,
api_key: String,
gas_category: GasCategory,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockNativeGasResponse {
system: Option<String>,
network: Option<String>,
unit: Option<String>,
max_price: Option<u64>,
block_prices: Vec<BlockPrice>,
estimated_base_fees: Vec<HashMap<String, Vec<BaseFeeEstimate>>>,
}
impl BlockNativeGasResponse {
pub fn get_estimation_for(
&self,
gas_category: &GasCategory,
) -> Result<EstimatedPrice, GasOracleError> {
let confidence = gas_category_to_confidence(gas_category);
Ok(self
.block_prices
.first()
.ok_or(GasOracleError::InvalidResponse)?
.estimated_prices
.iter()
.find(|p| p.confidence == confidence)
.ok_or(GasOracleError::GasCategoryNotSupported)?
.clone())
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockPrice {
block_number: u64,
estimated_transaction_count: u64,
base_fee_per_gas: f64,
estimated_prices: Vec<EstimatedPrice>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EstimatedPrice {
confidence: u64,
price: u64,
max_priority_fee_per_gas: f64,
max_fee_per_gas: f64,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BaseFeeEstimate {
confidence: u64,
base_fee: f64,
}
impl BlockNative {
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
pub fn new(api_key: String) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: String) -> Self {
Self {
client,
api_key,
url: BLOCKNATIVE_GAS_PRICE_ENDPOINT.try_into().unwrap(),
gas_category: GasCategory::Standard,
}
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform request to Blocknative, decode response
pub async fn request(&self) -> Result<BlockNativeGasResponse, GasOracleError> {
self.client
.get(self.url.as_ref())
.header(AUTHORIZATION, &self.api_key)
.send()
.await?
.error_for_status()?
.json()
.await
.map_err(GasOracleError::HttpClientError)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for BlockNative {
async fn fetch(&self) -> Result<U256, GasOracleError> {
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
Ok(U256::from(prices.price * 100_u64) * U256::from(GWEI_TO_WEI) / U256::from(100))
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
let base_fee = U256::from((prices.max_fee_per_gas * 100.0) as u64) *
U256::from(GWEI_TO_WEI) /
U256::from(100);
let prio_fee = U256::from((prices.max_priority_fee_per_gas * 100.0) as u64) *
U256::from(GWEI_TO_WEI) /
U256::from(100);
Ok((base_fee, prio_fee))
}
}

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use futures_locks::RwLock; use futures_locks::RwLock;
@ -19,6 +19,24 @@ pub struct Cache<T: GasOracle> {
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>); struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: GasOracle> GasOracle for Cache<T> {
async fn fetch(&self) -> Result<U256> {
self.fee.get(self.validity, || self.inner.fetch()).await
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
}
}
impl<T: GasOracle> Cache<T> {
pub fn new(validity: Duration, inner: T) -> Self {
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
}
}
impl<T: Clone> Cached<T> { impl<T: Clone> Cached<T> {
async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E> async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E>
where where
@ -50,21 +68,3 @@ impl<T: Clone> Cached<T> {
} }
} }
} }
impl<T: GasOracle> Cache<T> {
pub fn new(validity: Duration, inner: T) -> Self {
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: GasOracle> GasOracle for Cache<T> {
async fn fetch(&self) -> Result<U256, GasOracleError> {
self.fee.get(self.validity, || self.inner.fetch()).await
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
}
}

View File

@ -1,88 +1,78 @@
use std::collections::HashMap; #![allow(deprecated)]
use ethers_core::types::U256;
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; const URL: &str = "https://ethgasstation.info/api/ethgasAPI.json";
const ETH_GAS_STATION_URL_PREFIX: &str = "https://ethgasstation.info/api/ethgasAPI.json"; /// A client over HTTP for the [EthGasStation](https://ethgasstation.info) gas tracker API
/// that implements the `GasOracle` trait.
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[deprecated = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
#[must_use]
pub struct EthGasStation { pub struct EthGasStation {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
/// Eth Gas Station's response for the current recommended fast, standard and /// Eth Gas Station's response for the current recommended fast, standard and
/// safe low gas prices on the Ethereum network, along with the current block /// safe low gas prices on the Ethereum network, along with the current block
/// and wait times for each "speed". /// and wait times for each "speed".
pub struct EthGasStationResponse { #[derive(Clone, Debug, Deserialize, PartialEq)]
/// Recommended safe(expected to be mined in < 30 minutes) gas price in #[serde(rename_all = "camelCase")]
/// x10 Gwei (divide by 10 to convert it to gwei) pub struct Response {
pub safe_low: f64, /// Recommended safe (expected to be mined in < 30 minutes).
/// Recommended average(expected to be mined in < 5 minutes) gas price in ///
/// x10 Gwei (divide by 10 to convert it to gwei) /// In gwei * 10 (divide by 10 to convert it to gwei).
pub safe_low: u64,
/// Recommended average (expected to be mined in < 5 minutes).
///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub average: u64, pub average: u64,
/// Recommended fast(expected to be mined in < 2 minutes) gas price in /// Recommended fast (expected to be mined in < 2 minutes).
/// x10 Gwei (divide by 10 to convert it to gwei) ///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fast: u64, pub fast: u64,
/// Recommended fastest(expected to be mined in < 30 seconds) gas price /// Recommended fastest (expected to be mined in < 30 seconds).
/// in x10 Gwei(divide by 10 to convert it to gwei) ///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fastest: u64, pub fastest: u64,
// post eip-1559 fields // post eip-1559 fields
/// Average time (in seconds) to mine a single block.
#[serde(rename = "block_time")] // inconsistent json response naming... #[serde(rename = "block_time")] // inconsistent json response naming...
/// Average time(in seconds) to mine one single block
pub block_time: f64, pub block_time: f64,
/// The latest block number /// The latest block number.
pub block_num: u64, pub block_num: u64,
/// Smallest value of (gasUsed / gaslimit) from last 10 blocks /// Smallest value of `gasUsed / gaslimit` from the last 10 blocks.
pub speed: f64, pub speed: f64,
/// Waiting time(in minutes) for the `safe_low` gas price /// Waiting time (in minutes) for the `safe_low` gas price.
pub safe_low_wait: f64, pub safe_low_wait: f64,
/// Waiting time(in minutes) for the `average` gas price /// Waiting time (in minutes) for the `average` gas price.
pub avg_wait: f64, pub avg_wait: f64,
/// Waiting time(in minutes) for the `fast` gas price /// Waiting time (in minutes) for the `fast` gas price.
pub fast_wait: f64, pub fast_wait: f64,
/// Waiting time(in minutes) for the `fastest` gas price /// Waiting time (in minutes) for the `fastest` gas price.
pub fastest_wait: f64, pub fastest_wait: f64,
// What is this? // What is this?
pub gas_price_range: HashMap<u64, f64>, pub gas_price_range: HashMap<u64, f64>,
} }
impl EthGasStation { impl Response {
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle #[inline]
pub fn new(api_key: Option<&str>) -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
Self::with_client(Client::new(), api_key) match gas_category {
} GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.average,
/// Same as [`Self::new`] but with a custom [`Client`]. GasCategory::Fast => self.fast,
pub fn with_client(client: Client, api_key: Option<&str>) -> Self { GasCategory::Fastest => self.fastest,
let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url");
if let Some(key) = api_key {
url.query_pairs_mut().append_pair("api-key", key);
} }
EthGasStation { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<EthGasStationResponse, GasOracleError> {
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EthGasStationResponse>().await?)
} }
} }
@ -95,19 +85,43 @@ impl Default for EthGasStation {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for EthGasStation { impl GasOracle for EthGasStation {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10), // gas_price is in `gwei * 10`
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10), Ok(U256::from(gas_price) * GWEI_TO_WEI_U256 / U256::from(10_u64))
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl EthGasStation {
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle.
pub fn new(api_key: Option<&str>) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: Option<&str>) -> Self {
let mut url = Url::parse(URL).unwrap();
if let Some(key) = api_key {
url.query_pairs_mut().append_pair("api-key", key);
}
EthGasStation { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,56 +1,42 @@
use ethers_core::types::U256; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; const URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
const ETHERCHAIN_URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
/// A client over HTTP for the [Etherchain](https://www.etherchain.org/api/gasPriceOracle) gas tracker API /// A client over HTTP for the [Etherchain](https://www.etherchain.org/api/gasPriceOracle) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Etherchain { pub struct Etherchain {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EtherchainResponse { pub struct Response {
pub safe_low: f32, pub safe_low: f64,
pub standard: f32, pub standard: f64,
pub fast: f32, pub fast: f64,
pub fastest: f32, pub fastest: f64,
pub current_base_fee: f32, pub current_base_fee: f64,
pub recommended_base_fee: f32, pub recommended_base_fee: f64,
} }
impl Etherchain { impl Response {
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle. #[inline]
pub fn new() -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> f64 {
Self::with_client(Client::new()) match gas_category {
} GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
/// Same as [`Self::new`] but with a custom [`Client`]. GasCategory::Fast => self.fast,
pub fn with_client(client: Client) -> Self { GasCategory::Fastest => self.fastest,
let url = Url::parse(ETHERCHAIN_URL).expect("invalid url"); }
Etherchain { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<EtherchainResponse, GasOracleError> {
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EtherchainResponse>().await?)
} }
} }
@ -63,19 +49,39 @@ impl Default for Etherchain {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Etherchain { impl GasOracle for Etherchain {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI), Ok(from_gwei_f64(gas_price))
GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI),
GasCategory::Fast => U256::from((res.fast as u64) * GWEI_TO_WEI),
GasCategory::Fastest => U256::from((res.fastest as u64) * GWEI_TO_WEI),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl Etherchain {
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle.
pub fn new() -> Self {
Self::with_client(Client::new())
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client) -> Self {
let url = Url::parse(URL).unwrap();
Etherchain { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,18 +1,57 @@
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use ethers_etherscan::Client; use ethers_etherscan::Client;
use std::ops::{Deref, DerefMut};
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
/// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API /// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Etherscan { pub struct Etherscan {
client: Client, client: Client,
gas_category: GasCategory, gas_category: GasCategory,
} }
impl Deref for Etherscan {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl DerefMut for Etherscan {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Etherscan {
async fn fetch(&self) -> Result<U256> {
// handle unsupported gas categories before making the request
match self.gas_category {
GasCategory::SafeLow | GasCategory::Standard | GasCategory::Fast => {}
GasCategory::Fastest => return Err(GasOracleError::GasCategoryNotSupported),
}
let result = self.query().await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => result.safe_gas_price,
GasCategory::Standard => result.propose_gas_price,
GasCategory::Fast => result.fast_gas_price,
_ => unreachable!(),
};
Ok(U256::from(gas_price) * GWEI_TO_WEI_U256)
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
}
impl Etherscan { impl Etherscan {
/// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle. /// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle.
pub fn new(client: Client) -> Self { pub fn new(client: Client) -> Self {
@ -20,32 +59,13 @@ impl Etherscan {
} }
/// Sets the gas price category to be used when fetching the gas price. /// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self { pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category; self.gas_category = gas_category;
self self
} }
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] /// Perform a request to the gas price API and deserialize the response.
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub async fn query(&self) -> Result<ethers_etherscan::gas::GasOracle> {
impl GasOracle for Etherscan { Ok(self.client.gas_oracle().await?)
async fn fetch(&self) -> Result<U256, GasOracleError> {
if matches!(self.gas_category, GasCategory::Fastest) {
return Err(GasOracleError::GasCategoryNotSupported)
}
let result = self.client.gas_oracle().await?;
match self.gas_category {
GasCategory::SafeLow => Ok(U256::from(result.safe_gas_price * GWEI_TO_WEI)),
GasCategory::Standard => Ok(U256::from(result.propose_gas_price * GWEI_TO_WEI)),
GasCategory::Fast => Ok(U256::from(result.fast_gas_price * GWEI_TO_WEI)),
_ => Err(GasOracleError::GasCategoryNotSupported),
}
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }

View File

@ -1,64 +1,54 @@
use ethers_core::types::U256; use super::{GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; const URL: &str = "https://beaconcha.in/api/v1/execution/gasnow";
const GAS_NOW_URL: &str = "https://etherchain.org/api/gasnow"; /// A client over HTTP for the [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas tracker API
/// that implements the `GasOracle` trait.
/// A client over HTTP for the [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct GasNow { pub struct GasNow {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Deserialize)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
struct GasNowResponseWrapper { pub struct Response {
data: GasNowResponse, pub code: u64,
pub data: ResponseData,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct GasNowResponse { pub struct ResponseData {
pub rapid: u64, pub rapid: u64,
pub fast: u64, pub fast: u64,
pub standard: u64, pub standard: u64,
pub slow: u64, pub slow: u64,
pub timestamp: u64,
#[serde(rename = "priceUSD")]
pub price_usd: f64,
} }
impl GasNow { impl Response {
/// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle. #[inline]
pub fn new() -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
Self::with_client(Client::new()) self.data.gas_from_category(gas_category)
} }
}
/// Same as [`Self::new`] but with a custom [`Client`]. impl ResponseData {
pub fn with_client(client: Client) -> Self { fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
let url = Url::parse(GAS_NOW_URL).expect("invalid url"); match gas_category {
GasCategory::SafeLow => self.slow,
Self { client, url, gas_category: GasCategory::Standard } GasCategory::Standard => self.standard,
} GasCategory::Fast => self.fast,
GasCategory::Fastest => self.rapid,
/// Sets the gas price category to be used when fetching the gas price. }
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<GasNowResponse, GasOracleError> {
let res = self
.client
.get(self.url.as_ref())
.send()
.await?
.json::<GasNowResponseWrapper>()
.await?;
Ok(res.data)
} }
} }
@ -71,19 +61,39 @@ impl Default for GasNow {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for GasNow { impl GasOracle for GasNow {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from(res.slow), Ok(U256::from(gas_price))
GasCategory::Standard => U256::from(res.standard),
GasCategory::Fast => U256::from(res.fast),
GasCategory::Fastest => U256::from(res.rapid),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl GasNow {
/// Creates a new [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas price oracle.
pub fn new() -> Self {
Self::with_client(Client::new())
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client) -> Self {
let url = Url::parse(URL).unwrap();
Self { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use futures_util::future::join_all; use futures_util::future::join_all;
@ -28,13 +28,10 @@ impl Median {
self.oracles.push((weight, Box::new(oracle))); self.oracles.push((weight, Box::new(oracle)));
} }
pub async fn query_all<'a, Fn, Fut, O>( pub async fn query_all<'a, Fn, Fut, O>(&'a self, mut f: Fn) -> Result<Vec<(f32, O)>>
&'a self,
mut f: Fn,
) -> Result<Vec<(f32, O)>, GasOracleError>
where where
Fn: FnMut(&'a dyn GasOracle) -> Fut, Fn: FnMut(&'a dyn GasOracle) -> Fut,
Fut: Future<Output = Result<O, GasOracleError>>, Fut: Future<Output = Result<O>>,
{ {
// Process the oracles in parallel // Process the oracles in parallel
let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref())); let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref()));
@ -62,13 +59,13 @@ impl Median {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Median { impl GasOracle for Median {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let mut values = self.query_all(|oracle| oracle.fetch()).await?; let mut values = self.query_all(|oracle| oracle.fetch()).await?;
// `query_all` guarantees `values` is not empty // `query_all` guarantees `values` is not empty
Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap()) Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap())
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?; let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?;
// `query_all` guarantees `values` is not empty // `query_all` guarantees `values` is not empty
Ok(( Ok((

View File

@ -4,8 +4,8 @@ use ethers_core::types::{transaction::eip2718::TypedTransaction, *};
use ethers_providers::{FromErr, Middleware, PendingTransaction}; use ethers_providers::{FromErr, Middleware, PendingTransaction};
use thiserror::Error; use thiserror::Error;
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`.
#[derive(Debug)] #[derive(Debug)]
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`
pub struct GasOracleMiddleware<M, G> { pub struct GasOracleMiddleware<M, G> {
inner: M, inner: M,
gas_oracle: G, gas_oracle: G,
@ -21,7 +21,7 @@ where
} }
} }
#[derive(Error, Debug)] #[derive(Debug, Error)]
pub enum MiddlewareError<M: Middleware> { pub enum MiddlewareError<M: Middleware> {
#[error(transparent)] #[error(transparent)]
GasOracleError(#[from] GasOracleError), GasOracleError(#[from] GasOracleError),

View File

@ -1,54 +1,58 @@
mod blocknative; pub mod blocknative;
pub use blocknative::BlockNative; pub use blocknative::BlockNative;
mod eth_gas_station; pub mod eth_gas_station;
#[allow(deprecated)]
pub use eth_gas_station::EthGasStation; pub use eth_gas_station::EthGasStation;
mod etherchain; pub mod etherchain;
pub use etherchain::Etherchain; pub use etherchain::Etherchain;
mod etherscan; pub mod etherscan;
pub use etherscan::Etherscan; pub use etherscan::Etherscan;
mod middleware; pub mod middleware;
pub use middleware::{GasOracleMiddleware, MiddlewareError}; pub use middleware::{GasOracleMiddleware, MiddlewareError};
mod median; pub mod median;
pub use median::Median; pub use median::Median;
mod cache; pub mod cache;
pub use cache::Cache; pub use cache::Cache;
mod polygon; pub mod polygon;
pub use polygon::Polygon; pub use polygon::Polygon;
mod gas_now; pub mod gas_now;
pub use gas_now::GasNow; pub use gas_now::GasNow;
mod provider_oracle; pub mod provider_oracle;
pub use provider_oracle::ProviderOracle; pub use provider_oracle::ProviderOracle;
use ethers_core::types::U256;
use async_trait::async_trait; use async_trait::async_trait;
use auto_impl::auto_impl; use auto_impl::auto_impl;
use ethers_core::types::U256;
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
use std::error::Error; use std::{error::Error, fmt::Debug};
use thiserror::Error; use thiserror::Error;
const GWEI_TO_WEI: u64 = 1000000000; pub(crate) const GWEI_TO_WEI: u64 = 1_000_000_000;
pub(crate) const GWEI_TO_WEI_U256: U256 = U256([0, 0, 0, GWEI_TO_WEI]);
/// Various gas price categories. Choose one of the available pub type Result<T, E = GasOracleError> = std::result::Result<T, E>;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
/// Generic [`GasOracle`] gas price categories.
#[derive(Clone, Copy, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum GasCategory { pub enum GasCategory {
SafeLow, SafeLow,
#[default]
Standard, Standard,
Fast, Fast,
Fastest, Fastest,
} }
#[derive(Error, Debug)] /// Error thrown by a [`GasOracle`].
/// Error thrown when fetching data from the `GasOracle` #[derive(Debug, Error)]
pub enum GasOracleError { pub enum GasOracleError {
/// An internal error in the HTTP request made from the underlying /// An internal error in the HTTP request made from the underlying
/// gas oracle /// gas oracle
@ -83,48 +87,69 @@ pub enum GasOracleError {
UnsupportedChain, UnsupportedChain,
/// Error thrown when the provider failed. /// Error thrown when the provider failed.
#[error("Chain is not supported by the oracle")] #[error("Provider error: {0}")]
ProviderError(#[from] Box<dyn Error + Send + Sync>), ProviderError(#[from] Box<dyn Error + Send + Sync>),
} }
/// `GasOracle` is a trait that an underlying gas oracle needs to implement. /// An Ethereum gas price oracle.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// use ethers_middleware::{ /// use ethers_core::types::U256;
/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle}, /// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
/// };
/// ///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> { /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key")); /// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); /// let gas_price = oracle.fetch().await?;
/// /// assert!(gas_price > U256::zero());
/// let data_1 = eth_gas_station_oracle.fetch().await?;
/// let data_2 = etherscan_oracle.fetch().await?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[auto_impl(&, Box, Arc)] #[auto_impl(&, Box, Arc)]
pub trait GasOracle: Send + Sync + std::fmt::Debug { pub trait GasOracle: Send + Sync + Debug {
/// Makes an asynchronous HTTP query to the underlying `GasOracle` /// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current gas
/// price estimate.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use ethers_middleware::{ /// use ethers_core::types::U256;
/// gas_oracle::{Etherchain, GasCategory, GasOracle}, /// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
/// };
/// ///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> { /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let etherchain_oracle = Etherchain::new().category(GasCategory::Fastest); /// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let data = etherchain_oracle.fetch().await?; /// let gas_price = oracle.fetch().await?;
/// assert!(gas_price > U256::zero());
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
async fn fetch(&self) -> Result<U256, GasOracleError>; async fn fetch(&self) -> Result<U256>;
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>; /// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current max
/// gas fee and priority gas fee estimates.
///
/// # Example
///
/// ```no_run
/// use ethers_core::types::U256;
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let (max_fee, priority_fee) = oracle.estimate_eip1559_fees().await?;
/// assert!(max_fee > U256::zero());
/// assert!(priority_fee > U256::zero());
/// # Ok(())
/// # }
/// ```
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)>;
}
#[inline]
#[doc(hidden)]
pub(crate) fn from_gwei_f64(gwei: f64) -> U256 {
ethers_core::types::u256_from_f64_saturating(gwei) * GWEI_TO_WEI_U256
} }

View File

@ -1,16 +1,17 @@
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::{u256_from_f64_saturating, Chain, U256}; use ethers_core::types::{Chain, U256};
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2"; const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/v2";
const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2"; const MUMBAI_URL: &str = "https://gasstation-mumbai.matic.today/v2";
/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API /// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
/// Queries over HTTP and implements the `GasOracle` trait /// Queries over HTTP and implements the `GasOracle` trait.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Polygon { pub struct Polygon {
client: Client, client: Client,
url: Url, url: Url,
@ -18,74 +19,87 @@ pub struct Polygon {
} }
/// The response from the Polygon gas station API. /// The response from the Polygon gas station API.
///
/// Gas prices are in Gwei. /// Gas prices are in Gwei.
#[derive(Debug, Deserialize, PartialEq)] #[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Response { pub struct Response {
estimated_base_fee: f64, pub estimated_base_fee: f64,
safe_low: GasEstimate, pub safe_low: GasEstimate,
standard: GasEstimate, pub standard: GasEstimate,
fast: GasEstimate, pub fast: GasEstimate,
} }
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GasEstimate { pub struct GasEstimate {
max_priority_fee: f64, pub max_priority_fee: f64,
max_fee: f64, pub max_fee: f64,
} }
impl Polygon { impl Response {
pub fn new(chain: Chain) -> Result<Self, GasOracleError> { #[inline]
Self::with_client(Client::new(), chain) pub fn estimate_from_category(&self, gas_category: GasCategory) -> GasEstimate {
match gas_category {
GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.fast,
}
} }
}
pub fn with_client(client: Client, chain: Chain) -> Result<Self, GasOracleError> { impl Default for Polygon {
// TODO: Sniff chain from chain id. fn default() -> Self {
let url = match chain { Self::new(Chain::Polygon).unwrap()
Chain::Polygon => Url::parse(GAS_PRICE_ENDPOINT).unwrap(),
Chain::PolygonMumbai => Url::parse(MUMBAI_GAS_PRICE_ENDPOINT).unwrap(),
_ => return Err(GasOracleError::UnsupportedChain),
};
Ok(Self { client, url, gas_category: GasCategory::Standard })
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform request to Blocknative, decode response
pub async fn request(&self) -> Result<(f64, GasEstimate), GasOracleError> {
let response: Response =
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
let estimate = match self.gas_category {
GasCategory::SafeLow => response.safe_low,
GasCategory::Standard => response.standard,
GasCategory::Fast => response.fast,
GasCategory::Fastest => response.fast,
};
Ok((response.estimated_base_fee, estimate))
} }
} }
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Polygon { impl GasOracle for Polygon {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let (base_fee, estimate) = self.request().await?; let response = self.query().await?;
let fee = base_fee + estimate.max_priority_fee; let base = response.estimated_base_fee;
Ok(from_gwei(fee)) let prio = response.estimate_from_category(self.gas_category).max_priority_fee;
let fee = base + prio;
Ok(from_gwei_f64(fee))
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let (_, estimate) = self.request().await?; let response = self.query().await?;
Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee))) let estimate = response.estimate_from_category(self.gas_category);
let max = from_gwei_f64(estimate.max_fee);
let prio = from_gwei_f64(estimate.max_priority_fee);
Ok((max, prio))
} }
} }
fn from_gwei(gwei: f64) -> U256 { impl Polygon {
u256_from_f64_saturating(gwei * 1.0e9_f64) pub fn new(chain: Chain) -> Result<Self> {
Self::with_client(Client::new(), chain)
}
pub fn with_client(client: Client, chain: Chain) -> Result<Self> {
// TODO: Sniff chain from chain id.
let url = match chain {
Chain::Polygon => MAINNET_URL,
Chain::PolygonMumbai => MUMBAI_URL,
_ => return Err(GasOracleError::UnsupportedChain),
};
Ok(Self { client, url: Url::parse(url).unwrap(), gas_category: GasCategory::Standard })
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
} }

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use ethers_providers::Middleware; use ethers_providers::Middleware;
@ -6,7 +6,8 @@ use std::fmt::Debug;
/// Gas oracle from a [`Middleware`] implementation such as an /// Gas oracle from a [`Middleware`] implementation such as an
/// Ethereum RPC provider. /// Ethereum RPC provider.
#[derive(Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct ProviderOracle<M: Middleware> { pub struct ProviderOracle<M: Middleware> {
provider: M, provider: M,
} }
@ -21,16 +22,16 @@ impl<M: Middleware> ProviderOracle<M> {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<M: Middleware> GasOracle for ProviderOracle<M> impl<M: Middleware> GasOracle for ProviderOracle<M>
where where
<M as Middleware>::Error: 'static, M::Error: 'static,
{ {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
self.provider self.provider
.get_gas_price() .get_gas_price()
.await .await
.map_err(|err| GasOracleError::ProviderError(Box::new(err))) .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
// TODO: Allow configuring different estimation functions. // TODO: Allow configuring different estimation functions.
self.provider self.provider
.estimate_eip1559_fees(None) .estimate_eip1559_fees(None)

View File

@ -1,22 +1,21 @@
use ethers_contract::Lazy; use ethers_contract::Lazy;
use ethers_core::types::*; use ethers_core::types::*;
use std::{collections::HashMap, str::FromStr}; use std::collections::HashMap;
/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding /// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding
/// DsProxyFactory contract addresses as values /// DsProxyFactory contract addresses as values
pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| { pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
let mut m = HashMap::new(); let mut m = HashMap::with_capacity(1);
// mainnet // mainnet
let addr = let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap();
Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed"); m.insert(U256::from(1_u64), addr);
m.insert(U256::from(1u8), addr);
m m
}); });
/// Generated with abigen:
/// ///
/// Generated with
/// ```ignore /// ```ignore
/// # use ethers_contract::abigen; /// # use ethers_contract::abigen;
/// abigen!(DsProxyFactory, /// abigen!(DsProxyFactory,
@ -26,7 +25,6 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
/// } /// }
/// ); /// );
/// ``` /// ```
// Auto-generated type-safe bindings
pub use dsproxyfactory_mod::*; pub use dsproxyfactory_mod::*;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
mod dsproxyfactory_mod { mod dsproxyfactory_mod {

View File

@ -1,69 +1,67 @@
#![cfg(not(target_arch = "wasm32"))] #![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] use ethers_core::{rand::thread_rng, types::U64};
async fn build_raw_middleware_stack() { use ethers_middleware::{
let (provider, mock) = Provider::mocked(); builder::MiddlewareBuilder,
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{GasNow, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
let signer = LocalWallet::new(&mut thread_rng()); #[tokio::test]
let address = signer.address(); async fn build_raw_middleware_stack() {
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); let (provider, mock) = Provider::mocked();
let provider = provider let signer = LocalWallet::new(&mut thread_rng());
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) let address = signer.address();
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
.wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
// push a response let provider = provider
mock.push(U64::from(12u64)).unwrap(); .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
let block: U64 = provider.get_block_number().await.unwrap(); .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
assert_eq!(block.as_u64(), 12); .wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
provider.get_block_number().await.unwrap_err(); // push a response
mock.push(U64::from(12u64)).unwrap();
let block: U64 = provider.get_block_number().await.unwrap();
assert_eq!(block.as_u64(), 12);
// 2 calls were made provider.get_block_number().await.unwrap_err();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
#[tokio::test] // 2 calls were made
async fn build_declarative_middleware_stack() { mock.assert_request("eth_blockNumber", ()).unwrap();
let (provider, mock) = Provider::mocked(); mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
let signer = LocalWallet::new(&mut thread_rng()); }
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); #[tokio::test]
let gas_oracle = EthGasStation::new(None); async fn build_declarative_middleware_stack() {
let (provider, mock) = Provider::mocked();
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) let signer = LocalWallet::new(&mut thread_rng());
.gas_oracle(gas_oracle) let address = signer.address();
.with_signer(signer) let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
.nonce_manager(address); let gas_oracle = GasNow::new();
// push a response let provider = provider
mock.push(U64::from(12u64)).unwrap(); .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
let block: U64 = provider.get_block_number().await.unwrap(); .gas_oracle(gas_oracle)
assert_eq!(block.as_u64(), 12); .with_signer(signer)
.nonce_manager(address);
provider.get_block_number().await.unwrap_err();
// push a response
// 2 calls were made mock.push(U64::from(12u64)).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap(); let block: U64 = provider.get_block_number().await.unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap(); assert_eq!(block.as_u64(), 12);
mock.assert_request("eth_blockNumber", ()).unwrap_err();
} 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();
} }

View File

@ -1,4 +1,5 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
use ethers_core::types::*; use ethers_core::types::*;
use ethers_middleware::{ use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},

View File

@ -1,16 +1,14 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
use std::convert::TryFrom;
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::{types::*, utils::Anvil}; use ethers_core::{types::*, utils::Anvil};
use ethers_middleware::gas_oracle::{ use ethers_middleware::gas_oracle::{
EthGasStation, Etherchain, Etherscan, GasCategory, GasOracle, GasOracleError, BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError,
GasOracleMiddleware, GasOracleMiddleware, Polygon, ProviderOracle, Result,
}; };
use ethers_providers::{Http, Middleware, Provider}; use ethers_providers::{Http, Middleware, Provider};
use serial_test::serial; use serial_test::serial;
use std::convert::TryFrom;
#[derive(Debug)] #[derive(Debug)]
struct FakeGasOracle { struct FakeGasOracle {
@ -20,17 +18,18 @@ struct FakeGasOracle {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for FakeGasOracle { impl GasOracle for FakeGasOracle {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
Ok(self.gas_price) Ok(self.gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
#[tokio::test] #[tokio::test]
async fn using_gas_oracle() { #[serial]
async fn provider_using_gas_oracle() {
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
let from = anvil.addresses()[0]; let from = anvil.addresses()[0];
@ -38,11 +37,11 @@ async fn using_gas_oracle() {
// connect to the network // connect to the network
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap(); let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
// initial base fee
let base_fee = 1_000_000_000u64;
// assign a gas oracle to use // assign a gas oracle to use
let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() }; let expected_gas_price = U256::from(1234567890_u64);
let expected_gas_price = gas_oracle.fetch().await.unwrap(); let gas_oracle = FakeGasOracle { gas_price: expected_gas_price };
let gas_price = gas_oracle.fetch().await.unwrap();
assert_eq!(gas_price, expected_gas_price);
let provider = GasOracleMiddleware::new(provider, gas_oracle); let provider = GasOracleMiddleware::new(provider, gas_oracle);
@ -55,35 +54,70 @@ async fn using_gas_oracle() {
} }
#[tokio::test] #[tokio::test]
async fn eth_gas_station() { #[serial]
// initialize and fetch gas estimates from EthGasStation async fn provider_oracle() {
let eth_gas_station_oracle = EthGasStation::default(); // spawn anvil and connect to it
let data = eth_gas_station_oracle.fetch().await; let anvil = Anvil::new().spawn();
data.unwrap(); let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
// assert that provider.get_gas_price() and oracle.fetch() return the same value
let expected_gas_price = provider.get_gas_price().await.unwrap();
let provider_oracle = ProviderOracle::new(provider);
let gas = provider_oracle.fetch().await.unwrap();
assert_eq!(gas, expected_gas_price);
}
#[tokio::test]
async fn blocknative() {
let gas_now_oracle = BlockNative::default();
let gas_price = gas_now_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[tokio::test]
#[ignore = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
#[allow(deprecated)]
async fn eth_gas_station() {
let eth_gas_station_oracle = ethers_middleware::gas_oracle::EthGasStation::default();
let gas_price = eth_gas_station_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[tokio::test]
#[ignore = "Etherchain / beaconcha.in's `gasPriceOracle` API currently returns 404: https://www.etherchain.org/api/gasPriceOracle"]
async fn etherchain() {
let etherchain_oracle = Etherchain::default();
let gas_price = etherchain_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
} }
#[tokio::test] #[tokio::test]
#[serial]
async fn etherscan() { async fn etherscan() {
let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap(); let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap();
// initialize and fetch gas estimates from Etherscan // initialize and fetch gas estimates from Etherscan
// since etherscan does not support `fastest` category, we expect an error // since etherscan does not support `fastest` category, we expect an error
let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest); let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest);
let data = etherscan_oracle.fetch().await; let error = etherscan_oracle.fetch().await.unwrap_err();
data.unwrap_err(); assert!(matches!(error, GasOracleError::GasCategoryNotSupported));
// but fetching the `standard` gas price should work fine // but fetching the `standard` gas price should work fine
let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow); let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow);
let data = etherscan_oracle.fetch().await; let gas_price = etherscan_oracle.fetch().await.unwrap();
data.unwrap(); assert!(gas_price > U256::zero());
} }
#[tokio::test] #[tokio::test]
async fn etherchain() { async fn gas_now() {
// initialize and fetch gas estimates from Etherchain let gas_now_oracle = GasNow::default();
let etherchain_oracle = Etherchain::default().category(GasCategory::Fast); let gas_price = gas_now_oracle.fetch().await.unwrap();
let data = etherchain_oracle.fetch().await; assert!(gas_price > U256::zero());
data.unwrap(); }
#[tokio::test]
async fn polygon() {
let polygon_oracle = Polygon::default();
let gas_price = polygon_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
} }

View File

@ -1,18 +1,18 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn nonce_manager() {
use ethers_core::types::*;
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer};
use std::time::Duration;
use ethers_core::types::*;
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer};
use std::time::Duration;
#[tokio::test]
async fn nonce_manager() {
let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000u64)); let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000u64));
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let wallet = std::env::var("GOERLI_PRIVATE_KEY") let wallet = std::env::var("GOERLI_PRIVATE_KEY")
.unwrap() .expect("GOERLI_PRIVATE_KEY is not defined")
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap() .unwrap()
.with_chain_id(chain_id); .with_chain_id(chain_id);

View File

@ -1,14 +1,22 @@
#![allow(unused)] #![allow(unused)]
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_contract::ContractFactory;
use ethers_core::{ use ethers_core::{
types::{BlockNumber, TransactionRequest}, abi::Abi,
utils::parse_units, types::*,
utils::{parse_ether, parse_units, Anvil},
}; };
use ethers_middleware::signer::SignerMiddleware; use ethers_middleware::signer::SignerMiddleware;
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
use ethers_solc::Solc;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration}; use std::{
convert::TryFrom,
iter::Cycle,
sync::{atomic::AtomicU8, Arc},
time::Duration,
};
static WALLETS: Lazy<TestWallets> = Lazy::new(|| { static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
TestWallets { TestWallets {
@ -22,8 +30,6 @@ static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn send_eth() { async fn send_eth() {
use ethers_core::utils::Anvil;
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
// this private key belongs to the above mnemonic // this private key belongs to the above mnemonic
@ -95,10 +101,6 @@ async fn pending_txs_with_confirmations_testnet() {
generic_pending_txs_test(provider, address).await; generic_pending_txs_test(provider, address).await;
} }
#[cfg(not(feature = "celo"))]
use ethers_core::types::{Address, Eip1559TransactionRequest};
use ethers_core::utils::parse_ether;
// different keys to avoid nonce errors // different keys to avoid nonce errors
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
@ -195,8 +197,6 @@ async fn test_send_transaction() {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn send_transaction_handles_tx_from_field() { async fn send_transaction_handles_tx_from_field() {
use ethers_core::utils::Anvil;
// launch anvil // launch anvil
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
@ -240,14 +240,6 @@ async fn send_transaction_handles_tx_from_field() {
#[tokio::test] #[tokio::test]
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
async fn deploy_and_call_contract() { async fn deploy_and_call_contract() {
use ethers_contract::ContractFactory;
use ethers_core::{
abi::Abi,
types::{BlockNumber, Bytes, H256, U256},
};
use ethers_solc::Solc;
use std::sync::Arc;
// compiles the given contract and returns the ABI and Bytecode // compiles the given contract and returns the ABI and Bytecode
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
let path = format!("./tests/solidity-contracts/{path}"); let path = format!("./tests/solidity-contracts/{path}");

View File

@ -1,95 +1,93 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#[cfg(not(feature = "celo"))]
mod tests {
use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
use std::convert::TryFrom;
#[tokio::test] use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
async fn mock_with_middleware() { use ethers_middleware::{
let (provider, mock) = Provider::mocked(); gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{GasCategory, GasNow, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
use std::convert::TryFrom;
// add a bunch of middlewares #[tokio::test]
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); async fn mock_with_middleware() {
let signer = LocalWallet::new(&mut thread_rng()); let (provider, mock) = Provider::mocked();
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = GasOracleMiddleware::new(provider, gas_oracle);
let provider = SignerMiddleware::new(provider, signer);
let provider = NonceManagerMiddleware::new(provider, address);
// push a response // add a bunch of middlewares
use ethers_core::types::U64; let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
mock.push(U64::from(12u64)).unwrap(); let signer = LocalWallet::new(&mut thread_rng());
let blk = provider.get_block_number().await.unwrap(); let address = signer.address();
assert_eq!(blk.as_u64(), 12); let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = GasOracleMiddleware::new(provider, gas_oracle);
let provider = SignerMiddleware::new(provider, signer);
let provider = NonceManagerMiddleware::new(provider, address);
// now that the response is gone, there's nothing left // push a response
// TODO: This returns: use ethers_core::types::U64;
// MiddlewareError( mock.push(U64::from(12u64)).unwrap();
// MiddlewareError( let blk = provider.get_block_number().await.unwrap();
// MiddlewareError( assert_eq!(blk.as_u64(), 12);
// MiddlewareError(
// JsonRpcClientError(EmptyResponses)
// ))))
// Can we flatten it in any way? Maybe inherent to the middleware
// infrastructure
provider.get_block_number().await.unwrap_err();
// 2 calls were made // now that the response is gone, there's nothing left
mock.assert_request("eth_blockNumber", ()).unwrap(); // TODO: This returns:
mock.assert_request("eth_blockNumber", ()).unwrap(); // MiddlewareError(
mock.assert_request("eth_blockNumber", ()).unwrap_err(); // MiddlewareError(
} // MiddlewareError(
// MiddlewareError(
// JsonRpcClientError(EmptyResponses)
// ))))
// Can we flatten it in any way? Maybe inherent to the middleware
// infrastructure
provider.get_block_number().await.unwrap_err();
#[tokio::test] // 2 calls were made
async fn can_stack_middlewares() { mock.assert_request("eth_blockNumber", ()).unwrap();
let anvil = Anvil::new().block_time(5u64).spawn(); mock.assert_request("eth_blockNumber", ()).unwrap();
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); mock.assert_request("eth_blockNumber", ()).unwrap_err();
let signer: LocalWallet = anvil.keys()[0].clone().into(); }
let address = signer.address();
#[tokio::test]
// the base provider async fn can_stack_middlewares() {
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap()); let anvil = Anvil::new().block_time(5u64).spawn();
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
let signer = signer.with_chain_id(chain_id); let signer: LocalWallet = anvil.keys()[0].clone().into();
let address = signer.address();
// the Gas Price escalator middleware is the first middleware above the provider,
// so that it receives the transaction last, after all the other middleware // the base provider
// have modified it accordingly let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); let signer = signer.with_chain_id(chain_id);
// The gas price middleware MUST be below the signing middleware for things to work // the Gas Price escalator middleware is the first middleware above the provider,
let provider = GasOracleMiddleware::new(provider, gas_oracle); // so that it receives the transaction last, after all the other middleware
// have modified it accordingly
// The signing middleware signs txs let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
use std::sync::Arc; let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = Arc::new(SignerMiddleware::new(provider, signer));
// The gas price middleware MUST be below the signing middleware for things to work
// The nonce manager middleware MUST be above the signing middleware so that it overrides let provider = GasOracleMiddleware::new(provider, gas_oracle);
// the nonce and the signer does not make any eth_getTransaction count calls
let provider = NonceManagerMiddleware::new(provider, address); // The signing middleware signs txs
use std::sync::Arc;
let tx = TransactionRequest::new(); let provider = Arc::new(SignerMiddleware::new(provider, signer));
let mut pending_txs = Vec::new();
for _ in 0..10 { // The nonce manager middleware MUST be above the signing middleware so that it overrides
let pending = provider.send_transaction(tx.clone(), None).await.unwrap(); // the nonce and the signer does not make any eth_getTransaction count calls
let hash = *pending; let provider = NonceManagerMiddleware::new(provider, address);
let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
dbg!(gas_price); let tx = TransactionRequest::new();
pending_txs.push(pending); let mut pending_txs = Vec::new();
} for _ in 0..10 {
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
let receipts = futures_util::future::join_all(pending_txs); let hash = *pending;
dbg!(receipts.await); let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
} dbg!(gas_price);
pending_txs.push(pending);
}
let receipts = futures_util::future::join_all(pending_txs);
dbg!(receipts.await);
} }

View File

@ -1,5 +1,5 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#![allow(unused)]
use ethers_contract::{BaseContract, ContractFactory}; use ethers_contract::{BaseContract, ContractFactory};
use ethers_core::{abi::Abi, types::*, utils::Anvil}; use ethers_core::{abi::Abi, types::*, utils::Anvil};
use ethers_middleware::{ use ethers_middleware::{
@ -24,7 +24,6 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
} }
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_transformer() { async fn ds_proxy_transformer() {
// randomness // randomness
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -83,7 +82,6 @@ async fn ds_proxy_transformer() {
} }
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_code() { async fn ds_proxy_code() {
// randomness // randomness
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();