diff --git a/ethers-middleware/README.md b/ethers-middleware/README.md index 97d37ab2..75a9b328 100644 --- a/ethers-middleware/README.md +++ b/ethers-middleware/README.md @@ -23,13 +23,13 @@ use ethers_providers::{Middleware, Provider, Http}; use std::sync::Arc; use std::convert::TryFrom; use ethers_signers::{LocalWallet, Signer}; -use ethers_middleware::{*,gas_oracle::*}; +use ethers_middleware::{gas_oracle::{GasOracle, GasNow}, MiddlewareBuilder}; fn builder_example() { let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; let signer = key.parse::().unwrap(); let address = signer.address(); - let gas_oracle = EthGasStation::new(None); + let gas_oracle = GasNow::new(); let provider = Provider::::try_from("http://localhost:8545") .unwrap() @@ -58,7 +58,7 @@ fn builder_example_wrap_into() { .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| GasOracleMiddleware::new(p, GasNow::new())) .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_middleware::{ gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency}, - gas_oracle::{GasOracleMiddleware, EthGasStation, GasCategory}, + gas_oracle::{GasOracleMiddleware, GasCategory, GasNow}, signer::SignerMiddleware, nonce_manager::NonceManagerMiddleware, }; @@ -91,8 +91,8 @@ let signer = LocalWallet::new(&mut rand::thread_rng()); let address = signer.address(); let provider = SignerMiddleware::new(provider, signer); -// Use EthGasStation as the gas oracle -let gas_oracle = EthGasStation::new(None); +// Use GasNow as the gas oracle +let gas_oracle = GasNow::new(); let provider = GasOracleMiddleware::new(provider, gas_oracle); // Manage nonces locally diff --git a/ethers-middleware/src/builder.rs b/ethers-middleware/src/builder.rs index b1827407..f824add9 100644 --- a/ethers-middleware/src/builder.rs +++ b/ethers-middleware/src/builder.rs @@ -16,14 +16,14 @@ use ethers_signers::Signer; /// use std::sync::Arc; /// use std::convert::TryFrom; /// use ethers_signers::{LocalWallet, Signer}; -/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*}; +/// use ethers_middleware::{*, gas_escalator::*, gas_oracle::*}; /// /// fn builder_example() { /// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; /// let signer = key.parse::().unwrap(); /// let address = signer.address(); /// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::); -/// let gas_oracle = EthGasStation::new(None); +/// let gas_oracle = GasNow::new(); /// /// let provider = Provider::::try_from("http://localhost:8545") /// .unwrap() @@ -43,7 +43,7 @@ use ethers_signers::Signer; /// .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| GasOracleMiddleware::new(p, GasNow::new())) /// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer /// } /// ``` diff --git a/ethers-middleware/src/gas_escalator/mod.rs b/ethers-middleware/src/gas_escalator/mod.rs index a71adbf3..f3ab1840 100644 --- a/ethers-middleware/src/gas_escalator/mod.rs +++ b/ethers-middleware/src/gas_escalator/mod.rs @@ -46,7 +46,7 @@ pub enum Frequency { /// use ethers_providers::{Provider, Http}; /// use ethers_middleware::{ /// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware}, -/// gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware}, +/// gas_oracle::{GasNow, GasCategory, GasOracleMiddleware}, /// }; /// use std::{convert::TryFrom, time::Duration, sync::Arc}; /// @@ -60,7 +60,7 @@ pub enum Frequency { /// }; /// /// // ... 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); /// ``` pub struct GasEscalatorMiddleware { diff --git a/ethers-middleware/src/gas_oracle/blocknative.rs b/ethers-middleware/src/gas_oracle/blocknative.rs index 11832c5a..84b037c0 100644 --- a/ethers-middleware/src/gas_oracle/blocknative.rs +++ b/ethers-middleware/src/gas_oracle/blocknative.rs @@ -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 ethers_core::types::U256; use reqwest::{header::AUTHORIZATION, Client}; use serde::Deserialize; -use std::{collections::HashMap, convert::TryInto}; +use std::collections::HashMap; 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, + 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, + pub estimated_base_fees: Option>>>, +} + +#[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, +} + +#[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 { + 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 { + 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) -> 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) -> 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 { + 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 { match gas_category { GasCategory::SafeLow => 80, @@ -16,124 +131,3 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 { 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, - network: Option, - unit: Option, - max_price: Option, - block_prices: Vec, - estimated_base_fees: Vec>>, -} - -impl BlockNativeGasResponse { - pub fn get_estimation_for( - &self, - gas_category: &GasCategory, - ) -> Result { - 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, -} - -#[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 { - 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 { - 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)) - } -} diff --git a/ethers-middleware/src/gas_oracle/cache.rs b/ethers-middleware/src/gas_oracle/cache.rs index 6f8b162b..d840cb0f 100644 --- a/ethers-middleware/src/gas_oracle/cache.rs +++ b/ethers-middleware/src/gas_oracle/cache.rs @@ -1,4 +1,4 @@ -use crate::gas_oracle::{GasOracle, GasOracleError}; +use super::{GasOracle, Result}; use async_trait::async_trait; use ethers_core::types::U256; use futures_locks::RwLock; @@ -19,6 +19,24 @@ pub struct Cache { #[derive(Default, Debug)] struct Cached(RwLock>); +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GasOracle for Cache { + async fn fetch(&self) -> Result { + 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 Cache { + pub fn new(validity: Duration, inner: T) -> Self { + Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() } + } +} + impl Cached { async fn get(&self, validity: Duration, fetch: F) -> Result where @@ -50,21 +68,3 @@ impl Cached { } } } - -impl Cache { - 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 GasOracle for Cache { - async fn fetch(&self) -> Result { - 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 - } -} diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index 683caf37..73f1572c 100644 --- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs +++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs @@ -1,88 +1,78 @@ -use std::collections::HashMap; - -use ethers_core::types::U256; +#![allow(deprecated)] +use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256}; use async_trait::async_trait; +use ethers_core::types::U256; use reqwest::Client; use serde::Deserialize; +use std::collections::HashMap; 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/api/ethgasAPI.json) gas tracker API -/// that implements the `GasOracle` trait +/// A client over HTTP for the [EthGasStation](https://ethgasstation.info) gas tracker API +/// that implements the `GasOracle` trait. #[derive(Clone, Debug)] +#[deprecated = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"] +#[must_use] pub struct EthGasStation { client: Client, url: Url, gas_category: GasCategory, } -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] /// 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 /// and wait times for each "speed". -pub struct EthGasStationResponse { - /// Recommended safe(expected to be mined in < 30 minutes) gas price in - /// x10 Gwei (divide by 10 to convert it to gwei) - pub safe_low: f64, - /// Recommended average(expected to be mined in < 5 minutes) gas price in - /// x10 Gwei (divide by 10 to convert it to gwei) +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Response { + /// Recommended safe (expected to be mined in < 30 minutes). + /// + /// 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, - /// Recommended fast(expected to be mined in < 2 minutes) gas price in - /// x10 Gwei (divide by 10 to convert it to gwei) + /// Recommended fast (expected to be mined in < 2 minutes). + /// + /// In gwei * 10 (divide by 10 to convert it to gwei). pub fast: u64, - /// Recommended fastest(expected to be mined in < 30 seconds) gas price - /// in x10 Gwei(divide by 10 to convert it to gwei) + /// Recommended fastest (expected to be mined in < 30 seconds). + /// + /// In gwei * 10 (divide by 10 to convert it to gwei). pub fastest: u64, // post eip-1559 fields + /// Average time (in seconds) to mine a single block. #[serde(rename = "block_time")] // inconsistent json response naming... - /// Average time(in seconds) to mine one single block pub block_time: f64, - /// The latest block number + /// The latest block number. 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, - /// 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, - /// Waiting time(in minutes) for the `average` gas price + /// Waiting time (in minutes) for the `average` gas price. 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, - /// Waiting time(in minutes) for the `fastest` gas price + /// Waiting time (in minutes) for the `fastest` gas price. pub fastest_wait: f64, // What is this? pub gas_price_range: HashMap, } -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(ETH_GAS_STATION_URL_PREFIX).expect("invalid url"); - if let Some(key) = api_key { - url.query_pairs_mut().append_pair("api-key", key); +impl Response { + #[inline] + pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 { + match gas_category { + GasCategory::SafeLow => self.safe_low, + GasCategory::Standard => self.average, + GasCategory::Fast => self.fast, + GasCategory::Fastest => self.fastest, } - 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 { - Ok(self.client.get(self.url.as_ref()).send().await?.json::().await?) } } @@ -95,19 +85,43 @@ impl Default for EthGasStation { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for EthGasStation { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { let res = self.query().await?; - let gas_price = match self.gas_category { - GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10), - GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10), - GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10), - GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10), - }; - - Ok(gas_price) + let gas_price = res.gas_from_category(self.gas_category); + // gas_price is in `gwei * 10` + Ok(U256::from(gas_price) * GWEI_TO_WEI_U256 / U256::from(10_u64)) } - async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { 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 { + let response = + self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?; + Ok(response) + } +} diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs index 3fc94e6a..5d419929 100644 --- a/ethers-middleware/src/gas_oracle/etherchain.rs +++ b/ethers-middleware/src/gas_oracle/etherchain.rs @@ -1,56 +1,42 @@ -use ethers_core::types::U256; - +use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result}; use async_trait::async_trait; +use ethers_core::types::U256; use reqwest::Client; use serde::Deserialize; use url::Url; -use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; - -const ETHERCHAIN_URL: &str = "https://www.etherchain.org/api/gasPriceOracle"; +const URL: &str = "https://www.etherchain.org/api/gasPriceOracle"; /// 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)] +#[must_use] pub struct Etherchain { client: Client, url: Url, gas_category: GasCategory, } -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct EtherchainResponse { - pub safe_low: f32, - pub standard: f32, - pub fast: f32, - pub fastest: f32, - pub current_base_fee: f32, - pub recommended_base_fee: f32, +pub struct Response { + pub safe_low: f64, + pub standard: f64, + pub fast: f64, + pub fastest: f64, + pub current_base_fee: f64, + pub recommended_base_fee: f64, } -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(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 { - Ok(self.client.get(self.url.as_ref()).send().await?.json::().await?) +impl Response { + #[inline] + pub fn gas_from_category(&self, gas_category: GasCategory) -> f64 { + match gas_category { + GasCategory::SafeLow => self.safe_low, + GasCategory::Standard => self.standard, + GasCategory::Fast => self.fast, + GasCategory::Fastest => self.fastest, + } } } @@ -63,19 +49,39 @@ impl Default for Etherchain { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for Etherchain { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { let res = self.query().await?; - let gas_price = match self.gas_category { - GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI), - 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) + let gas_price = res.gas_from_category(self.gas_category); + Ok(from_gwei_f64(gas_price)) } - async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { 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 { + let response = + self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?; + Ok(response) + } +} diff --git a/ethers-middleware/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs index b39cebff..9d8da4c0 100644 --- a/ethers-middleware/src/gas_oracle/etherscan.rs +++ b/ethers-middleware/src/gas_oracle/etherscan.rs @@ -1,18 +1,57 @@ +use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256}; use async_trait::async_trait; - use ethers_core::types::U256; use ethers_etherscan::Client; - -use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; +use std::ops::{Deref, DerefMut}; /// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API /// that implements the `GasOracle` trait #[derive(Clone, Debug)] +#[must_use] pub struct Etherscan { client: Client, 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 { + // 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 { /// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle. 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. - #[must_use] pub fn category(mut self, gas_category: GasCategory) -> Self { self.gas_category = gas_category; self } -} -#[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 { - 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) + /// Perform a request to the gas price API and deserialize the response. + pub async fn query(&self) -> Result { + Ok(self.client.gas_oracle().await?) } } diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index 65ddc46c..29de2c46 100644 --- a/ethers-middleware/src/gas_oracle/gas_now.rs +++ b/ethers-middleware/src/gas_oracle/gas_now.rs @@ -1,64 +1,54 @@ -use ethers_core::types::U256; - +use super::{GasCategory, GasOracle, GasOracleError, Result}; use async_trait::async_trait; +use ethers_core::types::U256; use reqwest::Client; use serde::Deserialize; 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 [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API -/// that implements the `GasOracle` trait +/// A client over HTTP for the [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas tracker API +/// that implements the `GasOracle` trait. #[derive(Clone, Debug)] +#[must_use] pub struct GasNow { client: Client, url: Url, gas_category: GasCategory, } -#[derive(Deserialize)] -struct GasNowResponseWrapper { - data: GasNowResponse, +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub struct Response { + pub code: u64, + pub data: ResponseData, } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct GasNowResponse { +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub struct ResponseData { pub rapid: u64, pub fast: u64, pub standard: u64, pub slow: u64, + pub timestamp: u64, + #[serde(rename = "priceUSD")] + pub price_usd: f64, } -impl GasNow { - /// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle. - pub fn new() -> Self { - Self::with_client(Client::new()) +impl Response { + #[inline] + pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 { + self.data.gas_from_category(gas_category) } +} - /// Same as [`Self::new`] but with a custom [`Client`]. - pub fn with_client(client: Client) -> Self { - let url = Url::parse(GAS_NOW_URL).expect("invalid url"); - - 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 - } - - pub async fn query(&self) -> Result { - let res = self - .client - .get(self.url.as_ref()) - .send() - .await? - .json::() - .await?; - Ok(res.data) +impl ResponseData { + fn gas_from_category(&self, gas_category: GasCategory) -> u64 { + match gas_category { + GasCategory::SafeLow => self.slow, + GasCategory::Standard => self.standard, + GasCategory::Fast => self.fast, + GasCategory::Fastest => self.rapid, + } } } @@ -71,19 +61,39 @@ impl Default for GasNow { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for GasNow { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { let res = self.query().await?; - let gas_price = match self.gas_category { - GasCategory::SafeLow => U256::from(res.slow), - GasCategory::Standard => U256::from(res.standard), - GasCategory::Fast => U256::from(res.fast), - GasCategory::Fastest => U256::from(res.rapid), - }; - - Ok(gas_price) + let gas_price = res.gas_from_category(self.gas_category); + Ok(U256::from(gas_price)) } - async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { 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 { + let response = + self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?; + Ok(response) + } +} diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs index 2149ce41..c6dd6c7e 100644 --- a/ethers-middleware/src/gas_oracle/median.rs +++ b/ethers-middleware/src/gas_oracle/median.rs @@ -1,4 +1,4 @@ -use crate::gas_oracle::{GasOracle, GasOracleError}; +use super::{GasOracle, GasOracleError, Result}; use async_trait::async_trait; use ethers_core::types::U256; use futures_util::future::join_all; @@ -28,13 +28,10 @@ impl Median { self.oracles.push((weight, Box::new(oracle))); } - pub async fn query_all<'a, Fn, Fut, O>( - &'a self, - mut f: Fn, - ) -> Result, GasOracleError> + pub async fn query_all<'a, Fn, Fut, O>(&'a self, mut f: Fn) -> Result> where Fn: FnMut(&'a dyn GasOracle) -> Fut, - Fut: Future>, + Fut: Future>, { // Process the oracles in parallel 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(not(target_arch = "wasm32"), async_trait)] impl GasOracle for Median { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { let mut values = self.query_all(|oracle| oracle.fetch()).await?; // `query_all` guarantees `values` is not empty 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?; // `query_all` guarantees `values` is not empty Ok(( diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs index 272b0150..918ec4b0 100644 --- a/ethers-middleware/src/gas_oracle/middleware.rs +++ b/ethers-middleware/src/gas_oracle/middleware.rs @@ -4,8 +4,8 @@ use ethers_core::types::{transaction::eip2718::TypedTransaction, *}; use ethers_providers::{FromErr, Middleware, PendingTransaction}; use thiserror::Error; +/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`. #[derive(Debug)] -/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice` pub struct GasOracleMiddleware { inner: M, gas_oracle: G, @@ -21,7 +21,7 @@ where } } -#[derive(Error, Debug)] +#[derive(Debug, Error)] pub enum MiddlewareError { #[error(transparent)] GasOracleError(#[from] GasOracleError), diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index 4a7a701c..9be17750 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -1,54 +1,58 @@ -mod blocknative; +pub mod blocknative; pub use blocknative::BlockNative; -mod eth_gas_station; +pub mod eth_gas_station; +#[allow(deprecated)] pub use eth_gas_station::EthGasStation; -mod etherchain; +pub mod etherchain; pub use etherchain::Etherchain; -mod etherscan; +pub mod etherscan; pub use etherscan::Etherscan; -mod middleware; +pub mod middleware; pub use middleware::{GasOracleMiddleware, MiddlewareError}; -mod median; +pub mod median; pub use median::Median; -mod cache; +pub mod cache; pub use cache::Cache; -mod polygon; +pub mod polygon; pub use polygon::Polygon; -mod gas_now; +pub mod gas_now; pub use gas_now::GasNow; -mod provider_oracle; +pub mod provider_oracle; pub use provider_oracle::ProviderOracle; -use ethers_core::types::U256; - use async_trait::async_trait; use auto_impl::auto_impl; +use ethers_core::types::U256; use reqwest::Error as ReqwestError; -use std::error::Error; +use std::{error::Error, fmt::Debug}; 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 -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub type Result = std::result::Result; + +/// Generic [`GasOracle`] gas price categories. +#[derive(Clone, Copy, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum GasCategory { SafeLow, + #[default] Standard, Fast, Fastest, } -#[derive(Error, Debug)] -/// Error thrown when fetching data from the `GasOracle` +/// Error thrown by a [`GasOracle`]. +#[derive(Debug, Error)] pub enum GasOracleError { /// An internal error in the HTTP request made from the underlying /// gas oracle @@ -83,48 +87,69 @@ pub enum GasOracleError { UnsupportedChain, /// Error thrown when the provider failed. - #[error("Chain is not supported by the oracle")] + #[error("Provider error: {0}")] ProviderError(#[from] Box), } -/// `GasOracle` is a trait that an underlying gas oracle needs to implement. +/// An Ethereum gas price oracle. /// /// # Example /// /// ```no_run -/// use ethers_middleware::{ -/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle}, -/// }; +/// use ethers_core::types::U256; +/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle}; /// /// # async fn foo() -> Result<(), Box> { -/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key")); -/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); -/// -/// let data_1 = eth_gas_station_oracle.fetch().await?; -/// let data_2 = etherscan_oracle.fetch().await?; +/// let oracle = GasNow::default().category(GasCategory::SafeLow); +/// let gas_price = oracle.fetch().await?; +/// assert!(gas_price > U256::zero()); /// # Ok(()) /// # } /// ``` #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[auto_impl(&, Box, Arc)] -pub trait GasOracle: Send + Sync + std::fmt::Debug { - /// Makes an asynchronous HTTP query to the underlying `GasOracle` +pub trait GasOracle: Send + Sync + Debug { + /// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current gas + /// price estimate. /// /// # Example /// - /// ``` - /// use ethers_middleware::{ - /// gas_oracle::{Etherchain, GasCategory, GasOracle}, - /// }; + /// ```no_run + /// use ethers_core::types::U256; + /// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle}; /// /// # async fn foo() -> Result<(), Box> { - /// let etherchain_oracle = Etherchain::new().category(GasCategory::Fastest); - /// let data = etherchain_oracle.fetch().await?; + /// let oracle = GasNow::default().category(GasCategory::SafeLow); + /// let gas_price = oracle.fetch().await?; + /// assert!(gas_price > U256::zero()); /// # Ok(()) /// # } /// ``` - async fn fetch(&self) -> Result; + async fn fetch(&self) -> Result; - 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> { + /// 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 } diff --git a/ethers-middleware/src/gas_oracle/polygon.rs b/ethers-middleware/src/gas_oracle/polygon.rs index 47a3415d..5fd63545 100644 --- a/ethers-middleware/src/gas_oracle/polygon.rs +++ b/ethers-middleware/src/gas_oracle/polygon.rs @@ -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 ethers_core::types::{u256_from_f64_saturating, Chain, U256}; +use ethers_core::types::{Chain, U256}; use reqwest::Client; use serde::Deserialize; use url::Url; -const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2"; -const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2"; +const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/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 -/// Queries over HTTP and implements the `GasOracle` trait +/// Queries over HTTP and implements the `GasOracle` trait. #[derive(Clone, Debug)] +#[must_use] pub struct Polygon { client: Client, url: Url, @@ -18,74 +19,87 @@ pub struct Polygon { } /// The response from the Polygon gas station API. +/// /// Gas prices are in Gwei. #[derive(Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Response { - estimated_base_fee: f64, - safe_low: GasEstimate, - standard: GasEstimate, - fast: GasEstimate, + pub estimated_base_fee: f64, + pub safe_low: GasEstimate, + pub standard: GasEstimate, + pub fast: GasEstimate, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct GasEstimate { - max_priority_fee: f64, - max_fee: f64, + pub max_priority_fee: f64, + pub max_fee: f64, } -impl Polygon { - pub fn new(chain: Chain) -> Result { - Self::with_client(Client::new(), chain) +impl Response { + #[inline] + 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 { - // TODO: Sniff chain from chain id. - let url = match chain { - 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)) +impl Default for Polygon { + fn default() -> Self { + Self::new(Chain::Polygon).unwrap() } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for Polygon { - async fn fetch(&self) -> Result { - let (base_fee, estimate) = self.request().await?; - let fee = base_fee + estimate.max_priority_fee; - Ok(from_gwei(fee)) + async fn fetch(&self) -> Result { + let response = self.query().await?; + let base = response.estimated_base_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> { - let (_, estimate) = self.request().await?; - Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee))) + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { + let response = self.query().await?; + 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 { - u256_from_f64_saturating(gwei * 1.0e9_f64) +impl Polygon { + pub fn new(chain: Chain) -> Result { + Self::with_client(Client::new(), chain) + } + + pub fn with_client(client: Client, chain: Chain) -> Result { + // 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 { + let response = + self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?; + Ok(response) + } } diff --git a/ethers-middleware/src/gas_oracle/provider_oracle.rs b/ethers-middleware/src/gas_oracle/provider_oracle.rs index 894bb5ab..c97c857d 100644 --- a/ethers-middleware/src/gas_oracle/provider_oracle.rs +++ b/ethers-middleware/src/gas_oracle/provider_oracle.rs @@ -1,4 +1,4 @@ -use crate::gas_oracle::{GasOracle, GasOracleError}; +use super::{GasOracle, GasOracleError, Result}; use async_trait::async_trait; use ethers_core::types::U256; use ethers_providers::Middleware; @@ -6,7 +6,8 @@ use std::fmt::Debug; /// Gas oracle from a [`Middleware`] implementation such as an /// Ethereum RPC provider. -#[derive(Debug)] +#[derive(Clone, Debug)] +#[must_use] pub struct ProviderOracle { provider: M, } @@ -21,16 +22,16 @@ impl ProviderOracle { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for ProviderOracle where - ::Error: 'static, + M::Error: 'static, { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { self.provider .get_gas_price() .await .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. self.provider .estimate_eip1559_fees(None) diff --git a/ethers-middleware/src/transformer/ds_proxy/factory.rs b/ethers-middleware/src/transformer/ds_proxy/factory.rs index 11fb1380..122c7bf1 100644 --- a/ethers-middleware/src/transformer/ds_proxy/factory.rs +++ b/ethers-middleware/src/transformer/ds_proxy/factory.rs @@ -1,22 +1,21 @@ use ethers_contract::Lazy; 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 /// DsProxyFactory contract addresses as values pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { - let mut m = HashMap::new(); + let mut m = HashMap::with_capacity(1); // mainnet - let addr = - Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed"); - m.insert(U256::from(1u8), addr); + let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap(); + m.insert(U256::from(1_u64), addr); m }); +/// Generated with abigen: /// -/// Generated with /// ```ignore /// # use ethers_contract::abigen; /// abigen!(DsProxyFactory, @@ -26,7 +25,6 @@ pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { /// } /// ); /// ``` -// Auto-generated type-safe bindings pub use dsproxyfactory_mod::*; #[allow(clippy::too_many_arguments)] mod dsproxyfactory_mod { diff --git a/ethers-middleware/tests/builder.rs b/ethers-middleware/tests/builder.rs index 7c988bb8..d99c603e 100644 --- a/ethers-middleware/tests/builder.rs +++ b/ethers-middleware/tests/builder.rs @@ -1,69 +1,67 @@ #![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(); +use ethers_core::{rand::thread_rng, types::U64}; +use ethers_middleware::{ + 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()); - let address = signer.address(); - let escalator = GeometricGasPrice::new(1.125, 60u64, None::); +#[tokio::test] +async fn build_raw_middleware_stack() { + let (provider, mock) = Provider::mocked(); - 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)); + let signer = LocalWallet::new(&mut thread_rng()); + let address = signer.address(); + let escalator = GeometricGasPrice::new(1.125, 60u64, None::); - // push a response - mock.push(U64::from(12u64)).unwrap(); - let block: U64 = provider.get_block_number().await.unwrap(); - assert_eq!(block.as_u64(), 12); + let provider = provider + .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) + .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new())) + .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 - mock.assert_request("eth_blockNumber", ()).unwrap(); - mock.assert_request("eth_blockNumber", ()).unwrap(); - mock.assert_request("eth_blockNumber", ()).unwrap_err(); - } + provider.get_block_number().await.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::); - 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(); - } + // 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::); + let gas_oracle = GasNow::new(); + + 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(); } diff --git a/ethers-middleware/tests/gas_escalator.rs b/ethers-middleware/tests/gas_escalator.rs index a9eff59c..c2bfbad6 100644 --- a/ethers-middleware/tests/gas_escalator.rs +++ b/ethers-middleware/tests/gas_escalator.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] + use ethers_core::types::*; use ethers_middleware::{ gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs index 10db9ba0..9464fb3d 100644 --- a/ethers-middleware/tests/gas_oracle.rs +++ b/ethers-middleware/tests/gas_oracle.rs @@ -1,16 +1,14 @@ #![cfg(not(target_arch = "wasm32"))] -use std::convert::TryFrom; - use async_trait::async_trait; - use ethers_core::{types::*, utils::Anvil}; use ethers_middleware::gas_oracle::{ - EthGasStation, Etherchain, Etherscan, GasCategory, GasOracle, GasOracleError, - GasOracleMiddleware, + BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError, + GasOracleMiddleware, Polygon, ProviderOracle, Result, }; use ethers_providers::{Http, Middleware, Provider}; use serial_test::serial; +use std::convert::TryFrom; #[derive(Debug)] struct FakeGasOracle { @@ -20,17 +18,18 @@ struct FakeGasOracle { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for FakeGasOracle { - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { 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) } } #[tokio::test] -async fn using_gas_oracle() { +#[serial] +async fn provider_using_gas_oracle() { let anvil = Anvil::new().spawn(); let from = anvil.addresses()[0]; @@ -38,11 +37,11 @@ async fn using_gas_oracle() { // connect to the network let provider = Provider::::try_from(anvil.endpoint()).unwrap(); - // initial base fee - let base_fee = 1_000_000_000u64; // assign a gas oracle to use - let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() }; - let expected_gas_price = gas_oracle.fetch().await.unwrap(); + let expected_gas_price = U256::from(1234567890_u64); + 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); @@ -55,35 +54,70 @@ async fn using_gas_oracle() { } #[tokio::test] -async fn eth_gas_station() { - // initialize and fetch gas estimates from EthGasStation - let eth_gas_station_oracle = EthGasStation::default(); - let data = eth_gas_station_oracle.fetch().await; - data.unwrap(); +#[serial] +async fn provider_oracle() { + // spawn anvil and connect to it + let anvil = Anvil::new().spawn(); + let provider = Provider::::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] -#[serial] async fn etherscan() { let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap(); // initialize and fetch gas estimates from Etherscan // since etherscan does not support `fastest` category, we expect an error let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest); - let data = etherscan_oracle.fetch().await; - data.unwrap_err(); + let error = etherscan_oracle.fetch().await.unwrap_err(); + assert!(matches!(error, GasOracleError::GasCategoryNotSupported)); // but fetching the `standard` gas price should work fine let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow); - let data = etherscan_oracle.fetch().await; - data.unwrap(); + let gas_price = etherscan_oracle.fetch().await.unwrap(); + assert!(gas_price > U256::zero()); } #[tokio::test] -async fn etherchain() { - // initialize and fetch gas estimates from Etherchain - let etherchain_oracle = Etherchain::default().category(GasCategory::Fast); - let data = etherchain_oracle.fetch().await; - data.unwrap(); +async fn gas_now() { + let gas_now_oracle = GasNow::default(); + let gas_price = gas_now_oracle.fetch().await.unwrap(); + assert!(gas_price > U256::zero()); +} + +#[tokio::test] +async fn polygon() { + let polygon_oracle = Polygon::default(); + let gas_price = polygon_oracle.fetch().await.unwrap(); + assert!(gas_price > U256::zero()); } diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs index ce7bac22..a1c74915 100644 --- a/ethers-middleware/tests/nonce_manager.rs +++ b/ethers-middleware/tests/nonce_manager.rs @@ -1,18 +1,18 @@ -#![cfg(not(target_arch = "wasm32"))] -#[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; +#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] +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 chain_id = provider.get_chainid().await.unwrap().as_u64(); let wallet = std::env::var("GOERLI_PRIVATE_KEY") - .unwrap() + .expect("GOERLI_PRIVATE_KEY is not defined") .parse::() .unwrap() .with_chain_id(chain_id); diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs index a7c16a8c..9c2176ed 100644 --- a/ethers-middleware/tests/signer.rs +++ b/ethers-middleware/tests/signer.rs @@ -1,14 +1,22 @@ #![allow(unused)] -use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI}; +use ethers_contract::ContractFactory; use ethers_core::{ - types::{BlockNumber, TransactionRequest}, - utils::parse_units, + abi::Abi, + types::*, + utils::{parse_ether, parse_units, Anvil}, }; use ethers_middleware::signer::SignerMiddleware; +use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI}; use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; +use ethers_solc::Solc; 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 = Lazy::new(|| { TestWallets { @@ -22,8 +30,6 @@ static WALLETS: Lazy = Lazy::new(|| { #[tokio::test] #[cfg(not(feature = "celo"))] async fn send_eth() { - use ethers_core::utils::Anvil; - let anvil = Anvil::new().spawn(); // 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; } -#[cfg(not(feature = "celo"))] -use ethers_core::types::{Address, Eip1559TransactionRequest}; -use ethers_core::utils::parse_ether; - // different keys to avoid nonce errors #[tokio::test] #[cfg(not(feature = "celo"))] @@ -195,8 +197,6 @@ async fn test_send_transaction() { #[tokio::test] #[cfg(not(feature = "celo"))] async fn send_transaction_handles_tx_from_field() { - use ethers_core::utils::Anvil; - // launch anvil let anvil = Anvil::new().spawn(); @@ -240,14 +240,6 @@ async fn send_transaction_handles_tx_from_field() { #[tokio::test] #[cfg(feature = "celo")] 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 fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { let path = format!("./tests/solidity-contracts/{path}"); diff --git a/ethers-middleware/tests/stack.rs b/ethers-middleware/tests/stack.rs index c7cb1ea2..bf70056f 100644 --- a/ethers-middleware/tests/stack.rs +++ b/ethers-middleware/tests/stack.rs @@ -1,95 +1,93 @@ -#![cfg(not(target_arch = "wasm32"))] -#[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; +#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] - #[tokio::test] - async fn mock_with_middleware() { - let (provider, mock) = Provider::mocked(); +use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil}; +use ethers_middleware::{ + 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 - let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); - let signer = LocalWallet::new(&mut thread_rng()); - let address = signer.address(); - let escalator = GeometricGasPrice::new(1.125, 60u64, None::); - 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); +#[tokio::test] +async fn mock_with_middleware() { + let (provider, mock) = Provider::mocked(); - // push a response - use ethers_core::types::U64; - mock.push(U64::from(12u64)).unwrap(); - let blk = provider.get_block_number().await.unwrap(); - assert_eq!(blk.as_u64(), 12); + // add a bunch of middlewares + let gas_oracle = GasNow::new().category(GasCategory::SafeLow); + let signer = LocalWallet::new(&mut thread_rng()); + let address = signer.address(); + let escalator = GeometricGasPrice::new(1.125, 60u64, None::); + 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 - // TODO: This returns: - // MiddlewareError( - // 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(); + // push a response + use ethers_core::types::U64; + mock.push(U64::from(12u64)).unwrap(); + let blk = provider.get_block_number().await.unwrap(); + assert_eq!(blk.as_u64(), 12); - // 2 calls were made - mock.assert_request("eth_blockNumber", ()).unwrap(); - mock.assert_request("eth_blockNumber", ()).unwrap(); - mock.assert_request("eth_blockNumber", ()).unwrap_err(); - } + // now that the response is gone, there's nothing left + // TODO: This returns: + // MiddlewareError( + // 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] - async fn can_stack_middlewares() { - let anvil = Anvil::new().block_time(5u64).spawn(); - let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); - let signer: LocalWallet = anvil.keys()[0].clone().into(); - let address = signer.address(); - - // the base provider - let provider = Arc::new(Provider::::try_from(anvil.endpoint()).unwrap()); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - let signer = signer.with_chain_id(chain_id); - - // the Gas Price escalator middleware is the first middleware above the provider, - // so that it receives the transaction last, after all the other middleware - // have modified it accordingly - let escalator = GeometricGasPrice::new(1.125, 60u64, None::); - let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); - - // The gas price middleware MUST be below the signing middleware for things to work - let provider = GasOracleMiddleware::new(provider, gas_oracle); - - // The signing middleware signs txs - use std::sync::Arc; - let provider = Arc::new(SignerMiddleware::new(provider, signer)); - - // The nonce manager middleware MUST be above the signing middleware so that it overrides - // the nonce and the signer does not make any eth_getTransaction count calls - let provider = NonceManagerMiddleware::new(provider, address); - - let tx = TransactionRequest::new(); - let mut pending_txs = Vec::new(); - for _ in 0..10 { - let pending = provider.send_transaction(tx.clone(), None).await.unwrap(); - let hash = *pending; - 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); - } + // 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 can_stack_middlewares() { + let anvil = Anvil::new().block_time(5u64).spawn(); + let gas_oracle = GasNow::new().category(GasCategory::SafeLow); + let signer: LocalWallet = anvil.keys()[0].clone().into(); + let address = signer.address(); + + // the base provider + let provider = Arc::new(Provider::::try_from(anvil.endpoint()).unwrap()); + let chain_id = provider.get_chainid().await.unwrap().as_u64(); + let signer = signer.with_chain_id(chain_id); + + // the Gas Price escalator middleware is the first middleware above the provider, + // so that it receives the transaction last, after all the other middleware + // have modified it accordingly + let escalator = GeometricGasPrice::new(1.125, 60u64, None::); + let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); + + // The gas price middleware MUST be below the signing middleware for things to work + let provider = GasOracleMiddleware::new(provider, gas_oracle); + + // The signing middleware signs txs + use std::sync::Arc; + let provider = Arc::new(SignerMiddleware::new(provider, signer)); + + // The nonce manager middleware MUST be above the signing middleware so that it overrides + // the nonce and the signer does not make any eth_getTransaction count calls + let provider = NonceManagerMiddleware::new(provider, address); + + let tx = TransactionRequest::new(); + let mut pending_txs = Vec::new(); + for _ in 0..10 { + let pending = provider.send_transaction(tx.clone(), None).await.unwrap(); + let hash = *pending; + 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); } diff --git a/ethers-middleware/tests/transformer.rs b/ethers-middleware/tests/transformer.rs index f27ef7f0..c4b37c0a 100644 --- a/ethers-middleware/tests/transformer.rs +++ b/ethers-middleware/tests/transformer.rs @@ -1,5 +1,5 @@ -#![cfg(not(target_arch = "wasm32"))] -#![allow(unused)] +#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] + use ethers_contract::{BaseContract, ContractFactory}; use ethers_core::{abi::Abi, types::*, utils::Anvil}; use ethers_middleware::{ @@ -24,7 +24,6 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { } #[tokio::test] -#[cfg(not(feature = "celo"))] async fn ds_proxy_transformer() { // randomness let mut rng = rand::thread_rng(); @@ -83,7 +82,6 @@ async fn ds_proxy_transformer() { } #[tokio::test] -#[cfg(not(feature = "celo"))] async fn ds_proxy_code() { // randomness let mut rng = rand::thread_rng();