esplora/
lib.rs

1//! An extensible blocking/async Esplora client
2//!
3//! This library provides an extensible blocking and
4//! async Esplora client to query Esplora's backend.
5//!
6//! The library provides the possibility to build a blocking
7//! client using [`minreq`] and an async client using [`reqwest`].
8//! The library supports communicating to Esplora via a proxy
9//! and also using TLS (SSL) for secure communication.
10//!
11//!
12//! ## Usage
13//!
14//! You can create a blocking client as follows:
15//!
16//! ```no_run
17//! # #[cfg(feature = "blocking")]
18//! # {
19//! use esplora::Builder;
20//! let builder = Builder::new("https://blockstream.info/testnet/api");
21//! let blocking_client = builder.build_blocking();
22//! # Ok::<(), esplora::Error>(());
23//! # }
24//! ```
25//!
26//! Here is an example of how to create an asynchronous client.
27//!
28//! ```no_run
29//! # #[cfg(all(feature = "async", feature = "tokio"))]
30//! # {
31//! use esplora::Builder;
32//! let builder = Builder::new("https://blockstream.info/testnet/api");
33//! let async_client = builder.build_async();
34//! # Ok::<(), esplora::Error>(());
35//! # }
36//! ```
37//!
38//! ## Features
39//!
40//! By default the library enables all features. To specify
41//! specific features, set `default-features` to `false` in your `Cargo.toml`
42//! and specify the features you want. This will look like this:
43//!
44//! `bp-esplora = { version = "*", default-features = false, features = ["blocking"] }`
45//!
46//! * `blocking` enables [`minreq`], the blocking client with proxy.
47//! * `blocking-https` enables [`minreq`], the blocking client with proxy and TLS (SSL) capabilities
48//!   using the default [`minreq`] backend.
49//! * `blocking-https-rustls` enables [`minreq`], the blocking client with proxy and TLS (SSL)
50//!   capabilities using the `rustls` backend.
51//! * `blocking-https-native` enables [`minreq`], the blocking client with proxy and TLS (SSL)
52//!   capabilities using the platform's native TLS backend (likely OpenSSL).
53//! * `blocking-https-bundled` enables [`minreq`], the blocking client with proxy and TLS (SSL)
54//!   capabilities using a bundled OpenSSL library backend.
55//! * `blocking-wasm` enables [`minreq`], the blocking client without proxy.
56//! * `tokio` enables [`tokio`], the default async runtime.
57//! * `async` enables [`reqwest`], the async client with proxy capabilities.
58//! * `async-https` enables [`reqwest`], the async client with support for proxying and TLS (SSL)
59//!   using the default [`reqwest`] TLS backend.
60//! * `async-https-native` enables [`reqwest`], the async client with support for proxying and TLS
61//!   (SSL) using the platform's native TLS backend (likely OpenSSL).
62//! * `async-https-rustls` enables [`reqwest`], the async client with support for proxying and TLS
63//!   (SSL) using the `rustls` TLS backend.
64//! * `async-https-rustls-manual-roots` enables [`reqwest`], the async client with support for
65//!   proxying and TLS (SSL) using the `rustls` TLS backend without using its the default root
66//!   certificates.
67
68#![allow(clippy::result_large_err)]
69
70#[macro_use]
71extern crate amplify;
72#[macro_use]
73extern crate serde_with;
74
75use std::collections::HashMap;
76use std::num::TryFromIntError;
77use std::time::Duration;
78
79use amplify::hex;
80use bp::Txid;
81
82#[cfg(feature = "async")]
83pub use r#async::Sleeper;
84
85pub mod api;
86#[cfg(feature = "async")]
87pub mod r#async;
88#[cfg(feature = "blocking")]
89pub mod blocking;
90
91pub use api::*;
92#[cfg(feature = "blocking")]
93pub use blocking::BlockingClient;
94#[cfg(feature = "async")]
95pub use r#async::AsyncClient;
96
97/// Response status codes for which the request may be retried.
98const RETRYABLE_ERROR_CODES: [u16; 3] = [
99    429, // TOO_MANY_REQUESTS
100    500, // INTERNAL_SERVER_ERROR
101    503, // SERVICE_UNAVAILABLE
102];
103
104/// Base backoff in milliseconds.
105const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256);
106
107/// Default max retries.
108const DEFAULT_MAX_RETRIES: usize = 6;
109
110/// Get a fee value in sats/vbytes from the estimates
111/// that matches the confirmation target set as parameter.
112///
113/// Returns `None` if no feerate estimate is found at or below `target`
114/// confirmations.
115pub fn convert_fee_rate(target: usize, estimates: HashMap<u16, f64>) -> Option<f32> {
116    estimates
117        .into_iter()
118        .filter(|(k, _)| *k as usize <= target)
119        .max_by_key(|(k, _)| *k)
120        .map(|(_, v)| v as f32)
121}
122
123#[derive(Debug, Clone)]
124pub struct Config {
125    /// Optional URL of the proxy to use to make requests to the Esplora server
126    ///
127    /// The string should be formatted as: `<protocol>://<user>:<password>@host:<port>`.
128    ///
129    /// Note that the format of this value and the supported protocols change slightly between the
130    /// blocking version of the client (using `ureq`) and the async version (using `reqwest`). For more
131    /// details check with the documentation of the two crates. Both of them are compiled with
132    /// the `socks` feature enabled.
133    ///
134    /// The proxy is ignored when targeting `wasm32`.
135    pub proxy: Option<String>,
136    /// Socket timeout.
137    pub timeout: Option<u64>,
138    /// Number of times to retry a request.
139    pub max_retries: usize,
140    /// HTTP headers to set on every request made to Esplora server.
141    pub headers: HashMap<String, String>,
142}
143
144impl Default for Config {
145    fn default() -> Self {
146        Config {
147            proxy: None,
148            timeout: Some(30),
149            headers: HashMap::new(),
150            max_retries: DEFAULT_MAX_RETRIES,
151        }
152    }
153}
154
155#[derive(Debug, Clone)]
156pub struct Builder {
157    /// The URL of the Esplora server.
158    pub base_url: String,
159    /// Optional URL of the proxy to use to make requests to the Esplora server
160    ///
161    /// The string should be formatted as:
162    /// `<protocol>://<user>:<password>@host:<port>`.
163    ///
164    /// Note that the format of this value and the supported protocols change
165    /// slightly between the blocking version of the client (using `minreq`)
166    /// and the async version (using `reqwest`). For more details check with
167    /// the documentation of the two crates. Both of them are compiled with
168    /// the `socks` feature enabled.
169    ///
170    /// The proxy is ignored when targeting `wasm32`.
171    pub proxy: Option<String>,
172    /// Socket timeout.
173    pub timeout: Option<u64>,
174    /// HTTP headers to set on every request made to Esplora server.
175    pub headers: HashMap<String, String>,
176    /// Max retries
177    pub max_retries: usize,
178}
179
180impl Builder {
181    /// Instantiate a new builder
182    pub fn new(base_url: &str) -> Self {
183        Builder {
184            base_url: base_url.to_string(),
185            proxy: None,
186            timeout: None,
187            headers: HashMap::new(),
188            max_retries: DEFAULT_MAX_RETRIES,
189        }
190    }
191
192    /// Instantiate a builder from a URL and a config
193    pub fn from_config(base_url: &str, config: Config) -> Self {
194        Builder {
195            base_url: base_url.to_string(),
196            proxy: config.proxy,
197            timeout: config.timeout,
198            headers: config.headers,
199            max_retries: config.max_retries,
200        }
201    }
202
203    /// Set the proxy of the builder
204    pub fn proxy(mut self, proxy: &str) -> Self {
205        self.proxy = Some(proxy.to_string());
206        self
207    }
208
209    /// Set the timeout of the builder
210    pub fn timeout(mut self, timeout: u64) -> Self {
211        self.timeout = Some(timeout);
212        self
213    }
214
215    /// Add a header to set on each request
216    pub fn header(mut self, key: &str, value: &str) -> Self {
217        self.headers.insert(key.to_string(), value.to_string());
218        self
219    }
220
221    /// Set the maximum number of times to retry a request if the response status
222    /// is one of [`RETRYABLE_ERROR_CODES`].
223    pub fn max_retries(mut self, count: usize) -> Self {
224        self.max_retries = count;
225        self
226    }
227
228    /// Build a blocking client from builder
229    #[cfg(feature = "blocking")]
230    pub fn build_blocking(self) -> Result<BlockingClient, Error> {
231        BlockingClient::from_builder(self)
232    }
233
234    /// Build an asynchronous client from builder
235    #[cfg(all(feature = "async", feature = "tokio"))]
236    pub fn build_async(self) -> Result<AsyncClient, Error> {
237        AsyncClient::from_builder(self)
238    }
239
240    /// Build an asynchronous client from builder where the returned client uses a
241    /// user-defined [`Sleeper`].
242    #[cfg(feature = "async")]
243    pub fn build_async_with_sleeper<S: Sleeper>(self) -> Result<AsyncClient<S>, Error> {
244        AsyncClient::from_builder(self)
245    }
246}
247
248/// Errors that can happen during a request to `Esplora` servers.
249#[derive(Debug, Display, Error, From)]
250#[display(inner)]
251pub enum Error {
252    /// Error during `minreq` HTTP request
253    #[cfg(feature = "blocking")]
254    #[from]
255    Minreq(minreq::Error),
256
257    /// Error during reqwest HTTP request
258    #[cfg(feature = "async")]
259    #[from]
260    Reqwest(reqwest::Error),
261
262    /// HTTP response error {status}: {message}
263    #[display(doc_comments)]
264    HttpResponse { status: u16, message: String },
265
266    /// The server sent an invalid response
267    #[display(doc_comments)]
268    InvalidServerData,
269
270    /// Invalid number returned
271    #[from]
272    Parsing(std::num::ParseIntError),
273
274    /// Invalid status code, unable to convert to `u16`
275    #[display(doc_comments)]
276    StatusCode(TryFromIntError),
277
278    /// Invalid Bitcoin data returned
279    #[display(doc_comments)]
280    BitcoinEncoding,
281
282    /// Invalid hex data returned
283    #[from]
284    Hex(hex::Error),
285
286    /// Transaction {0} not found
287    #[display(doc_comments)]
288    TransactionNotFound(Txid),
289
290    /// Invalid HTTP Header name specified
291    #[display(doc_comments)]
292    InvalidHttpHeaderName(String),
293
294    /// Invalid HTTP Header value specified
295    #[display(doc_comments)]
296    InvalidHttpHeaderValue(String),
297}