Repository: gakonst/ethers-rs
Pull request #369 (2021-08-10 23:10:23)

feat: fee estimation with custom/default fn
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

Fee estimation/recommendation not available for EIP-1559 type transactions

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

Default estimator is a port of [this](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts)

* Add `estimate_eip1559_fees` method to `Middleware` and implement in the `JsonRpcProvider`
* I believe none of the Gas oracle APIs support EIP-1559 fee recommendation (so that's simply an Err at the moment)
* Ethers-rs users can even provide their own `fn` to estimate fees using the `base_fee_per_gas` and `fee_history.reward`


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index aacef4f9..2647fbfc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2836,9 +2836,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.2.19"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
+checksum = "b9cbe87a2fa7e35900ce5de20220a582a9483a7063811defce79d7cbd59d4cfe"
 dependencies = [
  "ansi_term",
  "chrono",
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 5717468a..8809187f 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -36,6 +36,7 @@ pub use rlp;
 use crate::types::{Address, Bytes, U256};
 use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey};
 use std::convert::TryInto;
+use std::ops::Neg;
 use thiserror::Error;
 
 #[derive(Debug, Error)]
@@ -47,6 +48,19 @@ pub enum FormatBytes32StringError {
 /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
 pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]);
 
+/// The number of blocks from the past for which the fee rewards are fetched for fee estimation.
+pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10;
+/// The default percentile of gas premiums that are fetched for fee estimation.
+pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0;
+/// The default max priority fee per gas, used in case the base fee is within a threshold.
+pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000;
+/// The threshold for base fee below which we use the default priority fee, and beyond which we
+/// estimate an appropriate value for priority fee.
+pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000;
+/// The threshold max change/difference (in %) at which we will ignore the fee history values
+/// under it.
+pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200;
+
 /// Format the output for the user which prefer to see values
 /// in ether (instead of wei)
 ///
@@ -187,6 +201,92 @@ pub fn parse_bytes32_string(bytes: &[u8; 32]) -> Result<&str, std::str::Utf8Erro
     std::str::from_utf8(&bytes[..length])
 }
 
+/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts)
+pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec<Vec<U256>>) -> (U256, U256) {
+    let max_priority_fee_per_gas =
+        if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) {
+            U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)
+        } else {
+            std::cmp::max(
+                estimate_priority_fee(rewards),
+                U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE),
+            )
+        };
+    let potential_max_fee = base_fee_surged(base_fee_per_gas);
+    let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee {
+        max_priority_fee_per_gas + potential_max_fee
+    } else {
+        potential_max_fee
+    };
+    (max_fee_per_gas, max_priority_fee_per_gas)
+}
+
+fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
+    let mut rewards: Vec<U256> = rewards
+        .iter()
+        .map(|r| r[0])
+        .filter(|r| *r > U256::zero())
+        .collect();
+    if rewards.is_empty() {
+        return U256::zero();
+    }
+    if rewards.len() == 1 {
+        return rewards[0];
+    }
+    // Sort the rewards as we will eventually take the median.
+    rewards.sort();
+
+    // A copy of the same vector is created for convenience to calculate percentage change
+    // between subsequent fee values.
+    let mut rewards_copy = rewards.clone();
+    rewards_copy.rotate_left(1);
+
+    let mut percentage_change: Vec<i64> = rewards
+        .iter()
+        .zip(rewards_copy.iter())
+        .map(|(a, b)| {
+            if b > a {
+                ((b - a).low_u32() as i64 * 100) / (a.low_u32() as i64)
+            } else {
+                (((a - b).low_u32() as i64 * 100) / (a.low_u32() as i64)).neg()
+            }
+        })
+        .collect();
+    percentage_change.pop();
+
+    // Fetch the max of the percentage change, and that element's index.
+    let max_change = percentage_change.iter().max().unwrap();
+    let max_change_index = percentage_change
+        .iter()
+        .position(|&c| c == *max_change)
+        .unwrap();
+
+    // If we encountered a big change in fees at a certain position, then consider only
+    // the values >= it.
+    let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE
+        && (max_change_index >= (rewards.len() / 2))
+    {
+        rewards[max_change_index..].to_vec()
+    } else {
+        rewards
+    };
+
+    // Return the median.
+    values[values.len() / 2]
+}
+
+fn base_fee_surged(base_fee_per_gas: U256) -> U256 {
+    if base_fee_per_gas <= U256::from(40_000_000_000u64) {
+        base_fee_per_gas * 2
+    } else if base_fee_per_gas <= U256::from(100_000_000_000u64) {
+        base_fee_per_gas * 16 / 10
+    } else if base_fee_per_gas <= U256::from(200_000_000_000u64) {
+        base_fee_per_gas * 14 / 10
+    } else {
+        base_fee_per_gas * 12 / 10
+    }
+}
+
 /// A bit of hack to find an unused TCP port.
 ///
 /// Does not guarantee that the given port is unused after the function exists, just that it was
