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
This commit is contained in:
Georgios Konstantopoulos 2021-08-19 11:38:12 +03:00 committed by GitHub
parent 5fdeba9c0d
commit 635236f061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 79 deletions

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use ethers_core::types::U256; use ethers_core::types::U256;
use async_trait::async_trait; 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 /// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct EthGasStation { pub struct EthGasStation {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Deserialize)] #[derive(Clone, Debug, Deserialize, PartialEq)]
struct EthGasStationResponse { #[serde(rename_all = "camelCase")]
#[serde(rename = "safeLow")] /// Eth Gas Station's response for the current recommended fast, standard and
safe_low: f64, /// safe low gas prices on the Ethereum network, along with the current block
average: u64, /// and wait times for each "speed".
fast: u64, pub struct EthGasStationResponse {
fastest: u64, /// 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 { impl EthGasStation {
@ -49,19 +81,22 @@ impl EthGasStation {
self.gas_category = gas_category; self.gas_category = gas_category;
self self
} }
}
#[async_trait] pub async fn query(&self) -> Result<EthGasStationResponse, GasOracleError> {
impl GasOracle for EthGasStation { Ok(self
async fn fetch(&self) -> Result<U256, GasOracleError> {
let res = self
.client .client
.get(self.url.as_ref()) .get(self.url.as_ref())
.send() .send()
.await? .await?
.json::<EthGasStationResponse>() .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 { let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10), 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::Standard => U256::from((res.average * GWEI_TO_WEI) / 10),

View File

@ -3,7 +3,6 @@ use ethers_core::types::U256;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use serde_aux::prelude::*;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; 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 /// 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(Debug)] #[derive(Clone, Debug)]
pub struct Etherchain { pub struct Etherchain {
client: Client, client: Client,
url: Url, url: Url,
@ -25,17 +24,15 @@ impl Default for Etherchain {
} }
} }
#[derive(Deserialize)] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
struct EtherchainResponse { #[serde(rename_all = "camelCase")]
#[serde(deserialize_with = "deserialize_number_from_string")] pub struct EtherchainResponse {
#[serde(rename = "safeLow")] pub safe_low: f32,
safe_low: f32, pub standard: f32,
#[serde(deserialize_with = "deserialize_number_from_string")] pub fast: f32,
standard: f32, pub fastest: f32,
#[serde(deserialize_with = "deserialize_number_from_string")] pub current_base_fee: f32,
fast: f32, pub recommended_base_fee: f32,
#[serde(deserialize_with = "deserialize_number_from_string")]
fastest: f32,
} }
impl Etherchain { impl Etherchain {
@ -55,19 +52,22 @@ impl Etherchain {
self.gas_category = gas_category; self.gas_category = gas_category;
self self
} }
}
#[async_trait] pub async fn query(&self) -> Result<EtherchainResponse, GasOracleError> {
impl GasOracle for Etherchain { Ok(self
async fn fetch(&self) -> Result<U256, GasOracleError> {
let res = self
.client .client
.get(self.url.as_ref()) .get(self.url.as_ref())
.send() .send()
.await? .await?
.json::<EtherchainResponse>() .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 { let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI), GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI),
GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI), GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI),

View File

@ -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 /// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct Etherscan { pub struct Etherscan {
client: Client, client: Client,
url: Url, url: Url,
@ -21,21 +21,40 @@ pub struct Etherscan {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct EtherscanResponse { struct EtherscanResponseWrapper {
result: EtherscanResponseInner, result: EtherscanResponse,
} }
#[derive(Deserialize)] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
struct EtherscanResponseInner { #[serde(rename_all = "PascalCase")]
pub struct EtherscanResponse {
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "SafeGasPrice")] pub safe_gas_price: u64,
safe_gas_price: u64,
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "ProposeGasPrice")] pub propose_gas_price: u64,
propose_gas_price: u64,
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "FastGasPrice")] pub fast_gas_price: u64,
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<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 { impl Etherscan {
@ -60,6 +79,17 @@ impl Etherscan {
self.gas_category = gas_category; self.gas_category = gas_category;
self 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] #[async_trait]
@ -69,18 +99,12 @@ impl GasOracle for Etherscan {
return Err(GasOracleError::GasCategoryNotSupported); return Err(GasOracleError::GasCategoryNotSupported);
} }
let res = self let res = self.query().await?;
.client
.get(self.url.as_ref())
.send()
.await?
.json::<EtherscanResponse>()
.await?;
match self.gas_category { match self.gas_category {
GasCategory::SafeLow => Ok(U256::from(res.result.safe_gas_price * GWEI_TO_WEI)), GasCategory::SafeLow => Ok(U256::from(res.safe_gas_price * GWEI_TO_WEI)),
GasCategory::Standard => Ok(U256::from(res.result.propose_gas_price * GWEI_TO_WEI)), GasCategory::Standard => Ok(U256::from(res.propose_gas_price * GWEI_TO_WEI)),
GasCategory::Fast => Ok(U256::from(res.result.fast_gas_price * GWEI_TO_WEI)), GasCategory::Fast => Ok(U256::from(res.fast_gas_price * GWEI_TO_WEI)),
_ => Err(GasOracleError::GasCategoryNotSupported), _ => Err(GasOracleError::GasCategoryNotSupported),
} }
} }

