From 635236f061504ba99c0bbbae7a0982ee2cdceac1 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 19 Aug 2021 11:38:12 +0300 Subject: [PATCH] feat: improve gas oracles (#392) * fix: re-enable ethgasstation tests and add new fields on response * fix: re-enable etherchain tests and add new fields on response * feat: add new etherscan response fields * feat: use gasnow v3 * chore: derive more traits for response types --- .../src/gas_oracle/eth_gas_station.rs | 63 ++++++++++++++---- .../src/gas_oracle/etherchain.rs | 38 +++++------ ethers-middleware/src/gas_oracle/etherscan.rs | 66 +++++++++++++------ ethers-middleware/src/gas_oracle/gas_now.rs | 43 ++++++------ ethers-middleware/tests/gas_oracle.rs | 5 -- 5 files changed, 136 insertions(+), 79 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index f5c37d09..12386a6d 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, } 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 { - let res = self + pub async fn query(&self) -> Result { + Ok(self .client .get(self.url.as_ref()) .send() .await? .json::() - .await?; + .await?) + } +} +#[async_trait] +impl GasOracle for EthGasStation { + 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), diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs index 74057f05..e114353f 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 { - let res = self + pub async fn query(&self) -> Result { + Ok(self .client .get(self.url.as_ref()) .send() .await? .json::() - .await?; + .await?) + } +} +#[async_trait] +impl GasOracle for Etherchain { + 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), diff --git a/ethers-middleware/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs index d7ce2d17..e72b9cfa 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")] - #[serde(rename = "SafeGasPrice")] - safe_gas_price: u64, + pub safe_gas_price: u64, #[serde(deserialize_with = "deserialize_number_from_string")] - #[serde(rename = "ProposeGasPrice")] - propose_gas_price: u64, + pub propose_gas_price: u64, #[serde(deserialize_with = "deserialize_number_from_string")] - #[serde(rename = "FastGasPrice")] - fast_gas_price: u64, + pub fast_gas_price: u64, + #[serde(deserialize_with = "deserialize_number_from_string")] + pub last_block: u64, + #[serde(deserialize_with = "deserialize_number_from_string")] + #[serde(rename = "suggestBaseFee")] + pub suggested_base_fee: f64, + #[serde(deserialize_with = "deserialize_f64_vec")] + #[serde(rename = "gasUsedRatio")] + pub gas_used_ratio: Vec, +} + +use serde::de; +use std::str::FromStr; +fn deserialize_f64_vec<'de, D>(deserializer: D) -> Result, 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 { + let res = self + .client + .get(self.url.as_ref()) + .send() + .await? + .json::() + .await?; + Ok(res.result) + } } #[async_trait] @@ -69,18 +99,12 @@ impl GasOracle for Etherscan { return Err(GasOracleError::GasCategoryNotSupported); } - let res = self - .client - .get(self.url.as_ref()) - .send() - .await? - .json::() - .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), } } diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index bd6f3cf9..a39161e5 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,23 +54,28 @@ impl GasNow { self.gas_category = gas_category; self } -} -#[async_trait] -impl GasOracle for GasNow { - async fn fetch(&self) -> Result { + pub async fn query(&self) -> Result { let res = self .client .get(self.url.as_ref()) .send() .await? - .json::() + .json::() .await?; + Ok(res.data) + } +} +#[async_trait] +impl GasOracle for GasNow { + async fn fetch(&self) -> Result { + 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) 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);