use std::time::Duration;
use reqwest::Client;
use crate::error::{MempoolError, Result};
use crate::types::{AddressInfo, BlockInfo, FeeEstimates, Transaction, Utxo};
pub const MAINNET_URL: &str = "https://mempool.space/api";
pub const TESTNET_URL: &str = "https://mempool.space/testnet/api";
pub const SIGNET_URL: &str = "https://mempool.space/signet/api";
pub struct MempoolClient {
client: Client,
base_url: String,
}
impl MempoolClient {
pub fn new() -> Self {
Self::with_base_url(MAINNET_URL)
}
pub fn testnet() -> Self {
Self::with_base_url(TESTNET_URL)
}
pub fn signet() -> Self {
Self::with_base_url(SIGNET_URL)
}
pub fn with_base_url(base_url: &str) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn http_client(&self) -> &Client {
&self.client
}
async fn get<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
let url = format!("{}{}", self.base_url, endpoint);
let response = self.client.get(&url).send().await?;
let status = response.status();
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
return Err(MempoolError::RateLimited);
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(MempoolError::ApiError {
status: status.as_u16(),
message,
});
}
response
.json()
.await
.map_err(|e| MempoolError::ParseError(e.to_string()))
}
async fn post(&self, endpoint: &str, body: &str) -> Result<String> {
let url = format!("{}{}", self.base_url, endpoint);
let response = self.client
.post(&url)
.header("Content-Type", "text/plain")
.body(body.to_string())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
return Err(MempoolError::RateLimited);
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(MempoolError::ApiError {
status: status.as_u16(),
message,
});
}
response
.text()
.await
.map_err(|e| MempoolError::ParseError(e.to_string()))
}
pub async fn get_fees(&self) -> Result<FeeEstimates> {
self.get("/v1/fees/recommended").await
}
pub async fn get_address(&self, address: &str) -> Result<AddressInfo> {
self.get(&format!("/address/{}", address)).await
}
pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
self.get(&format!("/address/{}/utxo", address)).await
}
pub async fn get_address_txs(&self, address: &str) -> Result<Vec<Transaction>> {
self.get(&format!("/address/{}/txs", address)).await
}
pub async fn get_tx(&self, txid: &str) -> Result<Transaction> {
self.get(&format!("/tx/{}", txid)).await
}
pub async fn get_tx_hex(&self, txid: &str) -> Result<String> {
let url = format!("{}/tx/{}/hex", self.base_url, txid);
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(MempoolError::ApiError {
status: status.as_u16(),
message,
});
}
response
.text()
.await
.map_err(|e| MempoolError::ParseError(e.to_string()))
}
pub async fn broadcast(&self, hex: &str) -> Result<String> {
self.post("/tx", hex).await
}
pub async fn get_block_height(&self) -> Result<u64> {
let url = format!("{}/blocks/tip/height", self.base_url);
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(MempoolError::ApiError {
status: status.as_u16(),
message,
});
}
let text = response.text().await?;
text.trim()
.parse()
.map_err(|_| MempoolError::ParseError("Invalid block height".into()))
}
pub async fn get_block_hash(&self, height: u64) -> Result<String> {
let url = format!("{}/block-height/{}", self.base_url, height);
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(MempoolError::ApiError {
status: status.as_u16(),
message,
});
}
response
.text()
.await
.map_err(|e| MempoolError::ParseError(e.to_string()))
}
pub async fn get_block(&self, hash: &str) -> Result<BlockInfo> {
self.get(&format!("/block/{}", hash)).await
}
}
impl Default for MempoolClient {
fn default() -> Self {
Self::new()
}
}