View File

@ -7,11 +7,11 @@ use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; 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 /// A client over HTTP for the [GasNow](https://www.gasnow.org/api/v1/gas/price) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct GasNow { pub struct GasNow {
client: Client, client: Client,
url: Url, url: Url,
@ -25,18 +25,16 @@ impl Default for GasNow {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct GasNowResponse { struct GasNowResponseWrapper {
data: GasNowResponseInner, data: GasNowResponse,
} }
#[derive(Deserialize)] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
struct GasNowResponseInner { pub struct GasNowResponse {
#[serde(rename = "top50")] pub rapid: u64,
top_50: u64, pub fast: u64,
#[serde(rename = "top200")] pub standard: u64,
top_200: u64, pub slow: u64,
#[serde(rename = "top400")]
top_400: u64,
} }
impl GasNow { impl GasNow {
@ -56,23 +54,28 @@ impl GasNow {
self.gas_category = gas_category; self.gas_category = gas_category;
self self
} }
}
#[async_trait] pub async fn query(&self) -> Result<GasNowResponse, GasOracleError> {
impl GasOracle for GasNow {
async fn fetch(&self) -> Result<U256, GasOracleError> {
let res = self let res = self
.client .client
.get(self.url.as_ref()) .get(self.url.as_ref())
.send() .send()
.await? .await?
.json::<GasNowResponse>() .json::<GasNowResponseWrapper>()
.await?; .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 { let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from(res.data.top_400), GasCategory::SafeLow => U256::from(res.slow),
GasCategory::Standard => U256::from(res.data.top_200), GasCategory::Standard => U256::from(res.standard),
_ => U256::from(res.data.top_50), GasCategory::Fast => U256::from(res.fast),
GasCategory::Fastest => U256::from(res.rapid),
}; };
Ok(gas_price) Ok(gas_price)

View File

@ -32,8 +32,6 @@ async fn using_gas_oracle() {
} }
#[tokio::test] #[tokio::test]
#[ignore]
// TODO: Re-enable, EthGasStation changed its response api @ https://ethgasstation.info/api/ethgasAPI.json
async fn eth_gas_station() { async fn eth_gas_station() {
// initialize and fetch gas estimates from EthGasStation // initialize and fetch gas estimates from EthGasStation
let eth_gas_station_oracle = EthGasStation::new(None); let eth_gas_station_oracle = EthGasStation::new(None);
@ -60,9 +58,6 @@ async fn etherscan() {
} }
#[tokio::test] #[tokio::test]
#[ignore]
// TODO: Etherchain has Cloudflare DDOS protection which makes the request fail
// https://twitter.com/gakonst/status/1421796226316578816
async fn etherchain() { async fn etherchain() {
// initialize and fetch gas estimates from Etherchain // initialize and fetch gas estimates from Etherchain
let etherchain_oracle = Etherchain::new().category(GasCategory::Fast); let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);