eth_tx_manager/gas_oracle/
eth_gas_station.rs1use 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#[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 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 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 let gas_oracle = ETHGasStationOracle::new("works".to_string());
131
132 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 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 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 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 }
164
165 #[tokio::test]
166 #[ignore]
167 async fn test_eth_gas_station_oracle_invalid_api_key() {
168 let invalid1 = ETHGasStationOracle::new("invalid".to_string());
170 let invalid2 = ETHGasStationOracle::new("".to_string());
171
172 let result = invalid1.get_info(Priority::Normal).await;
174 assert!(result.is_ok());
175
176 let result = invalid2.get_info(Priority::Normal).await;
178 assert!(result.is_ok());
179 }
180}