Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(polygon): remove dependency on gas station api #391

Merged
merged 1 commit into from
Sep 19, 2023
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
39 changes: 5 additions & 34 deletions src/common/gas.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::Arc;

use ethers::{
prelude::gas_oracle::{GasCategory, GasOracle},
prelude::gas_oracle::GasCategory,
types::{transaction::eip2718::TypedTransaction, Address, Chain, U256},
};
use tokio::try_join;
Expand Down Expand Up @@ -210,19 +210,10 @@ impl<P: ProviderLike> FeeEstimator<P> {

async fn get_priority_fee(&self) -> anyhow::Result<U256> {
if POLYGON_CHAIN_IDS.contains(&self.chain_id) {
let gas_oracle =
Polygon::new(Chain::try_from(self.chain_id)?)?.category(GasCategory::Fast);
match gas_oracle.estimate_eip1559_fees().await {
Ok(fees) => Ok(fees.1),
// Polygon gas station is very unreliable, fallback to max priority fee if it fails
// Fast can be 10% faster than what is returned by `eth_maxPriorityFeePerGas`
// so we increase the max priority fee by 10% to ensure that multiple
// calls to this endpoint give reasonably similar results.
Err(_) => Ok(math::increase_by_percent(
self.provider.get_max_priority_fee().await?,
10,
)),
}
let gas_oracle = Polygon::new(Arc::clone(&self.provider)).category(GasCategory::Fast);

let fees = gas_oracle.estimate_eip1559_fees().await?;
Ok(fees.1)
} else if self.use_bundle_priority_fee {
self.provider.get_max_priority_fee().await
} else {
Expand All @@ -231,12 +222,6 @@ impl<P: ProviderLike> FeeEstimator<P> {
}
}

const GWEI_TO_WEI: u64 = 1_000_000_000;

pub fn from_gwei_f64(gwei: f64) -> U256 {
U256::from((gwei * GWEI_TO_WEI as f64).ceil() as u64)
}

const NON_EIP_1559_CHAIN_IDS: &[u64] = &[
Chain::Arbitrum as u64,
Chain::ArbitrumNova as u64,
Expand All @@ -246,17 +231,3 @@ const NON_EIP_1559_CHAIN_IDS: &[u64] = &[
fn is_known_non_eip_1559_chain(chain_id: u64) -> bool {
NON_EIP_1559_CHAIN_IDS.contains(&chain_id)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_gwei_conversion() {
let max_priority_fee: f64 = 1.8963421368;

let result = from_gwei_f64(max_priority_fee);

assert_eq!(result, U256::from(1896342137));
}
}
205 changes: 67 additions & 138 deletions src/common/polygon.rs
Original file line number Diff line number Diff line change
@@ -1,169 +1,98 @@
// This code was taken from ethers-rs, we will remove it once our PR is merged for rounding errors
use std::sync::Arc;

use ethers::{
prelude::gas_oracle::{GasCategory, GasOracle, GasOracleError, Result},
types::{Chain, U256},
prelude::gas_oracle::{GasCategory, Result},
providers::ProviderError,
types::{BlockNumber, U256},
};
use reqwest::Client;
use serde::Deserialize;
use tonic::async_trait;
use url::Url;

use super::gas::from_gwei_f64;
use crate::common::types::ProviderLike;

const MAINNET_URL: &str = "https://gasstation.polygon.technology/v2";
const MUMBAI_URL: &str = "https://gasstation-testnet.polygon.technology/v2";

/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
/// Queries over HTTP and implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[must_use]
pub struct Polygon {
client: Client,
url: Url,
#[derive(Debug)]
pub(crate) struct Polygon<P> {
provider: Arc<P>,
gas_category: GasCategory,
}

/// The response from the Polygon gas station API.
///
/// Gas prices are in __Gwei__.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Response {
#[serde(deserialize_with = "deserialize_stringified_f64")]
pub estimated_base_fee: f64,
pub safe_low: GasEstimate,
pub standard: GasEstimate,
pub fast: GasEstimate,
}

#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GasEstimate {
#[serde(deserialize_with = "deserialize_stringified_f64")]
pub max_priority_fee: f64,
#[serde(deserialize_with = "deserialize_stringified_f64")]
pub max_fee: f64,
#[derive(Clone, Copy, Deserialize, PartialEq)]
pub(crate) struct GasEstimate {
pub(crate) max_priority_fee: U256,
pub(crate) max_fee: U256,
}

fn deserialize_stringified_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
impl<P> Polygon<P>
where
D: serde::Deserializer<'de>,
P: ProviderLike,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum F64OrString {
F64(serde_json::Number),
String(String),
}
match Deserialize::deserialize(deserializer)? {
F64OrString::F64(f) => f
.as_f64()
.ok_or_else(|| serde::de::Error::custom("invalid f64")),
F64OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
}
}

impl Response {
#[inline]
pub fn estimate_from_category(&self, gas_category: GasCategory) -> GasEstimate {
match gas_category {
GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.fast,
}
}
}

impl Default for Polygon {
fn default() -> Self {
Self::new(Chain::Polygon).unwrap()
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Polygon {
async fn fetch(&self) -> Result<U256> {
let response = self.query().await?;
let base = response.estimated_base_fee;
let prio = response
.estimate_from_category(self.gas_category)
.max_priority_fee;
let fee = base + prio;
Ok(from_gwei_f64(fee))
}

async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let response = self.query().await?;
let estimate = response.estimate_from_category(self.gas_category);
let max = from_gwei_f64(estimate.max_fee);
let prio = from_gwei_f64(estimate.max_priority_fee);
Ok((max, prio))
}
}

impl Polygon {
pub fn new(chain: Chain) -> Result<Self> {
#[cfg(not(target_arch = "wasm32"))]
static APP_USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);

let builder = Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let builder = builder.user_agent(APP_USER_AGENT);

Self::with_client(builder.build()?, chain)
}

pub fn with_client(client: Client, chain: Chain) -> Result<Self> {
// TODO: Sniff chain from chain id.
let url = match chain {
Chain::Polygon => MAINNET_URL,
Chain::PolygonMumbai => MUMBAI_URL,
_ => return Err(GasOracleError::UnsupportedChain),
};
Ok(Self {
client,
url: Url::parse(url).unwrap(),
pub(crate) fn new(provider: Arc<P>) -> Self {
Self {
provider,
gas_category: GasCategory::Standard,
})
}
}

/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Chooses percentile to use based on the set category for eth_feeHistory
fn gas_category_percentile(&self) -> f64 {
match self.gas_category {
GasCategory::SafeLow => 10.0,
GasCategory::Standard => 25.0,
GasCategory::Fast | GasCategory::Fastest => 50.0,
}
}

/// Estimates max and priority gas and converts to U256
pub(crate) async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), ProviderError> {
let estimate = self.calculate_fees().await?;
let max = estimate.max_fee;
let prio = estimate.max_priority_fee;
Ok((max, prio))
}

/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response = self
.client
.get(self.url.clone())
.send()
.await?
.error_for_status()?
.json()
pub(crate) async fn calculate_fees(&self) -> Result<GasEstimate, ProviderError> {
let gas_percentile = self.gas_category_percentile();
let fee_history = self
.provider
.fee_history(15, BlockNumber::Latest, &[gas_percentile])
.await?;
Ok(response)

let (base_fee_per_gas, _mod) = fee_history
.base_fee_per_gas
.iter()
.fold(U256::from(0), |acc, val| acc.saturating_add(*val))
.div_mod(U256::from(fee_history.base_fee_per_gas.len()));

let estimate = calculate_estimate_from_rewards(&fee_history.reward, base_fee_per_gas);
Ok(estimate)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_polygon_gas_station_response() {
let s = r#"{"safeLow":{"maxPriorityFee":"30.739827732","maxFee":"335.336914674"},"standard":{"maxPriorityFee":"57.257993430","maxFee":"361.855080372"},"fast":{"maxPriorityFee":"103.414268558","maxFee":"408.011355500"},"estimatedBaseFee":"304.597086942","blockTime":2,"blockNumber":43975155}"#;
let _resp: Response = serde_json::from_str(s).unwrap();
/// Calculates the estimate based on the index of inner vector
/// and skips the average if block is empty
fn calculate_estimate_from_rewards(reward: &[Vec<U256>], base_fee_per_gas: U256) -> GasEstimate {
let (sum, count): (U256, U256) = reward
.iter()
.filter(|b| !b[0].is_zero())
.map(|b| b[0])
.fold((0.into(), 0.into()), |(sum, count), val| {
(sum.saturating_add(val), count.saturating_add(1.into()))
});

let mut average = sum;

if !count.is_zero() {
let (avg, _mod) = average.div_mod(count);
average = avg;
}

#[test]
fn parse_polygon_testnet_gas_station_response() {
let s = r#"{"safeLow":{"maxPriorityFee":1.3999999978,"maxFee":1.4000000157999999},"standard":{"maxPriorityFee":1.5199999980666665,"maxFee":1.5200000160666665},"fast":{"maxPriorityFee":2.0233333273333334,"maxFee":2.0233333453333335},"estimatedBaseFee":1.8e-8,"blockTime":2,"blockNumber":36917340}"#;
let _resp: Response = serde_json::from_str(s).unwrap();
GasEstimate {
max_priority_fee: average,
max_fee: base_fee_per_gas + average,
}
}
20 changes: 18 additions & 2 deletions src/common/types/provider_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use ethers::{
providers::{JsonRpcClient, Middleware, Provider, ProviderError},
types::{
transaction::eip2718::TypedTransaction, Address, Block, BlockId, BlockNumber, Bytes,
Eip1559TransactionRequest, Filter, GethDebugTracingOptions, GethTrace, Log, Transaction,
TransactionReceipt, TxHash, H160, H256, U256, U64,
Eip1559TransactionRequest, FeeHistory, Filter, GethDebugTracingOptions, GethTrace, Log,
Transaction, TransactionReceipt, TxHash, H160, H256, U256, U64,
},
};
#[cfg(test)]
Expand Down Expand Up @@ -53,6 +53,13 @@ pub trait ProviderLike: Send + Sync + 'static {
block: Option<BlockId>,
) -> Result<Bytes, ProviderError>;

async fn fee_history<T: Into<U256> + Send + Sync + Serialize + 'static>(
&self,
t: T,
block_number: BlockNumber,
reward_percentiles: &[f64],
) -> Result<FeeHistory, ProviderError>;

async fn get_block_number(&self) -> anyhow::Result<u64>;

async fn get_block<T: Into<BlockId> + Send + Sync + 'static>(
Expand Down Expand Up @@ -138,6 +145,15 @@ impl<C: JsonRpcClient + 'static> ProviderLike for Provider<C> {
Middleware::call(self, tx, block).await
}

async fn fee_history<T: Into<U256> + Send + Sync + Serialize + 'static>(
&self,
t: T,
block_number: BlockNumber,
reward_percentiles: &[f64],
) -> Result<FeeHistory, ProviderError> {
Middleware::fee_history(self, t, block_number, reward_percentiles).await
}

async fn get_block_number(&self) -> anyhow::Result<u64> {
Ok(Middleware::get_block_number(self)
.await
Expand Down
Loading