@@ -451,4 +551,33 @@ mod tests {
             FormatBytes32StringError::TextTooLong
         ));
     }
+
+    #[test]
+    fn test_eip1559_default_estimator() {
+        // If the base fee is below the triggering base fee, we should get the default priority fee
+        // with the base fee surged.
+        let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) - 1;
+        let rewards: Vec<Vec<U256>> = vec![vec![]];
+        let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards);
+        assert_eq!(
+            priority_fee,
+            U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)
+        );
+        assert_eq!(base_fee, base_fee_surged(base_fee_per_gas));
+
+        // If the base fee is above the triggering base fee, we calculate the priority fee using
+        // the fee history (rewards).
+        let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) + 1;
+        let rewards: Vec<Vec<U256>> = vec![
+            vec![100_000_000_000u64.into()],
+            vec![105_000_000_000u64.into()],
+            vec![102_000_000_000u64.into()],
+        ]; // say, last 3 blocks
+        let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards.clone());
+        assert_eq!(base_fee, base_fee_surged(base_fee_per_gas));
+        assert_eq!(priority_fee, estimate_priority_fee(rewards.clone()));
+
+        // The median should be taken because none of the changes are big enough to ignore values.
+        assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into());
+    }
 }
diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs
index f5c37d09..f8f0e6d4 100644
--- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs
+++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs
@@ -1,3 +1,5 @@
+use std::collections::HashMap;
+
 use ethers_core::types::U256;
 
 use async_trait::async_trait;
@@ -11,20 +13,50 @@ const ETH_GAS_STATION_URL_PREFIX: &str = "https://ethgasstation.info/api/ethgasA
 
 /// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
 /// that implements the `GasOracle` trait
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct EthGasStation {
     client: Client,
     url: Url,
     gas_category: GasCategory,
 }
 
-#[derive(Deserialize)]
-struct EthGasStationResponse {
-    #[serde(rename = "safeLow")]
-    safe_low: f64,
-    average: u64,
-    fast: u64,
-    fastest: u64,
+#[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)
+    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)
+    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)
+    pub fastest: u64,
+
+    // post eip-1559 fields
+    #[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
+    pub block_num: u64,
+    /// Smallest value of (gasUsed / gaslimit) from last 10 blocks
+    pub speed: f64,
+    /// Waiting time(in minutes) for the `safe_low` gas price
+    pub safe_low_wait: f64,
+    /// Waiting time(in minutes) for the `average` gas price
+    pub avg_wait: f64,
+    /// Waiting time(in minutes) for the `fast` gas price
+    pub fast_wait: f64,
+    /// Waiting time(in minutes) for the `fastest` gas price
+    pub fastest_wait: f64,
+    // What is this?
+    pub gas_price_range: HashMap<u64, f64>,
 }
 
 impl EthGasStation {
@@ -49,19 +81,22 @@ impl EthGasStation {
         self.gas_category = gas_category;
         self
     }
-}
 
