Skip to main content

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}