feat: blocknative gas oracle (#1175)

* gas oracle: add more error variants

* gas oracle: adds BlockNative oracle

* pdate changelog
This commit is contained in:
georgewhewell 2022-04-25 17:50:55 +02:00 committed by GitHub
parent 9d53c73af0
commit a866cd5726
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 0 deletions

View File

@ -237,6 +237,8 @@
- Ensure a consistent chain ID between a Signer and Provider in SignerMiddleware - Ensure a consistent chain ID between a Signer and Provider in SignerMiddleware
[#1095](https://gakonst/ethers-rs/pull/1095) [#1095](https://gakonst/ethers-rs/pull/1095)
- Add BlockNative gas oracle [#1175](https://github.com/gakonst/ethers-rs/pull/1175)
### 0.6.0 ### 0.6.0

View File

@ -0,0 +1,130 @@
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::{
header::{HeaderMap, HeaderValue, AUTHORIZATION},
Client, ClientBuilder,
};
use serde::Deserialize;
use std::{collections::HashMap, convert::TryInto, iter::FromIterator};
use url::Url;
const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices";
fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
match gas_category {
GasCategory::SafeLow => 80,
GasCategory::Standard => 90,
GasCategory::Fast => 95,
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,
gas_category: GasCategory,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockNativeGasResponse {
system: Option<String>,
network: Option<String>,
unit: Option<String>,
max_price: Option<u64>,
block_prices: Vec<BlockPrice>,
estimated_base_fees: Vec<HashMap<String, Vec<BaseFeeEstimate>>>,
}
impl BlockNativeGasResponse {
pub fn get_estimation_for(
&self,
gas_category: &GasCategory,
) -> Result<EstimatedPrice, GasOracleError> {
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<EstimatedPrice>,
}
#[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: &str) -> Self {
let header_value = HeaderValue::from_str(api_key).unwrap();
let headers = HeaderMap::from_iter([(AUTHORIZATION, header_value)]);
let client = ClientBuilder::new().default_headers(headers).build().unwrap();
Self {
client,
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<BlockNativeGasResponse, GasOracleError> {
Ok(self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?)
}
}
#[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<U256, GasOracleError> {
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))
}
}

View File

@ -1,3 +1,6 @@
mod blocknative;
pub use blocknative::BlockNative;
mod eth_gas_station; mod eth_gas_station;
pub use eth_gas_station::EthGasStation; pub use eth_gas_station::EthGasStation;
@ -35,6 +38,14 @@ pub enum GasOracleError {
#[error(transparent)] #[error(transparent)]
HttpClientError(#[from] ReqwestError), HttpClientError(#[from] ReqwestError),
/// An error decoding JSON response from gas oracle
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),
/// An error with oracle response type
#[error("invalid oracle response")]
InvalidResponse,
/// An internal error in the Etherscan client request made from the underlying /// An internal error in the Etherscan client request made from the underlying
/// gas oracle /// gas oracle
#[error(transparent)] #[error(transparent)]