-#[async_trait]
-impl GasOracle for EthGasStation {
-    async fn fetch(&self) -> Result<U256, GasOracleError> {
-        let res = self
+    pub async fn query(&self) -> Result<EthGasStationResponse, GasOracleError> {
+        Ok(self
             .client
             .get(self.url.as_ref())
             .send()
             .await?
             .json::<EthGasStationResponse>()
-            .await?;
+            .await?)
+    }
+}
 
+#[async_trait]
+impl GasOracle for EthGasStation {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        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),
@@ -71,4 +106,8 @@ impl GasOracle for EthGasStation {
 
         Ok(gas_price)
     }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        Err(GasOracleError::Eip1559EstimationNotSupported)
+    }
 }
diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs
index 74057f05..89ecca27 100644
--- a/ethers-middleware/src/gas_oracle/etherchain.rs
+++ b/ethers-middleware/src/gas_oracle/etherchain.rs
@@ -3,7 +3,6 @@ use ethers_core::types::U256;
 use async_trait::async_trait;
 use reqwest::Client;
 use serde::Deserialize;
-use serde_aux::prelude::*;
 use url::Url;
 
 use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
@@ -12,7 +11,7 @@ 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
 /// that implements the `GasOracle` trait
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Etherchain {
     client: Client,
     url: Url,
@@ -25,17 +24,15 @@ impl Default for Etherchain {
     }
 }
 
-#[derive(Deserialize)]
-struct EtherchainResponse {
-    #[serde(deserialize_with = "deserialize_number_from_string")]
-    #[serde(rename = "safeLow")]
-    safe_low: f32,
-    #[serde(deserialize_with = "deserialize_number_from_string")]
-    standard: f32,
-    #[serde(deserialize_with = "deserialize_number_from_string")]
-    fast: f32,
-    #[serde(deserialize_with = "deserialize_number_from_string")]
-    fastest: f32,
+#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
+#[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,
 }
 
 impl Etherchain {
@@ -55,19 +52,22 @@ impl Etherchain {
         self.gas_category = gas_category;
         self
     }
-}
 
-#[async_trait]
-impl GasOracle for Etherchain {
-    async fn fetch(&self) -> Result<U256, GasOracleError> {
-        let res = self
+    pub async fn query(&self) -> Result<EtherchainResponse, GasOracleError> {
+        Ok(self
             .client
             .get(self.url.as_ref())
             .send()
             .await?
             .json::<EtherchainResponse>()
-            .await?;
+            .await?)
+    }
+}
 
+#[async_trait]
+impl GasOracle for Etherchain {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        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),
@@ -77,4 +77,8 @@ impl GasOracle for Etherchain {
 
         Ok(gas_price)
     }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        Err(GasOracleError::Eip1559EstimationNotSupported)
+    }
 }
diff --git a/ethers-middleware/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs
index d7ce2d17..658516b0 100644
--- a/ethers-middleware/src/gas_oracle/etherscan.rs
+++ b/ethers-middleware/src/gas_oracle/etherscan.rs
@@ -13,7 +13,7 @@ const ETHERSCAN_URL_PREFIX: &str =
 
 /// 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(Debug)]
