1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait;
use ethers_core::types::{Chain, U256};
use reqwest::Client;
use serde::Deserialize;
use url::Url;

const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/v2";
const MUMBAI_URL: &str = "https://gasstation-mumbai.matic.today/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,
    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 {
    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 {
    pub max_priority_fee: f64,
    pub max_fee: f64,
}

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> {
        Self::with_client(Client::new(), 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(), 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
    }

    /// 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().await?;
        Ok(response)
    }
}

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

    #[test]
    fn parse_polygon_gas_station_response() {
        let s = r#"{"safeLow":{"maxPriorityFee":2.1267086610666666,"maxFee":2.1267086760666665},"standard":{"maxPriorityFee":2.3482958369333335,"maxFee":2.3482958519333335},"fast":{"maxPriorityFee":2.793454819,"maxFee":2.793454834},"estimatedBaseFee":1.5e-8,"blockTime":2,"blockNumber":30328888}"#;
        let _resp: Response = serde_json::from_str(s).unwrap();
    }
}