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}