use std::{str::FromStr, time::Duration};
use async_trait::async_trait;
use bitcoin::{Address, Transaction, Txid};
use esplora_client::r#async::AsyncClient;
use super::{
AddressUtxo, ChainClient, ChainClientError, DEFAULT_TIMEOUT, EsploraError, EsploraUtxo,
OutpointSpend, Outspend, SpendingTx,
};
use crate::{
fee,
transaction::{decode_tx_hex, encode_tx_hex},
types::Chain,
};
pub(crate) const HISTORY_PAGE: usize = 25;
#[derive(Debug, Clone)]
pub struct BitcoinEsplora(AsyncClient);
impl std::ops::Deref for BitcoinEsplora {
type Target = AsyncClient;
fn deref(&self) -> &AsyncClient {
&self.0
}
}
impl BitcoinEsplora {
pub fn new(url: &str) -> Result<Self, EsploraError> {
Self::with_timeout(url, DEFAULT_TIMEOUT)
}
pub fn with_timeout(url: &str, timeout: Duration) -> Result<Self, EsploraError> {
let client = esplora_client::Builder::new(url)
.timeout(timeout.as_secs())
.build_async()?;
Ok(Self(client))
}
pub async fn get_fee_rate(&self, target: usize) -> Result<f64, EsploraError> {
let estimates = self.0.get_fee_estimates().await?;
let estimate = esplora_client::convert_fee_rate(target, estimates)
.map(f64::from)
.ok_or(EsploraError::NoFeeEstimate(target))?;
if !estimate.is_finite() || estimate < 0.0 {
return Err(EsploraError::InvalidFeeRate(estimate));
}
Ok(estimate.max(fee::min_fee_rate(Chain::Bitcoin)))
}
pub async fn get_tx_confirmations(&self, txid: &Txid) -> Result<Option<u32>, EsploraError> {
let Some(tx) = self.0.get_tx_info(txid).await? else {
return Ok(None);
};
let status = tx.status;
let block_height = match status.block_height {
Some(h) if status.confirmed => h,
_ => return Ok(Some(0)),
};
let tip = self.0.get_height().await?;
Ok(Some(tip.saturating_sub(block_height) + 1))
}
pub async fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
Ok(self.0.get_tx(txid).await?)
}
pub async fn broadcast(&self, tx: &Transaction) -> Result<Txid, EsploraError> {
let txid = tx.compute_txid();
self.0.broadcast(tx).await?;
Ok(txid)
}
pub async fn get_height(&self) -> Result<u32, EsploraError> {
Ok(self.0.get_height().await?)
}
pub async fn get_outspend(
&self,
txid: &Txid,
vout: u32,
) -> Result<Option<Outspend>, EsploraError> {
let Some(status) = self.0.get_output_status(txid, vout as u64).await? else {
return Ok(None);
};
Ok(match (status.spent, status.txid, status.vin) {
(true, Some(txid), Some(vin)) => Some(Outspend {
txid,
vin: vin as u32,
}),
_ => None,
})
}
pub async fn get_confirmation_height(&self, txid: &Txid) -> Result<Option<u32>, EsploraError> {
let status = self.0.get_tx_status(txid).await?;
Ok(status.block_height.filter(|_| status.confirmed))
}
pub async fn address_utxos(&self, address: &Address) -> Result<Vec<EsploraUtxo>, EsploraError> {
let utxos = self.0.get_address_utxos(address).await?;
Ok(utxos
.into_iter()
.map(|u| EsploraUtxo {
txid: u.txid,
vout: u.vout,
value: u.value,
})
.collect())
}
pub async fn address_txs(
&self,
address: &Address,
) -> Result<Vec<(Transaction, Option<u32>)>, EsploraError> {
let mut txs = Vec::new();
let mut last_seen = None;
loop {
let page = self.0.get_address_txs(address, last_seen).await?;
let confirmed = page.iter().filter(|tx| tx.status.confirmed).count();
last_seen = page
.iter()
.rev()
.find(|tx| tx.status.confirmed)
.map(|tx| tx.txid);
txs.extend(page.iter().map(|tx| {
let height = tx.status.block_height.filter(|_| tx.status.confirmed);
(tx.to_tx(), height)
}));
if confirmed < HISTORY_PAGE {
return Ok(txs);
}
}
}
}
impl From<EsploraError> for ChainClientError {
fn from(e: EsploraError) -> Self {
ChainClientError::Other(e.into())
}
}
#[derive(Debug, Clone)]
pub struct BitcoinChainClient(BitcoinEsplora);
impl BitcoinChainClient {
pub fn new(url: &str) -> Result<Self, EsploraError> {
Ok(Self(BitcoinEsplora::new(url)?))
}
}
#[async_trait]
impl ChainClient for BitcoinChainClient {
async fn get_fee_rate(&self, target: usize) -> Result<f64, ChainClientError> {
match self.0.get_fee_rate(target).await {
Err(EsploraError::NoFeeEstimate(_)) => Ok(fee::min_fee_rate(Chain::Bitcoin)),
rate => Ok(rate?),
}
}
async fn get_tx_confirmations(&self, txid: &str) -> Result<Option<u32>, ChainClientError> {
Ok(self.0.get_tx_confirmations(&parse_txid(txid)?).await?)
}
async fn get_tx(&self, txid: &str) -> Result<Option<String>, ChainClientError> {
Ok(self
.0
.get_tx(&parse_txid(txid)?)
.await?
.map(|tx| encode_tx_hex(&tx)))
}
async fn broadcast(&self, tx_hex: &str) -> Result<String, ChainClientError> {
let tx = decode_tx_hex(tx_hex)?;
Ok(self.0.broadcast(&tx).await?.to_string())
}
async fn get_height(&self) -> Result<u32, ChainClientError> {
Ok(self.0.get_height().await?)
}
async fn get_confirmation_height(&self, txid: &str) -> Result<Option<u32>, ChainClientError> {
Ok(self.0.get_confirmation_height(&parse_txid(txid)?).await?)
}
async fn address_utxos(&self, address: &str) -> Result<Vec<AddressUtxo>, ChainClientError> {
let utxos = self.0.address_utxos(&parse_address(address)?).await?;
Ok(utxos
.into_iter()
.map(|utxo| AddressUtxo {
txid: utxo.txid.to_string(),
vout: utxo.vout,
value: Some(utxo.value.to_sat()),
})
.collect())
}
async fn outpoint_spend(
&self,
txid: &str,
vout: u32,
) -> Result<OutpointSpend, ChainClientError> {
let Some(outspend) = self.0.get_outspend(&parse_txid(txid)?, vout).await? else {
return Ok(OutpointSpend::Unspent);
};
let (spending_txid, vin) = (outspend.txid, outspend.vin);
let confirmations = self
.0
.get_tx_confirmations(&spending_txid)
.await?
.unwrap_or(0);
let Some(tx) = self.0.get_tx(&spending_txid).await? else {
return Err(ChainClientError::Other(anyhow::anyhow!(
"spending tx {spending_txid} reported by outspend but not found"
)));
};
let tx_hex = encode_tx_hex(&tx);
Ok(OutpointSpend::Spent(SpendingTx {
txid: spending_txid.to_string(),
vin,
confirmations,
tx_hex,
}))
}
}
fn parse_txid(txid: &str) -> Result<Txid, ChainClientError> {
Txid::from_str(txid)
.map_err(|e| ChainClientError::Other(anyhow::anyhow!("invalid txid {txid}: {e}")))
}
fn parse_address(address: &str) -> Result<Address, ChainClientError> {
Ok(Address::from_str(address)
.map_err(|e| ChainClientError::Other(anyhow::anyhow!("invalid address {address}: {e}")))?
.assume_checked())
}