esplora_client/
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_client::Builder;
20//! let builder = Builder::new("https://blockstream.info/testnet/api");
21//! let blocking_client = builder.build_blocking();
22//! # Ok::<(), esplora_client::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_client::Builder;
32//! let builder = Builder::new("https://blockstream.info/testnet/api");
33//! let async_client = builder.build_async();
34//! # Ok::<(), esplora_client::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//! `esplora-client = { version = "*", default-features = false, features =
45//! ["blocking"] }`
46//!
47//! * `blocking` enables [`minreq`], the blocking client with proxy.
48//! * `blocking-https` enables [`minreq`], the blocking client with proxy and TLS (SSL) capabilities
49//!   using the default [`minreq`] backend.
50//! * `blocking-https-rustls` enables [`minreq`], the blocking client with proxy and TLS (SSL)
51//!   capabilities using the `rustls` backend.
52//! * `blocking-https-native` enables [`minreq`], the blocking client with proxy and TLS (SSL)
53//!   capabilities using the platform's native TLS backend (likely OpenSSL).
54//! * `blocking-https-bundled` enables [`minreq`], the blocking client with proxy and TLS (SSL)
55//!   capabilities using a bundled OpenSSL library backend.
56//! * `async` enables [`reqwest`], the async client with proxy capabilities.
57//! * `async-https` enables [`reqwest`], the async client with support for proxying and TLS (SSL)
58//!   using the default [`reqwest`] TLS backend.
59//! * `async-https-native` enables [`reqwest`], the async client with support for proxying and TLS
60//!   (SSL) using the platform's native TLS backend (likely OpenSSL).
61//! * `async-https-rustls` enables [`reqwest`], the async client with support for proxying and TLS
62//!   (SSL) using the `rustls` TLS backend.
63//! * `async-https-rustls-manual-roots` enables [`reqwest`], the async client with support for
64//!   proxying and TLS (SSL) using the `rustls` TLS backend without using its the default root
65//!   certificates.
66//!
67//! [`dont remove this line or cargo doc will break`]: https://example.com
68#![cfg_attr(not(feature = "minreq"), doc = "[`minreq`]: https://docs.rs/minreq")]
69#![cfg_attr(not(feature = "reqwest"), doc = "[`reqwest`]: https://docs.rs/reqwest")]
70#![allow(clippy::result_large_err)]
71
72use std::collections::HashMap;
73use std::fmt;
74use std::num::TryFromIntError;
75use std::time::Duration;
76
77#[cfg(feature = "async")]
78pub use r#async::Sleeper;
79
80pub mod api;
81#[cfg(feature = "async")]
82pub mod r#async;
83#[cfg(feature = "blocking")]
84pub mod blocking;
85
86pub use api::*;
87#[cfg(feature = "blocking")]
88pub use blocking::BlockingClient;
89#[cfg(feature = "async")]
90pub use r#async::AsyncClient;
91
92/// Response status codes for which the request may be retried.
93pub const RETRYABLE_ERROR_CODES: [u16; 3] = [
94    429, // TOO_MANY_REQUESTS
95    500, // INTERNAL_SERVER_ERROR
96    503, // SERVICE_UNAVAILABLE
97];
98
99/// Base backoff in milliseconds.
100const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256);
101
102/// Default max retries.
103const DEFAULT_MAX_RETRIES: usize = 6;
104
105/// Get a fee value in sats/vbytes from the estimates
106/// that matches the confirmation target set as parameter.
107///
108/// Returns `None` if no feerate estimate is found at or below `target`
109/// confirmations.
110pub fn convert_fee_rate(target: usize, estimates: HashMap<u16, f64>) -> Option<f32> {
111    estimates
112        .into_iter()
113        .filter(|(k, _)| *k as usize <= target)
114        .max_by_key(|(k, _)| *k)
115        .map(|(_, v)| v as f32)
116}
117
118#[derive(Debug, Clone)]
119pub struct Builder {
120    /// The URL of the Esplora server.
121    pub base_url: String,
122    /// Optional URL of the proxy to use to make requests to the Esplora server
123    ///
124    /// The string should be formatted as:
125    /// `<protocol>://<user>:<password>@host:<port>`.
126    ///
127    /// Note that the format of this value and the supported protocols change
128    /// slightly between the blocking version of the client (using `minreq`)
129    /// and the async version (using `reqwest`). For more details check with
130    /// the documentation of the two crates. Both of them are compiled with
131    /// the `socks` feature enabled.
132    ///
133    /// The proxy is ignored when targeting `wasm32`.
134    pub proxy: Option<String>,
135    /// Socket timeout.
136    pub timeout: Option<u64>,
137    /// HTTP headers to set on every request made to Esplora server.
138    pub headers: HashMap<String, String>,
139    /// Max retries
140    pub max_retries: usize,
141}
142
143impl Builder {
144    /// Instantiate a new builder
145    pub fn new(base_url: &str) -> Self {
146        Builder {
147            base_url: base_url.to_string(),
148            proxy: None,
149            timeout: None,
150            headers: HashMap::new(),
151            max_retries: DEFAULT_MAX_RETRIES,
152        }
153    }
154
155    /// Set the proxy of the builder
156    pub fn proxy(mut self, proxy: &str) -> Self {
157        self.proxy = Some(proxy.to_string());
158        self
159    }
160
161    /// Set the timeout of the builder
162    pub fn timeout(mut self, timeout: u64) -> Self {
163        self.timeout = Some(timeout);
164        self
165    }
166
167    /// Add a header to set on each request
168    pub fn header(mut self, key: &str, value: &str) -> Self {
169        self.headers.insert(key.to_string(), value.to_string());
170        self
171    }
172
173    /// Set the maximum number of times to retry a request if the response status
174    /// is one of [`RETRYABLE_ERROR_CODES`].
175    pub fn max_retries(mut self, count: usize) -> Self {
176        self.max_retries = count;
177        self
178    }
179
180    /// Build a blocking client from builder
181    #[cfg(feature = "blocking")]
182    pub fn build_blocking(self) -> BlockingClient {
183        BlockingClient::from_builder(self)
184    }
185
186    /// Build an asynchronous client from builder
187    #[cfg(all(feature = "async", feature = "tokio"))]
188    pub fn build_async(self) -> Result<AsyncClient, Error> {
189        AsyncClient::from_builder(self)
190    }
191
192    /// Build an asynchronous client from builder where the returned client uses a
193    /// user-defined [`Sleeper`].
194    #[cfg(feature = "async")]
195    pub fn build_async_with_sleeper<S: Sleeper>(self) -> Result<AsyncClient<S>, Error> {
196        AsyncClient::from_builder(self)
197    }
198}
199
200/// Errors that can happen during a request to `Esplora` servers.
201#[derive(Debug)]
202pub enum Error {
203    /// Error during `minreq` HTTP request
204    #[cfg(feature = "blocking")]
205    Minreq(::minreq::Error),
206    /// Error during reqwest HTTP request
207    #[cfg(feature = "async")]
208    Reqwest(::reqwest::Error),
209    /// HTTP response error
210    HttpResponse { status: u16, message: String },
211    /// Invalid number returned
212    Parsing(std::num::ParseIntError),
213    /// Invalid status code, unable to convert to `u16`
214    StatusCode(TryFromIntError),
215    /// Invalid Bitcoin data returned
216    BitcoinEncoding(bitcoin::consensus::encode::Error),
217    /// Invalid hex data returned (attempting to create an array)
218    HexToArray(bitcoin::hex::HexToArrayError),
219    /// Invalid hex data returned (attempting to create a vector)
220    HexToBytes(bitcoin::hex::HexToBytesError),
221    /// Transaction not found
222    TransactionNotFound(Txid),
223    /// Block Header height not found
224    HeaderHeightNotFound(u32),
225    /// Block Header hash not found
226    HeaderHashNotFound(BlockHash),
227    /// Invalid HTTP Header name specified
228    InvalidHttpHeaderName(String),
229    /// Invalid HTTP Header value specified
230    InvalidHttpHeaderValue(String),
231    /// The server sent an invalid response
232    InvalidResponse,
233}
234
235impl fmt::Display for Error {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        write!(f, "{self:?}")
238    }
239}
240
241macro_rules! impl_error {
242    ( $from:ty, $to:ident ) => {
243        impl_error!($from, $to, Error);
244    };
245    ( $from:ty, $to:ident, $impl_for:ty ) => {
246        impl std::convert::From<$from> for $impl_for {
247            fn from(err: $from) -> Self {
248                <$impl_for>::$to(err)
249            }
250        }
251    };
252}
253
254impl std::error::Error for Error {}
255#[cfg(feature = "blocking")]
256impl_error!(::minreq::Error, Minreq, Error);
257#[cfg(feature = "async")]
258impl_error!(::reqwest::Error, Reqwest, Error);
259impl_error!(std::num::ParseIntError, Parsing, Error);
260impl_error!(bitcoin::consensus::encode::Error, BitcoinEncoding, Error);
261impl_error!(bitcoin::hex::HexToArrayError, HexToArray, Error);
262impl_error!(bitcoin::hex::HexToBytesError, HexToBytes, Error);
263
264#[cfg(test)]
265mod test {
266    use super::*;
267    use electrsd::{corepc_node, ElectrsD};
268    use lazy_static::lazy_static;
269    use std::env;
270    use tokio::sync::Mutex;
271    #[cfg(all(feature = "blocking", feature = "async"))]
272    use {
273        bitcoin::{hashes::Hash, Amount},
274        corepc_node::AddressType,
275        electrsd::electrum_client::ElectrumApi,
276        std::time::Duration,
277        tokio::sync::OnceCell,
278    };
279
280    lazy_static! {
281        static ref BITCOIND: corepc_node::Node = {
282            let bitcoind_exe = env::var("BITCOIND_EXE")
283                .ok()
284                .or_else(|| corepc_node::downloaded_exe_path().ok())
285                .expect(
286                    "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
287                );
288            let conf = corepc_node::Conf::default();
289            corepc_node::Node::with_conf(bitcoind_exe, &conf).unwrap()
290        };
291        static ref ELECTRSD: ElectrsD = {
292            let electrs_exe = env::var("ELECTRS_EXE")
293                .ok()
294                .or_else(electrsd::downloaded_exe_path)
295                .expect(
296                    "you need to provide env var ELECTRS_EXE or specify an electrsd version feature",
297                );
298            let mut conf = electrsd::Conf::default();
299            conf.http_enabled = true;
300            ElectrsD::with_conf(electrs_exe, &BITCOIND, &conf).unwrap()
301        };
302        static ref MINER: Mutex<()> = Mutex::new(());
303    }
304
305    #[cfg(all(feature = "blocking", feature = "async"))]
306    static PREMINE: OnceCell<()> = OnceCell::const_new();
307
308    #[cfg(all(feature = "blocking", feature = "async"))]
309    async fn setup_clients() -> (BlockingClient, AsyncClient) {
310        setup_clients_with_headers(HashMap::new()).await
311    }
312
313    #[cfg(all(feature = "blocking", feature = "async"))]
314    async fn setup_clients_with_headers(
315        headers: HashMap<String, String>,
316    ) -> (BlockingClient, AsyncClient) {
317        PREMINE
318            .get_or_init(|| async {
319                let _miner = MINER.lock().await;
320                generate_blocks_and_wait(101);
321            })
322            .await;
323
324        let esplora_url = ELECTRSD.esplora_url.as_ref().unwrap();
325
326        let mut builder = Builder::new(&format!("http://{esplora_url}"));
327        if !headers.is_empty() {
328            builder.headers = headers;
329        }
330
331        let blocking_client = builder.build_blocking();
332
333        let builder_async = Builder::new(&format!("http://{esplora_url}"));
334
335        #[cfg(feature = "tokio")]
336        let async_client = builder_async.build_async().unwrap();
337
338        #[cfg(not(feature = "tokio"))]
339        let async_client = builder_async
340            .build_async_with_sleeper::<r#async::DefaultSleeper>()
341            .unwrap();
342
343        (blocking_client, async_client)
344    }
345
346    #[cfg(all(feature = "blocking", feature = "async"))]
347    fn generate_blocks_and_wait(num: usize) {
348        let cur_height = BITCOIND.client.get_block_count().unwrap().0;
349        generate_blocks(num);
350        wait_for_block(cur_height as usize + num);
351    }
352
353    #[cfg(all(feature = "blocking", feature = "async"))]
354    fn generate_blocks(num: usize) {
355        let address = BITCOIND
356            .client
357            .new_address_with_type(AddressType::Legacy)
358            .unwrap();
359        let _block_hashes = BITCOIND.client.generate_to_address(num, &address).unwrap();
360    }
361
362    #[cfg(all(feature = "blocking", feature = "async"))]
363    fn wait_for_block(min_height: usize) {
364        let mut header = ELECTRSD.client.block_headers_subscribe().unwrap();
365        loop {
366            if header.height >= min_height {
367                break;
368            }
369            header = exponential_backoff_poll(|| {
370                ELECTRSD.trigger().unwrap();
371                ELECTRSD.client.ping().unwrap();
372                ELECTRSD.client.block_headers_pop().unwrap()
373            });
374        }
375    }
376
377    #[cfg(all(feature = "blocking", feature = "async"))]
378    fn exponential_backoff_poll<T, F>(mut poll: F) -> T
379    where
380        F: FnMut() -> Option<T>,
381    {
382        let mut delay = Duration::from_millis(64);
383        loop {
384            match poll() {
385                Some(data) => break data,
386                None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
387                None => {}
388            }
389
390            std::thread::sleep(delay);
391        }
392    }
393
394    #[test]
395    fn feerate_parsing() {
396        let esplora_fees = serde_json::from_str::<HashMap<u16, f64>>(
397            r#"{
398  "25": 1.015,
399  "5": 2.3280000000000003,
400  "12": 2.0109999999999997,
401  "15": 1.018,
402  "17": 1.018,
403  "11": 2.0109999999999997,
404  "3": 3.01,
405  "2": 4.9830000000000005,
406  "6": 2.2359999999999998,
407  "21": 1.018,
408  "13": 1.081,
409  "7": 2.2359999999999998,
410  "8": 2.2359999999999998,
411  "16": 1.018,
412  "20": 1.018,
413  "22": 1.017,
414  "23": 1.017,
415  "504": 1,
416  "9": 2.2359999999999998,
417  "14": 1.018,
418  "10": 2.0109999999999997,
419  "24": 1.017,
420  "1008": 1,
421  "1": 4.9830000000000005,
422  "4": 2.3280000000000003,
423  "19": 1.018,
424  "144": 1,
425  "18": 1.018
426}
427"#,
428        )
429        .unwrap();
430        assert!(convert_fee_rate(1, HashMap::new()).is_none());
431        assert_eq!(convert_fee_rate(6, esplora_fees.clone()).unwrap(), 2.236);
432        assert_eq!(
433            convert_fee_rate(26, esplora_fees.clone()).unwrap(),
434            1.015,
435            "should inherit from value for 25"
436        );
437        assert!(
438            convert_fee_rate(0, esplora_fees).is_none(),
439            "should not return feerate for 0 target"
440        );
441    }
442
443    #[cfg(all(feature = "blocking", feature = "async"))]
444    #[tokio::test]
445    async fn test_get_tx() {
446        let (blocking_client, async_client) = setup_clients().await;
447
448        let address = BITCOIND
449            .client
450            .new_address_with_type(AddressType::Legacy)
451            .unwrap();
452        let txid = BITCOIND
453            .client
454            .send_to_address(&address, Amount::from_sat(1000))
455            .unwrap()
456            .txid()
457            .unwrap();
458        let _miner = MINER.lock().await;
459        generate_blocks_and_wait(1);
460
461        let tx = blocking_client.get_tx(&txid).unwrap();
462        let tx_async = async_client.get_tx(&txid).await.unwrap();
463        assert_eq!(tx, tx_async);
464    }
465
466    #[cfg(all(feature = "blocking", feature = "async"))]
467    #[tokio::test]
468    async fn test_get_tx_no_opt() {
469        let (blocking_client, async_client) = setup_clients().await;
470
471        let address = BITCOIND
472            .client
473            .new_address_with_type(AddressType::Legacy)
474            .unwrap();
475        let txid = BITCOIND
476            .client
477            .send_to_address(&address, Amount::from_sat(1000))
478            .unwrap()
479            .txid()
480            .unwrap();
481        let _miner = MINER.lock().await;
482        generate_blocks_and_wait(1);
483
484        let tx_no_opt = blocking_client.get_tx_no_opt(&txid).unwrap();
485        let tx_no_opt_async = async_client.get_tx_no_opt(&txid).await.unwrap();
486        assert_eq!(tx_no_opt, tx_no_opt_async);
487    }
488
489    #[cfg(all(feature = "blocking", feature = "async"))]
490    #[tokio::test]
491    async fn test_get_tx_status() {
492        let (blocking_client, async_client) = setup_clients().await;
493
494        let address = BITCOIND
495            .client
496            .new_address_with_type(AddressType::Legacy)
497            .unwrap();
498        let txid = BITCOIND
499            .client
500            .send_to_address(&address, Amount::from_sat(1000))
501            .unwrap()
502            .txid()
503            .unwrap();
504        let _miner = MINER.lock().await;
505        generate_blocks_and_wait(1);
506
507        let tx_status = blocking_client.get_tx_status(&txid).unwrap();
508        let tx_status_async = async_client.get_tx_status(&txid).await.unwrap();
509        assert_eq!(tx_status, tx_status_async);
510        assert!(tx_status.confirmed);
511
512        // Bogus txid returns a TxStatus with false, None, None, None
513        let txid = Txid::hash(b"ayyyy lmao");
514        let tx_status = blocking_client.get_tx_status(&txid).unwrap();
515        let tx_status_async = async_client.get_tx_status(&txid).await.unwrap();
516        assert_eq!(tx_status, tx_status_async);
517        assert!(!tx_status.confirmed);
518        assert!(tx_status.block_height.is_none());
519        assert!(tx_status.block_hash.is_none());
520        assert!(tx_status.block_time.is_none());
521    }
522
523    #[cfg(all(feature = "blocking", feature = "async"))]
524    #[tokio::test]
525    async fn test_get_tx_info() {
526        let (blocking_client, async_client) = setup_clients().await;
527
528        let address = BITCOIND
529            .client
530            .new_address_with_type(AddressType::Legacy)
531            .unwrap();
532        let txid = BITCOIND
533            .client
534            .send_to_address(&address, Amount::from_sat(1000))
535            .unwrap()
536            .txid()
537            .unwrap();
538        let _miner = MINER.lock().await;
539        generate_blocks_and_wait(1);
540
541        let tx_res = BITCOIND
542            .client
543            .get_transaction(txid)
544            .unwrap()
545            .into_model()
546            .unwrap();
547        let tx_exp: Transaction = tx_res.tx;
548        let tx_block_height = BITCOIND
549            .client
550            .get_block_header_verbose(&tx_res.block_hash.unwrap())
551            .unwrap()
552            .into_model()
553            .unwrap()
554            .height;
555
556        let tx_info = blocking_client
557            .get_tx_info(&txid)
558            .unwrap()
559            .expect("must get tx");
560        let tx_info_async = async_client
561            .get_tx_info(&txid)
562            .await
563            .unwrap()
564            .expect("must get tx");
565        assert_eq!(tx_info, tx_info_async);
566        assert_eq!(tx_info.txid, txid);
567        assert_eq!(tx_info.to_tx(), tx_exp);
568        assert_eq!(tx_info.size, tx_exp.total_size());
569        assert_eq!(tx_info.weight(), tx_exp.weight());
570        assert_eq!(tx_info.fee(), tx_res.fee.unwrap().unsigned_abs());
571        assert!(tx_info.status.confirmed);
572        assert_eq!(tx_info.status.block_height, Some(tx_block_height));
573        assert_eq!(tx_info.status.block_hash, tx_res.block_hash);
574        assert_eq!(
575            tx_info.status.block_time,
576            tx_res.block_time.map(|bt| bt as u64)
577        );
578
579        let txid = Txid::hash(b"not exist");
580        assert_eq!(blocking_client.get_tx_info(&txid).unwrap(), None);
581        assert_eq!(async_client.get_tx_info(&txid).await.unwrap(), None);
582    }
583
584    #[cfg(all(feature = "blocking", feature = "async"))]
585    #[tokio::test]
586    async fn test_get_header_by_hash() {
587        let (blocking_client, async_client) = setup_clients().await;
588
589        let block_hash = BITCOIND
590            .client
591            .get_block_hash(23)
592            .unwrap()
593            .block_hash()
594            .unwrap();
595
596        let block_header = blocking_client.get_header_by_hash(&block_hash).unwrap();
597        let block_header_async = async_client.get_header_by_hash(&block_hash).await.unwrap();
598        assert_eq!(block_header, block_header_async);
599    }
600
601    #[cfg(all(feature = "blocking", feature = "async"))]
602    #[tokio::test]
603    async fn test_get_block_status() {
604        let (blocking_client, async_client) = setup_clients().await;
605
606        let block_hash = BITCOIND
607            .client
608            .get_block_hash(21)
609            .unwrap()
610            .block_hash()
611            .unwrap();
612        let next_block_hash = BITCOIND
613            .client
614            .get_block_hash(22)
615            .unwrap()
616            .block_hash()
617            .unwrap();
618
619        let expected = BlockStatus {
620            in_best_chain: true,
621            height: Some(21),
622            next_best: Some(next_block_hash),
623        };
624
625        let block_status = blocking_client.get_block_status(&block_hash).unwrap();
626        let block_status_async = async_client.get_block_status(&block_hash).await.unwrap();
627        assert_eq!(expected, block_status);
628        assert_eq!(expected, block_status_async);
629    }
630
631    #[cfg(all(feature = "blocking", feature = "async"))]
632    #[tokio::test]
633    async fn test_get_non_existing_block_status() {
634        // Esplora returns the same status for orphaned blocks as for non-existing
635        // blocks: non-existing: https://blockstream.info/api/block/0000000000000000000000000000000000000000000000000000000000000000/status
636        // orphaned: https://blockstream.info/api/block/000000000000000000181b1a2354620f66868a723c0c4d5b24e4be8bdfc35a7f/status
637        // (Here the block is cited as orphaned: https://bitcoinchain.com/block_explorer/block/000000000000000000181b1a2354620f66868a723c0c4d5b24e4be8bdfc35a7f/ )
638        // For this reason, we only test for the non-existing case here.
639
640        let (blocking_client, async_client) = setup_clients().await;
641
642        let block_hash = BlockHash::all_zeros();
643
644        let expected = BlockStatus {
645            in_best_chain: false,
646            height: None,
647            next_best: None,
648        };
649
650        let block_status = blocking_client.get_block_status(&block_hash).unwrap();
651        let block_status_async = async_client.get_block_status(&block_hash).await.unwrap();
652        assert_eq!(expected, block_status);
653        assert_eq!(expected, block_status_async);
654    }
655
656    #[cfg(all(feature = "blocking", feature = "async"))]
657    #[tokio::test]
658    async fn test_get_block_by_hash() {
659        let (blocking_client, async_client) = setup_clients().await;
660
661        let block_hash = BITCOIND
662            .client
663            .get_block_hash(21)
664            .unwrap()
665            .block_hash()
666            .unwrap();
667
668        let expected = Some(BITCOIND.client.get_block(block_hash).unwrap());
669
670        let block = blocking_client.get_block_by_hash(&block_hash).unwrap();
671        let block_async = async_client.get_block_by_hash(&block_hash).await.unwrap();
672        assert_eq!(expected, block);
673        assert_eq!(expected, block_async);
674    }
675
676    #[cfg(all(feature = "blocking", feature = "async"))]
677    #[tokio::test]
678    async fn test_that_errors_are_propagated() {
679        let (blocking_client, async_client) = setup_clients().await;
680
681        let address = BITCOIND
682            .client
683            .new_address_with_type(AddressType::Legacy)
684            .unwrap();
685        let txid = BITCOIND
686            .client
687            .send_to_address(&address, Amount::from_sat(1000))
688            .unwrap()
689            .txid()
690            .unwrap();
691        let _miner = MINER.lock().await;
692        generate_blocks_and_wait(1);
693
694        let tx = blocking_client.get_tx(&txid).unwrap();
695        let async_res = async_client.broadcast(tx.as_ref().unwrap()).await;
696        let blocking_res = blocking_client.broadcast(tx.as_ref().unwrap());
697        assert!(async_res.is_err());
698        assert!(matches!(
699            async_res.unwrap_err(),
700            Error::HttpResponse { status: 400, message } if message.contains("-27")
701        ));
702        assert!(blocking_res.is_err());
703        assert!(matches!(
704            blocking_res.unwrap_err(),
705            Error::HttpResponse { status: 400, message } if message.contains("-27")
706        ));
707    }
708
709    #[cfg(all(feature = "blocking", feature = "async"))]
710    #[tokio::test]
711    async fn test_get_block_by_hash_not_existing() {
712        let (blocking_client, async_client) = setup_clients().await;
713
714        let block = blocking_client
715            .get_block_by_hash(&BlockHash::all_zeros())
716            .unwrap();
717        let block_async = async_client
718            .get_block_by_hash(&BlockHash::all_zeros())
719            .await
720            .unwrap();
721        assert!(block.is_none());
722        assert!(block_async.is_none());
723    }
724
725    #[cfg(all(feature = "blocking", feature = "async"))]
726    #[tokio::test]
727    async fn test_get_merkle_proof() {
728        let (blocking_client, async_client) = setup_clients().await;
729
730        let address = BITCOIND
731            .client
732            .new_address_with_type(AddressType::Legacy)
733            .unwrap();
734        let txid = BITCOIND
735            .client
736            .send_to_address(&address, Amount::from_sat(1000))
737            .unwrap()
738            .txid()
739            .unwrap();
740        let _miner = MINER.lock().await;
741        generate_blocks_and_wait(1);
742
743        let merkle_proof = blocking_client.get_merkle_proof(&txid).unwrap().unwrap();
744        let merkle_proof_async = async_client.get_merkle_proof(&txid).await.unwrap().unwrap();
745        assert_eq!(merkle_proof, merkle_proof_async);
746        assert!(merkle_proof.pos > 0);
747    }
748
749    #[cfg(all(feature = "blocking", feature = "async"))]
750    #[tokio::test]
751    async fn test_get_merkle_block() {
752        let (blocking_client, async_client) = setup_clients().await;
753
754        let address = BITCOIND
755            .client
756            .new_address_with_type(AddressType::Legacy)
757            .unwrap();
758        let txid = BITCOIND
759            .client
760            .send_to_address(&address, Amount::from_sat(1000))
761            .unwrap()
762            .txid()
763            .unwrap();
764        let _miner = MINER.lock().await;
765        generate_blocks_and_wait(1);
766
767        let merkle_block = blocking_client.get_merkle_block(&txid).unwrap().unwrap();
768        let merkle_block_async = async_client.get_merkle_block(&txid).await.unwrap().unwrap();
769        assert_eq!(merkle_block, merkle_block_async);
770
771        let mut matches = vec![txid];
772        let mut indexes = vec![];
773        let root = merkle_block
774            .txn
775            .extract_matches(&mut matches, &mut indexes)
776            .unwrap();
777        assert_eq!(root, merkle_block.header.merkle_root);
778        assert_eq!(indexes.len(), 1);
779        assert!(indexes[0] > 0);
780    }
781
782    #[cfg(all(feature = "blocking", feature = "async"))]
783    #[tokio::test]
784    async fn test_get_output_status() {
785        let (blocking_client, async_client) = setup_clients().await;
786
787        let address = BITCOIND
788            .client
789            .new_address_with_type(AddressType::Legacy)
790            .unwrap();
791        let txid = BITCOIND
792            .client
793            .send_to_address(&address, Amount::from_sat(1000))
794            .unwrap()
795            .txid()
796            .unwrap();
797        let _miner = MINER.lock().await;
798        generate_blocks_and_wait(1);
799
800        let output_status = blocking_client
801            .get_output_status(&txid, 1)
802            .unwrap()
803            .unwrap();
804        let output_status_async = async_client
805            .get_output_status(&txid, 1)
806            .await
807            .unwrap()
808            .unwrap();
809
810        assert_eq!(output_status, output_status_async);
811    }
812
813    #[cfg(all(feature = "blocking", feature = "async"))]
814    #[tokio::test]
815    async fn test_get_height() {
816        let (blocking_client, async_client) = setup_clients().await;
817        let block_height = blocking_client.get_height().unwrap();
818        let block_height_async = async_client.get_height().await.unwrap();
819        assert!(block_height > 0);
820        assert_eq!(block_height, block_height_async);
821    }
822
823    #[cfg(all(feature = "blocking", feature = "async"))]
824    #[tokio::test]
825    async fn test_get_tip_hash() {
826        let (blocking_client, async_client) = setup_clients().await;
827        let tip_hash = blocking_client.get_tip_hash().unwrap();
828        let tip_hash_async = async_client.get_tip_hash().await.unwrap();
829        assert_eq!(tip_hash, tip_hash_async);
830    }
831
832    #[cfg(all(feature = "blocking", feature = "async"))]
833    #[tokio::test]
834    async fn test_get_block_hash() {
835        let (blocking_client, async_client) = setup_clients().await;
836
837        let block_hash = BITCOIND
838            .client
839            .get_block_hash(21)
840            .unwrap()
841            .block_hash()
842            .unwrap();
843
844        let block_hash_blocking = blocking_client.get_block_hash(21).unwrap();
845        let block_hash_async = async_client.get_block_hash(21).await.unwrap();
846        assert_eq!(block_hash, block_hash_blocking);
847        assert_eq!(block_hash, block_hash_async);
848    }
849
850    #[cfg(all(feature = "blocking", feature = "async"))]
851    #[tokio::test]
852    async fn test_get_txid_at_block_index() {
853        let (blocking_client, async_client) = setup_clients().await;
854
855        let block_hash = BITCOIND
856            .client
857            .get_block_hash(23)
858            .unwrap()
859            .block_hash()
860            .unwrap();
861
862        let txid_at_block_index = blocking_client
863            .get_txid_at_block_index(&block_hash, 0)
864            .unwrap()
865            .unwrap();
866        let txid_at_block_index_async = async_client
867            .get_txid_at_block_index(&block_hash, 0)
868            .await
869            .unwrap()
870            .unwrap();
871        assert_eq!(txid_at_block_index, txid_at_block_index_async);
872    }
873
874    #[cfg(all(feature = "blocking", feature = "async"))]
875    #[tokio::test]
876    async fn test_get_fee_estimates() {
877        let (blocking_client, async_client) = setup_clients().await;
878        let fee_estimates = blocking_client.get_fee_estimates().unwrap();
879        let fee_estimates_async = async_client.get_fee_estimates().await.unwrap();
880        assert_eq!(fee_estimates.len(), fee_estimates_async.len());
881    }
882
883    #[cfg(all(feature = "blocking", feature = "async"))]
884    #[tokio::test]
885    async fn test_scripthash_txs() {
886        let (blocking_client, async_client) = setup_clients().await;
887
888        let address = BITCOIND
889            .client
890            .new_address_with_type(AddressType::Legacy)
891            .unwrap();
892        let txid = BITCOIND
893            .client
894            .send_to_address(&address, Amount::from_sat(1000))
895            .unwrap()
896            .txid()
897            .unwrap();
898        let _miner = MINER.lock().await;
899        generate_blocks_and_wait(1);
900
901        let expected_tx = BITCOIND
902            .client
903            .get_transaction(txid)
904            .unwrap()
905            .into_model()
906            .unwrap()
907            .tx;
908        let script = &expected_tx.output[0].script_pubkey;
909        let scripthash_txs_txids: Vec<Txid> = blocking_client
910            .scripthash_txs(script, None)
911            .unwrap()
912            .iter()
913            .map(|tx| tx.txid)
914            .collect();
915        let scripthash_txs_txids_async: Vec<Txid> = async_client
916            .scripthash_txs(script, None)
917            .await
918            .unwrap()
919            .iter()
920            .map(|tx| tx.txid)
921            .collect();
922        assert_eq!(scripthash_txs_txids, scripthash_txs_txids_async);
923    }
924
925    #[cfg(all(feature = "blocking", feature = "async"))]
926    #[tokio::test]
927    async fn test_get_blocks() {
928        let (blocking_client, async_client) = setup_clients().await;
929        let start_height = BITCOIND.client.get_block_count().unwrap().0;
930        let blocks1 = blocking_client.get_blocks(None).unwrap();
931        let blocks_async1 = async_client.get_blocks(None).await.unwrap();
932        assert_eq!(blocks1[0].time.height, start_height as u32);
933        assert_eq!(blocks1, blocks_async1);
934        generate_blocks_and_wait(10);
935        let blocks2 = blocking_client.get_blocks(None).unwrap();
936        let blocks_async2 = async_client.get_blocks(None).await.unwrap();
937        assert_eq!(blocks2, blocks_async2);
938        assert_ne!(blocks2, blocks1);
939        let blocks3 = blocking_client
940            .get_blocks(Some(start_height as u32))
941            .unwrap();
942        let blocks_async3 = async_client
943            .get_blocks(Some(start_height as u32))
944            .await
945            .unwrap();
946        assert_eq!(blocks3, blocks_async3);
947        assert_eq!(blocks3[0].time.height, start_height as u32);
948        assert_eq!(blocks3, blocks1);
949        let blocks_genesis = blocking_client.get_blocks(Some(0)).unwrap();
950        let blocks_genesis_async = async_client.get_blocks(Some(0)).await.unwrap();
951        assert_eq!(blocks_genesis, blocks_genesis_async);
952    }
953
954    #[cfg(all(feature = "blocking", feature = "async"))]
955    #[tokio::test]
956    async fn test_get_tx_with_http_header() {
957        let headers = [(
958            "Authorization".to_string(),
959            "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==".to_string(),
960        )]
961        .into();
962        let (blocking_client, async_client) = setup_clients_with_headers(headers).await;
963
964        let address = BITCOIND
965            .client
966            .new_address_with_type(AddressType::Legacy)
967            .unwrap();
968        let txid = BITCOIND
969            .client
970            .send_to_address(&address, Amount::from_sat(1000))
971            .unwrap()
972            .txid()
973            .unwrap();
974        let _miner = MINER.lock().await;
975        generate_blocks_and_wait(1);
976
977        let tx = blocking_client.get_tx(&txid).unwrap();
978        let tx_async = async_client.get_tx(&txid).await.unwrap();
979        assert_eq!(tx, tx_async);
980    }
981
982    #[cfg(all(feature = "blocking", feature = "async"))]
983    #[tokio::test]
984    async fn test_get_address_stats() {
985        let (blocking_client, async_client) = setup_clients().await;
986
987        let address = BITCOIND
988            .client
989            .new_address_with_type(AddressType::Legacy)
990            .unwrap();
991
992        let _txid = BITCOIND
993            .client
994            .send_to_address(&address, Amount::from_sat(1000))
995            .unwrap()
996            .txid()
997            .unwrap();
998
999        let address_stats_blocking = blocking_client.get_address_stats(&address).unwrap();
1000        let address_stats_async = async_client.get_address_stats(&address).await.unwrap();
1001        assert_eq!(address_stats_blocking, address_stats_async);
1002        assert_eq!(address_stats_async.chain_stats.funded_txo_count, 0);
1003
1004        let _miner = MINER.lock().await;
1005        generate_blocks_and_wait(1);
1006
1007        let address_stats_blocking = blocking_client.get_address_stats(&address).unwrap();
1008        let address_stats_async = async_client.get_address_stats(&address).await.unwrap();
1009        assert_eq!(address_stats_blocking, address_stats_async);
1010        assert_eq!(address_stats_async.chain_stats.funded_txo_count, 1);
1011        assert_eq!(address_stats_async.chain_stats.funded_txo_sum, 1000);
1012    }
1013
1014    #[cfg(all(feature = "blocking", feature = "async"))]
1015    #[tokio::test]
1016    async fn test_get_address_txs() {
1017        let (blocking_client, async_client) = setup_clients().await;
1018
1019        let address = BITCOIND
1020            .client
1021            .new_address_with_type(AddressType::Legacy)
1022            .unwrap();
1023
1024        let txid = BITCOIND
1025            .client
1026            .send_to_address(&address, Amount::from_sat(1000))
1027            .unwrap()
1028            .txid()
1029            .unwrap();
1030
1031        let _miner = MINER.lock().await;
1032        generate_blocks_and_wait(1);
1033
1034        let address_txs_blocking = blocking_client.get_address_txs(&address, None).unwrap();
1035        let address_txs_async = async_client.get_address_txs(&address, None).await.unwrap();
1036
1037        assert_eq!(address_txs_blocking, address_txs_async);
1038        assert_eq!(address_txs_async[0].txid, txid);
1039    }
1040}