-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(polygon): remove dependency on gas station api
- Loading branch information
1 parent
69b6b0d
commit dd0bae5
Showing
4 changed files
with
95 additions
and
178 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |