Skip to content

Commit

Permalink
feat(polygon): remove dependency on gas station api
Browse files Browse the repository at this point in the history
  • Loading branch information
0xfourzerofour committed Sep 19, 2023
1 parent 69b6b0d commit dd0bae5
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 178 deletions.
14 changes: 12 additions & 2 deletions crates/provider/src/ethers/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use ethers::{
providers::{JsonRpcClient, Middleware, Provider as EthersProvider, ProviderError},
types::{
transaction::eip2718::TypedTransaction, Address, Block, BlockId, BlockNumber, Bytes,
Eip1559TransactionRequest, Filter, GethDebugTracingCallOptions, GethDebugTracingOptions,
GethTrace, Log, Transaction, TransactionReceipt, TxHash, H160, H256, U256, U64,
Eip1559TransactionRequest, FeeHistory, Filter, GethDebugTracingCallOptions,
GethDebugTracingOptions, GethTrace, Log, Transaction, TransactionReceipt, TxHash, H160,
H256, U256, U64,
},
};
use rundler_types::{
Expand Down Expand Up @@ -51,6 +52,15 @@ impl<C: JsonRpcClient + 'static> Provider for EthersProvider<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
14 changes: 11 additions & 3 deletions crates/provider/src/traits/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use std::{fmt::Debug, sync::Arc};
use ethers::{
providers::ProviderError,
types::{
transaction::eip2718::TypedTransaction, Address, Block, BlockId, Bytes, Filter,
GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, Log, Transaction,
TransactionReceipt, TxHash, H256, U256,
transaction::eip2718::TypedTransaction, Address, Block, BlockId, BlockNumber, Bytes,
FeeHistory, Filter, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, Log,
Transaction, TransactionReceipt, TxHash, H256, U256,
},
};
#[cfg(feature = "test-utils")]
Expand Down Expand Up @@ -45,6 +45,14 @@ pub trait Provider: Send + Sync + 'static {
T: Debug + Serialize + Send + Sync + 'static,
R: Serialize + DeserializeOwned + Debug + Send + 'static;

/// Get fee history given a number of blocks and reward percentiles
async fn fee_history<T: Into<U256> + Serialize + Send + Sync + 'static>(
&self,
t: T,
block_number: BlockNumber,
reward_percentiles: &[f64],
) -> Result<FeeHistory, ProviderError>;

/// Simulate a transaction via an eth_call
async fn call(
&self,
Expand Down
39 changes: 5 additions & 34 deletions crates/sim/src/gas/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::{Address, Chain, U256},
};
use rundler_provider::Provider;
Expand Down Expand Up @@ -271,19 +271,10 @@ impl<P: Provider> 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 @@ -292,12 +283,6 @@ impl<P: Provider> FeeEstimator<P> {
}
}

const GWEI_TO_WEI: u64 = 1_000_000_000;

pub(crate) 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 @@ -307,17 +292,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));
}
}
206 changes: 67 additions & 139 deletions crates/sim/src/gas/polygon.rs
Original file line number Diff line number Diff line change
@@ -1,169 +1,97 @@
// This code was taken from ethers-rs, we will remove it once our PR is merged for rounding errors
use std::sync::Arc;

use async_trait::async_trait;
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 rundler_provider::Provider;
use serde::Deserialize;
use url::Url;

use super::gas::from_gwei_f64;

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(crate) 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")]
struct Response {
#[serde(deserialize_with = "deserialize_stringified_f64")]
estimated_base_fee: f64,
safe_low: GasEstimate,
standard: GasEstimate,
fast: GasEstimate,
#[derive(Clone, Copy, Deserialize, PartialEq)]
pub(crate) struct GasEstimate {
pub(crate) max_priority_fee: U256,
pub(crate) max_fee: U256,
}

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

fn deserialize_stringified_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
impl<P> Polygon<P>
where
D: serde::Deserializer<'de>,
P: Provider,
{
#[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]
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(crate) 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)
}

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(crate) 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.
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,
}
}

0 comments on commit dd0bae5

Please sign in to comment.