esplora_client/
async.rs

1// Bitcoin Dev Kit
2// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3//
4// Copyright (c) 2020-2025 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, HashSet};
15use std::marker::PhantomData;
16use std::str::FromStr;
17use std::time::Duration;
18
19use bitcoin::block::Header as BlockHeader;
20use bitcoin::consensus::encode::serialize_hex;
21use bitcoin::consensus::{deserialize, serialize, Decodable};
22use bitcoin::hashes::{sha256, Hash};
23use bitcoin::hex::{DisplayHex, FromHex};
24use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid};
25
26#[allow(unused_imports)]
27use log::{debug, error, info, trace};
28
29use reqwest::{header, Body, Client, Response};
30
31use crate::{
32    AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
33    MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus,
34    Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
35};
36
37/// An async client for interacting with an Esplora API server.
38#[derive(Debug, Clone)]
39pub struct AsyncClient<S = DefaultSleeper> {
40    /// The URL of the Esplora Server.
41    url: String,
42    /// The inner [`reqwest::Client`] to make HTTP requests.
43    client: Client,
44    /// Number of times to retry a request
45    max_retries: usize,
46    /// Marker for the type of sleeper used
47    marker: PhantomData<S>,
48}
49
50impl<S: Sleeper> AsyncClient<S> {
51    /// Build an [`AsyncClient`] 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    /// Build an [`AsyncClient`] from a [`Client`].
86    pub fn from_client(url: String, client: Client) -> Self {
87        AsyncClient {
88            url,
89            client,
90            max_retries: crate::DEFAULT_MAX_RETRIES,
91            marker: PhantomData,
92        }
93    }
94
95    /// Make an HTTP GET request to given URL, deserializing to any `T` that
96    /// implement [`bitcoin::consensus::Decodable`].
97    ///
98    /// It should be used when requesting Esplora endpoints that can be directly
99    /// deserialized to native `rust-bitcoin` types, which implements
100    /// [`bitcoin::consensus::Decodable`] from `&[u8]`.
101    ///
102    /// # Errors
103    ///
104    /// This function will return an error either from the HTTP client, or the
105    /// [`bitcoin::consensus::Decodable`] deserialization.
106    async fn get_response<T: Decodable>(&self, path: &str) -> Result<T, Error> {
107        let url = format!("{}{}", self.url, path);
108        let response = self.get_with_retry(&url).await?;
109
110        if !response.status().is_success() {
111            return Err(Error::HttpResponse {
112                status: response.status().as_u16(),
113                message: response.text().await?,
114            });
115        }
116
117        Ok(deserialize::<T>(&response.bytes().await?)?)
118    }
119
120    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
121    ///
122    /// It uses [`AsyncEsploraClient::get_response`] internally.
123    ///
124    /// See [`AsyncEsploraClient::get_response`] above for full documentation.
125    async fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
126        match self.get_response::<T>(path).await {
127            Ok(res) => Ok(Some(res)),
128            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
129            Err(e) => Err(e),
130        }
131    }
132
133    /// Make an HTTP GET request to given URL, deserializing to any `T` that
134    /// implements [`serde::de::DeserializeOwned`].
135    ///
136    /// It should be used when requesting Esplora endpoints that have a specific
137    /// defined API, mostly defined in [`crate::api`].
138    ///
139    /// # Errors
140    ///
141    /// This function will return an error either from the HTTP client, or the
142    /// [`serde::de::DeserializeOwned`] deserialization.
143    async fn get_response_json<T: serde::de::DeserializeOwned>(
144        &self,
145        path: &str,
146    ) -> Result<T, Error> {
147        let url = format!("{}{}", self.url, path);
148        let response = self.get_with_retry(&url).await?;
149
150        if !response.status().is_success() {
151            return Err(Error::HttpResponse {
152                status: response.status().as_u16(),
153                message: response.text().await?,
154            });
155        }
156
157        response.json::<T>().await.map_err(Error::Reqwest)
158    }
159
160    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
161    ///
162    /// It uses [`AsyncEsploraClient::get_response_json`] internally.
163    ///
164    /// See [`AsyncEsploraClient::get_response_json`] above for full
165    /// documentation.
166    async fn get_opt_response_json<T: serde::de::DeserializeOwned>(
167        &self,
168        url: &str,
169    ) -> Result<Option<T>, Error> {
170        match self.get_response_json(url).await {
171            Ok(res) => Ok(Some(res)),
172            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
173            Err(e) => Err(e),
174        }
175    }
176
177    /// Make an HTTP GET request to given URL, deserializing to any `T` that
178    /// implements [`bitcoin::consensus::Decodable`].
179    ///
180    /// It should be used when requesting Esplora endpoints that are expected
181    /// to return a hex string decodable to native `rust-bitcoin` types which
182    /// implement [`bitcoin::consensus::Decodable`] from `&[u8]`.
183    ///
184    /// # Errors
185    ///
186    /// This function will return an error either from the HTTP client, or the
187    /// [`bitcoin::consensus::Decodable`] deserialization.
188    async fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
189        let url = format!("{}{}", self.url, path);
190        let response = self.get_with_retry(&url).await?;
191
192        if !response.status().is_success() {
193            return Err(Error::HttpResponse {
194                status: response.status().as_u16(),
195                message: response.text().await?,
196            });
197        }
198
199        let hex_str = response.text().await?;
200        Ok(deserialize(&Vec::from_hex(&hex_str)?)?)
201    }
202
203    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
204    ///
205    /// It uses [`AsyncEsploraClient::get_response_hex`] internally.
206    ///
207    /// See [`AsyncEsploraClient::get_response_hex`] above for full
208    /// documentation.
209    async fn get_opt_response_hex<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
210        match self.get_response_hex(path).await {
211            Ok(res) => Ok(Some(res)),
212            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
213            Err(e) => Err(e),
214        }
215    }
216
217    /// Make an HTTP GET request to given URL, deserializing to `String`.
218    ///
219    /// It should be used when requesting Esplora endpoints that can return
220    /// `String` formatted data that can be parsed downstream.
221    ///
222    /// # Errors
223    ///
224    /// This function will return an error either from the HTTP client.
225    async fn get_response_text(&self, path: &str) -> Result<String, Error> {
226        let url = format!("{}{}", self.url, path);
227        let response = self.get_with_retry(&url).await?;
228
229        if !response.status().is_success() {
230            return Err(Error::HttpResponse {
231                status: response.status().as_u16(),
232                message: response.text().await?,
233            });
234        }
235
236        Ok(response.text().await?)
237    }
238
239    /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
240    ///
241    /// It uses [`AsyncEsploraClient::get_response_text`] internally.
242    ///
243    /// See [`AsyncEsploraClient::get_response_text`] above for full
244    /// documentation.
245    async fn get_opt_response_text(&self, path: &str) -> Result<Option<String>, Error> {
246        match self.get_response_text(path).await {
247            Ok(s) => Ok(Some(s)),
248            Err(Error::HttpResponse { status: 404, .. }) => Ok(None),
249            Err(e) => Err(e),
250        }
251    }
252
253    /// Make an HTTP POST request to given URL, converting any `T` that
254    /// implement [`Into<Body>`] and setting query parameters, if any.
255    ///
256    /// # Errors
257    ///
258    /// This function will return an error either from the HTTP client, or the
259    /// response's [`serde_json`] deserialization.
260    async fn post_request_bytes<T: Into<Body>>(
261        &self,
262        path: &str,
263        body: T,
264        query_params: Option<HashSet<(&str, String)>>,
265    ) -> Result<Response, Error> {
266        let url: String = format!("{}{}", self.url, path);
267        let mut request = self.client.post(url).body(body);
268
269        for param in query_params.unwrap_or_default() {
270            request = request.query(&param);
271        }
272
273        let response = request.send().await?;
274
275        if !response.status().is_success() {
276            return Err(Error::HttpResponse {
277                status: response.status().as_u16(),
278                message: response.text().await?,
279            });
280        }
281
282        Ok(response)
283    }
284
285    /// Get a [`Transaction`] option given its [`Txid`]
286    pub async fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
287        self.get_opt_response(&format!("/tx/{txid}/raw")).await
288    }
289
290    /// Get a [`Transaction`] given its [`Txid`].
291    pub async fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
292        match self.get_tx(txid).await {
293            Ok(Some(tx)) => Ok(tx),
294            Ok(None) => Err(Error::TransactionNotFound(*txid)),
295            Err(e) => Err(e),
296        }
297    }
298
299    /// Get a [`Txid`] of a transaction given its index in a block with a given
300    /// hash.
301    pub async fn get_txid_at_block_index(
302        &self,
303        block_hash: &BlockHash,
304        index: usize,
305    ) -> Result<Option<Txid>, Error> {
306        match self
307            .get_opt_response_text(&format!("/block/{block_hash}/txid/{index}"))
308            .await?
309        {
310            Some(s) => Ok(Some(Txid::from_str(&s).map_err(Error::HexToArray)?)),
311            None => Ok(None),
312        }
313    }
314
315    /// Get the status of a [`Transaction`] given its [`Txid`].
316    pub async fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
317        self.get_response_json(&format!("/tx/{txid}/status")).await
318    }
319
320    /// Get transaction info given its [`Txid`].
321    pub async fn get_tx_info(&self, txid: &Txid) -> Result<Option<Tx>, Error> {
322        self.get_opt_response_json(&format!("/tx/{txid}")).await
323    }
324
325    /// Get the spend status of a [`Transaction`]'s outputs, given its [`Txid`].
326    pub async fn get_tx_outspends(&self, txid: &Txid) -> Result<Vec<OutputStatus>, Error> {
327        self.get_response_json(&format!("/tx/{txid}/outspends"))
328            .await
329    }
330
331    /// Get a [`BlockHeader`] given a particular block hash.
332    pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
333        self.get_response_hex(&format!("/block/{block_hash}/header"))
334            .await
335    }
336
337    /// Get the [`BlockStatus`] given a particular [`BlockHash`].
338    pub async fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
339        self.get_response_json(&format!("/block/{block_hash}/status"))
340            .await
341    }
342
343    /// Get a [`Block`] given a particular [`BlockHash`].
344    pub async fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
345        self.get_opt_response(&format!("/block/{block_hash}/raw"))
346            .await
347    }
348
349    /// Get a merkle inclusion proof for a [`Transaction`] with the given
350    /// [`Txid`].
351    pub async fn get_merkle_proof(&self, tx_hash: &Txid) -> Result<Option<MerkleProof>, Error> {
352        self.get_opt_response_json(&format!("/tx/{tx_hash}/merkle-proof"))
353            .await
354    }
355
356    /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the
357    /// given [`Txid`].
358    pub async fn get_merkle_block(&self, tx_hash: &Txid) -> Result<Option<MerkleBlock>, Error> {
359        self.get_opt_response_hex(&format!("/tx/{tx_hash}/merkleblock-proof"))
360            .await
361    }
362
363    /// Get the spending status of an output given a [`Txid`] and the output
364    /// index.
365    pub async fn get_output_status(
366        &self,
367        txid: &Txid,
368        index: u64,
369    ) -> Result<Option<OutputStatus>, Error> {
370        self.get_opt_response_json(&format!("/tx/{txid}/outspend/{index}"))
371            .await
372    }
373
374    /// Broadcast a [`Transaction`] to Esplora.
375    pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
376        let body = serialize::<Transaction>(transaction).to_lower_hex_string();
377        match self.post_request_bytes("/tx", body, None).await {
378            Ok(_resp) => Ok(()),
379            Err(e) => Err(e),
380        }
381    }
382
383    /// Broadcast a package of [`Transaction`]s to Esplora.
384    ///
385    /// If `maxfeerate` is provided, any transaction whose
386    /// fee is higher will be rejected.
387    ///
388    /// If `maxburnamount` is provided, any transaction
389    /// with higher provably unspendable outputs amount
390    /// will be rejected.
391    pub async fn submit_package(
392        &self,
393        transactions: &[Transaction],
394        maxfeerate: Option<f64>,
395        maxburnamount: Option<f64>,
396    ) -> Result<SubmitPackageResult, Error> {
397        let mut queryparams = HashSet::<(&str, String)>::new();
398        if let Some(maxfeerate) = maxfeerate {
399            queryparams.insert(("maxfeerate", maxfeerate.to_string()));
400        }
401        if let Some(maxburnamount) = maxburnamount {
402            queryparams.insert(("maxburnamount", maxburnamount.to_string()));
403        }
404
405        let serialized_txs = transactions
406            .iter()
407            .map(|tx| serialize_hex(&tx))
408            .collect::<Vec<_>>();
409
410        let response = self
411            .post_request_bytes(
412                "/txs/package",
413                serde_json::to_string(&serialized_txs).unwrap_or_default(),
414                Some(queryparams),
415            )
416            .await?;
417
418        Ok(response.json::<SubmitPackageResult>().await?)
419    }
420
421    /// Get the current height of the blockchain tip
422    pub async fn get_height(&self) -> Result<u32, Error> {
423        self.get_response_text("/blocks/tip/height")
424            .await
425            .map(|height| u32::from_str(&height).map_err(Error::Parsing))?
426    }
427
428    /// Get the [`BlockHash`] of the current blockchain tip.
429    pub async fn get_tip_hash(&self) -> Result<BlockHash, Error> {
430        self.get_response_text("/blocks/tip/hash")
431            .await
432            .map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
433    }
434
435    /// Get the [`BlockHash`] of a specific block height
436    pub async fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
437        self.get_response_text(&format!("/block-height/{block_height}"))
438            .await
439            .map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
440    }
441
442    /// Get information about a specific address, includes confirmed balance and transactions in
443    /// the mempool.
444    pub async fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
445        let path = format!("/address/{address}");
446        self.get_response_json(&path).await
447    }
448
449    /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions.
450    pub async fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
451        let script_hash = sha256::Hash::hash(script.as_bytes());
452        let path = format!("/scripthash/{script_hash}");
453        self.get_response_json(&path).await
454    }
455
456    /// Get transaction history for the specified address, sorted with newest first.
457    ///
458    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
459    /// More can be requested by specifying the last txid seen by the previous query.
460    pub async fn get_address_txs(
461        &self,
462        address: &Address,
463        last_seen: Option<Txid>,
464    ) -> Result<Vec<Tx>, Error> {
465        let path = match last_seen {
466            Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
467            None => format!("/address/{address}/txs"),
468        };
469
470        self.get_response_json(&path).await
471    }
472
473    /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first.
474    pub async fn get_mempool_address_txs(&self, address: &Address) -> Result<Vec<Tx>, Error> {
475        let path = format!("/address/{address}/txs/mempool");
476
477        self.get_response_json(&path).await
478    }
479
480    /// Get transaction history for the specified address/scripthash,
481    /// sorted with newest first. Returns 25 transactions per page.
482    /// More can be requested by specifying the last txid seen by the previous
483    /// query.
484    pub async fn scripthash_txs(
485        &self,
486        script: &Script,
487        last_seen: Option<Txid>,
488    ) -> Result<Vec<Tx>, Error> {
489        let script_hash = sha256::Hash::hash(script.as_bytes());
490        let path = match last_seen {
491            Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
492            None => format!("/scripthash/{script_hash:x}/txs"),
493        };
494
495        self.get_response_json(&path).await
496    }
497
498    /// Get mempool [`Transaction`] history for the
499    /// specified [`Script`] hash, sorted with newest first.
500    pub async fn get_mempool_scripthash_txs(&self, script: &Script) -> Result<Vec<Tx>, Error> {
501        let script_hash = sha256::Hash::hash(script.as_bytes());
502        let path = format!("/scripthash/{script_hash:x}/txs/mempool");
503
504        self.get_response_json(&path).await
505    }
506
507    /// Get statistics about the mempool.
508    pub async fn get_mempool_stats(&self) -> Result<MempoolStats, Error> {
509        self.get_response_json("/mempool").await
510    }
511
512    /// Get a list of the last 10 [`Transaction`]s to enter the mempool.
513    pub async fn get_mempool_recent_txs(&self) -> Result<Vec<MempoolRecentTx>, Error> {
514        self.get_response_json("/mempool/recent").await
515    }
516
517    /// Get the full list of [`Txid`]s in the mempool.
518    ///
519    /// The order of the [`Txid`]s is arbitrary.
520    pub async fn get_mempool_txids(&self) -> Result<Vec<Txid>, Error> {
521        self.get_response_json("/mempool/txids").await
522    }
523
524    /// Get a map where the key is the confirmation target (in number of
525    /// blocks) and the value is the estimated feerate (in sat/vB).
526    pub async fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, Error> {
527        self.get_response_json("/fee-estimates").await
528    }
529
530    /// Get a summary about a [`Block`], given its [`BlockHash`].
531    pub async fn get_block_info(&self, blockhash: &BlockHash) -> Result<BlockInfo, Error> {
532        let path = format!("/block/{blockhash}");
533
534        self.get_response_json(&path).await
535    }
536
537    /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
538    pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
539        let path = format!("/block/{blockhash}/txids");
540
541        self.get_response_json(&path).await
542    }
543
544    /// Get up to 25 [`Transaction`]s from a [`Block`], given its [`BlockHash`],
545    /// beginning at `start_index` (starts from 0 if `start_index` is `None`).
546    ///
547    /// The `start_index` value MUST be a multiple of 25,
548    /// else an error will be returned by Esplora.
549    pub async fn get_block_txs(
550        &self,
551        blockhash: &BlockHash,
552        start_index: Option<u32>,
553    ) -> Result<Vec<Tx>, Error> {
554        let path = match start_index {
555            None => format!("/block/{blockhash}/txs"),
556            Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"),
557        };
558
559        self.get_response_json(&path).await
560    }
561
562    /// Gets some recent block summaries starting at the tip or at `height` if
563    /// provided.
564    ///
565    /// The maximum number of summaries returned depends on the backend itself:
566    /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
567    pub async fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
568        let path = match height {
569            Some(height) => format!("/blocks/{height}"),
570            None => "/blocks".to_string(),
571        };
572        let blocks: Vec<BlockSummary> = self.get_response_json(&path).await?;
573        if blocks.is_empty() {
574            return Err(Error::InvalidResponse);
575        }
576        Ok(blocks)
577    }
578
579    /// Get all UTXOs locked to an address.
580    pub async fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
581        let path = format!("/address/{address}/utxo");
582
583        self.get_response_json(&path).await
584    }
585
586    /// Get all [`Utxo`]s locked to a [`Script`].
587    pub async fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
588        let script_hash = sha256::Hash::hash(script.as_bytes());
589        let path = format!("/scripthash/{script_hash}/utxo");
590
591        self.get_response_json(&path).await
592    }
593
594    /// Get the underlying base URL.
595    pub fn url(&self) -> &str {
596        &self.url
597    }
598
599    /// Get the underlying [`Client`].
600    pub fn client(&self) -> &Client {
601        &self.client
602    }
603
604    /// Sends a GET request to the given `url`, retrying failed attempts
605    /// for retryable error codes until max retries hit.
606    async fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
607        let mut delay = BASE_BACKOFF_MILLIS;
608        let mut attempts = 0;
609
610        loop {
611            match self.client.get(url).send().await? {
612                resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
613                    S::sleep(delay).await;
614                    attempts += 1;
615                    delay *= 2;
616                }
617                resp => return Ok(resp),
618            }
619        }
620    }
621}
622
623fn is_status_retryable(status: reqwest::StatusCode) -> bool {
624    RETRYABLE_ERROR_CODES.contains(&status.as_u16())
625}
626
627/// Sleeper trait that allows any async runtime to be used.
628pub trait Sleeper: 'static {
629    /// The `Future` type returned by the sleep function.
630    type Sleep: std::future::Future<Output = ()>;
631    /// Create a `Future` that completes after the specified [`Duration`].
632    fn sleep(dur: Duration) -> Self::Sleep;
633}
634
635/// The default `Sleeper` implementation using the underlying async runtime.
636#[derive(Debug, Clone, Copy)]
637pub struct DefaultSleeper;
638
639#[cfg(any(test, feature = "tokio"))]
640impl Sleeper for DefaultSleeper {
641    type Sleep = tokio::time::Sleep;
642
643    fn sleep(dur: std::time::Duration) -> Self::Sleep {
644        tokio::time::sleep(dur)
645    }
646}