+#[derive(Clone, Debug)]
 pub struct Etherscan {
     client: Client,
     url: Url,
@@ -21,21 +21,40 @@ pub struct Etherscan {
 }
 
 #[derive(Deserialize)]
-struct EtherscanResponse {
-    result: EtherscanResponseInner,
+struct EtherscanResponseWrapper {
+    result: EtherscanResponse,
 }
 
-#[derive(Deserialize)]
-struct EtherscanResponseInner {
+#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
+#[serde(rename_all = "PascalCase")]
+pub struct EtherscanResponse {
+    #[serde(deserialize_with = "deserialize_number_from_string")]
+    pub safe_gas_price: u64,
+    #[serde(deserialize_with = "deserialize_number_from_string")]
+    pub propose_gas_price: u64,
     #[serde(deserialize_with = "deserialize_number_from_string")]
-    #[serde(rename = "SafeGasPrice")]
-    safe_gas_price: u64,
+    pub fast_gas_price: u64,
     #[serde(deserialize_with = "deserialize_number_from_string")]
-    #[serde(rename = "ProposeGasPrice")]
-    propose_gas_price: u64,
+    pub last_block: u64,
     #[serde(deserialize_with = "deserialize_number_from_string")]
-    #[serde(rename = "FastGasPrice")]
-    fast_gas_price: u64,
+    #[serde(rename = "suggestBaseFee")]
+    pub suggested_base_fee: f64,
+    #[serde(deserialize_with = "deserialize_f64_vec")]
+    #[serde(rename = "gasUsedRatio")]
+    pub gas_used_ratio: Vec<f64>,
+}
+
+use serde::de;
+use std::str::FromStr;
+fn deserialize_f64_vec<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
+where
+    D: de::Deserializer<'de>,
+{
+    let str_sequence = String::deserialize(deserializer)?;
+    str_sequence
+        .split(',')
+        .map(|item| f64::from_str(item).map_err(|err| de::Error::custom(err.to_string())))
+        .collect()
 }
 
 impl Etherscan {
@@ -60,6 +79,17 @@ impl Etherscan {
         self.gas_category = gas_category;
         self
     }
+
+    pub async fn query(&self) -> Result<EtherscanResponse, GasOracleError> {
+        let res = self
+            .client
+            .get(self.url.as_ref())
+            .send()
+            .await?
+            .json::<EtherscanResponseWrapper>()
+            .await?;
+        Ok(res.result)
+    }
 }
 
 #[async_trait]
@@ -69,19 +99,17 @@ impl GasOracle for Etherscan {
             return Err(GasOracleError::GasCategoryNotSupported);
         }
 
-        let res = self
-            .client
-            .get(self.url.as_ref())
-            .send()
-            .await?
-            .json::<EtherscanResponse>()
-            .await?;
+        let res = self.query().await?;
 
         match self.gas_category {
-            GasCategory::SafeLow => Ok(U256::from(res.result.safe_gas_price * GWEI_TO_WEI)),
-            GasCategory::Standard => Ok(U256::from(res.result.propose_gas_price * GWEI_TO_WEI)),
-            GasCategory::Fast => Ok(U256::from(res.result.fast_gas_price * GWEI_TO_WEI)),
+            GasCategory::SafeLow => Ok(U256::from(res.safe_gas_price * GWEI_TO_WEI)),
+            GasCategory::Standard => Ok(U256::from(res.propose_gas_price * GWEI_TO_WEI)),
+            GasCategory::Fast => Ok(U256::from(res.fast_gas_price * GWEI_TO_WEI)),
             _ => Err(GasOracleError::GasCategoryNotSupported),
         }
     }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        Err(GasOracleError::Eip1559EstimationNotSupported)
+    }
 }
diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs
index bd6f3cf9..2bf6a049 100644
--- a/ethers-middleware/src/gas_oracle/gas_now.rs
+++ b/ethers-middleware/src/gas_oracle/gas_now.rs
@@ -7,11 +7,11 @@ use url::Url;
 
 use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
 
