diff --git a/Cargo.toml b/Cargo.toml
index 4d1cdc2..b52dcd3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0"
homepage = "https://github.com/bitcoindevkit/rust-esplora-client"
repository = "https://github.com/bitcoindevkit/rust-esplora-client"
documentation = "https://docs.rs/esplora-client/"
-description = "Bitcoin Esplora API client library. Supports plaintext, TLS and Onion servers. Blocking or async."
+description = "Asynchronous and blocking clients and types for interacting with Esplora servers over HTTP"
keywords = ["bitcoin", "esplora"]
readme = "README.md"
rust-version = "1.75.0" # MSRV
diff --git a/README.md b/README.md
index 195c11d..0c54476 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
# rust-esplora-client
-Bitcoin Esplora API client library. Supports plaintext, TLS and Onion servers. Blocking or async.
-
@@ -10,6 +8,8 @@ Bitcoin Esplora API client library. Supports plaintext, TLS and Onion servers. B
+Asynchronous and blocking clients and types for interacting with Esplora servers over HTTP.
+
## Developing
This project uses [`just`](https://github.com/casey/just) for command running, and
diff --git a/src/api.rs b/src/api.rs
index a1ea398..dcd8f1c 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -1,8 +1,13 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! Structs from the Esplora API
+//! # Esplora API
//!
-//! See:
+//! This module implements the types and deserializers
+//! needed to interact with an Esplora-compliant server.
+//!
+//! Refer to the [Esplora API] specification for the complete API reference.
+//!
+//! [Esplora API]:
use bitcoin::hash_types;
use serde::Deserialize;
@@ -17,34 +22,35 @@ pub use bitcoin::{
Wtxid,
};
-/// Information about an input from a [`Transaction`].
+/// An input to a [`Transaction`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Vin {
- /// The [`Txid`] of the previous [`Transaction`] this input spends from.
+ /// The [`Txid`] of the [`Transaction`] that created this output.
pub txid: Txid,
- /// The output index of the previous output in the [`Transaction`] that created it.
+ /// The index of this output in the [`Transaction`] that created it.
pub vout: u32,
- /// The previous output amount and ScriptPubKey.
- /// `None` if this is a coinbase input.
+ /// This input's previous output [`Amount`] and [script pubkey][Script].
+ ///
+ /// `None` if this input spends a coinbase output.
pub prevout: Option,
- /// The ScriptSig authorizes spending this input.
+ /// The [`Script`] that unlocks this input.
pub scriptsig: ScriptBuf,
- /// The Witness that authorizes spending this input, if this is a SegWit spend.
+ /// The Witness that unlocks this input.
#[serde(deserialize_with = "deserialize_witness", default)]
pub witness: Vec>,
- /// The sequence value for this input, used to set RBF and Locktime behavior.
+ /// The sequence value for this input.
pub sequence: u32,
/// Whether this is a coinbase input.
pub is_coinbase: bool,
}
-/// Information about a [`Transaction`]s output.
+/// An output from a [`Transaction`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Vout {
- /// The value of the output, in satoshis.
+ /// The output's [`Amount`].
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
- /// The ScriptPubKey that the output is locked to, as a [`ScriptBuf`].
+ /// The [script pubkey][Script] this output is locked to.
pub scriptpubkey: ScriptBuf,
}
@@ -53,35 +59,36 @@ pub struct Vout {
pub struct TxStatus {
/// Whether the [`Transaction`] is confirmed or not.
pub confirmed: bool,
- /// The block height the [`Transaction`] was confirmed in.
+ /// The block height that confirmed the [`Transaction`].
pub block_height: Option,
- /// The [`BlockHash`] of the block the [`Transaction`] was confirmed in.
+ /// The [`BlockHash`] of the block that confirmed the [`Transaction`].
+ ///
+ /// `None` if the [`Transaction`] was confirmed by the genesis block.
pub block_hash: Option,
- /// The time that the block was mined at, as a UNIX timestamp.
- /// Note: this timestamp is set by the miner and may not reflect the exact time of mining.
+ /// The UNIX timestamp of the block that confirmed the [`Transaction`], as claimed by the
+ /// miner.
pub block_time: Option,
}
-/// A Merkle inclusion proof for a transaction, given it's [`Txid`].
+/// A Merkle inclusion proof for a [`Transaction`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct MerkleProof {
- /// The height of the block the [`Transaction`] was confirmed in.
+ /// The block height that confirmed the [`Transaction`].
pub block_height: u32,
- /// A list of transaction hashes the current hash is paired with,
- /// recursively, in order to trace up to obtain the Merkle root of the
- /// [`Block`], deepest pairing first.
+ /// The Merkle proof of inclusion of a [`Transaction`] in a [`Block`].
+ ///
+ /// Elements are returned left-to-right and bottom-to-top.
pub merkle: Vec,
- /// The 0-based index of the position of the [`Transaction`] in the
- /// ordered list of [`Transaction`]s in the [`Block`].
+ /// The 0-indexed position of the [`Transaction`] in the [`Block`].
pub pos: usize,
}
-/// The spend status of a [`TxOut`].
+/// The status of a [`TxOut`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct OutputStatus {
- /// Whether the [`TxOut`] is spent or not.
+ /// Whether the [`TxOut`] is spent.
pub spent: bool,
- /// The [`Txid`] that spent this [`TxOut`].
+ /// The [`Txid`] of the [`Transaction`] that spent this [`TxOut`].
pub txid: Option,
/// The input index of this [`TxOut`] in the [`Transaction`] that spent it.
pub vin: Option,
@@ -89,19 +96,24 @@ pub struct OutputStatus {
pub status: Option,
}
-/// Information about a [`Block`]s status.
+/// The status of a [`Block`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct BlockStatus {
- /// Whether this [`Block`] belongs to the chain with the most
- /// Proof-of-Work (false for [`Block`]s that belong to a stale chain).
+ /// Whether this [`Block`] belongs to the chain with the most Proof-of-Work.
pub in_best_chain: bool,
/// The height of this [`Block`].
pub height: Option,
- /// The [`BlockHash`] of the [`Block`] that builds on top of this one.
+ /// The [`BlockHash`] of the [`Block`] that builds on top of this [`Block`].
pub next_best: Option,
}
-/// A [`Transaction`] in the format returned by Esplora.
+/// A transaction in the format returned by Esplora.
+///
+/// Unlike the native `rust-bitcoin` [`Transaction`], [`EsploraTx`]
+/// includes additional metadata such as the [`TxStatus`], transaction fee,
+/// and transaction [`Weight`], as indexed and reported by Esplora servers.
+///
+/// To convert it into a [`Transaction`], use [`EsploraTx::to_tx`] or `.into()`.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct EsploraTx {
/// The [`Txid`] of the [`Transaction`].
@@ -117,46 +129,57 @@ pub struct EsploraTx {
pub vout: Vec,
/// The [`Transaction`] size in raw bytes (NOT virtual bytes).
pub size: usize,
- /// The [`Transaction`]'s weight units.
+ /// The [`Transaction`]'s weight.
pub weight: Weight,
/// The confirmation status of the [`Transaction`].
pub status: TxStatus,
- /// The fee amount paid by the [`Transaction`], in satoshis.
+ /// The fee paid by the [`Transaction`], in satoshis.
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub fee: Amount,
}
-/// Information about a bitcoin [`Block`].
+/// A summary of a [`Block`].
+///
+/// Contains additional metadata about a [`Block`], but not the whole [`Block`].
+///
+/// For the complete [`Block`] contents, use the `get_block_by_hash` client method.
#[derive(Debug, Clone, Deserialize)]
pub struct BlockInfo {
- /// The [`Block`]'s [`BlockHash`].
+ /// The [`BlockHash`] of this [`Block`].
pub id: BlockHash,
- /// The [`Block`]'s height.
+ /// The block height of this [`Block`].
pub height: u32,
- /// The [`Block`]'s version.
+ /// The version of this [`Block`].
pub version: block::Version,
- /// The [`Block`]'s UNIX timestamp.
+ /// The UNIX timestamp of this [`Block`], as claimed by the miner.
pub timestamp: u64,
- /// The [`Block`]'s [`Transaction`] count.
+ /// The [`Transaction`] count for this [`Block`].
pub tx_count: u64,
- /// The [`Block`]'s size, in bytes.
+ /// The size of this [`Block`], in bytes.
pub size: usize,
- /// The [`Block`]'s weight.
+ /// The [`Weight`] of this [`Block`].
pub weight: Weight,
- /// The Merkle root of the transactions in the block.
+ /// The Merkle root of this [`Block`].
pub merkle_root: hash_types::TxMerkleNode,
- /// The [`BlockHash`] of the previous [`Block`] (`None` for the genesis block).
+ /// The [`BlockHash`] of the previous [`Block`].
+ ///
+ /// `None` for the genesis block.
pub previousblockhash: Option,
- /// The [`Block`]'s MTP (Median Time Past).
+ /// The Median Time Past value for this [`Block`].
pub mediantime: u64,
- /// The [`Block`]'s nonce value.
+ /// This [`Block`]'s nonce.
pub nonce: u32,
- /// The [`Block`]'s `bits` value as a [`CompactTarget`].
+ /// The [`Block`]'s `bits` value, encoded as a [`CompactTarget`].
pub bits: CompactTarget,
/// The [`Block`]'s difficulty target value.
pub difficulty: f64,
}
+/// A manual `PartialEq` implementation is required
+/// since [`BlockInfo::difficulty`] is an `f64`.
+///
+/// This treats two `NaN` difficulty values as equal,
+/// allowing [`BlockInfo`] to implement [`Eq`] correctly.
impl PartialEq for BlockInfo {
fn eq(&self, other: &Self) -> bool {
let Self { difficulty: d1, .. } = self;
@@ -179,12 +202,12 @@ impl PartialEq for BlockInfo {
}
impl Eq for BlockInfo {}
-/// Time-related information about a [`Block`].
+/// The UNIX timestamp and height of a [`Block`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct BlockTime {
- /// The [`Block`]'s timestamp.
+ /// The UNIX timestamp of the [`Block`], as claimed by the miner.
pub timestamp: u64,
- /// The [`Block`]'s height.
+ /// The block height of the [`Block`].
pub height: u32,
}
@@ -193,81 +216,82 @@ pub struct BlockTime {
#[deprecated(since = "0.13.0", note = "use `BlockInfo` instead")]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BlockSummary {
- /// The [`Block`]'s hash.
+ /// The [`BlockHash`] of the [`Block`].
pub id: BlockHash,
- /// The [`Block`]'s timestamp and height.
+ /// The UNIX timestamp and height of the [`Block`].
#[serde(flatten)]
pub time: BlockTime,
- /// The [`BlockHash`] of the previous [`Block`] (`None` for the genesis [`Block`]).
+ /// The [`BlockHash`] of the previous [`Block`].
+ ///
+ /// `None` for the genesis block.
pub previousblockhash: Option,
- /// The Merkle root of the [`Block`]'s [`Transaction`]s.
+ /// The Merkle root of this [`Block`].
pub merkle_root: TxMerkleNode,
}
/// Statistics about an [`Address`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct AddressStats {
- /// The [`Address`].
+ /// The [`Address`], as a [`String`].
pub address: String,
/// The summary of confirmed [`Transaction`]s for this [`Address`].
pub chain_stats: AddressTxsSummary,
- /// The summary of mempool [`Transaction`]s for this [`Address`].
+ /// The summary of unconfirmed mempool [`Transaction`]s for this [`Address`].
pub mempool_stats: AddressTxsSummary,
}
-/// A summary of [`Transaction`]s in which an [`Address`] was involved.
+/// A summary of [`Transaction`]s in which an [`Address`] is involved.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct AddressTxsSummary {
- /// The number of funded [`TxOut`]s.
+ /// The current number of funded [`TxOut`]s for this [`Address`].
pub funded_txo_count: u32,
- /// The sum of the funded [`TxOut`]s, in satoshis.
+ /// The total [`Amount`] of funded [`TxOut`]s for this [`Address`].
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub funded_txo_sum: Amount,
- /// The number of spent [`TxOut`]s.
+ /// The number of spent [`TxOut`]s for this [`Address`].
pub spent_txo_count: u32,
- /// The sum of the spent [`TxOut`]s, in satoshis.
+ /// The total [`Amount`] of spent [`TxOut`]s for this [`Address`].
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub spent_txo_sum: Amount,
- /// The total number of [`Transaction`]s.
+ /// The total number of [`Transaction`]s for this [`Address`].
pub tx_count: u32,
}
-/// Statistics about a particular [`Script`] hash's confirmed and mempool transactions.
+/// Statistics about a [scripthash](Script)'s confirmed and mempool [`Transaction`]s.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct ScriptHashStats {
- /// The summary of confirmed [`Transaction`]s for this [`Script`] hash.
+ /// The summary of confirmed [`Transaction`]s for this [scripthash](Script).
pub chain_stats: ScriptHashTxsSummary,
- /// The summary of mempool [`Transaction`]s for this [`Script`] hash.
+ /// The summary of mempool [`Transaction`]s for this [scripthash](Script).
pub mempool_stats: ScriptHashTxsSummary,
}
-/// Contains a summary of the [`Transaction`]s for a particular [`Script`] hash.
+/// A summary of [`Transaction`]s for a particular [scripthash](Script).
pub type ScriptHashTxsSummary = AddressTxsSummary;
-/// Information about a [`TxOut`]'s status: confirmation status,
-/// confirmation height, confirmation block hash and confirmation block time.
+/// The confirmation status of a [`TxOut`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct UtxoStatus {
- /// Whether or not the [`TxOut`] is confirmed.
+ /// Whether the [`TxOut`] is confirmed.
pub confirmed: bool,
/// The block height in which the [`TxOut`] was confirmed.
pub block_height: Option,
/// The block hash in which the [`TxOut`] was confirmed.
pub block_hash: Option,
- /// The UNIX timestamp in which the [`TxOut`] was confirmed.
+ /// The UNIX timestamp in which the [`TxOut`] was confirmed, as reported by the miner.
pub block_time: Option,
}
-/// Information about an [`TxOut`]'s outpoint, confirmation status and value.
+/// An unspent [`TxOut`], including its outpoint, confirmation status and value.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct Utxo {
- /// The [`Txid`] of the [`Transaction`] that created the [`TxOut`].
+ /// The [`Txid`] of the [`Transaction`] that created this [`TxOut`].
pub txid: Txid,
- /// The output index of the [`TxOut`] in the [`Transaction`] that created it.
+ /// The output index of this [`TxOut`] in the [`Transaction`] that created it.
pub vout: u32,
- /// The confirmation status of the [`TxOut`].
+ /// The confirmation status of this [`TxOut`].
pub status: UtxoStatus,
- /// The value of the [`TxOut`], in satoshis.
+ /// The value of this [`TxOut`].
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
}
@@ -275,18 +299,22 @@ pub struct Utxo {
/// Statistics about the mempool.
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub struct MempoolStats {
- /// The number of [`Transaction`]s in the mempool.
+ /// The number of [`Transaction`]s currently in the mempool.
pub count: usize,
/// The total size of mempool [`Transaction`]s, in virtual bytes.
pub vsize: usize,
- /// The total fee paid by mempool [`Transaction`]s, in satoshis.
+ /// The total fee paid by mempool [`Transaction`]s.
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub total_fee: Amount,
/// The mempool's fee rate distribution histogram.
///
- /// An array of `(feerate, vsize)` tuples, where each entry's `vsize` is the total vsize
- /// of [`Transaction`]s paying more than `feerate` but less than the previous entry's `feerate`
+ /// An array of `(feerate, vsize)` tuples, where each entry's
+ /// `vsize` is the total vsize of [`Transaction`]s paying more
+ /// than `feerate` but less than the previous entry's `feerate`
/// (except for the first entry, which has no upper bound).
+ ///
+ /// The Esplora API reports `vsize` in virtual bytes. This field
+ /// currently stores that raw value in [`Weight`].
#[serde(deserialize_with = "deserialize_fee_histogram")]
pub fee_histogram: Vec<(FeeRate, Weight)>,
}
@@ -294,75 +322,81 @@ pub struct MempoolStats {
/// A [`Transaction`] that recently entered the mempool.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct MempoolRecentTx {
- /// The [`Transaction`]'s ID, as a [`Txid`].
+ /// The [`Transaction`]'s [`Txid`].
pub txid: Txid,
- /// The [`Amount`] of fees paid by the transaction, in satoshis.
+ /// The fee paid by the [`Transaction`].
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub fee: Amount,
/// The [`Transaction`]'s size, in virtual bytes.
pub vsize: usize,
- /// Combined [`Amount`] of the [`Transaction`], in satoshis.
+ /// The combined value of the [`Transaction`]'s outputs.
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
}
-/// The result for a broadcasted package of [`Transaction`]s.
+/// The global result of a [`Transaction`] package submission.
#[derive(Deserialize, Debug)]
pub struct SubmitPackageResult {
- /// The transaction package result message. "success" indicates all transactions were accepted
- /// into or are already in the mempool.
+ /// The [`Transaction`] package result message.
+ ///
+ /// "success" indicates all transactions were
+ /// accepted or are already in the mempool.
pub package_msg: String,
- /// Transaction results keyed by [`Wtxid`].
+ /// The list of individual [`Transaction`] broadcast
+ /// results, keyed by each [`Transaction`]'s [`Wtxid`].
#[serde(rename = "tx-results")]
pub tx_results: HashMap,
- /// List of txids of replaced transactions.
+ /// The list of [`Txid`]s of replaced [`Transaction`]s.
#[serde(rename = "replaced-transactions")]
pub replaced_transactions: Option>,
}
-/// The result [`Transaction`] for a broadcasted package of [`Transaction`]s.
+/// A per-transaction result of a [`Transaction`] package submission.
#[derive(Deserialize, Debug)]
pub struct TxResult {
- /// The transaction id.
+ /// The [`Transaction`]'s [`Txid`].
pub txid: Txid,
- /// The [`Wtxid`] of a different transaction with the same [`Txid`] but different witness found
- /// in the mempool.
+ /// The [`Wtxid`] of a different [`Transaction`] with the same [`Txid`],
+ /// but different Witness found in the mempool.
///
- /// If set, this means the submitted transaction was ignored.
+ /// If `Some`, means the submitted [`Transaction`] was ignored.
#[serde(rename = "other-wtxid")]
pub other_wtxid: Option,
- /// Sigops-adjusted virtual transaction size.
+ /// `sigops`-adjusted transaction size, in virtual bytes.
pub vsize: Option,
- /// Transaction fees.
+ /// The effective fee paid by the [`Transaction`].
pub fees: Option,
- /// The transaction error string, if it was rejected by the mempool
+ /// The [`Transaction`] submission error string.
pub error: Option,
}
-/// The mempool fees for a resulting [`Transaction`] broadcasted by a package of [`Transaction`]s.
+/// The fees for a [`Transaction`] submitted as part of a package.
#[derive(Deserialize, Debug)]
pub struct MempoolFeesSubmitPackage {
- /// Transaction fee.
+ /// The base fee paid by the [`Transaction`].
#[serde(with = "bitcoin::amount::serde::as_btc")]
pub base: Amount,
- /// The effective feerate.
+ /// The effective feerate paid by this [`Transaction`].
///
- /// Will be `None` if the transaction was already in the mempool. For example, the package
- /// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method.
+ /// Is `None` if the transaction was already in the mempool.
#[serde(
rename = "effective-feerate",
default,
deserialize_with = "deserialize_feerate"
)]
pub effective_feerate: Option,
- /// If [`Self::effective_feerate`] is provided, this holds the [`Wtxid`]s of the transactions
- /// whose fees and vsizes are included in effective-feerate.
+ /// If [`Self::effective_feerate`] is provided, holds the
+ /// [`Wtxid`]s of the transactions whose fees and virtual
+ /// sizes are included in effective-feerate.
#[serde(rename = "effective-includes")]
pub effective_includes: Option>,
}
impl EsploraTx {
- /// Convert a transaction from the format returned by Esplora into a [`Transaction`].
+ /// Convert this [`EsploraTx`] into a [`Transaction`].
+ ///
+ /// This will drop the Esplora-specific metadata (fee, weight, confirmation status)
+ /// and reconstructs the [`Transaction`] from its inputs and outputs.
pub fn to_tx(&self) -> Transaction {
Transaction {
version: transaction::Version::non_standard(self.version),
@@ -393,7 +427,10 @@ impl EsploraTx {
}
}
- /// Get the confirmation time from an [`EsploraTx`].
+ /// Get the confirmation time of this [`EsploraTx`].
+ ///
+ /// If the transaction is confirmed, returns its [`BlockTime`] containing
+ /// confirmation height and UNIX timestamp. If not, returns `None`.
pub fn confirmation_time(&self) -> Option {
match self.status {
TxStatus {
@@ -406,15 +443,18 @@ impl EsploraTx {
}
}
- /// Get a list of the [`EsploraTx`]'s previous outputs.
+ /// Get the previous [`TxOut`]s spent by this transaction's inputs.
+ ///
+ /// Returns one [`Option`] per input, in order.
+ /// `None` if the input spends a coinbase output.
pub fn previous_outputs(&self) -> Vec> {
self.vin
.iter()
.cloned()
.map(|vin| {
- vin.prevout.map(|po| TxOut {
- script_pubkey: po.scriptpubkey,
- value: po.value,
+ vin.prevout.map(|prevout| TxOut {
+ script_pubkey: prevout.scriptpubkey,
+ value: prevout.value,
})
})
.collect()
@@ -433,6 +473,11 @@ impl From<&EsploraTx> for Transaction {
}
}
+/// Deserializes a witness from a list of hex-encoded strings.
+///
+/// The Esplora API represents witness data as an array of hex strings,
+/// e.g. `["deadbeef", "cafebabe"]`. This deserializer decodes each string
+/// into raw bytes.
fn deserialize_witness<'de, D>(d: D) -> Result>, D::Error>
where
D: serde::de::Deserializer<'de>,
@@ -444,6 +489,13 @@ where
.map_err(serde::de::Error::custom)
}
+/// Deserializes an optional [`FeeRate`] from an `f64` BTC/kvB value.
+///
+/// The Esplora API expresses effective feerates as BTC per kilovirtual-byte.
+/// This deserializer converts it to sat/kwu as required by [`FeeRate`].
+///
+/// Returns `None` if the value is absent, and an error if the resulting
+/// feerate would overflow.
fn deserialize_feerate<'de, D>(d: D) -> Result, D::Error>
where
D: serde::de::Deserializer<'de>,
@@ -461,6 +513,11 @@ where
Ok(Some(FeeRate::from_sat_per_kwu(sat_per_kwu as u64)))
}
+/// Deserializes a mempool fee histogram from `(sat/vB, vsize)` entries.
+///
+/// The Esplora API expresses fee histogram buckets as feerates in satoshis per
+/// virtual byte paired with each bucket's virtual size. This deserializer
+/// converts each feerate to sat/kwu as required by [`FeeRate`].
fn deserialize_fee_histogram<'de, D>(d: D) -> Result, D::Error>
where
D: serde::de::Deserializer<'de>,
diff --git a/src/async.rs b/src/async.rs
index 6bb5154..ce3a8fc 100644
--- a/src/async.rs
+++ b/src/async.rs
@@ -1,6 +1,34 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! Esplora by way of `reqwest` HTTP client.
+//! # Asynchronous Esplora Client
+//!
+//! This module implements [`AsyncClient`], an asynchronous HTTP client for
+//! interacting with an [Esplora] server by way of [`bitreq`].
+//!
+//! Use this client from async applications and libraries. Each method returns a
+//! future that sends the request, waits for the response, and decodes the body
+//! into the requested type.
+//!
+//! The client is configured through [`Builder`], including the
+//! base URL, proxy, socket timeout, custom headers, retry count, and maximum
+//! number of cached connections. Retry sleeping is abstracted through
+//! [`Sleeper`], so runtimes other than Tokio can provide their own sleep
+//! implementation.
+//!
+//! # Example
+//!
+//! ```rust,ignore
+//! # use esplora_client::{Builder, r#async::AsyncClient};
+//! # async fn example() -> Result<(), esplora_client::Error> {
+//!
+//! let client = Builder::new("https://mempool.space/api").build_async()?;
+//! let height = client.get_height().await?;
+//!
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! [Esplora]: https://github.com/Blockstream/esplora/blob/master/API.md
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
@@ -26,28 +54,54 @@ use crate::{
#[allow(deprecated)]
use crate::BlockSummary;
-/// An async client for interacting with an Esplora API server.
// FIXME: (@oleonardolima) there's no `Debug` implementation for `bitreq::Client`.
+/// An async client for interacting with an Esplora API server.
+///
+/// Use [`Builder`] to construct an instance of this client. The client stores
+/// the server base URL and request configuration, then exposes convenience
+/// methods for the transaction, block, address, scripthash, fee-estimate, and
+/// mempool endpoints.
+///
+/// The generic parameter `S` determines the asynchronous runtime used for
+/// sleeping between retries. Defaults to the Tokio-backed [`DefaultSleeper`].
+///
+/// # Retries
+///
+/// Failed requests are automatically retried up to `max_retries` times
+/// (configured via [`Builder`]) with exponential backoff, but only for
+/// retryable HTTP status codes. See [`crate::RETRYABLE_ERROR_CODES`] for the
+/// full list.
#[derive(Clone)]
pub struct AsyncClient {
- /// The URL of the Esplora Server.
+ /// The URL of the Esplora server.
url: String,
- /// The proxy is ignored when targeting `wasm32`.
+ /// The URL of the proxy host.
+ ///
+ /// NOTE: The proxy is ignored when targeting `wasm32`.
proxy: Option,
- /// Socket timeout.
+ /// Per-request socket timeout, in seconds.
timeout: Option,
- /// HTTP headers to set on every request made to Esplora server
+ /// HTTP headers to set on every request made to the Esplora server.
headers: HashMap,
- /// Number of times to retry a request
+ /// Maximum number of retry attempts for retryable responses.
max_retries: usize,
/// The inner [`bitreq::Client`] HTTP client to cache connections.
client: Client,
- /// Marker for the type of sleeper used
+ /// Marker for the sleeper implementation.
marker: PhantomData,
}
impl AsyncClient {
/// Build an [`AsyncClient`] from a [`Builder`].
+ ///
+ /// Configures the underlying [`bitreq::Client`] with
+ /// proxy, timeout, and headers specified in the [`Builder`].
+ /// No network request is made until a client method is awaited.
+ ///
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the HTTP client fails to build,
+ /// or if any of the provided header names or values are invalid.
pub fn from_builder(builder: Builder) -> Result {
Ok(AsyncClient {
url: builder.base_url,
@@ -60,17 +114,25 @@ impl AsyncClient {
})
}
- /// Get the underlying base URL.
+ /// Return the base URL of the Esplora server this client connects to.
+ ///
+ /// The returned value is the exact string provided to [`Builder::new`].
pub fn url(&self) -> &str {
&self.url
}
- /// Get the underlying [`Client`].
+ /// Return the underlying [`bitreq::Client`].
+ ///
+ /// This can be useful for callers that need access to shared connection
+ /// state managed by the HTTP client.
pub fn client(&self) -> &Client {
&self.client
}
/// Build a HTTP [`Request`] with given [`Method`] and URI `path`.
+ ///
+ /// Configures the request with the proxy, timeout, and headers set on
+ /// this client. Used internally by all other request helper methods.
pub(crate) fn build_request(&self, method: Method, path: &str) -> Result {
let mut request = Request::new(method, format!("{}{}", self.url, path));
@@ -91,8 +153,8 @@ impl AsyncClient {
Ok(request)
}
- /// Sends a GET request to the given `url`, retrying failed attempts
- /// for retryable error codes until max retries hit.
+ /// Sends a GET request to `url`, retrying on retryable status codes
+ /// with exponential backoff until [`AsyncClient::max_retries`] is reached.
async fn get_with_retry(&self, path: &str) -> Result {
let mut delay = BASE_BACKOFF_MILLIS;
let mut attempts = 0;
@@ -111,17 +173,14 @@ impl AsyncClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implement [`bitcoin::consensus::Decodable`].
+ /// Makes a GET request to `path`, deserializing the response body as raw
+ /// bytes into `T` using [`bitcoin::consensus::Decodable`].
///
- /// It should be used when requesting Esplora endpoints that can be directly
- /// deserialized to native `rust-bitcoin` types, which implements
- /// [`bitcoin::consensus::Decodable`] from `&[u8]`.
+ /// Use this for endpoints that return raw binary Bitcoin data.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`bitcoin::consensus::Decodable`] deserialization.
+ /// Returns an [`Error`] if the request fails or deserialization fails.
async fn get_response(&self, path: &str) -> Result {
let response = self.get_with_retry(path).await?;
@@ -134,11 +193,9 @@ impl AsyncClient {
Ok(deserialize::(response.as_bytes())?)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
- ///
- /// It uses [`AsyncEsploraClient::get_response`] internally.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// See [`AsyncEsploraClient::get_response`] above for full documentation.
+ /// Delegates to [`Self::get_response`]. See its documentation for details.
async fn get_opt_response(&self, path: &str) -> Result, Error> {
match self.get_response::(path).await {
Ok(res) => Ok(Some(res)),
@@ -147,16 +204,15 @@ impl AsyncClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implements [`serde::de::DeserializeOwned`].
+ /// Makes a GET request to `path`, deserializing the response body as JSON
+ /// into `T` using [`serde::de::DeserializeOwned`].
///
- /// It should be used when requesting Esplora endpoints that have a specific
- /// defined API, mostly defined in [`crate::api`].
+ /// Use this for endpoints that return Esplora-specific JSON types, as
+ /// defined in [`crate::api`].
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`serde::de::DeserializeOwned`] deserialization.
+ /// Returns an [`Error`] if the request fails or JSON deserialization fails.
async fn get_response_json(
&self,
path: &str,
@@ -172,12 +228,9 @@ impl AsyncClient {
response.json::().map_err(Error::BitReq)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// It uses [`AsyncEsploraClient::get_response_json`] internally.
- ///
- /// See [`AsyncEsploraClient::get_response_json`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_json`]. See its documentation for details.
async fn get_opt_response_json(
&self,
url: &str,
@@ -189,17 +242,15 @@ impl AsyncClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implements [`bitcoin::consensus::Decodable`].
+ /// Makes a GET request to `path`, deserializing the hex-encoded response
+ /// body into `T` using [`bitcoin::consensus::Decodable`].
///
- /// It should be used when requesting Esplora endpoints that are expected
- /// to return a hex string decodable to native `rust-bitcoin` types which
- /// implement [`bitcoin::consensus::Decodable`] from `&[u8]`.
+ /// Use this for endpoints that return hex-encoded Bitcoin data.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`bitcoin::consensus::Decodable`] deserialization.
+ /// Returns an [`Error`] if the request fails, hex decoding fails,
+ /// or consensus deserialization fails.
async fn get_response_hex(&self, path: &str) -> Result {
let response = self.get_with_retry(path).await?;
@@ -213,12 +264,9 @@ impl AsyncClient {
Ok(deserialize(&Vec::from_hex(hex_str)?)?)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
- ///
- /// It uses [`AsyncEsploraClient::get_response_hex`] internally.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// See [`AsyncEsploraClient::get_response_hex`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_hex`]. See its documentation for details.
async fn get_opt_response_hex(&self, path: &str) -> Result, Error> {
match self.get_response_hex(path).await {
Ok(res) => Ok(Some(res)),
@@ -227,14 +275,14 @@ impl AsyncClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to `String`.
+ /// Makes a GET request to `path`, returning the response body as a [`String`].
///
- /// It should be used when requesting Esplora endpoints that can return
- /// `String` formatted data that can be parsed downstream.
+ /// Use this for endpoints that return plain text data that needs further
+ /// parsing downstream.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client.
+ /// Returns an [`Error`] if the request fails.
async fn get_response_text(&self, path: &str) -> Result {
let response = self.get_with_retry(path).await?;
@@ -247,12 +295,9 @@ impl AsyncClient {
Ok(response.as_str()?.to_string())
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// It uses [`AsyncEsploraClient::get_response_text`] internally.
- ///
- /// See [`AsyncEsploraClient::get_response_text`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_text`]. See its documentation for details.
async fn get_opt_response_text(&self, path: &str) -> Result, Error> {
match self.get_response_text(path).await {
Ok(s) => Ok(Some(s)),
@@ -261,8 +306,10 @@ impl AsyncClient {
}
}
- /// Make an HTTP POST request to given URL, converting any `T` that
- /// implement [`Into`] and setting query parameters, if any.
+ /// Make an HTTP POST request to `path` with `body`.
+ ///
+ /// Configures query parameters, if any, and returns the raw response after
+ /// checking the HTTP status code.
///
/// # Errors
///
@@ -291,12 +338,17 @@ impl AsyncClient {
Ok(response)
}
- /// Get a [`Transaction`] option given its [`Txid`]
+ /// Get a raw [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns `None` if the transaction is not found.
pub async fn get_tx(&self, txid: &Txid) -> Result, Error> {
self.get_opt_response(&format!("/tx/{txid}/raw")).await
}
- /// Get a [`Transaction`] given its [`Txid`].
+ /// Get a raw [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns an [`Error::TransactionNotFound`] if the transaction is not found.
+ /// Prefer [`Self::get_tx`] if you want to handle the not-found case explicitly.
pub async fn get_tx_no_opt(&self, txid: &Txid) -> Result {
match self.get_tx(txid).await {
Ok(Some(tx)) => Ok(tx),
@@ -305,8 +357,10 @@ impl AsyncClient {
}
}
- /// Get a [`Txid`] of a transaction given its index in a block with a given
- /// hash.
+ /// Get the [`Txid`] of the transaction at position `index` within the
+ /// block identified by `block_hash`.
+ ///
+ /// Returns `None` if the block or index is not found.
pub async fn get_txid_at_block_index(
&self,
block_hash: &BlockHash,
@@ -321,56 +375,79 @@ impl AsyncClient {
}
}
- /// Get the status of a [`Transaction`] given its [`Txid`].
+ /// Get the confirmation status of a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns a [`TxStatus`] containing whether the transaction is confirmed,
+ /// and if so, the block height, hash, and timestamp it was confirmed in.
pub async fn get_tx_status(&self, txid: &Txid) -> Result {
self.get_response_json(&format!("/tx/{txid}/status")).await
}
- /// Get transaction info given its [`Txid`].
+ /// Get an [`EsploraTx`] given its [`Txid`].
+ ///
+ /// Unlike [`Self::get_tx`], returns the Esplora-specific [`EsploraTx`]
+ /// type, which includes additional metadata such as confirmation status,
+ /// fee, and weight. Returns `None` if the transaction is not found.
pub async fn get_tx_info(&self, txid: &Txid) -> Result, Error> {
self.get_opt_response_json(&format!("/tx/{txid}")).await
}
- /// Get the spend status of a [`Transaction`]'s outputs, given its [`Txid`].
+ /// Get the spend status of all outputs in a [`Transaction`], given its [`Txid`].
+ ///
+ /// Returns a [`Vec`] of [`OutputStatus`], one per output, ordered as they
+ /// appear in the [`Transaction`].
pub async fn get_tx_outspends(&self, txid: &Txid) -> Result, Error> {
self.get_response_json(&format!("/tx/{txid}/outspends"))
.await
}
- /// Get a [`BlockHeader`] given a particular block hash.
+ /// Get the [`BlockHeader`] of a [`Block`] given its [`BlockHash`].
pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result {
self.get_response_hex(&format!("/block/{block_hash}/header"))
.await
}
- /// Get the [`BlockStatus`] given a particular [`BlockHash`].
+ /// Get the [`BlockStatus`] of a [`Block`] given its [`BlockHash`].
+ ///
+ /// Returns a [`BlockStatus`] indicating whether this [`Block`] is part of
+ /// the best chain, its height, and the [`BlockHash`] of the next [`Block`],
+ /// if any.
pub async fn get_block_status(&self, block_hash: &BlockHash) -> Result {
self.get_response_json(&format!("/block/{block_hash}/status"))
.await
}
- /// Get a [`Block`] given a particular [`BlockHash`].
+ /// Get the full [`Block`] with the given [`BlockHash`].
+ ///
+ /// Returns `None` if the [`Block`] is not found.
pub async fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result, Error> {
self.get_opt_response(&format!("/block/{block_hash}/raw"))
.await
}
- /// Get a merkle inclusion proof for a [`Transaction`] with the given
- /// [`Txid`].
+ /// Get a Merkle inclusion proof for a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns a [`MerkleProof`] that can be used to verify the transaction's
+ /// inclusion in a block. Returns `None` if the transaction is not found or
+ /// is unconfirmed.
pub async fn get_merkle_proof(&self, tx_hash: &Txid) -> Result , Error> {
self.get_opt_response_json(&format!("/tx/{tx_hash}/merkle-proof"))
.await
}
- /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the
- /// given [`Txid`].
+ /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns `None` if the transaction is not found or is unconfirmed.
pub async fn get_merkle_block(&self, tx_hash: &Txid) -> Result , Error> {
self.get_opt_response_hex(&format!("/tx/{tx_hash}/merkleblock-proof"))
.await
}
- /// Get the spending status of an output given a [`Txid`] and the output
- /// index.
+ /// Get the spend status of a specific output, identified by its [`Txid`]
+ /// and output index.
+ ///
+ /// Returns an [`OutputStatus`] indicating whether the output has been
+ /// spent, and if so, by which transaction. Returns `None` if not found.
pub async fn get_output_status(
&self,
txid: &Txid,
@@ -380,7 +457,14 @@ impl AsyncClient {
.await
}
- /// Broadcast a [`Transaction`] to Esplora
+ /// Broadcast a [`Transaction`] to the Esplora server.
+ ///
+ /// The transaction is serialized and sent as a hex-encoded string.
+ /// Returns the [`Txid`] of the broadcasted transaction.
+ ///
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the request fails or the server rejects the transaction.
pub async fn broadcast(&self, transaction: &Transaction) -> Result {
let body = serialize::(transaction).to_lower_hex_string();
let response = self.post_request_bytes("/tx", body, None).await?;
@@ -388,14 +472,17 @@ impl AsyncClient {
Ok(txid)
}
- /// Broadcast a package of [`Transaction`]s to Esplora.
+ /// Broadcast a package of [`Transaction`]s to the Esplora server.
+ ///
+ /// Returns a [`SubmitPackageResult`] containing the result for each
+ /// transaction in the package, keyed by [`bitcoin::Wtxid`].
///
- /// If `maxfeerate` is provided, any transaction whose
- /// fee is higher will be rejected.
+ /// Optionally, `maxfeerate` and `maxburnamount` can be provided to reject
+ /// transactions that exceed these thresholds.
///
- /// If `maxburnamount` is provided, any transaction
- /// with higher provably unspendable outputs amount
- /// will be rejected.
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the request fails or the server rejects the package.
pub async fn submit_package(
&self,
transactions: &[Transaction],
@@ -431,7 +518,7 @@ impl AsyncClient {
Ok(result)
}
- /// Get the current height of the blockchain tip
+ /// Get the block height of the current blockchain tip.
pub async fn get_height(&self) -> Result {
self.get_response_text("/blocks/tip/height")
.await
@@ -445,31 +532,38 @@ impl AsyncClient {
.map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
}
- /// Get the [`BlockHash`] of a specific block height
+ /// Get the [`BlockHash`] of a [`Block`] given its height.
pub async fn get_block_hash(&self, block_height: u32) -> Result {
self.get_response_text(&format!("/block-height/{block_height}"))
.await
.map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
}
- /// Get information about a specific address, includes confirmed balance and transactions in
- /// the mempool.
+ /// Get statistics about an [`Address`].
+ ///
+ /// Returns an [`AddressStats`] containing confirmed and mempool transaction
+ /// summaries for the given address, including funded and spent output
+ /// counts and their total values.
pub async fn get_address_stats(&self, address: &Address) -> Result {
let path = format!("/address/{address}");
self.get_response_json(&path).await
}
- /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions.
+ /// Get statistics about a [`Script`] hash's confirmed and mempool transactions.
+ ///
+ /// Returns a [`ScriptHashStats`] containing transaction summaries for the
+ /// SHA256 hash of the given [`Script`].
pub async fn get_scripthash_stats(&self, script: &Script) -> Result {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}");
self.get_response_json(&path).await
}
- /// Get transaction history for the specified address, sorted with newest first.
+ /// Get confirmed transaction history for an [`Address`], sorted newest first.
///
/// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
- /// More can be requested by specifying the last txid seen by the previous query.
+ /// To paginate, pass the [`Txid`] of the last transaction seen in the
+ /// previous response as `last_seen`.
pub async fn get_address_txs(
&self,
address: &Address,
@@ -483,7 +577,7 @@ impl AsyncClient {
self.get_response_json(&path).await
}
- /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first.
+ /// Get unconfirmed mempool [`EsploraTx`]s for an [`Address`], sorted newest first.
pub async fn get_mempool_address_txs(
&self,
address: &Address,
@@ -493,10 +587,10 @@ impl AsyncClient {
self.get_response_json(&path).await
}
- /// Get transaction history for the specified address/scripthash,
- /// sorted with newest first. Returns 25 transactions per page.
- /// More can be requested by specifying the last txid seen by the previous
- /// query.
+ /// Get confirmed transaction history for a [`Script`] hash, sorted newest first.
+ ///
+ /// Returns 25 transactions per page. To paginate, pass the [`Txid`] of the
+ /// last transaction seen in the previous response as `last_seen`.
pub async fn get_scripthash_txs(
&self,
script: &Script,
@@ -511,8 +605,7 @@ impl AsyncClient {
self.get_response_json(&path).await
}
- /// Get mempool [`Transaction`] history for the
- /// specified [`Script`] hash, sorted with newest first.
+ /// Get unconfirmed mempool [`EsploraTx`]s for a [`Script`] hash, sorted newest first.
pub async fn get_mempool_scripthash_txs(
&self,
script: &Script,
@@ -523,12 +616,15 @@ impl AsyncClient {
self.get_response_json(&path).await
}
- /// Get statistics about the mempool.
+ /// Get global statistics about the mempool.
+ ///
+ /// Returns a [`MempoolStats`] containing the transaction count, total
+ /// virtual size, total fees, and fee rate histogram.
pub async fn get_mempool_stats(&self) -> Result {
self.get_response_json("/mempool").await
}
- /// Get a list of the last 10 [`Transaction`]s to enter the mempool.
+ /// Get the last 10 [`MempoolRecentTx`]s to enter the mempool.
pub async fn get_mempool_recent_txs(&self) -> Result, Error> {
self.get_response_json("/mempool/recent").await
}
@@ -551,25 +647,33 @@ impl AsyncClient {
Ok(estimates)
}
- /// Get a summary about a [`Block`], given its [`BlockHash`].
+ /// Get a [`BlockInfo`] summary for the [`Block`] with the given [`BlockHash`].
+ ///
+ /// [`BlockInfo`] includes metadata such as the height, timestamp,
+ /// [`Transaction`] count, size, and [`Weight`](bitcoin::Weight).
+ ///
+ /// This method does not return the full [`Block`].
pub async fn get_block_info(&self, blockhash: &BlockHash) -> Result {
let path = format!("/block/{blockhash}");
self.get_response_json(&path).await
}
- /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
+ /// Get all [`Txid`]s of [`Transaction`]s included in the [`Block`] with the
+ /// given [`BlockHash`].
pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result, Error> {
let path = format!("/block/{blockhash}/txids");
self.get_response_json(&path).await
}
- /// Get up to 25 [`Transaction`]s from a [`Block`], given its [`BlockHash`],
- /// beginning at `start_index` (starts from 0 if `start_index` is `None`).
+ /// Get up to 25 [`EsploraTx`]s from the [`Block`] with the given
+ /// [`BlockHash`], starting at `start_index`.
+ ///
+ /// If `start_index` is `None`, starts from the first transaction (index 0).
///
- /// The `start_index` value MUST be a multiple of 25,
- /// else an error will be returned by Esplora.
+ /// Note that `start_index` must be a multiple of 25, otherwise the server
+ /// will return an error.
pub async fn get_block_txs(
&self,
blockhash: &BlockHash,
@@ -583,11 +687,13 @@ impl AsyncClient {
self.get_response_json(&path).await
}
- /// Gets some recent block summaries starting at the tip or at `height` if
- /// provided.
+ /// Get [`BlockInfo`] summaries for recent [`Block`]s.
+ ///
+ /// If `height` is `Some(h)`, returns blocks starting from height `h`.
+ /// If `height` is `None`, returns blocks starting from the current tip.
///
/// The maximum number of summaries returned depends on the backend itself:
- /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
#[allow(deprecated)]
#[deprecated(since = "0.13.0", note = "use `get_block_infos` instead")]
pub async fn get_blocks(&self, height: Option) -> Result, Error> {
@@ -602,11 +708,19 @@ impl AsyncClient {
Ok(blocks)
}
- /// Gets some recent block summaries starting at the tip or at `height` if
- /// provided.
+ /// Get [`BlockInfo`] summaries for recent [`Block`]s.
+ ///
+ /// If `height` is `Some(h)`, returns blocks starting from height `h`.
+ /// If `height` is `None`, returns blocks starting from the current tip.
///
/// The maximum number of summaries returned depends on the backend itself:
- /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidResponse`] if the server returns an empty list.
+ ///
+ /// This method does not return the full [`Block`].
pub async fn get_block_infos(&self, height: Option) -> Result, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
@@ -619,14 +733,14 @@ impl AsyncClient {
Ok(blocks)
}
- /// Get all UTXOs locked to an address.
+ /// Get all confirmed [`Utxo`]s locked to the given [`Address`].
pub async fn get_address_utxos(&self, address: &Address) -> Result, Error> {
let path = format!("/address/{address}/utxo");
self.get_response_json(&path).await
}
- /// Get all [`Utxo`]s locked to a [`Script`].
+ /// Get all confirmed [`Utxo`]s locked to the given [`Script`].
pub async fn get_scripthash_utxos(&self, script: &Script) -> Result, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}/utxo");
@@ -635,15 +749,24 @@ impl AsyncClient {
}
}
-/// Sleeper trait that allows any async runtime to be used.
+/// A trait for abstracting over async sleep implementations.
+///
+/// [`AsyncClient`] uses this trait to wait between retry attempts without
+/// committing the client type to a specific async runtime.
+///
+/// The only provided implementation is [`DefaultSleeper`], which is backed by Tokio.
+/// Custom implementations can be provided to support other runtimes.
pub trait Sleeper: 'static {
- /// The `Future` type returned by the sleep function.
+ /// The [`Future`](std::future::Future) type returned by [`Sleeper::sleep`].
type Sleep: std::future::Future;
- /// Create a `Future` that completes after the specified [`Duration`].
+ /// Return a [`Future`](std::future::Future) that completes after `duration`.
fn sleep(dur: Duration) -> Self::Sleep;
}
-/// The default `Sleeper` implementation using the underlying async runtime.
+/// The default [`Sleeper`] implementation, backed by [`tokio::time::sleep`].
+///
+/// This type is available when the `tokio` feature is enabled or while running
+/// tests.
#[derive(Debug, Clone, Copy)]
pub struct DefaultSleeper;
diff --git a/src/blocking.rs b/src/blocking.rs
index 391981a..63f19cb 100644
--- a/src/blocking.rs
+++ b/src/blocking.rs
@@ -1,6 +1,31 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! Esplora by way of `minreq` HTTP client.
+//! # Blocking Esplora Client
+//!
+//! This module implements [`BlockingClient`], a synchronous HTTP client for
+//! interacting with an [Esplora] server by way of [`bitreq`].
+//!
+//! Use this client from synchronous applications, command-line tools, tests,
+//! or code paths where blocking the current thread is acceptable. Each method
+//! sends the request immediately and returns only after the response body has
+//! been read and decoded.
+//!
+//! The client is configured through [`Builder`], including the
+//! base URL, proxy, socket timeout, custom headers, and retry count.
+//!
+//! # Example
+//!
+//! ```rust,no_run
+//! # fn example() -> Result<(), esplora_client::Error> {
+//!
+//! let client = esplora_client::Builder::new("https://mempool.space/api").build_blocking();
+//! let height = client.get_height()?;
+//!
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! [Esplora]: https://github.com/Blockstream/esplora/blob/master/API.md
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
@@ -25,23 +50,43 @@ use crate::{
#[allow(deprecated)]
use crate::BlockSummary;
-/// A blocking client for interacting with an Esplora API server.
+/// A synchronous client for interacting with an Esplora API server.
+///
+/// Use [`Builder`] to construct an instance of this client. The client stores
+/// the server base URL and request configuration, then exposes convenience
+/// methods for the transaction, block, address, scripthash, fee-estimate, and
+/// mempool endpoints.
+///
+/// Each method blocks the current thread until the HTTP request completes and
+/// the response is parsed into the requested type.
+///
+/// # Retries
+///
+/// Failed requests are automatically retried up to `max_retries` times
+/// (configured via [`Builder`]) with exponential backoff, but only for
+/// retryable HTTP status codes. See [`crate::RETRYABLE_ERROR_CODES`] for the
+/// full list.
#[derive(Debug, Clone)]
pub struct BlockingClient {
/// The URL of the Esplora server.
url: String,
- /// The proxy is ignored when targeting `wasm32`.
+ /// The URL of the proxy host.
+ ///
+ /// NOTE: The proxy is ignored when targeting `wasm32`.
pub proxy: Option,
- /// Socket timeout.
+ /// Per-request socket timeout, in seconds.
pub timeout: Option,
- /// HTTP headers to set on every request made to Esplora server
+ /// HTTP headers to set on every request made to the Esplora server.
pub headers: HashMap,
- /// Number of times to retry a request
+ /// Maximum number of retry attempts for retryable responses.
pub max_retries: usize,
}
impl BlockingClient {
- /// Build a blocking client from a [`Builder`]
+ /// Build a [`BlockingClient`] from a [`Builder`].
+ ///
+ /// This consumes the builder configuration and stores it on the client.
+ /// No network request is made until a client method is called.
pub fn from_builder(builder: Builder) -> Self {
Self {
url: builder.base_url,
@@ -52,12 +97,17 @@ impl BlockingClient {
}
}
- /// Get the underlying base URL.
+ /// Return the base URL of the Esplora server this client connects to.
+ ///
+ /// The returned value is the exact string provided to [`Builder::new`].
pub fn url(&self) -> &str {
&self.url
}
/// Build a HTTP [`Request`] with given [`Method`] and URI `path`.
+ ///
+ /// Configures the request with the proxy, timeout, and headers set on
+ /// this client. Used internally by all other request helper methods.
pub(crate) fn build_request(&self, method: Method, path: &str) -> Result {
let mut request = Request::new(method, format!("{}{}", self.url, path));
@@ -76,8 +126,10 @@ impl BlockingClient {
Ok(request)
}
- /// Make an HTTP POST request to given URL, converting any `T` that
- /// implement [`Into`] and setting query parameters, if any.
+ /// Make an HTTP POST request to `path` with `body`.
+ ///
+ /// Configures query parameters, if any, and returns the raw response after
+ /// checking the HTTP status code.
///
/// # Errors
///
@@ -106,8 +158,8 @@ impl BlockingClient {
Ok(response)
}
- /// Makes a HTTP GET request to the given `url`, retrying failed attempts
- /// for retryable error codes until max retries hit.
+ /// Sends a GET request to `url`, retrying on retryable status codes
+ /// with exponential backoff until [`BlockingClient::max_retries`] is reached.
fn get_with_retry(&self, url: &str) -> Result {
let mut delay = BASE_BACKOFF_MILLIS;
let mut attempts = 0;
@@ -124,17 +176,14 @@ impl BlockingClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implement [`bitcoin::consensus::Decodable`].
+ /// Makes a GET request to `path`, deserializing the response body as
+ /// raw bytes into `T` using [`bitcoin::consensus::Decodable`].
///
- /// It should be used when requesting Esplora endpoints that can be directly
- /// deserialized to native `rust-bitcoin` types, which implements
- /// [`bitcoin::consensus::Decodable`] from `&[u8]`.
+ /// Use this for endpoints that return raw binary Bitcoin data.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`bitcoin::consensus::Decodable`] deserialization.
+ /// Returns an [`Error`] if the request fails or deserialization fails.
fn get_response(&self, path: &str) -> Result {
let response = self.get_with_retry(path)?;
@@ -147,11 +196,9 @@ impl BlockingClient {
Ok(deserialize::(response.as_bytes())?)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// It uses [`BlockingClient::get_response`] internally.
- ///
- /// See [`BlockingClient::get_response`] above for full documentation.
+ /// Delegates to [`Self::get_response`]. See its documentation for details.
fn get_opt_response(&self, path: &str) -> Result, Error> {
match self.get_response(path) {
Ok(response) => Ok(Some(response)),
@@ -160,17 +207,15 @@ impl BlockingClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implements [`bitcoin::consensus::Decodable`].
+ /// Makes a GET request to `path`, deserializing the hex-encoded response
+ /// body into `T` using [`bitcoin::consensus::Decodable`].
///
- /// It should be used when requesting Esplora endpoints that are expected
- /// to return a hex string decodable to native `rust-bitcoin` types which
- /// implement [`bitcoin::consensus::Decodable`] from `&[u8]`.
+ /// Use this for endpoints that return hex-encoded Bitcoin data.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`bitcoin::consensus::Decodable`] deserialization.
+ /// Returns an [`Error`] if the request fails, hex decoding fails,
+ /// or consensus deserialization fails.
fn get_response_hex(&self, path: &str) -> Result {
let response = self.get_with_retry(path)?;
@@ -184,12 +229,9 @@ impl BlockingClient {
deserialize(&Vec::from_hex(hex_str)?).map_err(Error::BitcoinEncoding)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
- ///
- /// It uses [`BlockingClient::get_response_hex`] internally.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// See [`BlockingClient::get_response_hex`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_hex`]. See its documentation for details.
fn get_opt_response_hex(&self, path: &str) -> Result, Error> {
match self.get_response_hex(path) {
Ok(res) => Ok(Some(res)),
@@ -198,16 +240,15 @@ impl BlockingClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to any `T` that
- /// implements [`serde::de::DeserializeOwned`].
+ /// Makes a GET request to `path`, deserializing the response body as JSON
+ /// into `T` using [`serde::de::DeserializeOwned`].
///
- /// It should be used when requesting Esplora endpoints that have a specific
- /// defined API, mostly defined in [`crate::api`].
+ /// Use this for endpoints that return Esplora-specific JSON types, as
+ /// defined in [`crate::api`].
///
/// # Errors
///
- /// This function will return an error either from the HTTP client, or the
- /// [`serde::de::DeserializeOwned`] deserialization.
+ /// Returns an [`Error`] if the request fails or JSON deserialization fails.
fn get_response_json<'a, T: serde::de::DeserializeOwned>(
&'a self,
path: &'a str,
@@ -223,12 +264,9 @@ impl BlockingClient {
response.json::().map_err(Error::BitReq)
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
- ///
- /// It uses [`BlockingClient::get_response_json`] internally.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// See [`BlockingClient::get_response_json`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_json`]. See its documentation for details.
fn get_opt_response_json(
&self,
path: &str,
@@ -240,14 +278,14 @@ impl BlockingClient {
}
}
- /// Make an HTTP GET request to given URL, deserializing to `String`.
+ /// Makes a GET request to `path`, returning the response body as a [`String`].
///
- /// It should be used when requesting Esplora endpoints that can return
- /// `String` formatted data that can be parsed downstream.
+ /// Use this for endpoints that return plain text data that needs further
+ /// parsing downstream.
///
/// # Errors
///
- /// This function will return an error either from the HTTP client.
+ /// Returns an [`Error`] if the request fails.
fn get_response_text(&self, path: &str) -> Result {
let response = self.get_with_retry(path)?;
@@ -260,12 +298,9 @@ impl BlockingClient {
Ok(response.as_str()?.to_string())
}
- /// Make an HTTP GET request to given URL, deserializing to `Option`.
+ /// Makes a GET request to `path`, returning `None` on a 404 response.
///
- /// It uses [`BlockingClient::get_response_text`] internally.
- ///
- /// See [`BlockingClient::get_response_text`] above for full
- /// documentation.
+ /// Delegates to [`Self::get_response_text`]. See its documentation for details.
fn get_opt_response_text(&self, path: &str) -> Result, Error> {
match self.get_response_text(path) {
Ok(s) => Ok(Some(s)),
@@ -274,12 +309,17 @@ impl BlockingClient {
}
}
- /// Get a [`Transaction`] option given its [`Txid`]
+ /// Get a raw [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns `None` if the transaction is not found.
pub fn get_tx(&self, txid: &Txid) -> Result , Error> {
self.get_opt_response(&format!("/tx/{txid}/raw"))
}
- /// Get a [`Transaction`] given its [`Txid`].
+ /// Get a raw [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns an [`Error::TransactionNotFound`] if the transaction is not found.
+ /// Prefer [`Self::get_tx`] if you want to handle the not-found case explicitly.
pub fn get_tx_no_opt(&self, txid: &Txid) -> Result {
match self.get_tx(txid) {
Ok(Some(tx)) => Ok(tx),
@@ -288,8 +328,10 @@ impl BlockingClient {
}
}
- /// Get a [`Txid`] of a transaction given its index in a block with a given
- /// hash.
+ /// Get the [`Txid`] of the transaction at position `index` within the
+ /// block identified by `block_hash`.
+ ///
+ /// Returns `None` if the block or index is not found.
pub fn get_txid_at_block_index(
&self,
block_hash: &BlockHash,
@@ -301,50 +343,73 @@ impl BlockingClient {
}
}
- /// Get the status of a [`Transaction`] given its [`Txid`].
+ /// Get the confirmation status of a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns a [`TxStatus`] containing whether the transaction is confirmed,
+ /// and if so, the block height, hash, and timestamp it was confirmed in.
pub fn get_tx_status(&self, txid: &Txid) -> Result {
self.get_response_json(&format!("/tx/{txid}/status"))
}
- /// Get transaction info given its [`Txid`].
+ /// Get an [`EsploraTx`] given its [`Txid`].
+ ///
+ /// Unlike [`Self::get_tx`], returns the Esplora-specific [`EsploraTx`]
+ /// type, which includes additional metadata such as confirmation status,
+ /// fee, and weight. Returns `None` if the transaction is not found.
pub fn get_tx_info(&self, txid: &Txid) -> Result, Error> {
self.get_opt_response_json(&format!("/tx/{txid}"))
}
- /// Get the spend status of a [`Transaction`]'s outputs, given its [`Txid`].
+ /// Get the spend status of all outputs in a [`Transaction`], given its [`Txid`].
+ ///
+ /// Returns a [`Vec`] of [`OutputStatus`], one per output, ordered as they
+ /// appear in the [`Transaction`].
pub fn get_tx_outspends(&self, txid: &Txid) -> Result, Error> {
self.get_response_json(&format!("/tx/{txid}/outspends"))
}
- /// Get a [`BlockHeader`] given a particular [`BlockHash`].
+ /// Get the [`BlockHeader`] of a [`Block`] given its [`BlockHash`].
pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result {
self.get_response_hex(&format!("/block/{block_hash}/header"))
}
- /// Get the [`BlockStatus`] given a particular [`BlockHash`].
+ /// Get the [`BlockStatus`] of a [`Block`] given its [`BlockHash`].
+ ///
+ /// Returns a [`BlockStatus`] indicating whether this [`Block`] is part of
+ /// the best chain, its height, and the [`BlockHash`] of the next [`Block`],
+ /// if any.
pub fn get_block_status(&self, block_hash: &BlockHash) -> Result {
self.get_response_json(&format!("/block/{block_hash}/status"))
}
- /// Get a [`Block`] given a particular [`BlockHash`].
+ /// Get the full [`Block`] with the given [`BlockHash`].
+ ///
+ /// Returns `None` if the [`Block`] is not found.
pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result, Error> {
self.get_opt_response(&format!("/block/{block_hash}/raw"))
}
- /// Get a merkle inclusion proof for a [`Transaction`] with the given
- /// [`Txid`].
+ /// Get a Merkle inclusion proof for a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns a [`MerkleProof`] that can be used to verify the transaction's
+ /// inclusion in a block. Returns `None` if the transaction is not found or
+ /// is unconfirmed.
pub fn get_merkle_proof(&self, txid: &Txid) -> Result , Error> {
self.get_opt_response_json(&format!("/tx/{txid}/merkle-proof"))
}
- /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the
- /// given [`Txid`].
+ /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] given its [`Txid`].
+ ///
+ /// Returns `None` if the transaction is not found or is unconfirmed.
pub fn get_merkle_block(&self, txid: &Txid) -> Result , Error> {
self.get_opt_response_hex(&format!("/tx/{txid}/merkleblock-proof"))
}
- /// Get the spending status of an output given a [`Txid`] and the output
- /// index.
+ /// Get the spend status of a specific output, identified by its [`Txid`]
+ /// and output index.
+ ///
+ /// Returns an [`OutputStatus`] indicating whether the output has been
+ /// spent, and if so, by which transaction. Returns `None` if not found.
pub fn get_output_status(
&self,
txid: &Txid,
@@ -353,7 +418,14 @@ impl BlockingClient {
self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
}
- /// Broadcast a [`Transaction`] to Esplora
+ /// Broadcast a [`Transaction`] to the Esplora server.
+ ///
+ /// The transaction is serialized and sent as a hex-encoded string.
+ /// Returns the [`Txid`] of the broadcasted transaction.
+ ///
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the request fails or the server rejects the transaction.
pub fn broadcast(&self, transaction: &Transaction) -> Result {
let body = serialize::(transaction).to_lower_hex_string();
@@ -363,14 +435,17 @@ impl BlockingClient {
Ok(txid)
}
- /// Broadcast a package of [`Transaction`]s to Esplora.
+ /// Broadcast a package of [`Transaction`]s to the Esplora server.
+ ///
+ /// Returns a [`SubmitPackageResult`] containing the result for each
+ /// transaction in the package, keyed by [`bitcoin::Wtxid`].
///
- /// If `maxfeerate` is provided, any transaction whose
- /// fee is higher will be rejected.
+ /// Optionally, `maxfeerate` and `maxburnamount` can be provided to reject
+ /// transactions that exceed these thresholds.
///
- /// If `maxburnamount` is provided, any transaction
- /// with higher provably unspendable outputs amount
- /// will be rejected.
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the request fails or the server rejects the package.
pub fn submit_package(
&self,
transactions: &[Transaction],
@@ -404,7 +479,7 @@ impl BlockingClient {
Ok(result)
}
- /// Get the height of the current blockchain tip.
+ /// Get the block height of the current blockchain tip.
pub fn get_height(&self) -> Result {
self.get_response_text("/blocks/tip/height")
.map(|s| u32::from_str(s.as_str()).map_err(Error::Parsing))?
@@ -416,25 +491,28 @@ impl BlockingClient {
.map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
}
- /// Get the [`BlockHash`] of a specific block height
+ /// Get the [`BlockHash`] of a [`Block`] given its height.
pub fn get_block_hash(&self, block_height: u32) -> Result {
self.get_response_text(&format!("/block-height/{block_height}"))
.map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
}
- /// Get statistics about the mempool.
+ /// Get global statistics about the mempool.
+ ///
+ /// Returns a [`MempoolStats`] containing the transaction count, total
+ /// virtual size, total fees, and fee rate histogram.
pub fn get_mempool_stats(&self) -> Result {
self.get_response_json("/mempool")
}
- /// Get a list of the last 10 [`Transaction`]s to enter the mempool.
+ /// Get the last 10 [`MempoolRecentTx`]s to enter the mempool.
pub fn get_mempool_recent_txs(&self) -> Result, Error> {
self.get_response_json("/mempool/recent")
}
- /// Get the full list of [`Txid`]s in the mempool.
+ /// Get the full list of [`Txid`]s currently in the mempool.
///
- /// The order of the txids is arbitrary and does not match bitcoind's.
+ /// The order of the returned [`Txid`]s is arbitrary.
pub fn get_mempool_txids(&self) -> Result, Error> {
self.get_response_json("/mempool/txids")
}
@@ -450,25 +528,31 @@ impl BlockingClient {
Ok(estimates)
}
- /// Get information about a specific address, includes confirmed balance and transactions in
- /// the mempool.
+ /// Get statistics about an [`Address`].
+ ///
+ /// Returns an [`AddressStats`] containing confirmed and mempool transaction
+ /// summaries for the given address, including funded and spent output
+ /// counts and their total values.
pub fn get_address_stats(&self, address: &Address) -> Result {
let path = format!("/address/{address}");
self.get_response_json(&path)
}
- /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions.
+ /// Get statistics about a [`Script`] hash's confirmed and mempool transactions.
+ ///
+ /// Returns a [`ScriptHashStats`] containing transaction summaries for the
+ /// SHA256 hash of the given [`Script`].
pub fn get_scripthash_stats(&self, script: &Script) -> Result {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}");
self.get_response_json(&path)
}
- /// Get transaction history for the specified address, sorted with newest
- /// first.
+ /// Get confirmed transaction history for an [`Address`], sorted newest first.
///
/// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
- /// More can be requested by specifying the last txid seen by the previous query.
+ /// To paginate, pass the [`Txid`] of the last transaction seen in the
+ /// previous response as `last_seen`.
pub fn get_address_txs(
&self,
address: &Address,
@@ -482,17 +566,17 @@ impl BlockingClient {
self.get_response_json(&path)
}
- /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first.
+ /// Get unconfirmed mempool [`EsploraTx`]s for an [`Address`], sorted newest first.
pub fn get_mempool_address_txs(&self, address: &Address) -> Result, Error> {
let path = format!("/address/{address}/txs/mempool");
self.get_response_json(&path)
}
- /// Get transaction history for the specified scripthash,
- /// sorted with newest first. Returns 25 transactions per page.
- /// More can be requested by specifying the last txid seen by the previous
- /// query.
+ /// Get confirmed transaction history for a [`Script`] hash, sorted newest first.
+ ///
+ /// Returns 25 transactions per page. To paginate, pass the [`Txid`] of the
+ /// last transaction seen in the previous response as `last_seen`.
pub fn get_scripthash_txs(
&self,
script: &Script,
@@ -506,8 +590,7 @@ impl BlockingClient {
self.get_response_json(&path)
}
- /// Get mempool [`Transaction`] history for the
- /// specified [`Script`] hash, sorted with newest first.
+ /// Get unconfirmed mempool [`EsploraTx`]s for a [`Script`] hash, sorted newest first.
pub fn get_mempool_scripthash_txs(&self, script: &Script) -> Result, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash:x}/txs/mempool");
@@ -515,25 +598,33 @@ impl BlockingClient {
self.get_response_json(&path)
}
- /// Get a summary about a [`Block`], given its [`BlockHash`].
+ /// Get a [`BlockInfo`] summary for the [`Block`] with the given [`BlockHash`].
+ ///
+ /// [`BlockInfo`] includes metadata such as the height, timestamp,
+ /// [`Transaction`] count, size, and [`Weight`](bitcoin::Weight).
+ ///
+ /// This method does not return the full [`Block`].
pub fn get_block_info(&self, blockhash: &BlockHash) -> Result {
let path = format!("/block/{blockhash}");
self.get_response_json(&path)
}
- /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
+ /// Get all [`Txid`]s of [`Transaction`]s included in the [`Block`] with the
+ /// given [`BlockHash`].
pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result, Error> {
let path = format!("/block/{blockhash}/txids");
self.get_response_json(&path)
}
- /// Get up to 25 [`Transaction`]s from a [`Block`], given its [`BlockHash`],
- /// beginning at `start_index` (starts from 0 if `start_index` is `None`).
+ /// Get up to 25 [`EsploraTx`]s from the [`Block`] with the given
+ /// [`BlockHash`], starting at `start_index`.
///
- /// The `start_index` value MUST be a multiple of 25,
- /// else an error will be returned by Esplora.
+ /// If `start_index` is `None`, starts from the first transaction (index 0).
+ ///
+ /// Note that `start_index` must be a multiple of 25, otherwise the server
+ /// will return an error.
pub fn get_block_txs(
&self,
blockhash: &BlockHash,
@@ -547,11 +638,13 @@ impl BlockingClient {
self.get_response_json(&path)
}
- /// Gets some recent block summaries starting at the tip or at `height` if
- /// provided.
+ /// Get [`BlockInfo`] summaries for recent [`Block`]s.
+ ///
+ /// If `height` is `Some(h)`, returns blocks starting from height `h`.
+ /// If `height` is `None`, returns blocks starting from the current tip.
///
/// The maximum number of summaries returned depends on the backend itself:
- /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
#[allow(deprecated)]
#[deprecated(since = "0.13.0", note = "use `get_block_infos` instead")]
pub fn get_blocks(&self, height: Option) -> Result, Error> {
@@ -566,11 +659,19 @@ impl BlockingClient {
Ok(blocks)
}
- /// Gets some recent block summaries starting at the tip or at `height` if
- /// provided.
+ /// Get [`BlockInfo`] summaries for recent [`Block`]s.
+ ///
+ /// If `height` is `Some(h)`, returns blocks starting from height `h`.
+ /// If `height` is `None`, returns blocks starting from the current tip.
///
/// The maximum number of summaries returned depends on the backend itself:
- /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidResponse`] if the server returns an empty list.
+ ///
+ /// This method does not return the full [`Block`].
pub fn get_block_infos(&self, height: Option) -> Result, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
@@ -583,14 +684,14 @@ impl BlockingClient {
Ok(blocks)
}
- /// Get all UTXOs locked to an address.
+ /// Get all confirmed [`Utxo`]s locked to the given [`Address`].
pub fn get_address_utxos(&self, address: &Address) -> Result, Error> {
let path = format!("/address/{address}/utxo");
self.get_response_json(&path)
}
- /// Get all [`Utxo`]s locked to a [`Script`].
+ /// Get all confirmed [`Utxo`]s locked to the given [`Script`].
pub fn get_scripthash_utxos(&self, script: &Script) -> Result, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}/utxo");
diff --git a/src/lib.rs b/src/lib.rs
index 41d03e3..5926a35 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,50 +1,68 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! An extensible blocking/async Esplora client
+//! # Esplora Client
//!
-//! This library provides an extensible blocking and
-//! async Esplora client to query Esplora's backend.
+//! A client library for querying [Esplora] HTTP APIs from Rust.
//!
-//! The library provides the possibility to build a blocking
-//! or async client, both using [`bitreq`].
-//! The library supports communicating to Esplora via a proxy
-//! and also using TLS (SSL) for secure communication.
+//! The crate exposes a shared set of Esplora response types in [`api`], plus
+//! optional blocking and async clients built on [`bitreq`]. Both clients use the
+//! same [`Builder`] configuration for the base URL, proxy, timeout, custom
+//! headers, and retry policy.
//!
+//! # Client Modes
//!
-//! ## Usage
+//! Enable the `blocking` feature to use `BlockingClient`, whose methods block
+//! the current thread until each request completes. Enable the `async` feature
+//! to use `AsyncClient`, whose methods return futures and require an async
+//! runtime. The default async sleeper is backed by Tokio when the `tokio`
+//! feature is enabled; custom runtimes can supply their own `Sleeper`.
//!
-//! You can create a blocking client as follows:
+//! # Examples
//!
-//! ```no_run
-//! # #[cfg(feature = "blocking")]
-//! # {
+//! Create a blocking client:
+//!
+//! ```rust,ignore
//! use esplora_client::Builder;
-//! let builder = Builder::new("https://blockstream.info/testnet/api");
-//! let blocking_client = builder.build_blocking();
-//! # Ok::<(), esplora_client::Error>(());
-//! # }
+//!
+//! fn main() -> Result<(), esplora_client::Error> {
+//! let builder = Builder::new("https://blockstream.info/testnet/api");
+//! let blocking_client = builder.build_blocking();
+//! let height = blocking_client.get_height()?;
+//!
+//! Ok(())
+//! }
//! ```
//!
-//! Here is an example of how to create an asynchronous client.
+//! Create an async client:
//!
-//! ```no_run
-//! # #[cfg(all(feature = "async", feature = "tokio"))]
-//! # {
+//! ```rust,ignore
//! use esplora_client::Builder;
+//!
+//! #[tokio::main]
+//! async fn main() -> Result<(), esplora_client::Error> {
//! let builder = Builder::new("https://blockstream.info/testnet/api");
-//! let async_client = builder.build_async();
-//! # Ok::<(), esplora_client::Error>(());
-//! # }
+//! let async_client = builder.build_async()?;
+//! let height = async_client.get_height().await?;
+//!
+//! Ok(())
+//! }
//! ```
//!
-//! ## Features
+//! # Retries
//!
-//! By default the library enables all features. To specify
-//! specific features, set `default-features` to `false` in your `Cargo.toml`
-//! and specify the features you want. This will look like this:
+//! Both clients retry responses with status codes listed in
+//! [`RETRYABLE_ERROR_CODES`]. Retry attempts use exponential backoff starting
+//! at 256 milliseconds and are controlled by [`Builder::max_retries`].
//!
-//! `esplora-client = { version = "*", default-features = false, features =
-//! ["blocking"] }`
+//! # Features
+//!
+//! By default the crate enables all features. To select only the pieces you
+//! need, set `default-features = false` in `Cargo.toml` and list the desired
+//! features explicitly:
+//!
+//! ```toml
+//! esplora-client = { version = "*", default-features = false, features = ["blocking"] }
+//! ```
//!
//! * `blocking` enables [`bitreq`], the blocking client with proxy.
//! * `blocking-https` enables [`bitreq`], the blocking client with proxy and TLS (SSL) capabilities
@@ -53,8 +71,8 @@
//! capabilities using the `rustls` backend.
//! * `blocking-https-native` enables [`bitreq`], the blocking client with proxy and TLS (SSL)
//! capabilities using the platform's native TLS backend (likely OpenSSL).
-//! * `blocking-https-bundled` enables [`bitreq`], the blocking client with proxy and TLS (SSL)
-//! capabilities using a bundled OpenSSL library backend.
+//! * `blocking-https-rustls-probe` enables [`bitreq`], the blocking client with proxy and TLS (SSL)
+//! capabilities using `rustls` and probed system roots.
//! * `async` enables [`bitreq`], the async client with proxy capabilities.
//! * `async-https` enables [`bitreq`], the async client with support for proxying and TLS (SSL)
//! using the default [`bitreq`] TLS backend.
@@ -62,12 +80,12 @@
//! (SSL) using the platform's native TLS backend (likely OpenSSL).
//! * `async-https-rustls` enables [`bitreq`], the async client with support for proxying and TLS
//! (SSL) using the `rustls` TLS backend.
-//! * `async-https-rustls-manual-roots` enables [`bitreq`], the async client with support for
-//! proxying and TLS (SSL) using the `rustls` TLS backend without using the default root
-//! certificates.
+//! * `async-https-rustls-probe` enables [`bitreq`], the async client with support for proxying and
+//! TLS (SSL) using `rustls` and probed system roots.
+//! * `tokio` enables the default async sleeper used by [`Builder::build_async`].
//!
-//! [`dont remove this line or cargo doc will break`]: https://example.com
-#![cfg_attr(not(feature = "bitreq"), doc = "[`bitreq`]: https://docs.rs/bitreq")]
+//! [Esplora]: https://github.com/Blockstream/esplora/blob/master/API.md
+//! [`bitreq`]: https://docs.rs/bitreq
#![allow(clippy::result_large_err)]
#![warn(missing_docs)]
#![allow(deprecated)]
@@ -95,65 +113,65 @@ pub use blocking::BlockingClient;
#[cfg(feature = "async")]
pub use r#async::AsyncClient;
-/// Response status codes for which the request may be retried.
+/// HTTP response status codes for which a request may be retried.
pub const RETRYABLE_ERROR_CODES: [u16; 3] = [
429, // TOO_MANY_REQUESTS
500, // INTERNAL_SERVER_ERROR
503, // SERVICE_UNAVAILABLE
];
-/// Base backoff in milliseconds.
+/// Base delay used by the exponential retry backoff.
#[cfg(any(feature = "blocking", feature = "async"))]
const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256);
-/// Default max retries.
+/// Default maximum number of retry attempts per request.
const DEFAULT_MAX_RETRIES: usize = 6;
-/// Default max cached connections
+/// Default maximum number of cached connections for the async client.
#[cfg(feature = "async")]
const DEFAULT_MAX_CONNECTIONS: usize = 10;
-/// Check if [`Response`] status is within 100-199.
+/// Check if the [`Response`] status code is informational (100-199).
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_informational(response: &Response) -> bool {
(100..200).contains(&response.status_code)
}
-/// Check if [`Response`] status is within 200-299.
+/// Check if the [`Response`] status code is successful (200-299).
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_success(response: &Response) -> bool {
(200..300).contains(&response.status_code)
}
-/// Check if [`Response`] status is within 300-399.
+/// Check if the [`Response`] status code is a redirection (300-399).
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_redirection(response: &Response) -> bool {
(300..400).contains(&response.status_code)
}
-/// Check if [`Response`] status is within 400-499.
+/// Check if the [`Response`] status code is a client error (400-499).
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_client_error(response: &Response) -> bool {
(400..500).contains(&response.status_code)
}
-/// Check if [`Response`] status is within 500-599.
+/// Check if the [`Response`] status code is a server error (500-599).
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_server_error(response: &Response) -> bool {
(500..600).contains(&response.status_code)
}
-/// Check if [`Response`] status is within the retryable ones.
+/// Check if the [`Response`] status code is retryable (429, 500, 503).
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_retryable(response: &Response) -> bool {
RETRYABLE_ERROR_CODES.contains(&(response.status_code as u16))
}
-/// Returns the [`FeeRate`] for the given confirmation target in blocks.
+/// Return the [`FeeRate`] for the given confirmation target in blocks.
///
/// Selects the highest confirmation target from `estimates` that is at or
/// below `target_blocks`, and returns its [`FeeRate`]. Returns `None` if no
@@ -166,7 +184,7 @@ pub fn convert_fee_rate(target_blocks: usize, estimates: HashMap)
.map(|(_, feerate)| feerate)
}
-/// Converts a [`HashMap`] of fee estimates expressed as sat/vB ([`f64`]) into a [`FeeRate`].
+/// Convert a [`HashMap`] of fee estimates expressed as sat/vB ([`f64`]) into [`FeeRate`]s.
pub fn sat_per_vbyte_to_feerate(estimates: HashMap) -> HashMap {
estimates
.into_iter()
@@ -174,37 +192,58 @@ pub fn sat_per_vbyte_to_feerate(estimates: HashMap) -> HashMap://:@host:`.
///
/// Note that the format of this value and the supported protocols change
- /// slightly between the blocking version of the client (using `minreq`)
- /// and the async version (using `bitreq`). For more details check with
- /// the documentation of the two crates. Both of them are compiled with
- /// the `socks` feature enabled.
+ /// slightly by target and enabled transport features. See [bitreq]'s
+ /// proxy documentation for the accepted schemes.
///
/// The proxy is ignored when targeting `wasm32`.
pub proxy: Option,
- /// Socket timeout.
+ /// Per-request socket timeout, in seconds.
pub timeout: Option,
- /// HTTP headers to set on every request made to Esplora server.
+ /// HTTP headers to set on every request made to the Esplora server.
pub headers: HashMap,
- /// Max retries
+ /// Maximum number of retry attempts for retryable HTTP responses.
pub max_retries: usize,
- /// The maximum number of cached connections.
+ /// Maximum number of cached connections for the async client.
#[cfg(feature = "async")]
pub max_connections: usize,
}
impl Builder {
- /// Instantiate a new builder
+ /// Create a [`Builder`] for an Esplora server base URL.
+ ///
+ /// The URL is stored exactly as provided and request paths are appended to
+ /// it. Do not include a trailing slash unless your server expects one.
pub fn new(base_url: &str) -> Self {
Builder {
base_url: base_url.to_string(),
@@ -217,94 +256,110 @@ impl Builder {
}
}
- /// Set the proxy of the builder
+ /// Set the proxy URL used for requests.
+ ///
+ /// The proxy is ignored when targeting `wasm32`.
pub fn proxy(mut self, proxy: &str) -> Self {
self.proxy = Some(proxy.to_string());
self
}
- /// Set the timeout of the builder
+ /// Set the per-request socket timeout, in seconds.
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
self
}
- /// Add a header to set on each request
+ /// Add or replace an HTTP header sent with every request.
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers.insert(key.to_string(), value.to_string());
self
}
- /// Set the maximum number of times to retry a request if the response status
- /// is one of [`RETRYABLE_ERROR_CODES`].
+ /// Set the maximum number of retry attempts for retryable responses.
+ ///
+ /// Only responses whose status code is listed in
+ /// [`RETRYABLE_ERROR_CODES`] are retried.
pub fn max_retries(mut self, count: usize) -> Self {
self.max_retries = count;
self
}
- /// Set the maximum number of cached connections in the client.
+ /// Set the maximum number of cached connections in the async client.
#[cfg(feature = "async")]
pub fn max_connections(mut self, count: usize) -> Self {
self.max_connections = count;
self
}
- /// Build a blocking client from builder
+ /// Build a [`BlockingClient`] from this configuration.
#[cfg(feature = "blocking")]
pub fn build_blocking(self) -> BlockingClient {
BlockingClient::from_builder(self)
}
- /// Build an asynchronous client from builder
+ /// Build an [`AsyncClient`] from this configuration.
+ ///
+ /// This uses `DefaultSleeper`, which is backed by Tokio.
+ ///
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the async HTTP client cannot be constructed.
#[cfg(all(feature = "async", feature = "tokio"))]
pub fn build_async(self) -> Result {
AsyncClient::from_builder(self)
}
- /// Build an asynchronous client from builder where the returned client uses a
- /// user-defined [`Sleeper`].
+ /// Build an [`AsyncClient`] with a user-defined [`Sleeper`].
+ ///
+ /// Use this when integrating with an async runtime other than Tokio or when
+ /// tests need a custom sleep implementation.
+ ///
+ /// # Errors
+ ///
+ /// Returns an [`Error`] if the async HTTP client cannot be constructed.
#[cfg(feature = "async")]
pub fn build_async_with_sleeper(self) -> Result, Error> {
AsyncClient::from_builder(self)
}
}
-/// Errors that can happen during a request to `Esplora` servers.
+/// Errors that can occur while building clients, sending requests, or decoding responses.
#[derive(Debug)]
pub enum Error {
- /// Error during `bitreq` HTTP request
+ /// Error during a [`bitreq`] HTTP request.
#[cfg(any(feature = "blocking", feature = "async"))]
BitReq(bitreq::Error),
- /// Error during JSON (de)serialization
+ /// Error during JSON serialization or deserialization.
SerdeJson(serde_json::Error),
- /// HTTP response error
+ /// Non-successful HTTP response from the Esplora server.
HttpResponse {
/// The HTTP status code returned by the server.
status: u16,
- /// The error message content.
+ /// The response body returned by the server.
message: String,
},
- /// Invalid number returned
+ /// Invalid integer returned by the server.
Parsing(std::num::ParseIntError),
- /// Invalid status code, unable to convert to `u16`
+ /// Invalid status code, unable to convert to `u16`.
StatusCode(TryFromIntError),
- /// Invalid Bitcoin data returned
+ /// Invalid Bitcoin consensus data returned by the server.
BitcoinEncoding(bitcoin::consensus::encode::Error),
- /// Invalid hex data returned (attempting to create an array)
+ /// Invalid fixed-size hex data returned by the server.
HexToArray(bitcoin::hex::HexToArrayError),
- /// Invalid hex data returned (attempting to create a vector)
+ /// Invalid variable-length hex data returned by the server.
HexToBytes(bitcoin::hex::HexToBytesError),
- /// Transaction not found
+ /// Transaction not found.
TransactionNotFound(Txid),
- /// Block Header height not found
+ /// Block header height not found.
HeaderHeightNotFound(u32),
- /// Block Header hash not found
+ /// Block header hash not found.
HeaderHashNotFound(BlockHash),
- /// Invalid HTTP Header name specified
+ /// Invalid HTTP header name specified in [`Builder::header`].
InvalidHttpHeaderName(String),
- /// Invalid HTTP Header value specified
+ /// Invalid HTTP header value specified in [`Builder::header`].
InvalidHttpHeaderValue(String),
- /// The server sent an invalid response
+ /// The server sent an invalid response.
InvalidResponse,
}