esplora_client/
blocking.rs

1// Bitcoin Dev Kit
2// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3//
4// Copyright (c) 2020-2025 Bitcoin Dev Kit Developers
5//
6// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
7// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
9// You may not use this file except in accordance with one or both of these
10// licenses.
11
12//! Esplora by way of `minreq` HTTP client.
13
14use std::collections::HashMap;
15use std::convert::TryFrom;
16use std::str::FromStr;
17use std::thread;
18
19use bitcoin::consensus::encode::serialize_hex;
20#[allow(unused_imports)]
21use log::{debug, error, info, trace};
22
23use minreq::{Proxy, Request, Response};
24
25use bitcoin::block::Header as BlockHeader;
26use bitcoin::consensus::{deserialize, serialize, Decodable};
27use bitcoin::hashes::{sha256, Hash};
28use bitcoin::hex::{DisplayHex, FromHex};
29use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid};
30
31use crate::{
32    AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
33    MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus,
34    Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
35};
36
37/// A blocking client for interacting with an Esplora API server.
38#[derive(Debug, Clone)]
39pub struct BlockingClient {
40    /// The URL of the Esplora server.
41    url: String,
42    /// The proxy is ignored when targeting `wasm32`.
43    pub proxy: Option<String>,
44    /// Socket timeout.
45    pub timeout: Option<u64>,
46    /// HTTP headers to set on every request made to Esplora server
47    pub headers: HashMap<String, String>,
48    /// Number of times to retry a request
49    pub max_retries: usize,
50}
51
52impl BlockingClient {
53    /// Build a blocking client from a [`Builder`]
54    pub fn from_builder(builder: Builder) -> Self {
55        Self {
56            url: builder.base_url,
57            proxy: builder.proxy,
58            timeout: builder.timeout,
59            headers: builder.headers,
60            max_retries: builder.max_retries,
61        }
62    }
63
64    /// Get the underlying base URL.
65    pub fn url(&self) -> &str {
66        &self.url
67    }
68
69    /// Perform a raw HTTP GET request with the given URI `path`.
70    pub fn get_request(&self, path: &str) -> Result<Request, Error> {
71        let mut request = minreq::get(format!("{}{}", self.url, path));
72
73        if let Some(proxy) = &self.proxy {
74            let proxy = Proxy::new(proxy.as_str())?;
75            request = request.with_proxy(proxy);
76        }
77
78        if let Some(timeout) = &self.timeout {
79            request = request.with_timeout(*timeout);
80        }
81
82        if !self.headers.is_empty() {
83            for (key, value) in &self.headers {
84                request = request.with_header(key, value);
85            }
86        }
87
88        Ok(request)
89    }
90
91    fn post_request<T>(&self, path: &str, body: T) -> Result<Request, Error>
92    where
93        T: Into<Vec<u8>>,
94    {
95        let mut request = minreq::post(format!("{}{}", self.url, path)).with_body(body);
96
97        if let Some(proxy) = &self.proxy {
98            let proxy = Proxy::new(proxy.as_str())?;
99            request = request.with_proxy(proxy);
100        }
101
102        if let Some(timeout) = &self.timeout {
103            request = request.with_timeout(*timeout);
104        }
105
106        Ok(request)
107    }
108
109    fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
110        match self.get_with_retry(path) {
111            Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
112            Ok(resp) if !is_status_ok(resp.status_code) => {
113                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
114                let message = resp.as_str().unwrap_or_default().to_string();
115                Err(Error::HttpResponse { status, message })
116            }
117            Ok(resp) => Ok(Some(deserialize::<T>(resp.as_bytes())?)),
118            Err(e) => Err(e),
119        }
120    }
121
122    fn get_opt_response_txid(&self, path: &str) -> Result<Option<Txid>, Error> {
123        match self.get_with_retry(path) {
124            Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
125            Ok(resp) if !is_status_ok(resp.status_code) => {
126                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
127                let message = resp.as_str().unwrap_or_default().to_string();
128                Err(Error::HttpResponse { status, message })
129            }
130            Ok(resp) => Ok(Some(
131                Txid::from_str(resp.as_str().map_err(Error::Minreq)?).map_err(Error::HexToArray)?,
132            )),
133            Err(e) => Err(e),
134        }
135    }
136
137    fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
138        match self.get_with_retry(path) {
139            Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
140            Ok(resp) if !is_status_ok(resp.status_code) => {
141                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
142                let message = resp.as_str().unwrap_or_default().to_string();
143                Err(Error::HttpResponse { status, message })
144            }
145            Ok(resp) => {
146                let hex_str = resp.as_str().map_err(Error::Minreq)?;
147                let hex_vec = Vec::from_hex(hex_str)?;
148                deserialize::<T>(&hex_vec)
149                    .map_err(Error::BitcoinEncoding)
150                    .map(|r| Some(r))
151            }
152            Err(e) => Err(e),
153        }
154    }
155
156    fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
157        match self.get_with_retry(path) {
158            Ok(resp) if !is_status_ok(resp.status_code) => {
159                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
160                let message = resp.as_str().unwrap_or_default().to_string();
161                Err(Error::HttpResponse { status, message })
162            }
163            Ok(resp) => {
164                let hex_str = resp.as_str().map_err(Error::Minreq)?;
165                let hex_vec = Vec::from_hex(hex_str)?;
166                deserialize::<T>(&hex_vec).map_err(Error::BitcoinEncoding)
167            }
168            Err(e) => Err(e),
169        }
170    }
171
172    fn get_response_json<'a, T: serde::de::DeserializeOwned>(
173        &'a self,
174        path: &'a str,
175    ) -> Result<T, Error> {
176        let response = self.get_with_retry(path);
177        match response {
178            Ok(resp) if !is_status_ok(resp.status_code) => {
179                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
180                let message = resp.as_str().unwrap_or_default().to_string();
181                Err(Error::HttpResponse { status, message })
182            }
183            Ok(resp) => Ok(resp.json::<T>().map_err(Error::Minreq)?),
184            Err(e) => Err(e),
185        }
186    }
187
188    fn get_opt_response_json<T: serde::de::DeserializeOwned>(
189        &self,
190        path: &str,
191    ) -> Result<Option<T>, Error> {
192        match self.get_with_retry(path) {
193            Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
194            Ok(resp) if !is_status_ok(resp.status_code) => {
195                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
196                let message = resp.as_str().unwrap_or_default().to_string();
197                Err(Error::HttpResponse { status, message })
198            }
199            Ok(resp) => Ok(Some(resp.json::<T>()?)),
200            Err(e) => Err(e),
201        }
202    }
203
204    fn get_response_str(&self, path: &str) -> Result<String, Error> {
205        match self.get_with_retry(path) {
206            Ok(resp) if !is_status_ok(resp.status_code) => {
207                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
208                let message = resp.as_str().unwrap_or_default().to_string();
209                Err(Error::HttpResponse { status, message })
210            }
211            Ok(resp) => Ok(resp.as_str()?.to_string()),
212            Err(e) => Err(e),
213        }
214    }
215
216    /// Get a [`Transaction`] option given its [`Txid`]
217    pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
218        self.get_opt_response(&format!("/tx/{txid}/raw"))
219    }
220
221    /// Get a [`Transaction`] given its [`Txid`].
222    pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
223        match self.get_tx(txid) {
224            Ok(Some(tx)) => Ok(tx),
225            Ok(None) => Err(Error::TransactionNotFound(*txid)),
226            Err(e) => Err(e),
227        }
228    }
229
230    /// Get a [`Txid`] of a transaction given its index in a block with a given
231    /// hash.
232    pub fn get_txid_at_block_index(
233        &self,
234        block_hash: &BlockHash,
235        index: usize,
236    ) -> Result<Option<Txid>, Error> {
237        self.get_opt_response_txid(&format!("/block/{block_hash}/txid/{index}"))
238    }
239
240    /// Get the status of a [`Transaction`] given its [`Txid`].
241    pub fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
242        self.get_response_json(&format!("/tx/{txid}/status"))
243    }
244
245    /// Get transaction info given its [`Txid`].
246    pub fn get_tx_info(&self, txid: &Txid) -> Result<Option<Tx>, Error> {
247        self.get_opt_response_json(&format!("/tx/{txid}"))
248    }
249
250    /// Get the spend status of a [`Transaction`]'s outputs, given its [`Txid`].
251    pub fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
252        self.get_response_json(&format!("/tx/{txid}/outspends"))
253    }
254
255    /// Get a [`BlockHeader`] given a particular [`BlockHash`].
256    pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
257        self.get_response_hex(&format!("/block/{block_hash}/header"))
258    }
259
260    /// Get the [`BlockStatus`] given a particular [`BlockHash`].
261    pub fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
262        self.get_response_json(&format!("/block/{block_hash}/status"))
263    }
264
265    /// Get a [`Block`] given a particular [`BlockHash`].
266    pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
267        self.get_opt_response(&format!("/block/{block_hash}/raw"))
268    }
269
270    /// Get a merkle inclusion proof for a [`Transaction`] with the given
271    /// [`Txid`].
272    pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, Error> {
273        self.get_opt_response_json(&format!("/tx/{txid}/merkle-proof"))
274    }
275
276    /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the
277    /// given [`Txid`].
278    pub fn get_merkle_block(&self, txid: &Txid) -> Result<Option<MerkleBlock>, Error> {
279        self.get_opt_response_hex(&format!("/tx/{txid}/merkleblock-proof"))
280    }
281
282    /// Get the spending status of an output given a [`Txid`] and the output
283    /// index.
284    pub fn get_output_status(
285        &self,
286        txid: &Txid,
287        index: u64,
288    ) -> Result<Option<OutputStatus>, Error> {
289        self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
290    }
291
292    /// Broadcast a [`Transaction`] to Esplora.
293    pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
294        let request = self.post_request(
295            "/tx",
296            serialize(transaction)
297                .to_lower_hex_string()
298                .as_bytes()
299                .to_vec(),
300        )?;
301
302        match request.send() {
303            Ok(resp) if !is_status_ok(resp.status_code) => {
304                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
305                let message = resp.as_str().unwrap_or_default().to_string();
306                Err(Error::HttpResponse { status, message })
307            }
308            Ok(_resp) => Ok(()),
309            Err(e) => Err(Error::Minreq(e)),
310        }
311    }
312
313    /// Broadcast a package of [`Transaction`]s to Esplora.
314    ///
315    /// If `maxfeerate` is provided, any transaction whose
316    /// fee is higher will be rejected.
317    ///
318    /// If `maxburnamount` is provided, any transaction
319    /// with higher provably unspendable outputs amount
320    /// will be rejected.
321    pub fn submit_package(
322        &self,
323        transactions: &[Transaction],
324        maxfeerate: Option<f64>,
325        maxburnamount: Option<f64>,
326    ) -> Result<SubmitPackageResult, Error> {
327        let serialized_txs = transactions
328            .iter()
329            .map(|tx| serialize_hex(&tx))
330            .collect::<Vec<_>>();
331
332        let mut request = self.post_request(
333            "/txs/package",
334            serde_json::to_string(&serialized_txs)
335                .unwrap_or_default()
336                .into_bytes(),
337        )?;
338
339        if let Some(maxfeerate) = maxfeerate {
340            request = request.with_param("maxfeerate", maxfeerate.to_string())
341        }
342
343        if let Some(maxburnamount) = maxburnamount {
344            request = request.with_param("maxburnamount", maxburnamount.to_string())
345        }
346
347        match request.send() {
348            Ok(resp) if !is_status_ok(resp.status_code) => {
349                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
350                let message = resp.as_str().unwrap_or_default().to_string();
351                Err(Error::HttpResponse { status, message })
352            }
353            Ok(resp) => Ok(resp.json::<SubmitPackageResult>().map_err(Error::Minreq)?),
354            Err(e) => Err(Error::Minreq(e)),
355        }
356    }
357
358    /// Get the height of the current blockchain tip.
359    pub fn get_height(&self) -> Result<u32, Error> {
360        self.get_response_str("/blocks/tip/height")
361            .map(|s| u32::from_str(s.as_str()).map_err(Error::Parsing))?
362    }
363
364    /// Get the [`BlockHash`] of the current blockchain tip.
365    pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
366        self.get_response_str("/blocks/tip/hash")
367            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
368    }
369
370    /// Get the [`BlockHash`] of a specific block height
371    pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
372        self.get_response_str(&format!("/block-height/{block_height}"))
373            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
374    }
375
376    /// Get statistics about the mempool.
377    pub fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
378        self.get_response_json("/mempool")
379    }
380
381    /// Get a list of the last 10 [`Transaction`]s to enter the mempool.
382    pub fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
383        self.get_response_json("/mempool/recent")
384    }
385
386    /// Get the full list of [`Txid`]s in the mempool.
387    ///
388    /// The order of the txids is arbitrary and does not match bitcoind's.
389    pub fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
390        self.get_response_json("/mempool/txids")
391    }
392
393    /// Get a map where the key is the confirmation target (in number of
394    /// blocks) and the value is the estimated feerate (in sat/vB).
395    pub fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, Error> {
396        self.get_response_json("/fee-estimates")
397    }
398
399    /// Get information about a specific address, includes confirmed balance and transactions in
400    /// the mempool.
401    pub fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
402        let path = format!("/address/{address}");
403        self.get_response_json(&path)
404    }
405
406    /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions.
407    pub fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
408        let script_hash = sha256::Hash::hash(script.as_bytes());
409        let path = format!("/scripthash/{script_hash}");
410        self.get_response_json(&path)
411    }
412
413    /// Get transaction history for the specified address, sorted with newest
414    /// first.
415    ///
416    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
417    /// More can be requested by specifying the last txid seen by the previous query.
418    pub fn get_address_txs(
419        &self,
420        address: &Address,
421        last_seen: Option<Txid>,
422    ) -> Result<Vec<Tx>, Error> {
423        let path = match last_seen {
424            Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
425            None => format!("/address/{address}/txs"),
426        };
427
428        self.get_response_json(&path)
429    }
430
431    /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first.
432    pub fn get_mempool_address_txs(&self, address: &Address) -> Result<Vec<Tx>, Error> {
433        let path = format!("/address/{address}/txs/mempool");
434
435        self.get_response_json(&path)
436    }
437
438    /// Get transaction history for the specified scripthash,
439    /// sorted with newest first. Returns 25 transactions per page.
440    /// More can be requested by specifying the last txid seen by the previous
441    /// query.
442    pub fn scripthash_txs(
443        &self,
444        script: &Script,
445        last_seen: Option<Txid>,
446    ) -> Result<Vec<Tx>, Error> {
447        let script_hash = sha256::Hash::hash(script.as_bytes());
448        let path = match last_seen {
449            Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
450            None => format!("/scripthash/{script_hash:x}/txs"),
451        };
452        self.get_response_json(&path)
453    }
454
455    /// Get mempool [`Transaction`] history for the
456    /// specified [`Script`] hash, sorted with newest first.
457    pub fn get_mempool_scripthash_txs(&self, script: &Script) -> Result<Vec<Tx>, Error> {
458        let script_hash = sha256::Hash::hash(script.as_bytes());
459        let path = format!("/scripthash/{script_hash:x}/txs/mempool");
460
461        self.get_response_json(&path)
462    }
463
464    /// Get a summary about a [`Block`], given its [`BlockHash`].
465    pub fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
466        let path = format!("/block/{blockhash}");
467
468        self.get_response_json(&path)
469    }
470
471    /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
472    pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
473        let path = format!("/block/{blockhash}/txids");
474
475        self.get_response_json(&path)
476    }
477
478    /// Get up to 25 [`Transaction`]s from a [`Block`], given its [`BlockHash`],
479    /// beginning at `start_index` (starts from 0 if `start_index` is `None`).
480    ///
481    /// The `start_index` value MUST be a multiple of 25,
482    /// else an error will be returned by Esplora.
483    pub fn get_block_txs(
484        &self,
485        blockhash: &BlockHash,
486        start_index: Option<u32>,
487    ) -> Result<Vec<Tx>, Error> {
488        let path = match start_index {
489            None => format!("/block/{blockhash}/txs"),
490            Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"),
491        };
492
493        self.get_response_json(&path)
494    }
495
496    /// Gets some recent block summaries starting at the tip or at `height` if
497    /// provided.
498    ///
499    /// The maximum number of summaries returned depends on the backend itself:
500    /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
501    pub fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
502        let path = match height {
503            Some(height) => format!("/blocks/{height}"),
504            None => "/blocks".to_string(),
505        };
506        let blocks: Vec<BlockSummary> = self.get_response_json(&path)?;
507        if blocks.is_empty() {
508            return Err(Error::InvalidResponse);
509        }
510        Ok(blocks)
511    }
512
513    /// Get all UTXOs locked to an address.
514    pub fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
515        let path = format!("/address/{address}/utxo");
516
517        self.get_response_json(&path)
518    }
519
520    /// Get all [`Utxo`]s locked to a [`Script`].
521    pub fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
522        let script_hash = sha256::Hash::hash(script.as_bytes());
523        let path = format!("/scripthash/{script_hash}/utxo");
524
525        self.get_response_json(&path)
526    }
527
528    /// Sends a GET request to the given `url`, retrying failed attempts
529    /// for retryable error codes until max retries hit.
530    fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
531        let mut delay = BASE_BACKOFF_MILLIS;
532        let mut attempts = 0;
533
534        loop {
535            match self.get_request(url)?.send()? {
536                resp if attempts < self.max_retries && is_status_retryable(resp.status_code) => {
537                    thread::sleep(delay);
538                    attempts += 1;
539                    delay *= 2;
540                }
541                resp => return Ok(resp),
542            }
543        }
544    }
545}
546
547fn is_status_ok(status: i32) -> bool {
548    status == 200
549}
550
551fn is_status_not_found(status: i32) -> bool {
552    status == 404
553}
554
555fn is_status_retryable(status: i32) -> bool {
556    let status = status as u16;
557    RETRYABLE_ERROR_CODES.contains(&status)
558}