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:
parent
5fdeba9c0d
commit
635236f061
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue