Skip to content

Commit 9305e41

Browse files
committed
Merge #143: feat: implement missing methods
cb0f920 refactor(api): rename BlockInformation -> BlockInfo (valued mammal) e4d9e4b docs: update rustdoc links (valued mammal) 5d8bcf8 chore(deps): pin `tracing` to 0.1.41, pin `log` to 0.4.28 for MSRV (Luis Schwab) eed3bdb feat(test): add unified test for mempool methods (Luis Schwab) cd1cbf6 feat(client): add `get_scripthash_utxos` method (Luis Schwab) 758e157 feat(client): add `get_block_txs` method (Luis Schwab) 29df7f3 feat(client): add `get_block_txids` method (Luis Schwab) b1930eb feat(client): add `get_block_info` method (Luis Schwab) d623d46 feat(client): add `get_mempool_txids` method (Luis Schwab) 1043e01 feat(client): add `get_mempool_recent_txs` method (Luis Schwab) 68aadc4 feat(client): add `get_mempool_stats` method (Luis Schwab) ab4fdde feat(client): add `get_mempool_scripthash_txs` method (Luis Schwab) b24fe96 feat(client): add `get_mempool_address_txs` method (Luis Schwab) d2c1128 feat(client): add `get_scripthash_stats` method (Luis Schwab) 35f6a04 feat(client): add `get_tx_outspends` method (Luis Schwab) a5af9f8 feat(api): add `MempoolStats` and `MempoolRecentTx` structs (Luis Schwab) c910ab0 feat(api): add `ScriptHashStats` and `ScriptHashTxsSummary` structs (Luis Schwab) 1bb149e feat(api): add `BlockInformation` struct (Luis Schwab) 49b8276 chore(cargo): add `Bitcoin Dev Kit Developers` to `authors` field (Luis Schwab) Pull request description: This PR implements the missing methods needed to be feature complete with the [Esplora API specification](https://github.com/Blockstream/esplora/blob/master/API.md). Also adds `Bitcoin Dev Kit Developers` to `authors` on `Cargo.toml`, pins tracing to 0.1.41, and log to 0.4.28 for the 1.63.0 MSRV. ## Changelog - Add `OutputSpendStatus` struct - Add `ScriptHashTxsSummary` and `ScriptHashStats` structs - Add `BlockInformation` struct - Add `MempoolStats` struct - Add `MempoolRecentTxs` struct - Add `InvalidStartIndexValue` error variant - Add `get_tx_outspends` method (`GET /tx/:txid/outspends`) - Add `get_scripthash_stats` method (`GET /scripthash/:hash`) - Add `get_mempool_address_txs` method (`GET /address/:address/txs/mempool`) - Add `get_mempool_scripthash_txs` method (`GET /scripthash/:hash/txs/mempool`) - Add `get_scripthash_utxos` method (`GET /scripthash/:hash/utxo`) - Add `get_block_info` method (`GET /block/:hash`) - Add `get_block_txids` method (`GET /block/:hash/txids`) - Add `get_block_txs` method (`GET /block/:hash/txs[/:start_index]`) - Add `get_mempool_stats` method (`GET /mempool`) - Add `get_mempool_txids` method (`GET /mempool/txids`) - Add `get_mempool_recent_txids` method (`GET /mempool/recent`) ACKs for top commit: ValuedMammal: ACK cb0f920 Tree-SHA512: 08e8452cf1100b33b07120ec736f0ccd1d05687b55cdbc7a2ee93229ad32d18fb4d62942208f6dadf85eca73790f9739382d32716492020ccfee6af803165ea1
2 parents 81d1636 + cb0f920 commit 9305e41

File tree

7 files changed

+634
-24
lines changed

7 files changed

+634
-24
lines changed

.github/workflows/cont_integration.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ jobs:
6060
cargo update -p ring --precise "0.17.12"
6161
cargo update -p flate2 --precise "1.0.35"
6262
cargo update -p once_cell --precise "1.20.3"
63+
cargo update -p tracing --precise "0.1.41"
6364
cargo update -p tracing-core --precise "0.1.33"
6465
cargo update -p parking_lot --precise "0.12.3"
6566
cargo update -p parking_lot_core --precise "0.9.10"
@@ -70,6 +71,7 @@ jobs:
7071
cargo update -p openssl-sys --precise "0.9.109"
7172
cargo update -p syn --precise "2.0.106"
7273
cargo update -p quote --precise "1.0.41"
74+
cargo update -p log --precise "0.4.28"
7375
7476
cargo update -p bzip2-sys --precise "0.1.12+1.0.8" # dev-dependency
7577
- name: Build

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
name = "esplora-client"
33
version = "0.12.1"
44
edition = "2021"
5-
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
5+
authors = [
6+
"Alekos Filini <alekos.filini@gmail.com>",
7+
"Bitcoin Dev Kit Developers"
8+
]
69
license = "MIT"
710
homepage = "https://github.com/bitcoindevkit/rust-esplora-client"
811
repository = "https://github.com/bitcoindevkit/rust-esplora-client"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ cargo update -p native-tls --precise "0.2.13"
2929
cargo update -p ring --precise "0.17.12"
3030
cargo update -p flate2 --precise "1.0.35"
3131
cargo update -p once_cell --precise "1.20.3"
32+
cargo update -p tracing --precise "0.1.41"
3233
cargo update -p tracing-core --precise "0.1.33"
3334
cargo update -p parking_lot --precise "0.12.3"
3435
cargo update -p parking_lot_core --precise "0.9.10"
@@ -39,4 +40,5 @@ cargo update -p openssl --precise "0.10.73"
3940
cargo update -p openssl-sys --precise "0.9.109"
4041
cargo update -p syn --precise "2.0.106"
4142
cargo update -p quote --precise "1.0.41"
43+
cargo update -p log --precise "0.4.28"
4244
```

src/api.rs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
//!
33
//! See: <https://github.com/Blockstream/esplora/blob/master/API.md>
44
5+
use bitcoin::hash_types;
6+
use serde::Deserialize;
7+
58
pub use bitcoin::consensus::{deserialize, serialize};
69
pub use bitcoin::hex::FromHex;
7-
use bitcoin::Weight;
810
pub use bitcoin::{
9-
transaction, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness,
11+
absolute, block, transaction, Amount, Block, BlockHash, CompactTarget, OutPoint, Script,
12+
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, Weight, Witness,
1013
};
1114

12-
use serde::Deserialize;
13-
1415
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
1516
pub struct PrevOut {
1617
pub value: u64,
@@ -87,6 +88,59 @@ pub struct BlockTime {
8788
pub height: u32,
8889
}
8990

91+
/// Information about a bitcoin [`Block`].
92+
#[derive(Debug, Clone, Deserialize)]
93+
pub struct BlockInfo {
94+
/// The block's [`BlockHash`].
95+
pub id: BlockHash,
96+
/// The block's height.
97+
pub height: u32,
98+
/// The block's version.
99+
pub version: block::Version,
100+
/// The block's timestamp.
101+
pub timestamp: u64,
102+
/// The block's transaction count.
103+
pub tx_count: u64,
104+
/// The block's size, in bytes.
105+
pub size: usize,
106+
/// The block's weight.
107+
pub weight: u64,
108+
/// The merkle root of the transactions in the block.
109+
pub merkle_root: hash_types::TxMerkleNode,
110+
/// The [`BlockHash`] of the previous block (`None` for the genesis block).
111+
pub previousblockhash: Option<BlockHash>,
112+
/// The block's MTP (Median Time Past).
113+
pub mediantime: u64,
114+
/// The block's nonce value.
115+
pub nonce: u32,
116+
/// The block's `bits` value as a [`CompactTarget`].
117+
pub bits: CompactTarget,
118+
/// The block's difficulty target value.
119+
pub difficulty: f64,
120+
}
121+
122+
impl PartialEq for BlockInfo {
123+
fn eq(&self, other: &Self) -> bool {
124+
let Self { difficulty: d1, .. } = self;
125+
let Self { difficulty: d2, .. } = other;
126+
127+
self.id == other.id
128+
&& self.height == other.height
129+
&& self.version == other.version
130+
&& self.timestamp == other.timestamp
131+
&& self.tx_count == other.tx_count
132+
&& self.size == other.size
133+
&& self.weight == other.weight
134+
&& self.merkle_root == other.merkle_root
135+
&& self.previousblockhash == other.previousblockhash
136+
&& self.mediantime == other.mediantime
137+
&& self.nonce == other.nonce
138+
&& self.bits == other.bits
139+
&& ((d1.is_nan() && d2.is_nan()) || (d1 == d2))
140+
}
141+
}
142+
impl Eq for BlockInfo {}
143+
90144
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
91145
pub struct BlockSummary {
92146
pub id: BlockHash,
@@ -123,6 +177,18 @@ pub struct AddressTxsSummary {
123177
pub tx_count: u32,
124178
}
125179

180+
/// Statistics about a particular [`Script`] hash's confirmed and mempool transactions.
181+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
182+
pub struct ScriptHashStats {
183+
/// The summary of confirmed transactions for this [`Script`] hash.
184+
pub chain_stats: ScriptHashTxsSummary,
185+
/// The summary of mempool transactions for this [`Script`] hash.
186+
pub mempool_stats: ScriptHashTxsSummary,
187+
}
188+
189+
/// Contains a summary of the transactions for a particular [`Script`] hash.
190+
pub type ScriptHashTxsSummary = AddressTxsSummary;
191+
126192
/// Information about an UTXO's status: confirmation status,
127193
/// confirmation height, confirmation block hash and confirmation block time.
128194
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
@@ -150,6 +216,36 @@ pub struct Utxo {
150216
pub value: Amount,
151217
}
152218

219+
/// Statistics about the mempool.
220+
#[derive(Clone, Debug, PartialEq, Deserialize)]
221+
pub struct MempoolStats {
222+
/// The number of transactions in the mempool.
223+
pub count: usize,
224+
/// The total size of mempool transactions in virtual bytes.
225+
pub vsize: usize,
226+
/// The total fee paid by mempool transactions, in sats.
227+
pub total_fee: u64,
228+
/// The mempool's fee rate distribution histogram.
229+
///
230+
/// An array of `(feerate, vsize)` tuples, where each entry's `vsize` is the total vsize
231+
/// of transactions paying more than `feerate` but less than the previous entry's `feerate`
232+
/// (except for the first entry, which has no upper bound).
233+
pub fee_histogram: Vec<(f64, usize)>,
234+
}
235+
236+
/// A [`Transaction`] that recently entered the mempool.
237+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
238+
pub struct MempoolRecentTx {
239+
/// Transaction ID as a [`Txid`].
240+
pub txid: Txid,
241+
/// [`Amount`] of fees paid by the transaction, in satoshis.
242+
pub fee: u64,
243+
/// The transaction size, in virtual bytes.
244+
pub vsize: usize,
245+
/// Combined [`Amount`] of the transaction, in satoshis.
246+
pub value: u64,
247+
}
248+
153249
impl Tx {
154250
pub fn to_tx(&self) -> Transaction {
155251
Transaction {

src/async.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@ use std::collections::HashMap;
1515
use std::marker::PhantomData;
1616
use std::str::FromStr;
1717

18+
use bitcoin::block::Header as BlockHeader;
1819
use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
1920
use bitcoin::hashes::{sha256, Hash};
2021
use bitcoin::hex::{DisplayHex, FromHex};
21-
use bitcoin::Address;
22-
use bitcoin::{
23-
block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid,
24-
};
22+
use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid};
2523

2624
#[allow(unused_imports)]
2725
use log::{debug, error, info, trace};
2826

2927
use reqwest::{header, Client, Response};
3028

31-
use crate::api::AddressStats;
3229
use crate::{
33-
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, Utxo,
30+
AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
31+
MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, Tx, TxStatus, Utxo,
3432
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
3533
};
3634

@@ -315,6 +313,12 @@ impl<S: Sleeper> AsyncClient<S> {
315313
self.get_opt_response_json(&format!("/tx/{txid}")).await
316314
}
317315

316+
/// Get the spend status of a [`Transaction`]'s outputs, given it's [`Txid`].
317+
pub async fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
318+
self.get_response_json(&format!("/tx/{txid}/outspends"))
319+
.await
320+
}
321+
318322
/// Get a [`BlockHeader`] given a particular block hash.
319323
pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
320324
self.get_response_hex(&format!("/block/{block_hash}/header"))
@@ -391,7 +395,14 @@ impl<S: Sleeper> AsyncClient<S> {
391395
self.get_response_json(&path).await
392396
}
393397

394-
/// Get transaction history for the specified address/scripthash, sorted with newest first.
398+
/// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions.
399+
pub async fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
400+
let script_hash = sha256::Hash::hash(script.as_bytes());
401+
let path = format!("/scripthash/{script_hash}");
402+
self.get_response_json(&path).await
403+
}
404+
405+
/// Get transaction history for the specified address, sorted with newest first.
395406
///
396407
/// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
397408
/// More can be requested by specifying the last txid seen by the previous query.
@@ -408,7 +419,14 @@ impl<S: Sleeper> AsyncClient<S> {
408419
self.get_response_json(&path).await
409420
}
410421

411-
/// Get confirmed transaction history for the specified address/scripthash,
422+
/// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first.
423+
pub async fn get_mempool_address_txs(&self, address: &Address) -> Result<Vec<Tx>, Error> {
424+
let path = format!("/address/{address}/txs/mempool");
425+
426+
self.get_response_json(&path).await
427+
}
428+
429+
/// Get transaction history for the specified address/scripthash,
412430
/// sorted with newest first. Returns 25 transactions per page.
413431
/// More can be requested by specifying the last txid seen by the previous
414432
/// query.
@@ -426,12 +444,70 @@ impl<S: Sleeper> AsyncClient<S> {
426444
self.get_response_json(&path).await
427445
}
428446

447+
/// Get mempool [`Transaction`] history for the
448+
/// specified [`Script`] hash, sorted with newest first.
449+
pub async fn get_mempool_scripthash_txs(&self, script: &Script) -> Result<Vec<Tx>, Error> {
450+
let script_hash = sha256::Hash::hash(script.as_bytes());
451+
let path = format!("/scripthash/{script_hash:x}/txs/mempool");
452+
453+
self.get_response_json(&path).await
454+
}
455+
456+
/// Get statistics about the mempool.
457+
pub async fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
458+
self.get_response_json("/mempool").await
459+
}
460+
461+
// Get a list of the last 10 [`Transaction`]s to enter the mempool.
462+
pub async fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
463+
self.get_response_json("/mempool/recent").await
464+
}
465+
466+
/// Get the full list of [`Txid`]s in the mempool.
467+
///
468+
/// The order of the [`Txid`]s is arbitrary.
469+
pub async fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
470+
self.get_response_json("/mempool/txids").await
471+
}
472+
429473
/// Get an map where the key is the confirmation target (in number of
430474
/// blocks) and the value is the estimated feerate (in sat/vB).
431475
pub async fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, Error> {
432476
self.get_response_json("/fee-estimates").await
433477
}
434478

479+
/// Get a summary about a [`Block`], given it's [`BlockHash`].
480+
pub async fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
481+
let path = format!("/block/{blockhash}");
482+
483+
self.get_response_json(&path).await
484+
}
485+
486+
/// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
487+
pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
488+
let path = format!("/block/{blockhash}/txids");
489+
490+
self.get_response_json(&path).await
491+
}
492+
493+
/// Get up to 25 [`Transaction`]s from a [`Block`], given it's [`BlockHash`],
494+
/// beginning at `start_index` (starts from 0 if `start_index` is `None`).
495+
///
496+
/// The `start_index` value MUST be a multiple of 25,
497+
/// else an error will be returned by Esplora.
498+
pub async fn get_block_txs(
499+
&self,
500+
blockhash: &BlockHash,
501+
start_index: Option<u32>,
502+
) -> Result<Vec<Tx>, Error> {
503+
let path = match start_index {
504+
None => format!("/block/{blockhash}/txs"),
505+
Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"),
506+
};
507+
508+
self.get_response_json(&path).await
509+
}
510+
435511
/// Gets some recent block summaries starting at the tip or at `height` if
436512
/// provided.
437513
///
@@ -451,8 +527,17 @@ impl<S: Sleeper> AsyncClient<S> {
451527

452528
/// Get all UTXOs locked to an address.
453529
pub async fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
454-
self.get_response_json(&format!("/address/{address}/utxo"))
455-
.await
530+
let path = format!("/address/{address}/utxo");
531+
532+
self.get_response_json(&path).await
533+
}
534+
535+
/// Get all [`Utxo`]s locked to a [`Script`].
536+
pub async fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
537+
let script_hash = sha256::Hash::hash(script.as_bytes());
538+
let path = format!("/scripthash/{script_hash}/utxo");
539+
540+
self.get_response_json(&path).await
456541
}
457542

458543
/// Get the underlying base URL.

0 commit comments

Comments
 (0)