ethers_middleware/gas_oracle/
polygon.rs1use 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#[derive(Clone, Debug)]
14#[must_use]
15pub struct Polygon {
16 client: Client,
17 url: Url,
18 gas_category: GasCategory,
19}
20
21#[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 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 pub fn category(mut self, gas_category: GasCategory) -> Self {
122 self.gas_category = gas_category;
123 self
124 }
125
126 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}