ethers_middleware/gas_oracle/
polygon.rs

1use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
2use async_trait::async_trait;
3use ethers_core::types::{Chain, U256};
4use reqwest::Client;
5use serde::Deserialize;
6use url::Url;
7
8const MAINNET_URL: &str = "https://gasstation.polygon.technology/v2";
9const MUMBAI_URL: &str = "https://gasstation-testnet.polygon.technology/v2";
10
11/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
12/// Queries over HTTP and implements the `GasOracle` trait.
13#[derive(Clone, Debug)]
14#[must_use]
15pub struct Polygon {
16    client: Client,
17    url: Url,
18    gas_category: GasCategory,
19}
20
21/// The response from the Polygon gas station API.
22///
23/// Gas prices are in __Gwei__.
24#[derive(Debug, Deserialize, PartialEq)]
25#[serde(rename_all = "camelCase")]
26pub struct Response {
27    #[serde(deserialize_with = "deserialize_stringified_f64")]
28    pub estimated_base_fee: f64,
29    pub safe_low: GasEstimate,
30    pub standard: GasEstimate,
31    pub fast: GasEstimate,
32}
33
34#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
35#[serde(rename_all = "camelCase")]
36pub struct GasEstimate {
37    #[serde(deserialize_with = "deserialize_stringified_f64")]
38    pub max_priority_fee: f64,
39    #[serde(deserialize_with = "deserialize_stringified_f64")]
40    pub max_fee: f64,
41}
42
43fn deserialize_stringified_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
44where
45    D: serde::Deserializer<'de>,
46{
47    #[derive(Deserialize)]
48    #[serde(untagged)]
49    enum F64OrString {
50        F64(serde_json::Number),
51        String(String),
52    }
53    match Deserialize::deserialize(deserializer)? {
54        F64OrString::F64(f) => f.as_f64().ok_or_else(|| serde::de::Error::custom("invalid f64")),
55        F64OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
56    }
57}
58
59impl Response {
60    #[inline]
61    pub fn estimate_from_category(&self, gas_category: GasCategory) -> GasEstimate {
62        match gas_category {
63            GasCategory::SafeLow => self.safe_low,
64            GasCategory::Standard => self.standard,
65            GasCategory::Fast => self.fast,
66            GasCategory::Fastest => self.fast,
67        }
68    }
69}
70
71impl Default for Polygon {
72    fn default() -> Self {
73        Self::new(Chain::Polygon).unwrap()
74    }
75}
76
77#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
78#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
79impl GasOracle for Polygon {
80    async fn fetch(&self) -> Result<U256> {
81        let response = self.query().await?;
82        let base = response.estimated_base_fee;
83        let prio = response.estimate_from_category(self.gas_category).max_priority_fee;
84        let fee = base + prio;
85        Ok(from_gwei_f64(fee))
86    }
87
88    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
89        let response = self.query().await?;
90        let estimate = response.estimate_from_category(self.gas_category);
91        let max = from_gwei_f64(estimate.max_fee);
92        let prio = from_gwei_f64(estimate.max_priority_fee);
93        Ok((max, prio))
94    }
95}
96
97impl Polygon {
98    pub fn new(chain: Chain) -> Result<Self> {
99        #[cfg(not(target_arch = "wasm32"))]
100        static APP_USER_AGENT: &str =
101            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
102
103        let builder = Client::builder();
104        #[cfg(not(target_arch = "wasm32"))]
105        let builder = builder.user_agent(APP_USER_AGENT);
106
107        Self::with_client(builder.build()?, chain)
108    }
109
110    pub fn with_client(client: Client, chain: Chain) -> Result<Self> {
111        // TODO: Sniff chain from chain id.
112        let url = match chain {
113            Chain::Polygon => MAINNET_URL,
114            Chain::PolygonMumbai => MUMBAI_URL,
115            _ => return Err(GasOracleError::UnsupportedChain),
116        };
117        Ok(Self { client, url: Url::parse(url).unwrap(), gas_category: GasCategory::Standard })
118    }
119
120    /// Sets the gas price category to be used when fetching the gas price.
121    pub fn category(mut self, gas_category: GasCategory) -> Self {
122        self.gas_category = gas_category;
123        self
124    }
125
126    /// Perform a request to the gas price API and deserialize the response.
127    pub async fn query(&self) -> Result<Response> {
128        let response =
129            self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
130        Ok(response)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn parse_polygon_gas_station_response() {
140        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}"#;
141        let _resp: Response = serde_json::from_str(s).unwrap();
142    }
143
144    #[test]
145    fn parse_polygon_testnet_gas_station_response() {
146        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}"#;
147        let _resp: Response = serde_json::from_str(s).unwrap();
148    }
149}