Skip to main content

esplora_client/
blocking.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! # Blocking Esplora Client
4//!
5//! This module implements [`BlockingClient`], a synchronous HTTP client for
6//! interacting with an [Esplora] server by way of [`bitreq`].
7//!
8//! Use this client from synchronous applications, command-line tools, tests,
9//! or code paths where blocking the current thread is acceptable. Each method
10//! sends the request immediately and returns only after the response body has
11//! been read and decoded.
12//!
13//! The client is configured through [`Builder`], including the
14//! base URL, proxy, socket timeout, custom headers, and retry count.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! # fn example() -> Result<(), esplora_client::Error> {
20//!
21//! let client = esplora_client::Builder::new("https://mempool.space/api").build_blocking();
22//! let height = client.get_height()?;
23//!
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! [Esplora]: https://github.com/Blockstream/esplora/blob/master/API.md
29
30use std::collections::{HashMap, HashSet};
31use std::convert::TryFrom;
32use std::str::FromStr;
33use std::thread;
34use std::time::Duration;
35
36use bitcoin::consensus::encode::serialize_hex;
37use bitreq::{Method, Proxy, Request, Response};
38
39use bitcoin::block::Header as BlockHeader;
40use bitcoin::consensus::{deserialize, serialize, Decodable};
41use bitcoin::hashes::{sha256, Hash};
42use bitcoin::hex::{DisplayHex, FromHex};
43use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, MerkleBlock, Script, Transaction, Txid};
44
45use crate::{
46    duration_to_timeout_secs, is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats,
47    BlockInfo, BlockStatus, Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof,
48    OutputStatus, ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS,
49};
50
51#[allow(deprecated)]
52use crate::BlockSummary;
53
54/// A synchronous client for interacting with an Esplora API server.
55///
56/// Use [`Builder`] to construct an instance of this client. The client stores
57/// the server base URL and request configuration, then exposes convenience
58/// methods for the transaction, block, address, scripthash, fee-estimate, and
59/// mempool endpoints.
60///
61/// Each method blocks the current thread until the HTTP request completes and
62/// the response is parsed into the requested type.
63///
64/// # Retries
65///
66/// Failed requests are automatically retried up to `max_retries` times
67/// (configured via [`Builder`]) with exponential backoff, but only for
68/// retryable HTTP status codes. See [`crate::RETRYABLE_ERROR_CODES`] for the
69/// full list.
70#[derive(Debug, Clone)]
71pub struct BlockingClient {
72    /// The URL of the Esplora server.
73    url: String,
74    /// The URL of the proxy host.
75    ///
76    /// NOTE: The proxy is ignored when targeting `wasm32`.
77    pub proxy: Option<String>,
78    /// Per-request socket timeout.
79    pub timeout: Option<Duration>,
80    /// HTTP headers to set on every request made to the Esplora server.
81    pub headers: HashMap<String, String>,
82    /// Maximum number of retry attempts for retryable responses.
83    pub max_retries: usize,
84}
85
86impl BlockingClient {
87    /// Build a [`BlockingClient`] from a [`Builder`].
88    ///
89    /// This consumes the builder configuration and stores it on the client.
90    /// No network request is made until a client method is called.
91    pub fn from_builder(builder: Builder) -> Self {
92        Self {
93            url: builder.base_url,
94            proxy: builder.proxy,
95            timeout: builder.timeout,
96            headers: builder.headers,
97            max_retries: builder.max_retries,
98        }
99    }
100
101    /// Return the base URL of the Esplora server this client connects to.
102    ///
103    /// The returned value is the exact string provided to [`Builder::new`].
104    pub fn url(&self) -> &str {
105        &self.url
106    }
107
108    /// Build a HTTP [`Request`] with given [`Method`] and URI `path`.
109    ///
110    /// Configures the request with the proxy, timeout, and headers set on
111    /// this client. Used internally by all other request helper methods.
112    pub(crate) fn build_request(&self, method: Method, path: &str) -> Result<Request, Error> {
113        let mut request = Request::new(method, format!("{}{}", self.url, path));
114
115        if let Some(proxy) = &self.proxy {
116            request = request.with_proxy(Proxy::new_http(proxy)?);
117        }
118
119        if let Some(timeout) = self.timeout {
120            request = request.with_timeout(duration_to_timeout_secs(timeout));
121        }
122
123        if !self.headers.is_empty() {
124            request = request.with_headers(&self.headers);
125        }
126
127        Ok(request)
128    }
129
130    /// Make an HTTP POST request to `path` with `body`.
131    ///
132    /// Configures query parameters, if any, and returns the raw response after
133    /// checking the HTTP status code.
134    ///
135    /// # Errors
136    ///
137    /// This function will return an error either from the HTTP client, or the
138    /// response's [`serde_json`] deserialization.
139    pub fn post_request<T: Into<Vec<u8>>>(
140        &self,
141        path: &str,
142        body: T,
143        query_params: Option<HashSet<(&str, String)>>,
144    ) -> Result<Response, Error> {
145        let mut request = self.build_request(Method::Post, path)?.with_body(body);
146
147        for (key, value) in query_params.unwrap_or_default() {
148            request = request.with_param(key, value);
149        }
150
151        let response = request.send()?;
152
153        if !is_success(&response) {
154            let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
155            let message = response.as_str().unwrap_or_default().to_string();
156            return Err(Error::HttpResponse { status, message });
157        }
158
159        Ok(response)
160    }
161
162    /// Sends a GET request to `url`, retrying on retryable status codes
163    /// with exponential backoff until [`BlockingClient::max_retries`] is reached.
164    fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
165        let mut delay = BASE_BACKOFF_MILLIS;
166        let mut attempts = 0;
167
168        loop {
169            match self.build_request(Method::Get, url)?.send()? {
170                resp if attempts < self.max_retries && is_retryable(&resp) => {
171                    thread::sleep(delay);
172                    attempts += 1;
173                    delay *= 2;
174                }
175                resp => return Ok(resp),
176            }
177        }
178    }
179
180    /// Makes a GET request to `path`, deserializing the response body as
181    /// raw bytes into `T` using [`bitcoin::consensus::Decodable`].
182    ///
183    /// Use this for endpoints that return raw binary Bitcoin data.
184    ///
185    /// # Errors
186    ///
187    /// Returns an [`Error`] if the request fails or deserialization fails.
188    fn get_response<T: Decodable>(&self, path: &str) -> Result<T, Error> {
189        let response = self.get_with_retry(path)?;
190
191        if !is_success(&response) {
192            let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
193            let message = response.as_str().unwrap_or_default().to_string();
194            return Err(Error::HttpResponse { status, message });
195        }
196
197        Ok(deserialize::<T>(response.as_bytes())?)
198    }
199
200    /// Makes a GET request to `path`, returning `None` on a 404 response.
201    ///
202    /// Delegates to [`Self::get_response`]. See its documentation for details.
203    fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
204        match self.get_response(path) {
205            Ok(response) => Ok(Some(response)),
206            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
207            Err(e) => Err(e),
208        }
209    }
210
211    /// Makes a GET request to `path`, deserializing the hex-encoded response
212    /// body into `T` using [`bitcoin::consensus::Decodable`].
213    ///
214    /// Use this for endpoints that return hex-encoded Bitcoin data.
215    ///
216    /// # Errors
217    ///
218    /// Returns an [`Error`] if the request fails, hex decoding fails,
219    /// or consensus deserialization fails.
220    fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
221        let response = self.get_with_retry(path)?;
222
223        if !is_success(&response) {
224            let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
225            let message = response.as_str().unwrap_or_default().to_string();
226            return Err(Error::HttpResponse { status, message });
227        }
228
229        let hex_str = response.as_str()?;
230        deserialize(&Vec::from_hex(hex_str)?).map_err(Error::BitcoinEncoding)
231    }
232
233    /// Makes a GET request to `path`, returning `None` on a 404 response.
234    ///
235    /// Delegates to [`Self::get_response_hex`]. See its documentation for details.
236    fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
237        match self.get_response_hex(path) {
238            Ok(res) => Ok(Some(res)),
239            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
240            Err(e) => Err(e),
241        }
242    }
243
244    /// Makes a GET request to `path`, deserializing the response body as JSON
245    /// into `T` using [`serde::de::DeserializeOwned`].
246    ///
247    /// Use this for endpoints that return Esplora-specific JSON types, as
248    /// defined in [`crate::api`].
249    ///
250    /// # Errors
251    ///
252    /// Returns an [`Error`] if the request fails or JSON deserialization fails.
253    fn get_response_json<'a, T: serde::de::DeserializeOwned>(
254        &'a self,
255        path: &'a str,
256    ) -> Result<T, Error> {
257        let response = self.get_with_retry(path)?;
258
259        if !is_success(&response) {
260            let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
261            let message = response.as_str().unwrap_or_default().to_string();
262            return Err(Error::HttpResponse { status, message });
263        }
264
265        response.json::<T>().map_err(Error::BitReq)
266    }
267
268    /// Makes a GET request to `path`, returning `None` on a 404 response.
269    ///
270    /// Delegates to [`Self::get_response_json`]. See its documentation for details.
271    fn get_opt_response_json<T: serde::de::DeserializeOwned>(
272        &self,
273        path: &str,
274    ) -> Result<Option<T>, Error> {
275        match self.get_response_json(path) {
276            Ok(res) => Ok(Some(res)),
277            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
278            Err(e) => Err(e),
279        }
280    }
281
282    /// Makes a GET request to `path`, returning the response body as a [`String`].
283    ///
284    /// Use this for endpoints that return plain text data that needs further
285    /// parsing downstream.
286    ///
287    /// # Errors
288    ///
289    /// Returns an [`Error`] if the request fails.
290    fn get_response_text(&self, path: &str) -> Result<String, Error> {
291        let response = self.get_with_retry(path)?;
292
293        if !is_success(&response) {
294            let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
295            let message = response.as_str().unwrap_or_default().to_string();
296            return Err(Error::HttpResponse { status, message });
297        }
298
299        Ok(response.as_str()?.to_string())
300    }
301
302    /// Makes a GET request to `path`, returning `None` on a 404 response.
303    ///
304    /// Delegates to [`Self::get_response_text`]. See its documentation for details.
305    fn get_opt_response_text(&self, path: &str) -> Result<Option<String>, Error> {
306        match self.get_response_text(path) {
307            Ok(s) => Ok(Some(s)),
308            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
309            Err(e) => Err(e),
310        }
311    }
312
313    /// Get a raw [`Transaction`] given its [`Txid`].
314    ///
315    /// Returns `None` if the transaction is not found.
316    pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
317        self.get_opt_response(&format!("/tx/{txid}/raw"))
318    }
319
320    /// Get a raw [`Transaction`] given its [`Txid`].
321    ///
322    /// Returns an [`Error::TransactionNotFound`] if the transaction is not found.
323    /// Prefer [`Self::get_tx`] if you want to handle the not-found case explicitly.
324    pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
325        match self.get_tx(txid) {
326            Ok(Some(tx)) => Ok(tx),
327            Ok(None) => Err(Error::TransactionNotFound(*txid)),
328            Err(e) => Err(e),
329        }
330    }
331
332    /// Get the [`Txid`] of the transaction at position `index` within the
333    /// block identified by `block_hash`.
334    ///
335    /// Returns `None` if the block or index is not found.
336    pub fn get_txid_at_block_index(
337        &self,
338        block_hash: &BlockHash,
339        index: usize,
340    ) -> Result<Option<Txid>, Error> {
341        match self.get_opt_response_text(&format!("/block/{block_hash}/txid/{index}"))? {
342            Some(s) => Ok(Some(Txid::from_str(&s).map_err(Error::HexToArray)?)),
343            None => Ok(None),
344        }
345    }
346
347    /// Get the confirmation status of a [`Transaction`] given its [`Txid`].
348    ///
349    /// Returns a [`TxStatus`] containing whether the transaction is confirmed,
350    /// and if so, the block height, hash, and timestamp it was confirmed in.
351    pub fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
352        self.get_response_json(&format!("/tx/{txid}/status"))
353    }
354
355    /// Get an [`EsploraTx`] given its [`Txid`].
356    ///
357    /// Unlike [`Self::get_tx`], returns the Esplora-specific [`EsploraTx`]
358    /// type, which includes additional metadata such as confirmation status,
359    /// fee, and weight. Returns `None` if the transaction is not found.
360    pub fn get_tx_info(&self, txid: &Txid) -> Result<Option<EsploraTx>, Error> {
361        self.get_opt_response_json(&format!("/tx/{txid}"))
362    }
363
364    /// Get the spend status of all outputs in a [`Transaction`], given its [`Txid`].
365    ///
366    /// Returns a [`Vec`] of [`OutputStatus`], one per output, ordered as they
367    /// appear in the [`Transaction`].
368    pub fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
369        self.get_response_json(&format!("/tx/{txid}/outspends"))
370    }
371
372    /// Get the [`BlockHeader`] of a [`Block`] given its [`BlockHash`].
373    pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
374        self.get_response_hex(&format!("/block/{block_hash}/header"))
375    }
376
377    /// Get the [`BlockStatus`] of a [`Block`] given its [`BlockHash`].
378    ///
379    /// Returns a [`BlockStatus`] indicating whether this [`Block`] is part of
380    /// the best chain, its height, and the [`BlockHash`] of the next [`Block`],
381    /// if any.
382    pub fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
383        self.get_response_json(&format!("/block/{block_hash}/status"))
384    }
385
386    /// Get the full [`Block`] with the given [`BlockHash`].
387    ///
388    /// Returns `None` if the [`Block`] is not found.
389    pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
390        self.get_opt_response(&format!("/block/{block_hash}/raw"))
391    }
392
393    /// Get a Merkle inclusion proof for a [`Transaction`] given its [`Txid`].
394    ///
395    /// Returns a [`MerkleProof`] that can be used to verify the transaction's
396    /// inclusion in a block. Returns `None` if the transaction is not found or
397    /// is unconfirmed.
398    pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, Error> {
399        self.get_opt_response_json(&format!("/tx/{txid}/merkle-proof"))
400    }
401
402    /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] given its [`Txid`].
403    ///
404    /// Returns `None` if the transaction is not found or is unconfirmed.
405    pub fn get_merkle_block(&self, txid: &Txid) -> Result<Option<MerkleBlock>, Error> {
406        self.get_opt_response_hex(&format!("/tx/{txid}/merkleblock-proof"))
407    }
408
409    /// Get the spend status of a specific output, identified by its [`Txid`]
410    /// and output index.
411    ///
412    /// Returns an [`OutputStatus`] indicating whether the output has been
413    /// spent, and if so, by which transaction. Returns `None` if not found.
414    pub fn get_output_status(
415        &self,
416        txid: &Txid,
417        index: u64,
418    ) -> Result<Option<OutputStatus>, Error> {
419        self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
420    }
421
422    /// Broadcast a [`Transaction`] to the Esplora server.
423    ///
424    /// The transaction is serialized and sent as a hex-encoded string.
425    /// Returns the [`Txid`] of the broadcasted transaction.
426    ///
427    /// # Errors
428    ///
429    /// Returns an [`Error`] if the request fails or the server rejects the transaction.
430    pub fn broadcast(&self, transaction: &Transaction) -> Result<Txid, Error> {
431        let body = serialize::<Transaction>(transaction).to_lower_hex_string();
432
433        let response = self.post_request("/tx", body, None)?;
434        let txid = Txid::from_str(response.as_str()?).map_err(Error::HexToArray)?;
435
436        Ok(txid)
437    }
438
439    /// Broadcast a package of [`Transaction`]s to the Esplora server.
440    ///
441    /// Returns a [`SubmitPackageResult`] containing the result for each
442    /// transaction in the package, keyed by [`bitcoin::Wtxid`].
443    ///
444    /// Optionally, `maxfeerate` and `maxburnamount` can be provided to reject
445    /// transactions that exceed these thresholds.
446    ///
447    /// # Errors
448    ///
449    /// Returns an [`Error`] if the request fails or the server rejects the package.
450    pub fn submit_package(
451        &self,
452        transactions: &[Transaction],
453        maxfeerate: Option<FeeRate>,
454        maxburnamount: Option<Amount>,
455    ) -> Result<SubmitPackageResult, Error> {
456        let serialized_txs = transactions
457            .iter()
458            .map(|tx| serialize_hex(&tx))
459            .collect::<Vec<_>>();
460
461        let mut params = HashSet::<(&str, String)>::new();
462
463        // Esplora expects `maxfeerate` in sats/vB.
464        if let Some(maxfeerate) = maxfeerate {
465            params.insert(("maxfeerate", maxfeerate.to_sat_per_vb_ceil().to_string()));
466        }
467        // Esplora expects `maxburnamount` in BTC.
468        if let Some(maxburnamount) = maxburnamount {
469            params.insert(("maxburnamount", maxburnamount.to_btc().to_string()));
470        }
471
472        let response = self.post_request(
473            "/txs/package",
474            serde_json::to_string(&serialized_txs).map_err(Error::SerdeJson)?,
475            Some(params),
476        )?;
477
478        let result = response.json::<SubmitPackageResult>()?;
479
480        Ok(result)
481    }
482
483    /// Get the block height of the current blockchain tip.
484    pub fn get_height(&self) -> Result<u32, Error> {
485        self.get_response_text("/blocks/tip/height")
486            .map(|s| u32::from_str(s.as_str()).map_err(Error::Parsing))?
487    }
488
489    /// Get the [`BlockHash`] of the current blockchain tip.
490    pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
491        self.get_response_text("/blocks/tip/hash")
492            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
493    }
494
495    /// Get the [`BlockHash`] of a [`Block`] given its height.
496    pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
497        self.get_response_text(&format!("/block-height/{block_height}"))
498            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
499    }
500
501    /// Get global statistics about the mempool.
502    ///
503    /// Returns a [`MempoolStats`] containing the transaction count, total
504    /// virtual size, total fees, and fee rate histogram.
505    pub fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
506        self.get_response_json("/mempool")
507    }
508
509    /// Get the last 10 [`MempoolRecentTx`]s to enter the mempool.
510    pub fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
511        self.get_response_json("/mempool/recent")
512    }
513
514    /// Get the full list of [`Txid`]s currently in the mempool.
515    ///
516    /// The order of the returned [`Txid`]s is arbitrary.
517    pub fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
518        self.get_response_json("/mempool/txids")
519    }
520
521    /// Get fee estimates for a range of confirmation targets.
522    ///
523    /// Returns a [`HashMap`] where the key is the confirmation target in blocks
524    /// and the value is the estimated [`FeeRate`].
525    pub fn get_fee_estimates(&self) -> Result<HashMap<u16, FeeRate>, Error> {
526        let estimates_raw: HashMap<u16, f64> = self.get_response_json("/fee-estimates")?;
527        let estimates = sat_per_vbyte_to_feerate(estimates_raw);
528
529        Ok(estimates)
530    }
531
532    /// Get statistics about an [`Address`].
533    ///
534    /// Returns an [`AddressStats`] containing confirmed and mempool transaction
535    /// summaries for the given address, including funded and spent output
536    /// counts and their total values.
537    pub fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
538        let path = format!("/address/{address}");
539        self.get_response_json(&path)
540    }
541
542    /// Get statistics about a [`Script`] hash's confirmed and mempool transactions.
543    ///
544    /// Returns a [`ScriptHashStats`] containing transaction summaries for the
545    /// SHA256 hash of the given [`Script`].
546    pub fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
547        let script_hash = sha256::Hash::hash(script.as_bytes());
548        let path = format!("/scripthash/{script_hash}");
549        self.get_response_json(&path)
550    }
551
552    /// Get confirmed transaction history for an [`Address`], sorted newest first.
553    ///
554    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
555    /// To paginate, pass the [`Txid`] of the last transaction seen in the
556    /// previous response as `last_seen`.
557    pub fn get_address_txs(
558        &self,
559        address: &Address,
560        last_seen: Option<Txid>,
561    ) -> Result<Vec<EsploraTx>, Error> {
562        let path = match last_seen {
563            Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
564            None => format!("/address/{address}/txs"),
565        };
566
567        self.get_response_json(&path)
568    }
569
570    /// Get unconfirmed mempool [`EsploraTx`]s for an [`Address`], sorted newest first.
571    pub fn get_mempool_address_txs(&self, address: &Address) -> Result<Vec<EsploraTx>, Error> {
572        let path = format!("/address/{address}/txs/mempool");
573
574        self.get_response_json(&path)
575    }
576
577    /// Get confirmed transaction history for a [`Script`] hash, sorted newest first.
578    ///
579    /// Returns 25 transactions per page. To paginate, pass the [`Txid`] of the
580    /// last transaction seen in the previous response as `last_seen`.
581    pub fn get_scripthash_txs(
582        &self,
583        script: &Script,
584        last_seen: Option<Txid>,
585    ) -> Result<Vec<EsploraTx>, Error> {
586        let script_hash = sha256::Hash::hash(script.as_bytes());
587        let path = match last_seen {
588            Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
589            None => format!("/scripthash/{script_hash:x}/txs"),
590        };
591        self.get_response_json(&path)
592    }
593
594    /// Get unconfirmed mempool [`EsploraTx`]s for a [`Script`] hash, sorted newest first.
595    pub fn get_mempool_scripthash_txs(&self, script: &Script) -> Result<Vec<EsploraTx>, Error> {
596        let script_hash = sha256::Hash::hash(script.as_bytes());
597        let path = format!("/scripthash/{script_hash:x}/txs/mempool");
598
599        self.get_response_json(&path)
600    }
601
602    /// Get a [`BlockInfo`] summary for the [`Block`] with the given [`BlockHash`].
603    ///
604    /// [`BlockInfo`] includes metadata such as the height, timestamp,
605    /// [`Transaction`] count, size, and [`Weight`](bitcoin::Weight).
606    ///
607    /// This method does not return the full [`Block`].
608    pub fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
609        let path = format!("/block/{blockhash}");
610
611        self.get_response_json(&path)
612    }
613
614    /// Get all [`Txid`]s of [`Transaction`]s included in the [`Block`] with the
615    /// given [`BlockHash`].
616    pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
617        let path = format!("/block/{blockhash}/txids");
618
619        self.get_response_json(&path)
620    }
621
622    /// Get up to 25 [`EsploraTx`]s from the [`Block`] with the given
623    /// [`BlockHash`], starting at `start_index`.
624    ///
625    /// If `start_index` is `None`, starts from the first transaction (index 0).
626    ///
627    /// Note that `start_index` must be a multiple of 25, otherwise the server
628    /// will return an error.
629    pub fn get_block_txs(
630        &self,
631        blockhash: &BlockHash,
632        start_index: Option<u32>,
633    ) -> Result<Vec<EsploraTx>, Error> {
634        let path = match start_index {
635            None => format!("/block/{blockhash}/txs"),
636            Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"),
637        };
638
639        self.get_response_json(&path)
640    }
641
642    /// Get [`BlockInfo`] summaries for recent [`Block`]s.
643    ///
644    /// If `height` is `Some(h)`, returns blocks starting from height `h`.
645    /// If `height` is `None`, returns blocks starting from the current tip.
646    ///
647    /// The maximum number of summaries returned depends on the backend itself:
648    /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
649    #[allow(deprecated)]
650    #[deprecated(since = "0.13.0", note = "use `get_block_infos` instead")]
651    pub fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
652        let path = match height {
653            Some(height) => format!("/blocks/{height}"),
654            None => "/blocks".to_string(),
655        };
656        let blocks: Vec<BlockSummary> = self.get_response_json(&path)?;
657        if blocks.is_empty() {
658            return Err(Error::InvalidResponse);
659        }
660        Ok(blocks)
661    }
662
663    /// Get [`BlockInfo`] summaries for recent [`Block`]s.
664    ///
665    /// If `height` is `Some(h)`, returns blocks starting from height `h`.
666    /// If `height` is `None`, returns blocks starting from the current tip.
667    ///
668    /// The maximum number of summaries returned depends on the backend itself:
669    /// Esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
670    ///
671    /// # Errors
672    ///
673    /// Returns [`Error::InvalidResponse`] if the server returns an empty list.
674    ///
675    /// This method does not return the full [`Block`].
676    pub fn get_block_infos(&self, height: Option<u32>) -> Result<Vec<BlockInfo>, Error> {
677        let path = match height {
678            Some(height) => format!("/blocks/{height}"),
679            None => "/blocks".to_string(),
680        };
681        let blocks: Vec<BlockInfo> = self.get_response_json(&path)?;
682        if blocks.is_empty() {
683            return Err(Error::InvalidResponse);
684        }
685        Ok(blocks)
686    }
687
688    /// Get all confirmed [`Utxo`]s locked to the given [`Address`].
689    pub fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
690        let path = format!("/address/{address}/utxo");
691
692        self.get_response_json(&path)
693    }
694
695    /// Get all confirmed [`Utxo`]s locked to the given [`Script`].
696    pub fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
697        let script_hash = sha256::Hash::hash(script.as_bytes());
698        let path = format!("/scripthash/{script_hash}/utxo");
699
700        self.get_response_json(&path)
701    }
702}