eth_tx_manager/gas_oracle/
eth_gas_station.rs

1use async_trait::async_trait;
2use core::time::Duration;
3use ethers::types::U256;
4use reqwest::StatusCode;
5use serde::Deserialize;
6use std::fmt::Debug;
7use tracing::trace;
8
9use crate::gas_oracle::{EIP1559GasInfo, GasInfo, GasOracle, GasOracleInfo};
10use crate::transaction::Priority;
11
12/// Implementation that uses the ETH Gas Station API.
13
14#[derive(Debug, thiserror::Error)]
15pub enum ETHGasStationError {
16    #[error("GET request error: {0}")]
17    Request(reqwest::Error),
18
19    #[error("invalid status code: {0}")]
20    StatusCode(reqwest::StatusCode),
21
22    #[error("could not parse the request's response: {0}")]
23    ParseResponse(serde_json::Error),
24}
25
26#[derive(Clone, Debug)]
27pub struct ETHGasStationOracle {
28    api_key: String,
29}
30
31impl ETHGasStationOracle {
32    pub fn new(api_key: String) -> ETHGasStationOracle {
33        ETHGasStationOracle { api_key }
34    }
35}
36
37#[async_trait]
38impl GasOracle for ETHGasStationOracle {
39    type Error = ETHGasStationError;
40
41    #[tracing::instrument(level = "trace")]
42    async fn get_info(&self, priority: Priority) -> Result<GasOracleInfo, Self::Error> {
43        let url = format!(
44            "https://ethgasstation.info/api/ethgasAPI.json?api-key={}",
45            self.api_key
46        );
47
48        let res = reqwest::get(url)
49            .await
50            .map_err(ETHGasStationError::Request)?;
51        if res.status() != StatusCode::OK {
52            return Err(ETHGasStationError::StatusCode(res.status()));
53        }
54
55        let bytes = &res.bytes().await.map_err(ETHGasStationError::Request)?;
56        let response = serde_json::from_slice(bytes).map_err(ETHGasStationError::ParseResponse)?;
57        let gas_info = (response, priority).into();
58        trace!("gas info: {:?}", gas_info);
59        return Ok(gas_info);
60    }
61}
62
63#[derive(Debug, Deserialize)]
64struct ETHGasStationResponse {
65    block_time: f32,
66    fastest: u64,
67    fast: u64,
68    average: u64,
69    #[serde(rename = "safeLow")]
70    low: u64,
71    #[serde(rename = "fastestWait")]
72    fastest_time: f32,
73    #[serde(rename = "fastWait")]
74    fast_time: f32,
75    #[serde(rename = "avgWait")]
76    average_time: f32,
77    #[serde(rename = "safeLowWait")]
78    low_time: f32,
79}
80
81impl From<(ETHGasStationResponse, Priority)> for GasOracleInfo {
82    fn from((response, priority): (ETHGasStationResponse, Priority)) -> Self {
83        let (gas_price, mining_time) = match priority {
84            Priority::Low => (response.low, response.low_time),
85            Priority::Normal => (response.average, response.average_time),
86            Priority::High => (response.fast, response.fast_time),
87            Priority::ASAP => (response.fastest, response.fastest_time),
88        };
89
90        // max fee from 10*gwei to wei
91        let max_fee = U256::from(gas_price).checked_mul(U256::exp10(10)).unwrap();
92        let max_priority_fee = None;
93        let mining_time = Some(Duration::from_secs((mining_time * 60.) as u64));
94        let block_time = Some(Duration::from_secs((response.block_time) as u64));
95
96        GasOracleInfo {
97            gas_info: GasInfo::EIP1559(EIP1559GasInfo {
98                max_fee,
99                max_priority_fee,
100            }),
101            mining_time,
102            block_time,
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use crate::gas_oracle::{EIP1559GasInfo, ETHGasStationOracle, GasOracle, GasOracleInfo};
110    use crate::transaction::Priority;
111
112    use super::ETHGasStationError;
113
114    // Auxiliary.
115    fn unwrap_eip1559_gas_info(
116        result: Result<GasOracleInfo, ETHGasStationError>,
117    ) -> EIP1559GasInfo {
118        assert!(result.is_ok(), "{:?}", result);
119        let eip1559_gas_info: Result<EIP1559GasInfo, &'static str> =
120            result.unwrap().gas_info.try_into();
121        assert!(eip1559_gas_info.is_ok(), "{:?}", eip1559_gas_info);
122        eip1559_gas_info.unwrap()
123    }
124
125    #[tokio::test]
126    #[ignore]
127    async fn test_eth_gas_station_oracle_ok() {
128        // setup
129        // tracing_subscriber::fmt::init();
130        let gas_oracle = ETHGasStationOracle::new("works".to_string());
131
132        // ok => priority low
133        let result = gas_oracle.get_info(Priority::Low).await;
134        assert!(result.is_ok(), "{:?}", result);
135        let eip1559_gas_info_low = unwrap_eip1559_gas_info(result);
136        assert!(eip1559_gas_info_low.max_priority_fee.is_none());
137
138        // ok => priority normal
139        let result = gas_oracle.get_info(Priority::Normal).await;
140        assert!(result.is_ok());
141        let eip1559_gas_info_normal = unwrap_eip1559_gas_info(result);
142        assert!(eip1559_gas_info_normal.max_priority_fee.is_none());
143
144        // ok => priority high
145        let result = gas_oracle.get_info(Priority::High).await;
146        assert!(result.is_ok());
147        let eip1559_gas_info_high = unwrap_eip1559_gas_info(result);
148        assert!(eip1559_gas_info_high.max_priority_fee.is_none());
149
150        // ok => priority ASAP
151        let result = gas_oracle.get_info(Priority::ASAP).await;
152        assert!(result.is_ok());
153        let eip1559_gas_info_asap = unwrap_eip1559_gas_info(result);
154        assert!(eip1559_gas_info_asap.max_priority_fee.is_none());
155
156        // Despite common sense, these assertions are not always true for some
157        // reason. assert!(eip1559_gas_info_low.max_fee <=
158        // eip1559_gas_info_normal.max_fee);
159        // assert!(eip1559_gas_info_normal.max_fee <=
160        // eip1559_gas_info_high.max_fee);
161        // assert!(eip1559_gas_info_high.max_fee <=
162        // eip1559_gas_info_asap.max_fee);
163    }
164
165    #[tokio::test]
166    #[ignore]
167    async fn test_eth_gas_station_oracle_invalid_api_key() {
168        // setup
169        let invalid1 = ETHGasStationOracle::new("invalid".to_string());
170        let invalid2 = ETHGasStationOracle::new("".to_string());
171
172        // ok => invalid API key works (for some reason)
173        let result = invalid1.get_info(Priority::Normal).await;
174        assert!(result.is_ok());
175
176        // ok => empty API key works (for some reason)
177        let result = invalid2.get_info(Priority::Normal).await;
178        assert!(result.is_ok());
179    }
180}