ethers_etherscan/
gas.rs

1use crate::{Client, EtherscanError, Response, Result};
2use ethers_core::{types::U256, utils::parse_units};
3use serde::{de, Deserialize, Deserializer};
4use std::{collections::HashMap, str::FromStr};
5
6const WEI_PER_GWEI: u64 = 1_000_000_000;
7
8#[derive(Deserialize, Clone, Debug)]
9#[serde(rename_all = "PascalCase")]
10pub struct GasOracle {
11    /// Safe Gas Price in wei
12    #[serde(deserialize_with = "deser_gwei_amount")]
13    pub safe_gas_price: U256,
14    /// Propose Gas Price in wei
15    #[serde(deserialize_with = "deser_gwei_amount")]
16    pub propose_gas_price: U256,
17    /// Fast Gas Price in wei
18    #[serde(deserialize_with = "deser_gwei_amount")]
19    pub fast_gas_price: U256,
20    /// Last Block
21    #[serde(deserialize_with = "deserialize_number_from_string")]
22    pub last_block: u64,
23    /// Suggested Base Fee in wei
24    #[serde(deserialize_with = "deser_gwei_amount")]
25    #[serde(rename = "suggestBaseFee")]
26    pub suggested_base_fee: U256,
27    /// Gas Used Ratio
28    #[serde(deserialize_with = "deserialize_f64_vec")]
29    #[serde(rename = "gasUsedRatio")]
30    pub gas_used_ratio: Vec<f64>,
31}
32
33// This function is used to deserialize a string or number into a U256 with an
34// amount of gwei. If the contents is a number, deserialize it. If the contents
35// is a string, attempt to deser as first a decimal f64 then a decimal U256.
36fn deser_gwei_amount<'de, D>(deserializer: D) -> Result<U256, D::Error>
37where
38    D: Deserializer<'de>,
39{
40    #[derive(Deserialize)]
41    #[serde(untagged)]
42    enum StringOrInt {
43        Number(u64),
44        String(String),
45    }
46
47    match StringOrInt::deserialize(deserializer)? {
48        StringOrInt::Number(i) => Ok(U256::from(i) * WEI_PER_GWEI),
49        StringOrInt::String(s) => {
50            parse_units(s, "gwei").map(Into::into).map_err(serde::de::Error::custom)
51        }
52    }
53}
54
55fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result<T, D::Error>
56where
57    D: Deserializer<'de>,
58    T: FromStr + serde::Deserialize<'de>,
59    <T as FromStr>::Err: std::fmt::Display,
60{
61    #[derive(Deserialize)]
62    #[serde(untagged)]
63    enum StringOrInt<T> {
64        String(String),
65        Number(T),
66    }
67
68    match StringOrInt::<T>::deserialize(deserializer)? {
69        StringOrInt::String(s) => s.parse::<T>().map_err(serde::de::Error::custom),
70        StringOrInt::Number(i) => Ok(i),
71    }
72}
73
74fn deserialize_f64_vec<'de, D>(deserializer: D) -> core::result::Result<Vec<f64>, D::Error>
75where
76    D: de::Deserializer<'de>,
77{
78    let str_sequence = String::deserialize(deserializer)?;
79    str_sequence
80        .split(',')
81        .map(|item| f64::from_str(item).map_err(|err| de::Error::custom(err.to_string())))
82        .collect()
83}
84
85impl Client {
86    /// Returns the estimated time, in seconds, for a transaction to be confirmed on the blockchain
87    /// for the specified gas price
88    pub async fn gas_estimate(&self, gas_price: U256) -> Result<u32> {
89        let query = self.create_query(
90            "gastracker",
91            "gasestimate",
92            HashMap::from([("gasprice", gas_price.to_string())]),
93        );
94        let response: Response<String> = self.get_json(&query).await?;
95
96        if response.status == "1" {
97            Ok(u32::from_str(&response.result).map_err(|_| EtherscanError::GasEstimationFailed)?)
98        } else {
99            Err(EtherscanError::GasEstimationFailed)
100        }
101    }
102
103    /// Returns the current Safe, Proposed and Fast gas prices
104    /// Post EIP-1559 changes:
105    /// - Safe/Proposed/Fast gas price recommendations are now modeled as Priority Fees.
106    /// - New field `suggestBaseFee`, the baseFee of the next pending block
107    /// - New field `gasUsedRatio`, to estimate how busy the network is
108    pub async fn gas_oracle(&self) -> Result<GasOracle> {
109        let query = self.create_query("gastracker", "gasoracle", serde_json::Value::Null);
110        let response: Response<GasOracle> = self.get_json(&query).await?;
111
112        Ok(response.result)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn response_works() {
122        // Response from Polygon mainnet at 2023-04-05
123        let v = r#"{
124            "status": "1",
125            "message": "OK",
126            "result": {
127                "LastBlock": "41171167",
128                "SafeGasPrice": "119.9",
129                "ProposeGasPrice": "141.9",
130                "FastGasPrice": "142.9",
131                "suggestBaseFee": "89.82627877",
132                "gasUsedRatio": "0.399191166666667,0.4847166,0.997667533333333,0.538075133333333,0.343416033333333",
133                "UsdPrice": "1.15"
134            }
135        }"#;
136        let gas_oracle: Response<GasOracle> = serde_json::from_str(v).unwrap();
137        assert_eq!(gas_oracle.message, "OK");
138        assert_eq!(
139            gas_oracle.result.propose_gas_price,
140            parse_units("141.9", "gwei").unwrap().into()
141        );
142
143        let v = r#"{
144            "status":"1",
145            "message":"OK",
146            "result":{
147               "LastBlock":"13053741",
148               "SafeGasPrice":"20",
149               "ProposeGasPrice":"22",
150               "FastGasPrice":"24",
151               "suggestBaseFee":"19.230609716",
152               "gasUsedRatio":"0.370119078777807,0.8954731,0.550911766666667,0.212457033333333,0.552463633333333"
153            }
154        }"#;
155        let gas_oracle: Response<GasOracle> = serde_json::from_str(v).unwrap();
156        assert_eq!(gas_oracle.message, "OK");
157        assert_eq!(gas_oracle.result.propose_gas_price, parse_units(22, "gwei").unwrap().into());
158
159        // remove quotes around integers
160        let v = r#"{
161            "status":"1",
162            "message":"OK",
163            "result":{
164               "LastBlock":13053741,
165               "SafeGasPrice":20,
166               "ProposeGasPrice":22,
167               "FastGasPrice":24,
168               "suggestBaseFee":"19.230609716",
169               "gasUsedRatio":"0.370119078777807,0.8954731,0.550911766666667,0.212457033333333,0.552463633333333"
170            }
171        }"#;
172        let gas_oracle: Response<GasOracle> = serde_json::from_str(v).unwrap();
173        assert_eq!(gas_oracle.message, "OK");
174        assert_eq!(gas_oracle.result.propose_gas_price, parse_units(22, "gwei").unwrap().into());
175    }
176}