Skip to main content

esplora_client/
api.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! # Esplora API
4//!
5//! This module implements the types and deserializers
6//! needed to interact with an Esplora-compliant server.
7//!
8//! Refer to the [Esplora API] specification for the complete API reference.
9//!
10//! [Esplora API]: <https://github.com/Blockstream/esplora/blob/master/API.md>
11
12use serde::Deserialize;
13use std::collections::HashMap;
14
15pub use bitcoin::consensus::{deserialize, serialize};
16use bitcoin::hash_types;
17use bitcoin::hash_types::TxMerkleNode;
18pub use bitcoin::hex::FromHex;
19pub use bitcoin::{
20    absolute, block, transaction, Address, Amount, Block, BlockHash, CompactTarget, FeeRate,
21    OutPoint, Script, ScriptBuf, ScriptHash, Sequence, Transaction, TxIn, TxOut, Txid, Weight,
22    Witness, Wtxid,
23};
24
25/// An input to a [`Transaction`].
26#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
27pub struct Vin {
28    /// The [`Txid`] of the [`Transaction`] that created this output.
29    pub txid: Txid,
30    /// The index of this output in the [`Transaction`] that created it.
31    pub vout: u32,
32    /// This input's previous output [`Amount`] and [script pubkey][Script].
33    ///
34    /// `None` if this input spends a coinbase output.
35    pub prevout: Option<Vout>,
36    /// The [`Script`] that unlocks this input.
37    pub scriptsig: ScriptBuf,
38    /// The [`Witness`] that unlocks this input.
39    #[serde(default)]
40    pub witness: Witness,
41    /// The sequence value for this input.
42    pub sequence: Sequence,
43    /// Whether this is a coinbase input.
44    pub is_coinbase: bool,
45}
46
47/// An output from a [`Transaction`].
48#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
49pub struct Vout {
50    /// The output's [`Amount`].
51    #[serde(with = "bitcoin::amount::serde::as_sat")]
52    pub value: Amount,
53    /// The [script pubkey][Script] this output is locked to.
54    pub scriptpubkey: ScriptBuf,
55}
56
57/// The confirmation status of a [`Transaction`].
58#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
59pub struct TxStatus {
60    /// Whether the [`Transaction`] is confirmed or not.
61    pub confirmed: bool,
62    /// The block height that confirmed the [`Transaction`].
63    pub block_height: Option<u32>,
64    /// The [`BlockHash`] of the block that confirmed the [`Transaction`].
65    ///
66    /// `None` if the [`Transaction`] was confirmed by the genesis block.
67    pub block_hash: Option<BlockHash>,
68    /// The UNIX timestamp of the block that confirmed the [`Transaction`], as claimed by the
69    /// miner.
70    pub block_time: Option<u64>,
71}
72
73/// A Merkle inclusion proof for a [`Transaction`].
74#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
75pub struct MerkleProof {
76    /// The block height that confirmed the [`Transaction`].
77    pub block_height: u32,
78    /// The Merkle proof of inclusion of a [`Transaction`] in a [`Block`].
79    ///
80    /// Elements are returned left-to-right and bottom-to-top.
81    pub merkle: Vec<Txid>,
82    /// The 0-indexed position of the [`Transaction`] in the [`Block`].
83    pub pos: usize,
84}
85
86/// The status of a [`TxOut`].
87#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
88pub struct OutputStatus {
89    /// Whether the [`TxOut`] is spent.
90    pub spent: bool,
91    /// The [`Txid`] of the [`Transaction`] that spent this [`TxOut`].
92    pub txid: Option<Txid>,
93    /// The input index of this [`TxOut`] in the [`Transaction`] that spent it.
94    pub vin: Option<u64>,
95    /// Information about the [`Transaction`] that spent this [`TxOut`].
96    pub status: Option<TxStatus>,
97}
98
99/// The status of a [`Block`].
100#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
101pub struct BlockStatus {
102    /// Whether this [`Block`] belongs to the chain with the most Proof-of-Work.
103    pub in_best_chain: bool,
104    /// The height of this [`Block`].
105    pub height: Option<u32>,
106    /// The [`BlockHash`] of the [`Block`] that builds on top of this [`Block`].
107    pub next_best: Option<BlockHash>,
108}
109
110/// A transaction in the format returned by Esplora.
111///
112/// Unlike the native `rust-bitcoin` [`Transaction`], [`EsploraTx`]
113/// includes additional metadata such as the [`TxStatus`], transaction fee,
114/// and transaction [`Weight`], as indexed and reported by Esplora servers.
115///
116/// To convert it into a [`Transaction`], use [`EsploraTx::to_tx`] or `.into()`.
117#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
118pub struct EsploraTx {
119    /// The [`Txid`] of the [`Transaction`].
120    pub txid: Txid,
121    /// The version of the [`Transaction`].
122    pub version: transaction::Version,
123    /// The locktime of the [`Transaction`].
124    /// Sets a time or height after which the [`Transaction`] can be mined.
125    pub locktime: absolute::LockTime,
126    /// The array of inputs in the [`Transaction`].
127    pub vin: Vec<Vin>,
128    /// The array of outputs in the [`Transaction`].
129    pub vout: Vec<Vout>,
130    /// The [`Transaction`] size in raw bytes (NOT virtual bytes).
131    pub size: usize,
132    /// The [`Transaction`]'s weight.
133    pub weight: Weight,
134    /// The confirmation status of the [`Transaction`].
135    pub status: TxStatus,
136    /// The fee paid by the [`Transaction`], in satoshis.
137    #[serde(with = "bitcoin::amount::serde::as_sat")]
138    pub fee: Amount,
139}
140
141/// A summary of a [`Block`].
142///
143/// Contains additional metadata about a [`Block`], but not the whole [`Block`].
144///
145/// For the complete [`Block`] contents, use the `get_block_by_hash` client method.
146#[derive(Debug, Clone, Deserialize)]
147pub struct BlockInfo {
148    /// The [`BlockHash`] of this [`Block`].
149    pub id: BlockHash,
150    /// The block height of this [`Block`].
151    pub height: u32,
152    /// The version of this [`Block`].
153    pub version: block::Version,
154    /// The UNIX timestamp of this [`Block`], as claimed by the miner.
155    pub timestamp: u64,
156    /// The [`Transaction`] count for this [`Block`].
157    pub tx_count: u64,
158    /// The size of this [`Block`], in bytes.
159    pub size: usize,
160    /// The [`Weight`] of this [`Block`].
161    pub weight: Weight,
162    /// The Merkle root of this [`Block`].
163    pub merkle_root: hash_types::TxMerkleNode,
164    /// The [`BlockHash`] of the previous [`Block`].
165    ///
166    /// `None` for the genesis block.
167    pub previousblockhash: Option<BlockHash>,
168    /// The Median Time Past value for this [`Block`].
169    pub mediantime: u64,
170    /// This [`Block`]'s nonce.
171    pub nonce: u32,
172    /// The [`Block`]'s `bits` value, encoded as a [`CompactTarget`].
173    pub bits: CompactTarget,
174    /// The [`Block`]'s difficulty target value.
175    pub difficulty: f64,
176}
177
178/// A manual `PartialEq` implementation is required
179/// since [`BlockInfo::difficulty`] is an `f64`.
180///
181/// This treats two `NaN` difficulty values as equal,
182/// allowing [`BlockInfo`] to implement [`Eq`] correctly.
183impl PartialEq for BlockInfo {
184    fn eq(&self, other: &Self) -> bool {
185        let Self { difficulty: d1, .. } = self;
186        let Self { difficulty: d2, .. } = other;
187
188        self.id == other.id
189            && self.height == other.height
190            && self.version == other.version
191            && self.timestamp == other.timestamp
192            && self.tx_count == other.tx_count
193            && self.size == other.size
194            && self.weight == other.weight
195            && self.merkle_root == other.merkle_root
196            && self.previousblockhash == other.previousblockhash
197            && self.mediantime == other.mediantime
198            && self.nonce == other.nonce
199            && self.bits == other.bits
200            && ((d1.is_nan() && d2.is_nan()) || (d1 == d2))
201    }
202}
203impl Eq for BlockInfo {}
204
205/// The UNIX timestamp and height of a [`Block`].
206#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
207pub struct BlockTime {
208    /// The UNIX timestamp of the [`Block`], as claimed by the miner.
209    pub timestamp: u64,
210    /// The block height of the [`Block`].
211    pub height: u32,
212}
213
214/// Summary about a [`Block`].
215#[allow(deprecated)]
216#[deprecated(since = "0.13.0", note = "use `BlockInfo` instead")]
217#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
218pub struct BlockSummary {
219    /// The [`BlockHash`] of the [`Block`].
220    pub id: BlockHash,
221    /// The UNIX timestamp and height of the [`Block`].
222    #[serde(flatten)]
223    pub time: BlockTime,
224    /// The [`BlockHash`] of the previous [`Block`].
225    ///
226    /// `None` for the genesis block.
227    pub previousblockhash: Option<BlockHash>,
228    /// The Merkle root of this [`Block`].
229    pub merkle_root: TxMerkleNode,
230}
231
232/// Statistics about an [`Address`].
233#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
234pub struct AddressStats {
235    /// The [`Address`].
236    #[serde(deserialize_with = "deserialize_address_assume_checked")]
237    pub address: Address,
238    /// The summary of confirmed [`Transaction`]s for this [`Address`].
239    pub chain_stats: AddressTxsSummary,
240    /// The summary of unconfirmed mempool [`Transaction`]s for this [`Address`].
241    pub mempool_stats: AddressTxsSummary,
242}
243
244/// A summary of [`Transaction`]s in which an [`Address`] is involved.
245#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
246pub struct AddressTxsSummary {
247    /// The current number of funded [`TxOut`]s for this [`Address`].
248    pub funded_txo_count: u32,
249    /// The total [`Amount`] of funded [`TxOut`]s for this [`Address`].
250    #[serde(with = "bitcoin::amount::serde::as_sat")]
251    pub funded_txo_sum: Amount,
252    /// The number of spent [`TxOut`]s for this [`Address`].
253    pub spent_txo_count: u32,
254    /// The total [`Amount`] of spent [`TxOut`]s for this [`Address`].
255    #[serde(with = "bitcoin::amount::serde::as_sat")]
256    pub spent_txo_sum: Amount,
257    /// The total number of [`Transaction`]s for this [`Address`].
258    pub tx_count: u32,
259}
260
261/// Statistics about a [scripthash](Script)'s confirmed and mempool [`Transaction`]s.
262#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
263pub struct ScriptHashStats {
264    /// The summary of confirmed [`Transaction`]s for this [scripthash](Script).
265    pub chain_stats: ScriptHashTxsSummary,
266    /// The summary of mempool [`Transaction`]s for this [scripthash](Script).
267    pub mempool_stats: ScriptHashTxsSummary,
268}
269
270/// A summary of [`Transaction`]s for a particular [scripthash](Script).
271pub type ScriptHashTxsSummary = AddressTxsSummary;
272
273/// The confirmation status of a [`TxOut`].
274#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
275pub struct UtxoStatus {
276    /// Whether the [`TxOut`] is confirmed.
277    pub confirmed: bool,
278    /// The block height in which the [`TxOut`] was confirmed.
279    pub block_height: Option<u32>,
280    /// The block hash in which the [`TxOut`] was confirmed.
281    pub block_hash: Option<BlockHash>,
282    /// The UNIX timestamp in which the [`TxOut`] was confirmed, as reported by the miner.
283    pub block_time: Option<u64>,
284}
285
286/// An unspent [`TxOut`], including its outpoint, confirmation status and value.
287#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
288pub struct Utxo {
289    /// The [`Txid`] of the [`Transaction`] that created this [`TxOut`].
290    pub txid: Txid,
291    /// The output index of this [`TxOut`] in the [`Transaction`] that created it.
292    pub vout: u32,
293    /// The confirmation status of this [`TxOut`].
294    pub status: UtxoStatus,
295    /// The value of this [`TxOut`].
296    #[serde(with = "bitcoin::amount::serde::as_sat")]
297    pub value: Amount,
298}
299
300/// Statistics about the mempool.
301#[derive(Clone, Debug, PartialEq, Deserialize)]
302pub struct MempoolStats {
303    /// The number of [`Transaction`]s currently in the mempool.
304    pub count: usize,
305    /// The total size of mempool [`Transaction`]s, in virtual bytes.
306    pub vsize: usize,
307    /// The total fee paid by mempool [`Transaction`]s.
308    #[serde(with = "bitcoin::amount::serde::as_sat")]
309    pub total_fee: Amount,
310    /// The mempool's fee rate distribution histogram.
311    ///
312    /// An array of `(feerate, vsize)` tuples, where each entry's
313    /// `vsize` is the total vsize of [`Transaction`]s paying more
314    /// than `feerate` but less than the previous entry's `feerate`
315    /// (except for the first entry, which has no upper bound).
316    ///
317    /// The Esplora API reports `vsize` in virtual bytes. This field
318    /// currently stores that raw value in [`Weight`].
319    #[serde(deserialize_with = "deserialize_fee_histogram")]
320    pub fee_histogram: Vec<(FeeRate, Weight)>,
321}
322
323/// A [`Transaction`] that recently entered the mempool.
324#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
325pub struct MempoolRecentTx {
326    /// The [`Transaction`]'s [`Txid`].
327    pub txid: Txid,
328    /// The fee paid by the [`Transaction`].
329    #[serde(with = "bitcoin::amount::serde::as_sat")]
330    pub fee: Amount,
331    /// The [`Transaction`]'s size, in virtual bytes.
332    pub vsize: usize,
333    /// The combined value of the [`Transaction`]'s outputs.
334    #[serde(with = "bitcoin::amount::serde::as_sat")]
335    pub value: Amount,
336}
337
338/// The global result of a [`Transaction`] package submission.
339#[derive(Deserialize, Debug)]
340pub struct SubmitPackageResult {
341    /// The [`Transaction`] package result message.
342    ///
343    /// "success" indicates all transactions were
344    /// accepted or are already in the mempool.
345    pub package_msg: String,
346    /// The list of individual [`Transaction`] broadcast
347    /// results, keyed by each [`Transaction`]'s [`Wtxid`].
348    #[serde(rename = "tx-results")]
349    pub tx_results: HashMap<Wtxid, TxResult>,
350    /// The list of [`Txid`]s of replaced [`Transaction`]s.
351    #[serde(rename = "replaced-transactions")]
352    pub replaced_transactions: Option<Vec<Txid>>,
353}
354
355/// A per-transaction result of a [`Transaction`] package submission.
356#[derive(Deserialize, Debug)]
357pub struct TxResult {
358    /// The [`Transaction`]'s [`Txid`].
359    pub txid: Txid,
360    /// The [`Wtxid`] of a different [`Transaction`] with the same [`Txid`],
361    /// but different Witness found in the mempool.
362    ///
363    /// If `Some`, means the submitted [`Transaction`] was ignored.
364    #[serde(rename = "other-wtxid")]
365    pub other_wtxid: Option<Wtxid>,
366    /// `sigops`-adjusted transaction size, in virtual bytes.
367    pub vsize: Option<u32>,
368    /// The effective fee paid by the [`Transaction`].
369    pub fees: Option<MempoolFeesSubmitPackage>,
370    /// The [`Transaction`] submission error string.
371    pub error: Option<String>,
372}
373
374/// The fees for a [`Transaction`] submitted as part of a package.
375#[derive(Deserialize, Debug)]
376pub struct MempoolFeesSubmitPackage {
377    /// The base fee paid by the [`Transaction`].
378    #[serde(with = "bitcoin::amount::serde::as_btc")]
379    pub base: Amount,
380    /// The effective feerate paid by this [`Transaction`].
381    ///
382    /// Is `None` if the transaction was already in the mempool.
383    #[serde(
384        rename = "effective-feerate",
385        default,
386        deserialize_with = "deserialize_feerate"
387    )]
388    pub effective_feerate: Option<FeeRate>,
389    /// If [`Self::effective_feerate`] is provided, holds the
390    /// [`Wtxid`]s of the transactions whose fees and virtual
391    /// sizes are included in effective-feerate.
392    #[serde(rename = "effective-includes")]
393    pub effective_includes: Option<Vec<Wtxid>>,
394}
395
396impl EsploraTx {
397    /// Convert this [`EsploraTx`] into a [`Transaction`].
398    ///
399    /// This will drop the Esplora-specific metadata (fee, weight, confirmation status)
400    /// and reconstructs the [`Transaction`] from its inputs and outputs.
401    pub fn to_tx(&self) -> Transaction {
402        Transaction {
403            version: self.version,
404            lock_time: self.locktime,
405            input: self
406                .vin
407                .iter()
408                .cloned()
409                .map(|vin| TxIn {
410                    previous_output: OutPoint {
411                        txid: vin.txid,
412                        vout: vin.vout,
413                    },
414                    script_sig: vin.scriptsig,
415                    sequence: vin.sequence,
416                    witness: vin.witness,
417                })
418                .collect(),
419            output: self
420                .vout
421                .iter()
422                .cloned()
423                .map(|vout| TxOut {
424                    value: vout.value,
425                    script_pubkey: vout.scriptpubkey,
426                })
427                .collect(),
428        }
429    }
430
431    /// Get the confirmation time of this [`EsploraTx`].
432    ///
433    /// If the transaction is confirmed, returns its [`BlockTime`] containing
434    /// confirmation height and UNIX timestamp. If not, returns `None`.
435    pub fn confirmation_time(&self) -> Option<BlockTime> {
436        match self.status {
437            TxStatus {
438                confirmed: true,
439                block_height: Some(height),
440                block_time: Some(timestamp),
441                ..
442            } => Some(BlockTime { timestamp, height }),
443            _ => None,
444        }
445    }
446
447    /// Get the previous [`TxOut`]s spent by this transaction's inputs.
448    ///
449    /// Returns one [`Option<TxOut>`] per input, in order.
450    /// `None` if the input spends a coinbase output.
451    pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
452        self.vin
453            .iter()
454            .cloned()
455            .map(|vin| {
456                vin.prevout.map(|prevout| TxOut {
457                    script_pubkey: prevout.scriptpubkey,
458                    value: prevout.value,
459                })
460            })
461            .collect()
462    }
463}
464
465impl From<EsploraTx> for Transaction {
466    fn from(tx: EsploraTx) -> Self {
467        tx.to_tx()
468    }
469}
470
471impl From<&EsploraTx> for Transaction {
472    fn from(tx: &EsploraTx) -> Self {
473        tx.to_tx()
474    }
475}
476
477/// Deserializes an [`Address`] from an Esplora address string.
478///
479/// Esplora returns address strings without separately providing the expected
480/// network, so this deserializer parses the address and assumes the embedded
481/// network marker is correct.
482fn deserialize_address_assume_checked<'de, D>(d: D) -> Result<Address, D::Error>
483where
484    D: serde::de::Deserializer<'de>,
485{
486    let address = Address::<bitcoin::address::NetworkUnchecked>::deserialize(d)?;
487    Ok(address.assume_checked())
488}
489
490/// Deserializes an optional [`FeeRate`] from an `f64` BTC/kvB value.
491///
492/// The Esplora API expresses effective feerates as BTC per kilovirtual-byte.
493/// This deserializer converts it to sat/kwu as required by [`FeeRate`].
494///
495/// Returns `None` if the value is absent, and an error if the resulting
496/// feerate would overflow.
497fn deserialize_feerate<'de, D>(d: D) -> Result<Option<FeeRate>, D::Error>
498where
499    D: serde::de::Deserializer<'de>,
500{
501    use serde::de::Error;
502
503    let btc_per_kvb = match Option::<f64>::deserialize(d)? {
504        Some(v) => v,
505        None => return Ok(None),
506    };
507    let sat_per_kwu = btc_per_kvb * 25_000_000.0;
508    if sat_per_kwu.is_infinite() {
509        return Err(D::Error::custom("feerate overflow"));
510    }
511    Ok(Some(FeeRate::from_sat_per_kwu(sat_per_kwu as u64)))
512}
513
514/// Deserializes a mempool fee histogram from `(sat/vB, vsize)` entries.
515///
516/// The Esplora API expresses fee histogram buckets as feerates in satoshis per
517/// virtual byte paired with each bucket's virtual size. This deserializer
518/// converts each feerate to sat/kwu as required by [`FeeRate`].
519fn deserialize_fee_histogram<'de, D>(d: D) -> Result<Vec<(FeeRate, Weight)>, D::Error>
520where
521    D: serde::de::Deserializer<'de>,
522{
523    use serde::de::Error;
524    let raw = Vec::<(f64, Weight)>::deserialize(d)?;
525    raw.into_iter()
526        .map(|(sat_per_vb, vsize)| {
527            let sat_per_kwu = sat_per_vb * 250.0;
528            if !sat_per_kwu.is_finite() {
529                return Err(D::Error::custom("feerate overflow"));
530            }
531            Ok((FeeRate::from_sat_per_kwu(sat_per_kwu as u64), vsize))
532        })
533        .collect()
534}