waterfalls_client/
blocking.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//! Waterfalls by way of `minreq` HTTP client.
13
14use std::collections::HashMap;
15use std::convert::TryFrom;
16use std::str::FromStr;
17use std::thread;
18
19#[allow(unused_imports)]
20use log::{debug, error, info, trace};
21
22use minreq::{Proxy, Request, Response};
23
24use bitcoin::consensus::{deserialize, serialize, Decodable};
25use bitcoin::hex::{DisplayHex, FromHex};
26use bitcoin::Address;
27use bitcoin::{block::Header as BlockHeader, BlockHash, Transaction, Txid};
28
29use crate::{Builder, Error, WaterfallResponse, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES};
30
31#[derive(Debug, Clone)]
32pub struct BlockingClient {
33    /// The URL of the Waterfalls server.
34    url: String,
35    /// The proxy is ignored when targeting `wasm32`.
36    pub proxy: Option<String>,
37    /// Socket timeout.
38    pub timeout: Option<u64>,
39    /// HTTP headers to set on every request made to Waterfalls server
40    pub headers: HashMap<String, String>,
41    /// Number of times to retry a request
42    pub max_retries: usize,
43}
44
45impl BlockingClient {
46    /// Build a blocking client from a [`Builder`]
47    pub fn from_builder(builder: Builder) -> Self {
48        Self {
49            url: builder.base_url,
50            proxy: builder.proxy,
51            timeout: builder.timeout,
52            headers: builder.headers,
53            max_retries: builder.max_retries,
54        }
55    }
56
57    /// Get the underlying base URL.
58    pub fn url(&self) -> &str {
59        &self.url
60    }
61
62    /// Perform a raw HTTP GET request with the given URI `path`.
63    pub fn get_request(&self, path: &str) -> Result<Request, Error> {
64        let mut request = minreq::get(format!("{}{}", self.url, path));
65
66        if let Some(proxy) = &self.proxy {
67            let proxy = Proxy::new(proxy.as_str())?;
68            request = request.with_proxy(proxy);
69        }
70
71        if let Some(timeout) = &self.timeout {
72            request = request.with_timeout(*timeout);
73        }
74
75        if !self.headers.is_empty() {
76            for (key, value) in &self.headers {
77                request = request.with_header(key, value);
78            }
79        }
80
81        Ok(request)
82    }
83
84    fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
85        match self.get_with_retry(path) {
86            Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
87            Ok(resp) if !is_status_ok(resp.status_code) => {
88                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
89                let message = resp.as_str().unwrap_or_default().to_string();
90                Err(Error::HttpResponse { status, message })
91            }
92            Ok(resp) => Ok(Some(deserialize::<T>(resp.as_bytes())?)),
93            Err(e) => Err(e),
94        }
95    }
96
97    fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
98        match self.get_with_retry(path) {
99            Ok(resp) if !is_status_ok(resp.status_code) => {
100                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
101                let message = resp.as_str().unwrap_or_default().to_string();
102                Err(Error::HttpResponse { status, message })
103            }
104            Ok(resp) => {
105                let hex_str = resp.as_str().map_err(Error::Minreq)?;
106                let hex_vec = Vec::from_hex(hex_str).unwrap();
107                deserialize::<T>(&hex_vec).map_err(Error::BitcoinEncoding)
108            }
109            Err(e) => Err(e),
110        }
111    }
112
113    fn get_response_json_with_query<T: serde::de::DeserializeOwned>(
114        &self,
115        path: &str,
116        query_params: &[(&str, &str)],
117    ) -> Result<T, Error> {
118        let mut url = format!("{}{}", self.url, path);
119        if !query_params.is_empty() {
120            url.push('?');
121            for (i, (key, value)) in query_params.iter().enumerate() {
122                if i > 0 {
123                    url.push('&');
124                }
125                // URL encode the key and value to handle special characters
126                let encoded_key = urlencoding::encode(key);
127                let encoded_value = urlencoding::encode(value);
128                url.push_str(&format!("{encoded_key}={encoded_value}"));
129            }
130        }
131
132        let mut request = minreq::get(&url);
133
134        if let Some(proxy) = &self.proxy {
135            let proxy = Proxy::new(proxy.as_str())?;
136            request = request.with_proxy(proxy);
137        }
138
139        if let Some(timeout) = &self.timeout {
140            request = request.with_timeout(*timeout);
141        }
142
143        if !self.headers.is_empty() {
144            for (key, value) in &self.headers {
145                request = request.with_header(key, value);
146            }
147        }
148
149        match request.send() {
150            Ok(resp) if !is_status_ok(resp.status_code) => {
151                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
152                let message = resp.as_str().unwrap_or_default().to_string();
153                Err(Error::HttpResponse { status, message })
154            }
155            Ok(resp) => Ok(resp.json::<T>()?),
156            Err(e) => Err(Error::Minreq(e)),
157        }
158    }
159
160    fn get_response_str(&self, path: &str) -> Result<String, Error> {
161        match self.get_with_retry(path) {
162            Ok(resp) if !is_status_ok(resp.status_code) => {
163                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
164                let message = resp.as_str().unwrap_or_default().to_string();
165                Err(Error::HttpResponse { status, message })
166            }
167            Ok(resp) => Ok(resp.as_str()?.to_string()),
168            Err(e) => Err(e),
169        }
170    }
171
172    /// Get a [`Transaction`] option given its [`Txid`]
173    pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
174        self.get_opt_response(&format!("/tx/{txid}/raw"))
175    }
176
177    /// Get a [`Transaction`] given its [`Txid`].
178    pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
179        match self.get_tx(txid) {
180            Ok(Some(tx)) => Ok(tx),
181            Ok(None) => Err(Error::TransactionNotFound(*txid)),
182            Err(e) => Err(e),
183        }
184    }
185
186    /// Query the waterfalls endpoint with a descriptor
187    pub fn waterfalls(&self, descriptor: &str) -> Result<WaterfallResponse, Error> {
188        let path = "/v4/waterfalls";
189        self.get_response_json_with_query(path, &[("descriptor", descriptor)])
190    }
191
192    /// Query the waterfalls endpoint with addresses
193    pub fn waterfalls_addresses(&self, addresses: &[Address]) -> Result<WaterfallResponse, Error> {
194        let addresses_str = addresses
195            .iter()
196            .map(|a| a.to_string())
197            .collect::<Vec<String>>()
198            .join(",");
199        let path = "/v4/waterfalls";
200        self.get_response_json_with_query(path, &[("addresses", &addresses_str)])
201    }
202
203    /// Query waterfalls with version-specific parameters
204    pub fn waterfalls_version(
205        &self,
206        descriptor: &str,
207        version: u8,
208        page: Option<u32>,
209        to_index: Option<u32>,
210        utxo_only: bool,
211    ) -> Result<WaterfallResponse, Error> {
212        let path = format!("/v{version}/waterfalls");
213        let mut query_params = vec![
214            ("descriptor", descriptor.to_string()),
215            ("utxo_only", utxo_only.to_string()),
216        ];
217
218        if let Some(page) = page {
219            query_params.push(("page", page.to_string()));
220        }
221        if let Some(to_index) = to_index {
222            query_params.push(("to_index", to_index.to_string()));
223        }
224
225        let query_refs: Vec<(&str, &str)> =
226            query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
227        self.get_response_json_with_query(&path, &query_refs)
228    }
229
230    /// Get a [`BlockHeader`] given a particular block hash.
231    pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
232        self.get_response_hex(&format!("/block/{block_hash}/header"))
233    }
234
235    /// Get the server's public key for encryption
236    pub fn server_recipient(&self) -> Result<String, Error> {
237        self.get_response_str("/v1/server_recipient")
238    }
239
240    /// Get the server's address for message signing verification
241    pub fn server_address(&self) -> Result<String, Error> {
242        self.get_response_str("/v1/server_address")
243    }
244
245    /// Get time since last block with freshness indicator
246    pub fn time_since_last_block(&self) -> Result<String, Error> {
247        self.get_response_str("/v1/time_since_last_block")
248    }
249
250    /// Broadcast a [`Transaction`] to Waterfalls
251    pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
252        let mut request = minreq::post(format!("{}/tx", self.url)).with_body(
253            serialize(transaction)
254                .to_lower_hex_string()
255                .as_bytes()
256                .to_vec(),
257        );
258
259        if let Some(proxy) = &self.proxy {
260            let proxy = Proxy::new(proxy.as_str())?;
261            request = request.with_proxy(proxy);
262        }
263
264        if let Some(timeout) = &self.timeout {
265            request = request.with_timeout(*timeout);
266        }
267
268        match request.send() {
269            Ok(resp) if !is_status_ok(resp.status_code) => {
270                let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
271                let message = resp.as_str().unwrap_or_default().to_string();
272                Err(Error::HttpResponse { status, message })
273            }
274            Ok(_resp) => Ok(()),
275            Err(e) => Err(Error::Minreq(e)),
276        }
277    }
278
279    /// Get the [`BlockHash`] of the current blockchain tip.
280    pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
281        self.get_response_str("/blocks/tip/hash")
282            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
283    }
284
285    /// Get the [`BlockHash`] of a specific block height
286    pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
287        self.get_response_str(&format!("/block-height/{block_height}"))
288            .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
289    }
290
291    /// Get transaction history for the specified address in Esplora-compatible format
292    pub fn get_address_txs(&self, address: &Address) -> Result<String, Error> {
293        let path = format!("/address/{address}/txs");
294        self.get_response_str(&path)
295    }
296
297    /// Sends a GET request to the given `url`, retrying failed attempts
298    /// for retryable error codes until max retries hit.
299    fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
300        let mut delay = BASE_BACKOFF_MILLIS;
301        let mut attempts = 0;
302
303        loop {
304            match self.get_request(url)?.send()? {
305                resp if attempts < self.max_retries && is_status_retryable(resp.status_code) => {
306                    thread::sleep(delay);
307                    attempts += 1;
308                    delay *= 2;
309                }
310                resp => return Ok(resp),
311            }
312        }
313    }
314}
315
316fn is_status_ok(status: i32) -> bool {
317    status == 200
318}
319
320fn is_status_not_found(status: i32) -> bool {
321    status == 404
322}
323
324fn is_status_retryable(status: i32) -> bool {
325    let status = status as u16;
326    RETRYABLE_ERROR_CODES.contains(&status)
327}