Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat: blocknative gas oracle #1175

Merged
merged 3 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@

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


### 0.6.0

Expand Down
130 changes: 130 additions & 0 deletions ethers-middleware/src/gas_oracle/blocknative.rs
Original file line number Diff line number Diff line change
@@ -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))
}
}
11 changes: 11 additions & 0 deletions ethers-middleware/src/gas_oracle/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod blocknative;
pub use blocknative::BlockNative;

mod eth_gas_station;
pub use eth_gas_station::EthGasStation;

Expand Down Expand Up @@ -35,6 +38,14 @@ pub enum GasOracleError {
#[error(transparent)]
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
/// gas oracle
#[error(transparent)]
Expand Down