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}