use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::str::FromStr;
use std::time::Duration;
use bitcoin::block::Header as BlockHeader;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::consensus::{deserialize, serialize, Decodable};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, MerkleBlock, Script, Transaction, Txid};
use bitreq::{Client, Method, Proxy, Request, RequestExt, Response};
use crate::{
duration_to_timeout_secs, is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats,
BlockInfo, BlockStatus, Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof,
OutputStatus, ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS,
};
#[allow(deprecated)]
use crate::BlockSummary;
#[derive(Clone)]
pub struct AsyncClient<S = DefaultSleeper> {
url: String,
proxy: Option<String>,
timeout: Option<Duration>,
headers: HashMap<String, String>,
max_retries: usize,
client: Client,
marker: PhantomData<S>,
}
impl<S: Sleeper> AsyncClient<S> {
pub fn from_builder(builder: Builder) -> Result<Self, Error> {
Ok(AsyncClient {
url: builder.base_url,
proxy: builder.proxy,
timeout: builder.timeout,
headers: builder.headers,
max_retries: builder.max_retries,
client: Client::new(builder.max_connections),
marker: PhantomData,
})
}
pub fn url(&self) -> &str {
&self.url
}
pub fn client(&self) -> &Client {
&self.client
}
pub(crate) fn build_request(&self, method: Method, path: &str) -> Result<Request, Error> {
let mut request = Request::new(method, format!("{}{}", self.url, path));
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = &self.proxy {
request = request.with_proxy(Proxy::new_http(proxy)?);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(timeout) = self.timeout {
request = request.with_timeout(duration_to_timeout_secs(timeout));
}
if !self.headers.is_empty() {
request = request.with_headers(&self.headers);
}
Ok(request)
}
async fn get_with_retry(&self, path: &str) -> Result<Response, Error> {
let mut delay = BASE_BACKOFF_MILLIS;
let mut attempts = 0;
let request = self.build_request(Method::Get, path)?;
loop {
match request.clone().send_async_with_client(&self.client).await? {
response if attempts < self.max_retries && is_retryable(&response) => {
S::sleep(delay).await;
attempts += 1;
delay *= 2;
}
response => return Ok(response),
}
}
}
async fn get_response<T: Decodable>(&self, path: &str) -> Result<T, Error> {
let response = self.get_with_retry(path).await?;
if !is_success(&response) {
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
let message = response.as_str().unwrap_or_default().to_string();
return Err(Error::HttpResponse { status, message });
}
Ok(deserialize::<T>(response.as_bytes())?)
}
async fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
match self.get_response::<T>(path).await {
Ok(res) => Ok(Some(res)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
async fn get_response_json<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<T, Error> {
let response = self.get_with_retry(path).await?;
if !is_success(&response) {
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
let message = response.as_str().unwrap_or_default().to_string();
return Err(Error::HttpResponse { status, message });
}
response.json::<T>().map_err(Error::BitReq)
}
async fn get_opt_response_json<T: serde::de::DeserializeOwned>(
&self,
url: &str,
) -> Result<Option<T>, Error> {
match self.get_response_json(url).await {
Ok(res) => Ok(Some(res)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
async fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
let response = self.get_with_retry(path).await?;
if !is_success(&response) {
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
let message = response.as_str().unwrap_or_default().to_string();
return Err(Error::HttpResponse { status, message });
}
let hex_str = response.as_str()?;
Ok(deserialize(&Vec::from_hex(hex_str)?)?)
}
async fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
match self.get_response_hex(path).await {
Ok(res) => Ok(Some(res)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
async fn get_response_text(&self, path: &str) -> Result<String, Error> {
let response = self.get_with_retry(path).await?;
if !is_success(&response) {
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
let message = response.as_str().unwrap_or_default().to_string();
return Err(Error::HttpResponse { status, message });
}
Ok(response.as_str()?.to_string())
}
async fn get_opt_response_text(&self, path: &str) -> Result<Option<String>, Error> {
match self.get_response_text(path).await {
Ok(s) => Ok(Some(s)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
async fn post_request_bytes<T: Into<Vec<u8>>>(
&self,
path: &str,
body: T,
query_params: Option<HashSet<(&str, String)>>,
) -> Result<Response, Error> {
let mut request: bitreq::Request = self.build_request(Method::Post, path)?.with_body(body);
for (key, value) in query_params.unwrap_or_default() {
request = request.with_param(key, value);
}
let response = request.send_async_with_client(&self.client).await?;
if !is_success(&response) {
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
let message = response.as_str().unwrap_or_default().to_string();
return Err(Error::HttpResponse { status, message });
}
Ok(response)
}
pub async fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
self.get_opt_response(&format!("/tx/{txid}/raw")).await
}
pub async fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
match self.get_tx(txid).await {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(Error::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
pub async fn get_txid_at_block_index(
&self,
block_hash: &BlockHash,
index: usize,
) -> Result<Option<Txid>, Error> {
match self
.get_opt_response_text(&format!("/block/{block_hash}/txid/{index}"))
.await?
{
Some(s) => Ok(Some(Txid::from_str(&s).map_err(Error::HexToArray)?)),
None => Ok(None),
}
}
pub async fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
self.get_response_json(&format!("/tx/{txid}/status")).await
}
pub async fn get_tx_info(&self, txid: &Txid) -> Result<Option<EsploraTx>, Error> {
self.get_opt_response_json(&format!("/tx/{txid}")).await
}
pub async fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
self.get_response_json(&format!("/tx/{txid}/outspends"))
.await
}
pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
self.get_response_hex(&format!("/block/{block_hash}/header"))
.await
}
pub async fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
self.get_response_json(&format!("/block/{block_hash}/status"))
.await
}
pub async fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
self.get_opt_response(&format!("/block/{block_hash}/raw"))
.await
}
pub async fn get_merkle_proof(&self, tx_hash: &Txid) -> Result<Option<MerkleProof>, Error> {
self.get_opt_response_json(&format!("/tx/{tx_hash}/merkle-proof"))
.await
}
pub async fn get_merkle_block(&self, tx_hash: &Txid) -> Result<Option<MerkleBlock>, Error> {
self.get_opt_response_hex(&format!("/tx/{tx_hash}/merkleblock-proof"))
.await
}
pub async fn get_output_status(
&self,
txid: &Txid,
index: u64,
) -> Result<Option<OutputStatus>, Error> {
self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
.await
}
pub async fn broadcast(&self, transaction: &Transaction) -> Result<Txid, Error> {
let body = serialize::<Transaction>(transaction).to_lower_hex_string();
let response = self.post_request_bytes("/tx", body, None).await?;
let txid = Txid::from_str(response.as_str()?).map_err(Error::HexToArray)?;
Ok(txid)
}
pub async fn submit_package(
&self,
transactions: &[Transaction],
maxfeerate: Option<FeeRate>,
maxburnamount: Option<Amount>,
) -> Result<SubmitPackageResult, Error> {
let serialized_txs = transactions
.iter()
.map(|tx| serialize_hex(&tx))
.collect::<Vec<_>>();
let mut params = HashSet::<(&str, String)>::new();
if let Some(maxfeerate) = maxfeerate {
params.insert(("maxfeerate", maxfeerate.to_sat_per_vb_ceil().to_string()));
}
if let Some(maxburnamount) = maxburnamount {
params.insert(("maxburnamount", maxburnamount.to_btc().to_string()));
}
let response = self
.post_request_bytes(
"/txs/package",
serde_json::to_string(&serialized_txs).map_err(Error::SerdeJson)?,
Some(params),
)
.await?;
let result = response.json::<SubmitPackageResult>()?;
Ok(result)
}
pub async fn get_height(&self) -> Result<u32, Error> {
self.get_response_text("/blocks/tip/height")
.await
.map(|height| u32::from_str(&height).map_err(Error::Parsing))?
}
pub async fn get_tip_hash(&self) -> Result<BlockHash, Error> {
self.get_response_text("/blocks/tip/hash")
.await
.map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
}
pub async fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
self.get_response_text(&format!("/block-height/{block_height}"))
.await
.map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
}
pub async fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
let path = format!("/address/{address}");
self.get_response_json(&path).await
}
pub async fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}");
self.get_response_json(&path).await
}
pub async fn get_address_txs(
&self,
address: &Address,
last_seen: Option<Txid>,
) -> Result<Vec<EsploraTx>, Error> {
let path = match last_seen {
Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
None => format!("/address/{address}/txs"),
};
self.get_response_json(&path).await
}
pub async fn get_mempool_address_txs(
&self,
address: &Address,
) -> Result<Vec<EsploraTx>, Error> {
let path = format!("/address/{address}/txs/mempool");
self.get_response_json(&path).await
}
pub async fn get_scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<EsploraTx>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = match last_seen {
Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
None => format!("/scripthash/{script_hash:x}/txs"),
};
self.get_response_json(&path).await
}
pub async fn get_mempool_scripthash_txs(
&self,
script: &Script,
) -> Result<Vec<EsploraTx>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash:x}/txs/mempool");
self.get_response_json(&path).await
}
pub async fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
self.get_response_json("/mempool").await
}
pub async fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
self.get_response_json("/mempool/recent").await
}
pub async fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
self.get_response_json("/mempool/txids").await
}
pub async fn get_fee_estimates(&self) -> Result<HashMap<u16, FeeRate>, Error> {
let estimates_raw: HashMap<u16, f64> = self.get_response_json("/fee-estimates").await?;
let estimates = sat_per_vbyte_to_feerate(estimates_raw);
Ok(estimates)
}
pub async fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
let path = format!("/block/{blockhash}");
self.get_response_json(&path).await
}
pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
let path = format!("/block/{blockhash}/txids");
self.get_response_json(&path).await
}
pub async fn get_block_txs(
&self,
blockhash: &BlockHash,
start_index: Option<u32>,
) -> Result<Vec<EsploraTx>, Error> {
let path = match start_index {
None => format!("/block/{blockhash}/txs"),
Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"),
};
self.get_response_json(&path).await
}
#[allow(deprecated)]
#[deprecated(since = "0.13.0", note = "use `get_block_infos` instead")]
pub async fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let blocks: Vec<BlockSummary> = self.get_response_json(&path).await?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}
pub async fn get_block_infos(&self, height: Option<u32>) -> Result<Vec<BlockInfo>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let blocks: Vec<BlockInfo> = self.get_response_json(&path).await?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}
pub async fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
let path = format!("/address/{address}/utxo");
self.get_response_json(&path).await
}
pub async fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}/utxo");
self.get_response_json(&path).await
}
}
pub trait Sleeper: 'static {
type Sleep: std::future::Future<Output = ()>;
fn sleep(dur: Duration) -> Self::Sleep;
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultSleeper;
#[cfg(any(test, feature = "tokio"))]
impl Sleeper for DefaultSleeper {
type Sleep = tokio::time::Sleep;
fn sleep(dur: std::time::Duration) -> Self::Sleep {
tokio::time::sleep(dur)
}
}