use alloy::primitives::utils::parse_units;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::base::{BaseGasFeeEstimator, GasEstimatorError, GasEstimatorResult, GasPriceResult};
use crate::{
gas::types::{MaxFee, MaxPriorityFee},
network::ChainId,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EtherscanGasProviderSetupConfig {
pub api_key: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct EtherscanGasOracleResult {
#[serde(rename = "SafeGasPrice")]
safe_gas_price: String,
#[serde(rename = "ProposeGasPrice")]
propose_gas_price: String,
#[serde(rename = "FastGasPrice")]
fast_gas_price: String,
#[serde(rename = "suggestBaseFee")]
suggest_base_fee: String,
#[serde(rename = "gasUsedRatio")]
gas_used_ratio: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct EtherscanApiResponse {
status: String,
message: String,
result: EtherscanGasOracleResult,
}
impl EtherscanGasOracleResult {
fn parse_gwei_to_wei(gwei_str: &str) -> Result<u128, GasEstimatorError> {
parse_units(gwei_str, "gwei")
.map_err(|e| {
GasEstimatorError::CustomError(format!(
"Failed to parse gas price '{}': {}",
gwei_str, e
))
})?
.try_into()
.map_err(|_| GasEstimatorError::CustomError("Gwei value too large".to_string()))
}
pub fn to_base_result(&self) -> Result<GasEstimatorResult, GasEstimatorError> {
let base_fee = Self::parse_gwei_to_wei(&self.suggest_base_fee)?;
let safe_total = Self::parse_gwei_to_wei(&self.safe_gas_price)?;
let propose_total = Self::parse_gwei_to_wei(&self.propose_gas_price)?;
let fast_total = Self::parse_gwei_to_wei(&self.fast_gas_price)?;
let min_priority_fee = parse_units("1", "gwei").unwrap().try_into().unwrap();
let safe_priority = std::cmp::max(safe_total.saturating_sub(base_fee), min_priority_fee);
let propose_priority =
std::cmp::max(propose_total.saturating_sub(base_fee), min_priority_fee);
let fast_priority = std::cmp::max(fast_total.saturating_sub(base_fee), min_priority_fee);
let super_fast_priority = fast_priority * 120 / 100;
let super_fast_total = fast_total * 120 / 100;
let slow_result = GasPriceResult {
max_priority_fee: MaxPriorityFee::new(safe_priority),
max_fee: MaxFee::new(safe_total),
min_wait_time_estimate: Some(300), max_wait_time_estimate: Some(600), };
let medium_result = GasPriceResult {
max_priority_fee: MaxPriorityFee::new(propose_priority),
max_fee: MaxFee::new(propose_total),
min_wait_time_estimate: Some(60), max_wait_time_estimate: Some(180), };
let fast_result = GasPriceResult {
max_priority_fee: MaxPriorityFee::new(fast_priority),
max_fee: MaxFee::new(fast_total),
min_wait_time_estimate: Some(15), max_wait_time_estimate: Some(60), };
let super_fast_result = GasPriceResult {
max_priority_fee: MaxPriorityFee::new(super_fast_priority),
max_fee: MaxFee::new(super_fast_total),
min_wait_time_estimate: Some(5), max_wait_time_estimate: Some(15), };
Ok(GasEstimatorResult {
slow: slow_result,
medium: medium_result,
fast: fast_result,
super_fast: super_fast_result,
})
}
}
pub struct EtherscanGasFeeEstimator {
config: EtherscanGasProviderSetupConfig,
client: reqwest::Client,
}
impl EtherscanGasFeeEstimator {
pub fn new(config: EtherscanGasProviderSetupConfig) -> Result<Self, GasEstimatorError> {
let client = reqwest::Client::new();
Ok(Self { config, client })
}
fn get_api_base_url(&self, chain_id: &ChainId) -> Option<&'static str> {
match chain_id.u64() {
1 => Some("https://api.etherscan.io"),
11155111 => Some("https://api-sepolia.etherscan.io"), 17000 => Some("https://api-holesky.etherscan.io"),
137 => Some("https://api.polygonscan.com"),
80002 => Some("https://api-amoy.polygonscan.com"),
56 => Some("https://api.bscscan.com"),
97 => Some("https://api-testnet.bscscan.com"),
10 => Some("https://api-optimistic.etherscan.io"),
11155420 => Some("https://api-sepolia-optimistic.etherscan.io"),
42161 => Some("https://api.arbiscan.io"),
421614 => Some("https://api-sepolia.arbiscan.io"),
8453 => Some("https://api.basescan.org"),
84532 => Some("https://api-sepolia.basescan.org"),
43114 => Some("https://api.snowtrace.io"),
43113 => Some("https://api-testnet.snowtrace.io"),
250 => Some("https://api.ftmscan.com"),
4002 => Some("https://api-testnet.ftmscan.com"),
25 => Some("https://api.cronoscan.com"),
338 => Some("https://api-testnet.cronoscan.com"),
1284 => Some("https://api-moonbeam.moonscan.io"),
1287 => Some("https://api-moonbase.moonscan.io"),
1285 => Some("https://api-moonriver.moonscan.io"),
42220 => Some("https://api.celoscan.io"),
44787 => Some("https://api-alfajores.celoscan.io"),
100 => Some("https://api.gnosisscan.io"),
59144 => Some("https://api.lineascan.build"),
59141 => Some("https://api-sepolia.lineascan.build"),
534352 => Some("https://api.scrollscan.com"),
534351 => Some("https://api-sepolia.scrollscan.com"),
81457 => Some("https://api.blastscan.io"),
168587773 => Some("https://api-sepolia.blastscan.io"),
324 => Some("https://api-era.zksync.network"),
300 => Some("https://api-sepolia.era.zksync.network"),
5000 => Some("https://api.mantlescan.xyz"),
5003 => Some("https://api-sepolia.mantlescan.xyz"),
252 => Some("https://api.fraxscan.com"),
2522 => Some("https://api-holesky.fraxscan.com"),
34443 => Some("https://api.modescan.io"),
919 => Some("https://api-sepolia.modescan.io"),
_ => None,
}
}
}
#[async_trait]
impl BaseGasFeeEstimator for EtherscanGasFeeEstimator {
async fn get_gas_prices(
&self,
chain_id: &ChainId,
) -> Result<GasEstimatorResult, GasEstimatorError> {
let base_url = self.get_api_base_url(chain_id).ok_or_else(|| {
GasEstimatorError::CustomError(format!(
"Etherscan API not supported for chain {}",
chain_id.u64()
))
})?;
let url = format!(
"{}/api?module=gastracker&action=gasoracle&apikey={}",
base_url, self.config.api_key
);
let response =
self.client.get(&url).send().await.map_err(GasEstimatorError::ReqwestError)?;
if !response.status().is_success() {
return Err(GasEstimatorError::CustomError(format!(
"Etherscan API returned status: {}",
response.status()
)));
}
let api_response: EtherscanApiResponse =
response.json().await.map_err(|e| GasEstimatorError::CustomError(e.to_string()))?;
if api_response.status != "1" {
return Err(GasEstimatorError::CustomError(format!(
"Etherscan API error: {}",
api_response.message
)));
}
api_response.result.to_base_result()
}
fn is_chain_supported(&self, chain_id: &ChainId) -> bool {
self.get_api_base_url(chain_id).is_some()
}
}