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}