esplora_client/
async.rs

1// Bitcoin Dev Kit
2// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3//
4// Copyright (c) 2020-2021 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 `reqwest` HTTP client.
13
14use std::collections::HashMap;
15use std::marker::PhantomData;
16use std::str::FromStr;
17
18use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
19use bitcoin::hashes::{sha256, Hash};
20use bitcoin::hex::{DisplayHex, FromHex};
21use bitcoin::Address;
22use bitcoin::{
23    block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid,
24};
25
26#[allow(unused_imports)]
27use log::{debug, error, info, trace};
28
29use reqwest::{header, Client, Response};
30
31use crate::api::AddressStats;
32use crate::{
33    BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
34    BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
35};
36
37#[derive(Debug, Clone)]
38pub struct AsyncClient<S = DefaultSleeper> {
39    /// The URL of the Esplora Server.
40    url: String,
41    /// The inner [`reqwest::Client`] to make HTTP requests.
42    client: Client,
43    /// Number of times to retry a request
44    max_retries: usize,
45
46    /// Marker for the type of sleeper used
47    marker: PhantomData<S>,
48}
49
50impl<S: Sleeper> AsyncClient<S> {
51    /// Build an async client from a builder
52    pub fn from_builder(builder: Builder) -> Result<Self, Error> {
53        let mut client_builder = Client::builder();
54
55        #[cfg(not(target_arch = "wasm32"))]
56        if let Some(proxy) = &builder.proxy {
57            client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?);
58        }
59
60        #[cfg(not(target_arch = "wasm32"))]
61        if let Some(timeout) = builder.timeout {
62            client_builder = client_builder.timeout(core::time::Duration::from_secs(timeout));
63        }
64
65        if !builder.headers.is_empty() {
66            let mut headers = header::HeaderMap::new();
67            for (k, v) in builder.headers {
68                let header_name = header::HeaderName::from_lowercase(k.to_lowercase().as_bytes())
69                    .map_err(|_| Error::InvalidHttpHeaderName(k))?;
70                let header_value = header::HeaderValue::from_str(&v)
71                    .map_err(|_| Error::InvalidHttpHeaderValue(v))?;
72                headers.insert(header_name, header_value);
73            }
74            client_builder = client_builder.default_headers(headers);
75        }
76
77        Ok(AsyncClient {
78            url: builder.base_url,
79            client: client_builder.build()?,
80            max_retries: builder.max_retries,
81            marker: PhantomData,
82        })
83    }
84
85    pub fn from_client(url: String, client: Client) -> Self {
86        AsyncClient {
87            url,
88            client,
89            max_retries: crate::DEFAULT_MAX_RETRIES,
90            marker: PhantomData,
91        }
92    }
93
94    /// Make an HTTP GET request to given URL, deserializing to any `T` that
95    /// implement [`bitcoin::consensus::Decodable`].
96    ///
97    /// It should be used when requesting Esplora endpoints that can be directly
98    /// deserialized to native `rust-bitcoin` types, which implements
99    /// [`bitcoin::consensus::Decodable`] from `&[u8]`.
100    ///
101    /// # Errors
102    ///
103    /// This function will return an error either from the HTTP client, or the
104    /// [`bitcoin::consensus::Decodable`] deserialization.
105    async fn get_response<T: Decodable>(&self, path: &str) -> Result<T, Error> {
106        let url = format!("{}{}", self.url, path);
107        let response = self.get_with_retry(&url).await?;
108
109        if !response.status().is_success() {
110            return Err(Error::HttpResponse {
111                status: response.status().as_u16(),
112                message: response.text().await?,
113            });
114        }
115
116        Ok(deserialize::<T>(&response.bytes().await?)?)
117    }
118
119    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
120    ///
121    /// It uses [`AsyncEsploraClient::get_response`] internally.
122    ///
123    /// See [`AsyncEsploraClient::get_response`] above for full documentation.
124    async fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
125        match self.get_response::<T>(path).await {
126            Ok(res) => Ok(Some(res)),
127            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
128            Err(e) => Err(e),
129        }
130    }
131
132    /// Make an HTTP GET request to given URL, deserializing to any `T` that
133    /// implements [`serde::de::DeserializeOwned`].
134    ///
135    /// It should be used when requesting Esplora endpoints that have a specific
136    /// defined API, mostly defined in [`crate::api`].
137    ///
138    /// # Errors
139    ///
140    /// This function will return an error either from the HTTP client, or the
141    /// [`serde::de::DeserializeOwned`] deserialization.
142    async fn get_response_json<T: serde::de::DeserializeOwned>(
143        &self,
144        path: &str,
145    ) -> Result<T, Error> {
146        let url = format!("{}{}", self.url, path);
147        let response = self.get_with_retry(&url).await?;
148
149        if !response.status().is_success() {
150            return Err(Error::HttpResponse {
151                status: response.status().as_u16(),
152                message: response.text().await?,
153            });
154        }
155
156        response.json::<T>().await.map_err(Error::Reqwest)
157    }
158
159    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
160    ///
161    /// It uses [`AsyncEsploraClient::get_response_json`] internally.
162    ///
163    /// See [`AsyncEsploraClient::get_response_json`] above for full
164    /// documentation.
165    async fn get_opt_response_json<T: serde::de::DeserializeOwned>(
166        &self,
167        url: &str,
168    ) -> Result<Option<T>, Error> {
169        match self.get_response_json(url).await {
170            Ok(res) => Ok(Some(res)),
171            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
172            Err(e) => Err(e),
173        }
174    }
175
176    /// Make an HTTP GET request to given URL, deserializing to any `T` that
177    /// implements [`bitcoin::consensus::Decodable`].
178    ///
179    /// It should be used when requesting Esplora endpoints that are expected
180    /// to return a hex string decodable to native `rust-bitcoin` types which
181    /// implement [`bitcoin::consensus::Decodable`] from `&[u8]`.
182    ///
183    /// # Errors
184    ///
185    /// This function will return an error either from the HTTP client, or the
186    /// [`bitcoin::consensus::Decodable`] deserialization.
187    async fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
188        let url = format!("{}{}", self.url, path);
189        let response = self.get_with_retry(&url).await?;
190
191        if !response.status().is_success() {
192            return Err(Error::HttpResponse {
193                status: response.status().as_u16(),
194                message: response.text().await?,
195            });
196        }
197
198        let hex_str = response.text().await?;
199        Ok(deserialize(&Vec::from_hex(&hex_str)?)?)
200    }
201
202    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
203    ///
204    /// It uses [`AsyncEsploraClient::get_response_hex`] internally.
205    ///
206    /// See [`AsyncEsploraClient::get_response_hex`] above for full
207    /// documentation.
208    async fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
209        match self.get_response_hex(path).await {
210            Ok(res) => Ok(Some(res)),
211            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
212            Err(e) => Err(e),
213        }
214    }
215
216    /// Make an HTTP GET request to given URL, deserializing to `String`.
217    ///
218    /// It should be used when requesting Esplora endpoints that can return
219    /// `String` formatted data that can be parsed downstream.
220    ///
221    /// # Errors
222    ///
223    /// This function will return an error either from the HTTP client.
224    async fn get_response_text(&self, path: &str) -> Result<String, Error> {
225        let url = format!("{}{}", self.url, path);
226        let response = self.get_with_retry(&url).await?;
227
228        if !response.status().is_success() {
229            return Err(Error::HttpResponse {
230                status: response.status().as_u16(),
231                message: response.text().await?,
232            });
233        }
234
235        Ok(response.text().await?)
236    }
237
238    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
239    ///
240    /// It uses [`AsyncEsploraClient::get_response_text`] internally.
241    ///
242    /// See [`AsyncEsploraClient::get_response_text`] above for full
243    /// documentation.
244    async fn get_opt_response_text(&self, path: &str) -> Result<Option<String>, Error> {
245        match self.get_response_text(path).await {
246            Ok(s) => Ok(Some(s)),
247            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
248            Err(e) => Err(e),
249        }
250    }
251
252    /// Make an HTTP POST request to given URL, serializing from any `T` that
253    /// implement [`bitcoin::consensus::Encodable`].
254    ///
255    /// It should be used when requesting Esplora endpoints that expected a
256    /// native bitcoin type serialized with [`bitcoin::consensus::Encodable`].
257    ///
258    /// # Errors
259    ///
260    /// This function will return an error either from the HTTP client, or the
261    /// [`bitcoin::consensus::Encodable`] serialization.
262    async fn post_request_hex<T: Encodable>(&self, path: &str, body: T) -> Result<(), Error> {
263        let url = format!("{}{}", self.url, path);
264        let body = serialize::<T>(&body).to_lower_hex_string();
265
266        let response = self.client.post(url).body(body).send().await?;
267
268        if !response.status().is_success() {
269            return Err(Error::HttpResponse {
270                status: response.status().as_u16(),
271                message: response.text().await?,
272            });
273        }
274
275        Ok(())
276    }
277
278    /// Get a [`Transaction`] option given its [`Txid`]
279    pub async fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
280        self.get_opt_response(&format!("/tx/{txid}/raw")).await
281    }
282
283    /// Get a [`Transaction`] given its [`Txid`].
284    pub async fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
285        match self.get_tx(txid).await {
286            Ok(Some(tx)) => Ok(tx),
287            Ok(None) => Err(Error::TransactionNotFound(*txid)),
288            Err(e) => Err(e),
289        }
290    }
291
292    /// Get a [`Txid`] of a transaction given its index in a block with a given
293    /// hash.
294    pub async fn get_txid_at_block_index(
295        &self,
296        block_hash: &BlockHash,
297        index: usize,
298    ) -> Result<Option<Txid>, Error> {
299        match self
300            .get_opt_response_text(&format!("/block/{block_hash}/txid/{index}"))
301            .await?
302        {
303            Some(s) => Ok(Some(Txid::from_str(&s).map_err(Error::HexToArray)?)),
304            None => Ok(None),
305        }
306    }
307
308    /// Get the status of a [`Transaction`] given its [`Txid`].
309    pub async fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
310        self.get_response_json(&format!("/tx/{txid}/status")).await
311    }
312
313    /// Get transaction info given it's [`Txid`].
314    pub async fn get_tx_info(&self, txid: &Txid) -> Result<Option<Tx>, Error> {
315        self.get_opt_response_json(&format!("/tx/{txid}")).await
316    }
317
318    /// Get a [`BlockHeader`] given a particular block hash.
319    pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
320        self.get_response_hex(&format!("/block/{block_hash}/header"))
321            .await
322    }
323
324    /// Get the [`BlockStatus`] given a particular [`BlockHash`].
325    pub async fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
326        self.get_response_json(&format!("/block/{block_hash}/status"))
327            .await
328    }
329
330    /// Get a [`Block`] given a particular [`BlockHash`].
331    pub async fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
332        self.get_opt_response(&format!("/block/{block_hash}/raw"))
333            .await
334    }
335
336    /// Get a merkle inclusion proof for a [`Transaction`] with the given
337    /// [`Txid`].
338    pub async fn get_merkle_proof(&self, tx_hash: &Txid) -> Result<Option<MerkleProof>, Error> {
339        self.get_opt_response_json(&format!("/tx/{tx_hash}/merkle-proof"))
340            .await
341    }
342
343    /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the
344    /// given [`Txid`].
345    pub async fn get_merkle_block(&self, tx_hash: &Txid) -> Result<Option<MerkleBlock>, Error> {
346        self.get_opt_response_hex(&format!("/tx/{tx_hash}/merkleblock-proof"))
347            .await
348    }
349
350    /// Get the spending status of an output given a [`Txid`] and the output
351    /// index.
352    pub async fn get_output_status(
353        &self,
354        txid: &Txid,
355        index: u64,
356    ) -> Result<Option<OutputStatus>, Error> {
357        self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
358            .await
359    }
360
361    /// Broadcast a [`Transaction`] to Esplora
362    pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
363        self.post_request_hex("/tx", transaction).await
364    }
365
366    /// Get the current height of the blockchain tip
367    pub async fn get_height(&self) -> Result<u32, Error> {
368        self.get_response_text("/blocks/tip/height")
369            .await
370            .map(|height| u32::from_str(&height).map_err(Error::Parsing))?
371    }
372
373    /// Get the [`BlockHash`] of the current blockchain tip.
374    pub async fn get_tip_hash(&self) -> Result<BlockHash, Error> {
375        self.get_response_text("/blocks/tip/hash")
376            .await
377            .map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
378    }
379
380    /// Get the [`BlockHash`] of a specific block height
381    pub async fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
382        self.get_response_text(&format!("/block-height/{block_height}"))
383            .await
384            .map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
385    }
386
387    /// Get information about a specific address, includes confirmed balance and transactions in
388    /// the mempool.
389    pub async fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
390        let path = format!("/address/{address}");
391        self.get_response_json(&path).await
392    }
393
394    /// Get transaction history for the specified address/scripthash, sorted with newest first.
395    ///
396    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
397    /// More can be requested by specifying the last txid seen by the previous query.
398    pub async fn get_address_txs(
399        &self,
400        address: &Address,
401        last_seen: Option<Txid>,
402    ) -> Result<Vec<Tx>, Error> {
403        let path = match last_seen {
404            Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
405            None => format!("/address/{address}/txs"),
406        };
407
408        self.get_response_json(&path).await
409    }
410
411    /// Get confirmed transaction history for the specified address/scripthash,
412    /// sorted with newest first. Returns 25 transactions per page.
413    /// More can be requested by specifying the last txid seen by the previous
414    /// query.
415    pub async fn scripthash_txs(
416        &self,
417        script: &Script,
418        last_seen: Option<Txid>,
419    ) -> Result<Vec<Tx>, Error> {
420        let script_hash = sha256::Hash::hash(script.as_bytes());
421        let path = match last_seen {
422            Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
423            None => format!("/scripthash/{script_hash:x}/txs"),
424        };
425
426        self.get_response_json(&path).await
427    }
428
429    /// Get an map where the key is the confirmation target (in number of
430    /// blocks) and the value is the estimated feerate (in sat/vB).
431    pub async fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, Error> {
432        self.get_response_json("/fee-estimates").await
433    }
434
435    /// Gets some recent block summaries starting at the tip or at `height` if
436    /// provided.
437    ///
438    /// The maximum number of summaries returned depends on the backend itself:
439    /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
440    pub async fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
441        let path = match height {
442            Some(height) => format!("/blocks/{height}"),
443            None => "/blocks".to_string(),
444        };
445        let blocks: Vec<BlockSummary> = self.get_response_json(&path).await?;
446        if blocks.is_empty() {
447            return Err(Error::InvalidResponse);
448        }
449        Ok(blocks)
450    }
451
452    /// Get the underlying base URL.
453    pub fn url(&self) -> &str {
454        &self.url
455    }
456
457    /// Get the underlying [`Client`].
458    pub fn client(&self) -> &Client {
459        &self.client
460    }
461
462    /// Sends a GET request to the given `url`, retrying failed attempts
463    /// for retryable error codes until max retries hit.
464    async fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
465        let mut delay = BASE_BACKOFF_MILLIS;
466        let mut attempts = 0;
467
468        loop {
469            match self.client.get(url).send().await? {
470                resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
471                    S::sleep(delay).await;
472                    attempts += 1;
473                    delay *= 2;
474                }
475                resp => return Ok(resp),
476            }
477        }
478    }
479}
480
481fn is_status_retryable(status: reqwest::StatusCode) -> bool {
482    RETRYABLE_ERROR_CODES.contains(&status.as_u16())
483}
484
485pub trait Sleeper: 'static {
486    type Sleep: std::future::Future<Output = ()>;
487    fn sleep(dur: std::time::Duration) -> Self::Sleep;
488}
489
490#[derive(Debug, Clone, Copy)]
491pub struct DefaultSleeper;
492
493#[cfg(any(test, feature = "tokio"))]
494impl Sleeper for DefaultSleeper {
495    type Sleep = tokio::time::Sleep;
496
497    fn sleep(dur: std::time::Duration) -> Self::Sleep {
498        tokio::time::sleep(dur)
499    }
500}