-const GAS_NOW_URL: &str = "https://www.gasnow.org/api/v1/gas/price";
+const GAS_NOW_URL: &str = "https://www.gasnow.org/api/v3/gas/price";
 
 /// A client over HTTP for the [GasNow](https://www.gasnow.org/api/v1/gas/price) gas tracker API
 /// that implements the `GasOracle` trait
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct GasNow {
     client: Client,
     url: Url,
@@ -25,18 +25,16 @@ impl Default for GasNow {
 }
 
 #[derive(Deserialize)]
-struct GasNowResponse {
-    data: GasNowResponseInner,
+struct GasNowResponseWrapper {
+    data: GasNowResponse,
 }
 
-#[derive(Deserialize)]
-struct GasNowResponseInner {
-    #[serde(rename = "top50")]
-    top_50: u64,
-    #[serde(rename = "top200")]
-    top_200: u64,
-    #[serde(rename = "top400")]
-    top_400: u64,
+#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
+pub struct GasNowResponse {
+    pub rapid: u64,
+    pub fast: u64,
+    pub standard: u64,
+    pub slow: u64,
 }
 
 impl GasNow {
@@ -56,25 +54,34 @@ impl GasNow {
         self.gas_category = gas_category;
         self
     }
-}
 
-#[async_trait]
-impl GasOracle for GasNow {
-    async fn fetch(&self) -> Result<U256, GasOracleError> {
+    pub async fn query(&self) -> Result<GasNowResponse, GasOracleError> {
         let res = self
             .client
             .get(self.url.as_ref())
             .send()
             .await?
-            .json::<GasNowResponse>()
+            .json::<GasNowResponseWrapper>()
             .await?;
+        Ok(res.data)
+    }
+}
 
+#[async_trait]
+impl GasOracle for GasNow {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        let res = self.query().await?;
         let gas_price = match self.gas_category {
-            GasCategory::SafeLow => U256::from(res.data.top_400),
-            GasCategory::Standard => U256::from(res.data.top_200),
-            _ => U256::from(res.data.top_50),
+            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)
     }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        Err(GasOracleError::Eip1559EstimationNotSupported)
+    }
 }
diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs
index d77f3262..40ce3b5f 100644
--- a/ethers-middleware/src/gas_oracle/middleware.rs
+++ b/ethers-middleware/src/gas_oracle/middleware.rs
@@ -59,6 +59,13 @@ where
         Ok(self.gas_oracle.fetch().await?)
     }
 
+    async fn estimate_eip1559_fees(
+        &self,
+        _: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
+    ) -> Result<(U256, U256), Self::Error> {
+        Ok(self.gas_oracle.estimate_eip1559_fees().await?)
+    }
+
     async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
         &self,
         tx: T,
@@ -77,8 +84,17 @@ where
                     inner.tx.gas_price = Some(self.get_gas_price().await?);
                 }
             }
-            TypedTransaction::Eip1559(_) => {
-                return Err(MiddlewareError::UnsupportedTxType);
+            TypedTransaction::Eip1559(ref mut inner) => {
+                if inner.max_priority_fee_per_gas.is_none() || inner.max_fee_per_gas.is_none() {
+                    let (max_fee_per_gas, max_priority_fee_per_gas) =
+                        self.estimate_eip1559_fees(None).await?;
+                    if inner.max_priority_fee_per_gas.is_none() {
+                        inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
+                    }
+                    if inner.max_fee_per_gas.is_none() {
+                        inner.max_fee_per_gas = Some(max_fee_per_gas);
+                    }
+                }
             }
         };
         self.inner
diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs
index f6a94850..603ada92 100644
--- a/ethers-middleware/src/gas_oracle/mod.rs
+++ b/ethers-middleware/src/gas_oracle/mod.rs
@@ -42,6 +42,9 @@ pub enum GasOracleError {
     /// supported by the gas oracle API
     #[error("gas category not supported")]
     GasCategoryNotSupported,
+
+    #[error("EIP-1559 gas estimation not supported")]
+    Eip1559EstimationNotSupported,
 }
 
 /// `GasOracle` is a trait that an underlying gas oracle needs to implement.
@@ -80,4 +83,6 @@ pub trait GasOracle: Send + Sync + std::fmt::Debug {
     /// # }
     /// ```
     async fn fetch(&self) -> Result<U256, GasOracleError>;
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>;
 }
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index c09d494f..43b5f120 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -265,15 +265,19 @@ pub trait Middleware: Sync + Send + Debug {
                     inner.from = self.default_sender();
                 }
 
-                let (max_priority_fee_per_gas, max_fee_per_gas, gas) = futures_util::try_join!(
-                    // TODO: Replace with algorithms using eth_feeHistory
-                    maybe(inner.max_priority_fee_per_gas, self.get_gas_price()),
-                    maybe(inner.max_fee_per_gas, self.get_gas_price()),
-                    maybe(inner.gas, self.estimate_gas(&tx_clone)),
-                )?;
+                let gas = maybe(inner.gas, self.estimate_gas(&tx_clone)).await?;
                 inner.gas = Some(gas);
-                inner.max_fee_per_gas = Some(max_fee_per_gas);
-                inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
+
+                if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() {
+                    let (max_fee_per_gas, max_priority_fee_per_gas) =
+                        self.estimate_eip1559_fees(None).await?;
+                    if inner.max_fee_per_gas.is_none() {
+                        inner.max_fee_per_gas = Some(max_fee_per_gas);
+                    }
+                    if inner.max_priority_fee_per_gas.is_none() {
+                        inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
+                    }
+                }
             }
         };
 
