use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::str::FromStr;
use std::thread;
use std::time::Duration;
use bitcoin::consensus::encode::serialize_hex;
use bitreq::{Method, Proxy, Request, Response};
use bitcoin::block::Header as BlockHeader;
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 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(Debug, Clone)]
pub struct BlockingClient {
url: String,
pub proxy: Option<String>,
pub timeout: Option<Duration>,
pub headers: HashMap<String, String>,
pub max_retries: usize,
}
impl BlockingClient {
pub fn from_builder(builder: Builder) -> Self {
Self {
url: builder.base_url,
proxy: builder.proxy,
timeout: builder.timeout,
headers: builder.headers,
max_retries: builder.max_retries,
}
}
pub fn url(&self) -> &str {
&self.url
}
pub(crate) fn build_request(&self, method: Method, path: &str) -> Result<Request, Error> {
let mut request = Request::new(method, format!("{}{}", self.url, path));
if let Some(proxy) = &self.proxy {
request = request.with_proxy(Proxy::new_http(proxy)?);
}
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)
}
pub fn post_request<T: Into<Vec<u8>>>(
&self,
path: &str,
body: T,
query_params: Option<HashSet<(&str, String)>>,
) -> Result<Response, Error> {
let mut 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()?;
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)
}
fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
let mut delay = BASE_BACKOFF_MILLIS;
let mut attempts = 0;
loop {
match self.build_request(Method::Get, url)?.send()? {
resp if attempts < self.max_retries && is_retryable(&resp) => {
thread::sleep(delay);
attempts += 1;
delay *= 2;
}
resp => return Ok(resp),
}
}
}
fn get_response<T: Decodable>(&self, path: &str) -> Result<T, Error> {
let response = self.get_with_retry(path)?;
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())?)
}
fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
match self.get_response(path) {
Ok(response) => Ok(Some(response)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
let response = self.get_with_retry(path)?;
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()?;
deserialize(&Vec::from_hex(hex_str)?).map_err(Error::BitcoinEncoding)
}
fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
match self.get_response_hex(path) {
Ok(res) => Ok(Some(res)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
fn get_response_json<'a, T: serde::de::DeserializeOwned>(
&'a self,
path: &'a str,
) -> Result<T, Error> {
let response = self.get_with_retry(path)?;
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)
}
fn get_opt_response_json<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<Option<T>, Error> {
match self.get_response_json(path) {
Ok(res) => Ok(Some(res)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
fn get_response_text(&self, path: &str) -> Result<String, Error> {
let response = self.get_with_retry(path)?;
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())
}
fn get_opt_response_text(&self, path: &str) -> Result<Option<String>, Error> {
match self.get_response_text(path) {
Ok(s) => Ok(Some(s)),
Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
Err(e) => Err(e),
}
}
pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
self.get_opt_response(&format!("/tx/{txid}/raw"))
}
pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
match self.get_tx(txid) {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(Error::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
pub 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}"))? {
Some(s) => Ok(Some(Txid::from_str(&s).map_err(Error::HexToArray)?)),
None => Ok(None),
}
}
pub fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
self.get_response_json(&format!("/tx/{txid}/status"))
}
pub fn get_tx_info(&self, txid: &Txid) -> Result<Option<EsploraTx>, Error> {
self.get_opt_response_json(&format!("/tx/{txid}"))
}
pub fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
self.get_response_json(&format!("/tx/{txid}/outspends"))
}
pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
self.get_response_hex(&format!("/block/{block_hash}/header"))
}
pub fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
self.get_response_json(&format!("/block/{block_hash}/status"))
}
pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
self.get_opt_response(&format!("/block/{block_hash}/raw"))
}
pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, Error> {
self.get_opt_response_json(&format!("/tx/{txid}/merkle-proof"))
}
pub fn get_merkle_block(&self, txid: &Txid) -> Result<Option<MerkleBlock>, Error> {
self.get_opt_response_hex(&format!("/tx/{txid}/merkleblock-proof"))
}
pub fn get_output_status(
&self,
txid: &Txid,
index: u64,
) -> Result<Option<OutputStatus>, Error> {
self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
}
pub fn broadcast(&self, transaction: &Transaction) -> Result<Txid, Error> {
let body = serialize::<Transaction>(transaction).to_lower_hex_string();
let response = self.post_request("/tx", body, None)?;
let txid = Txid::from_str(response.as_str()?).map_err(Error::HexToArray)?;
Ok(txid)
}
pub 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(
"/txs/package",
serde_json::to_string(&serialized_txs).map_err(Error::SerdeJson)?,
Some(params),
)?;
let result = response.json::<SubmitPackageResult>()?;
Ok(result)
}
pub fn get_height(&self) -> Result<u32, Error> {
self.get_response_text("/blocks/tip/height")
.map(|s| u32::from_str(s.as_str()).map_err(Error::Parsing))?
}
pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
self.get_response_text("/blocks/tip/hash")
.map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
}
pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
self.get_response_text(&format!("/block-height/{block_height}"))
.map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
}
pub fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
self.get_response_json("/mempool")
}
pub fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
self.get_response_json("/mempool/recent")
}
pub fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
self.get_response_json("/mempool/txids")
}
pub fn get_fee_estimates(&self) -> Result<HashMap<u16, FeeRate>, Error> {
let estimates_raw: HashMap<u16, f64> = self.get_response_json("/fee-estimates")?;
let estimates = sat_per_vbyte_to_feerate(estimates_raw);
Ok(estimates)
}
pub fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
let path = format!("/address/{address}");
self.get_response_json(&path)
}
pub 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)
}
pub 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)
}
pub fn get_mempool_address_txs(&self, address: &Address) -> Result<Vec<EsploraTx>, Error> {
let path = format!("/address/{address}/txs/mempool");
self.get_response_json(&path)
}
pub 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)
}
pub 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)
}
pub fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
let path = format!("/block/{blockhash}");
self.get_response_json(&path)
}
pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
let path = format!("/block/{blockhash}/txids");
self.get_response_json(&path)
}
pub 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)
}
#[allow(deprecated)]
#[deprecated(since = "0.13.0", note = "use `get_block_infos` instead")]
pub 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)?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}
pub 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)?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}
pub fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
let path = format!("/address/{address}/utxo");
self.get_response_json(&path)
}
pub 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)
}
}