use crate::{
error::{GasNetworkError, Result},
types::*,
};
use reqwest::{Client, header::HeaderMap};
use url::Url;
const BASE_URL: &str = "https://api.blocknative.com";
const RPC_URL: &str = "https://rpc.gas.network";
#[derive(Debug, Clone)]
pub struct GasNetworkClient {
client: Client,
api_key: String,
base_url: Url,
rpc_url: Url,
}
impl GasNetworkClient {
pub fn new(api_key: String) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert("Authorization", format!("Bearer {}", api_key).parse().unwrap());
headers.insert("Content-Type", "application/json".parse().unwrap());
let client = Client::builder()
.default_headers(headers)
.build()?;
Ok(Self {
client,
api_key,
base_url: Url::parse(BASE_URL)?,
rpc_url: Url::parse(RPC_URL)?,
})
}
pub async fn get_gas_prices(&self, chain: Chain) -> Result<GasPriceResponse> {
let url = self.base_url
.join(&format!("/gasprices/blockprices?chain={}", chain.as_str()))?;
let response = self.client.get(url).send().await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(GasNetworkError::Api {
message: error_text,
});
}
let gas_prices: GasPriceResponse = response.json().await?;
Ok(gas_prices)
}
pub async fn get_base_fee_estimates(&self, chain: Chain) -> Result<BaseFeeResponse> {
if chain != Chain::Ethereum {
return Err(GasNetworkError::UnsupportedChain(
"Base fee estimates are only available for Ethereum".to_string()
));
}
let url = self.base_url
.join("/gasprices/basefee-estimates")?;
let response = self.client.get(url).send().await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(GasNetworkError::Api {
message: error_text,
});
}
let base_fee: BaseFeeResponse = response.json().await?;
Ok(base_fee)
}
pub async fn get_gas_distribution(&self, chain: Chain) -> Result<DistributionResponse> {
if chain != Chain::Ethereum {
return Err(GasNetworkError::UnsupportedChain(
"Gas distribution is only available for Ethereum".to_string()
));
}
let url = self.base_url
.join(&format!("/gasprices/distribution?chain={}", chain.as_str()))?;
let response = self.client.get(url).send().await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(GasNetworkError::Api {
message: error_text,
});
}
let distribution: DistributionResponse = response.json().await?;
Ok(distribution)
}
pub async fn get_oracle_data(&self, chain_id: u64) -> Result<OraclePayload> {
let url = self.rpc_url
.join(&format!("/oracle?chainId={}", chain_id))?;
let response = self.client.get(url).send().await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(GasNetworkError::Api {
message: error_text,
});
}
let oracle_data: OraclePayload = response.json().await?;
Ok(oracle_data)
}
pub async fn get_next_block_estimate(
&self,
chain: Chain,
confidence_level: Option<u8>,
) -> Result<GasPriceEstimate> {
let gas_prices = self.get_gas_prices(chain).await?;
let confidence = confidence_level.unwrap_or(90);
if let Some(block_price) = gas_prices.block_prices.first() {
block_price
.estimated_prices
.iter()
.find(|estimate| estimate.confidence >= confidence)
.cloned()
.ok_or_else(|| GasNetworkError::Api {
message: format!("No estimate found for confidence level {}", confidence),
})
} else {
Err(GasNetworkError::Api {
message: "No block prices available".to_string(),
})
}
}
pub fn supported_chains() -> Vec<Chain> {
vec![
Chain::Ethereum,
Chain::Polygon,
Chain::Bitcoin,
Chain::Sei,
Chain::Optimism,
Chain::Arbitrum,
Chain::Base,
Chain::Linea,
Chain::Unichain,
]
}
pub fn chains_supporting_base_fee() -> Vec<Chain> {
vec![Chain::Ethereum]
}
pub fn chains_supporting_distribution() -> Vec<Chain> {
vec![Chain::Ethereum]
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}