@@ -401,6 +405,16 @@ pub trait Middleware: Sync + Send + Debug {
         self.inner().get_gas_price().await.map_err(FromErr::from)
     }
 
+    async fn estimate_eip1559_fees(
+        &self,
+        estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
+    ) -> Result<(U256, U256), Self::Error> {
+        self.inner()
+            .estimate_eip1559_fees(estimator)
+            .await
+            .map_err(FromErr::from)
+    }
+
     async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> {
         self.inner().get_accounts().await.map_err(FromErr::from)
     }
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index 8e61b3ab..f10d1ea6 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -261,6 +261,37 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
         self.request("eth_gasPrice", ()).await
     }
 
+    /// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for
+    /// EIP-1559 compatible transactions.
+    async fn estimate_eip1559_fees(
+        &self,
+        estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
+    ) -> Result<(U256, U256), Self::Error> {
+        let base_fee_per_gas = self
+            .get_block(BlockNumber::Latest)
+            .await?
+            .ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?
+            .base_fee_per_gas
+            .ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?;
+
+        let fee_history = self
+            .fee_history(
+                utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS,
+                BlockNumber::Latest,
+                &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE],
+            )
+            .await?;
+
+        // use the provided fee estimator function, or fallback to the default implementation.
+        let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator {
+            es(base_fee_per_gas, fee_history.reward)
+        } else {
+            utils::eip1559_default_estimator(base_fee_per_gas, fee_history.reward)
+        };
+
+        Ok((max_fee_per_gas, max_priority_fee_per_gas))
+    }
+
     /// Gets the accounts on the node
     async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
         self.request("eth_accounts", ()).await
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 17627b69..9a8a5838 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -43,7 +43,7 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "usb", "mockhsm"] }
 tempfile = "3.2.0"
 tokio = { version = "1.5", default-features = false, features = ["macros"] }
 
-tracing-subscriber = "0.2.19"
+tracing-subscriber = "0.2.20"
 
 [features]
 celo = ["ethers-core/celo"]


Test patch:
diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs
index 75a4afac..8e2008e1 100644
--- a/ethers-contract/tests/contract.rs
+++ b/ethers-contract/tests/contract.rs
@@ -399,6 +399,7 @@ mod eth_tests {
         let _receipt = contract
             .method::<_, H256>("setValue", "hi".to_owned())
             .unwrap()
+            .legacy()
             .send()
             .await
             .unwrap()
diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs
index 1b406286..c0933d99 100644
--- a/ethers-middleware/tests/gas_oracle.rs
+++ b/ethers-middleware/tests/gas_oracle.rs
@@ -32,8 +32,6 @@ async fn using_gas_oracle() {
 }
 
 #[tokio::test]
-#[ignore]
-// TODO: Re-enable, EthGasStation changed its response api @ https://ethgasstation.info/api/ethgasAPI.json
 async fn eth_gas_station() {
     // initialize and fetch gas estimates from EthGasStation
     let eth_gas_station_oracle = EthGasStation::new(None);
@@ -60,9 +58,6 @@ async fn etherscan() {
 }
 
 #[tokio::test]
-#[ignore]
-// TODO: Etherchain has Cloudflare DDOS protection which makes the request fail
-// https://twitter.com/gakonst/status/1421796226316578816
 async fn etherchain() {
     // initialize and fetch gas estimates from Etherchain
     let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);
diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs
index f3390967..62edd568 100644
--- a/ethers-providers/tests/provider.rs
+++ b/ethers-providers/tests/provider.rs
@@ -199,6 +199,17 @@ mod eth_tests {
             .into();
         check_tx(&provider, tx, 2).await;
     }
+
+    #[tokio::test]
+    async fn eip1559_fee_estimation() {
+        let provider = Provider::<Http>::try_from(
+            "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
+        )
+        .unwrap();
+
+        let (_max_fee_per_gas, _max_priority_fee_per_gas) =
+            provider.estimate_eip1559_fees(None).await.unwrap();
+    }
 }
 
 #[cfg(feature = "celo")]