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