99// You may not use this file except in accordance with one or both of these
1010// licenses.
1111
12- //! Esplora by way of `reqwest` HTTP client.
13-
14- use std:: collections:: HashMap ;
15- use std:: marker:: PhantomData ;
16- use std:: str:: FromStr ;
12+ //! Esplora by way of `reqwest` or `async-minreq` HTTP client.
1713
1814use bitcoin:: consensus:: { deserialize, serialize, Decodable , Encodable } ;
1915use bitcoin:: hashes:: { sha256, Hash } ;
@@ -22,32 +18,44 @@ use bitcoin::Address;
2218use bitcoin:: {
2319 block:: Header as BlockHeader , Block , BlockHash , MerkleBlock , Script , Transaction , Txid ,
2420} ;
25-
26- #[ allow( unused_imports) ]
27- use log:: { debug, error, info, trace} ;
28-
29- use reqwest:: { header, Client , Response } ;
21+ use std:: collections:: HashMap ;
22+ use std:: marker:: PhantomData ;
23+ use std:: str:: FromStr ;
3024
3125use crate :: api:: AddressStats ;
3226use crate :: {
3327 BlockStatus , BlockSummary , Builder , Error , MerkleProof , OutputStatus , Tx , TxStatus ,
3428 BASE_BACKOFF_MILLIS , RETRYABLE_ERROR_CODES ,
3529} ;
30+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
31+ use async_minreq:: { Method , Request , Response } ;
32+ #[ cfg( feature = "async" ) ]
33+ use reqwest:: { header, Client , Response } ;
34+
35+ #[ allow( unused_imports) ]
36+ use log:: { debug, error, info, trace} ;
37+
38+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
39+ /// Valid HTTP code
40+ const VALID_HTTP_CODE : i32 = 299 ;
3641
3742#[ derive( Debug , Clone ) ]
3843pub struct AsyncClient < S = DefaultSleeper > {
3944 /// The URL of the Esplora Server.
4045 url : String ,
41- /// The inner [`reqwest::Client`] to make HTTP requests.
42- client : Client ,
43- /// Number of times to retry a request
46+ /// Number of times to retry a request.
4447 max_retries : usize ,
45-
46- /// Marker for the type of sleeper used
48+ #[ cfg( feature = "async" ) ]
49+ client : Client ,
50+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
51+ /// Default headers (applied to every request).
52+ headers : HashMap < String , String > ,
53+ /// Marker for the sleeper.
4754 marker : PhantomData < S > ,
4855}
4956
5057impl < S : Sleeper > AsyncClient < S > {
58+ #[ cfg( feature = "async" ) ]
5159 /// Build an async client from a builder
5260 pub fn from_builder ( builder : Builder ) -> Result < Self , Error > {
5361 let mut client_builder = Client :: builder ( ) ;
@@ -82,6 +90,18 @@ impl<S: Sleeper> AsyncClient<S> {
8290 } )
8391 }
8492
93+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
94+ /// Build an async client from a builder
95+ pub fn from_builder ( builder : Builder ) -> Result < Self , Error > {
96+ Ok ( AsyncClient {
97+ url : builder. base_url ,
98+ max_retries : builder. max_retries ,
99+ headers : builder. headers ,
100+ marker : PhantomData ,
101+ } )
102+ }
103+
104+ #[ cfg( feature = "async" ) ]
85105 pub fn from_client ( url : String , client : Client ) -> Self {
86106 AsyncClient {
87107 url,
@@ -90,6 +110,15 @@ impl<S: Sleeper> AsyncClient<S> {
90110 marker : PhantomData ,
91111 }
92112 }
113+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
114+ pub fn from_client ( url : String , headers : HashMap < String , String > ) -> Self {
115+ AsyncClient {
116+ url,
117+ headers,
118+ max_retries : crate :: DEFAULT_MAX_RETRIES ,
119+ marker : PhantomData ,
120+ }
121+ }
93122
94123 /// Make an HTTP GET request to given URL, deserializing to any `T` that
95124 /// implement [`bitcoin::consensus::Decodable`].
@@ -106,14 +135,32 @@ impl<S: Sleeper> AsyncClient<S> {
106135 let url = format ! ( "{}{}" , self . url, path) ;
107136 let response = self . get_with_retry ( & url) . await ?;
108137
109- if !response. status ( ) . is_success ( ) {
110- return Err ( Error :: HttpResponse {
111- status : response. status ( ) . as_u16 ( ) ,
112- message : response. text ( ) . await ?,
113- } ) ;
138+ #[ cfg( feature = "async" ) ]
139+ {
140+ if !response. status ( ) . is_success ( ) {
141+ return Err ( Error :: HttpResponse {
142+ status : response. status ( ) . as_u16 ( ) ,
143+ message : response. text ( ) . await ?,
144+ } ) ;
145+ }
146+
147+ Ok ( deserialize :: < T > ( & response. bytes ( ) . await ?) ?)
114148 }
115149
116- Ok ( deserialize :: < T > ( & response. bytes ( ) . await ?) ?)
150+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
151+ {
152+ if response. status_code > VALID_HTTP_CODE {
153+ return Err ( Error :: HttpResponse {
154+ status : response. status_code as u16 ,
155+ message : match response. as_str ( ) {
156+ Ok ( resp) => resp. to_string ( ) ,
157+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
158+ } ,
159+ } ) ;
160+ }
161+
162+ return Ok ( deserialize :: < T > ( response. as_bytes ( ) ) ?) ;
163+ }
117164 }
118165
119166 /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -146,14 +193,31 @@ impl<S: Sleeper> AsyncClient<S> {
146193 let url = format ! ( "{}{}" , self . url, path) ;
147194 let response = self . get_with_retry ( & url) . await ?;
148195
149- if !response. status ( ) . is_success ( ) {
150- return Err ( Error :: HttpResponse {
151- status : response. status ( ) . as_u16 ( ) ,
152- message : response. text ( ) . await ?,
153- } ) ;
196+ #[ cfg( feature = "async" ) ]
197+ {
198+ if !response. status ( ) . is_success ( ) {
199+ return Err ( Error :: HttpResponse {
200+ status : response. status ( ) . as_u16 ( ) ,
201+ message : response. text ( ) . await ?,
202+ } ) ;
203+ }
204+
205+ response. json :: < T > ( ) . await . map_err ( Error :: Reqwest )
154206 }
207+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
208+ {
209+ if response. status_code > VALID_HTTP_CODE {
210+ return Err ( Error :: HttpResponse {
211+ status : response. status_code as u16 ,
212+ message : match response. as_str ( ) {
213+ Ok ( resp) => resp. to_string ( ) ,
214+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
215+ } ,
216+ } ) ;
217+ }
155218
156- response. json :: < T > ( ) . await . map_err ( Error :: Reqwest )
219+ return response. json ( ) . map_err ( Error :: AsyncMinreq ) ;
220+ }
157221 }
158222
159223 /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -188,15 +252,38 @@ impl<S: Sleeper> AsyncClient<S> {
188252 let url = format ! ( "{}{}" , self . url, path) ;
189253 let response = self . get_with_retry ( & url) . await ?;
190254
191- if !response. status ( ) . is_success ( ) {
192- return Err ( Error :: HttpResponse {
193- status : response. status ( ) . as_u16 ( ) ,
194- message : response. text ( ) . await ?,
195- } ) ;
255+ #[ cfg( feature = "async" ) ]
256+ {
257+ if !response. status ( ) . is_success ( ) {
258+ return Err ( Error :: HttpResponse {
259+ status : response. status ( ) . as_u16 ( ) ,
260+ message : response. text ( ) . await ?,
261+ } ) ;
262+ }
263+
264+ let hex_str = response. text ( ) . await ?;
265+ Ok ( deserialize ( & Vec :: from_hex ( & hex_str) ?) ?)
196266 }
197267
198- let hex_str = response. text ( ) . await ?;
199- Ok ( deserialize ( & Vec :: from_hex ( & hex_str) ?) ?)
268+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
269+ {
270+ if response. status_code > VALID_HTTP_CODE {
271+ return Err ( Error :: HttpResponse {
272+ status : response. status_code as u16 ,
273+ message : match response. as_str ( ) {
274+ Ok ( resp) => resp. to_string ( ) ,
275+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
276+ } ,
277+ } ) ;
278+ }
279+
280+ let hex_str = match response. as_str ( ) {
281+ Ok ( resp) => resp. to_string ( ) ,
282+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
283+ } ;
284+
285+ return Ok ( deserialize ( & Vec :: from_hex ( & hex_str) ?) ?) ;
286+ }
200287 }
201288
202289 /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -225,14 +312,35 @@ impl<S: Sleeper> AsyncClient<S> {
225312 let url = format ! ( "{}{}" , self . url, path) ;
226313 let response = self . get_with_retry ( & url) . await ?;
227314
228- if !response. status ( ) . is_success ( ) {
229- return Err ( Error :: HttpResponse {
230- status : response. status ( ) . as_u16 ( ) ,
231- message : response. text ( ) . await ?,
232- } ) ;
315+ #[ cfg( feature = "async" ) ]
316+ {
317+ if !response. status ( ) . is_success ( ) {
318+ return Err ( Error :: HttpResponse {
319+ status : response. status ( ) . as_u16 ( ) ,
320+ message : response. text ( ) . await ?,
321+ } ) ;
322+ }
323+
324+ Ok ( response. text ( ) . await ?)
233325 }
234326
235- Ok ( response. text ( ) . await ?)
327+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
328+ {
329+ if response. status_code > VALID_HTTP_CODE {
330+ return Err ( Error :: HttpResponse {
331+ status : response. status_code as u16 ,
332+ message : match response. as_str ( ) {
333+ Ok ( resp) => resp. to_string ( ) ,
334+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
335+ } ,
336+ } ) ;
337+ }
338+
339+ return Ok ( match response. as_str ( ) {
340+ Ok ( resp) => resp. to_string ( ) ,
341+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
342+ } ) ;
343+ }
236344 }
237345
238346 /// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -263,15 +371,36 @@ impl<S: Sleeper> AsyncClient<S> {
263371 let url = format ! ( "{}{}" , self . url, path) ;
264372 let body = serialize :: < T > ( & body) . to_lower_hex_string ( ) ;
265373
266- let response = self . client . post ( url) . body ( body) . send ( ) . await ?;
374+ #[ cfg( feature = "async" ) ]
375+ {
376+ let response = self . client . post ( url) . body ( body) . send ( ) . await ?;
267377
268- if !response. status ( ) . is_success ( ) {
269- return Err ( Error :: HttpResponse {
270- status : response. status ( ) . as_u16 ( ) ,
271- message : response. text ( ) . await ?,
272- } ) ;
273- }
378+ if !response. status ( ) . is_success ( ) {
379+ return Err ( Error :: HttpResponse {
380+ status : response. status ( ) . as_u16 ( ) ,
381+ message : response. text ( ) . await ?,
382+ } ) ;
383+ }
384+
385+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
386+ {
387+ let mut request = Request :: new ( Method :: Post , & url) . with_body ( body) ;
388+ for ( key, value) in & self . headers {
389+ request = request. with_header ( key, value) ;
390+ }
274391
392+ let response = request. send ( ) . await . map_err ( Error :: AsyncMinreq ) ?;
393+ if response. status_code > VALID_HTTP_CODE {
394+ return Err ( Error :: HttpResponse {
395+ status : response. status_code as u16 ,
396+ message : match response. as_str ( ) {
397+ Ok ( resp) => resp. to_string ( ) ,
398+ Err ( _) => return Err ( Error :: InvalidResponse ) ,
399+ } ,
400+ } ) ;
401+ }
402+ }
403+ }
275404 Ok ( ( ) )
276405 }
277406
@@ -454,6 +583,7 @@ impl<S: Sleeper> AsyncClient<S> {
454583 & self . url
455584 }
456585
586+ #[ cfg( feature = "async" ) ]
457587 /// Get the underlying [`Client`].
458588 pub fn client ( & self ) -> & Client {
459589 & self . client
@@ -465,6 +595,7 @@ impl<S: Sleeper> AsyncClient<S> {
465595 let mut delay = BASE_BACKOFF_MILLIS ;
466596 let mut attempts = 0 ;
467597
598+ #[ cfg( feature = "async" ) ]
468599 loop {
469600 match self . client . get ( url) . send ( ) . await ? {
470601 resp if attempts < self . max_retries && is_status_retryable ( resp. status ( ) ) => {
@@ -475,13 +606,40 @@ impl<S: Sleeper> AsyncClient<S> {
475606 resp => return Ok ( resp) ,
476607 }
477608 }
609+
610+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
611+ {
612+ loop {
613+ let mut request = Request :: new ( Method :: Get , url) ;
614+ for ( key, value) in & self . headers {
615+ request = request. with_header ( key, value) ;
616+ }
617+
618+ match request. send ( ) . await ? {
619+ resp if attempts < self . max_retries
620+ && is_status_retryable ( resp. status_code ) =>
621+ {
622+ S :: sleep ( delay) . await ;
623+ attempts += 1 ;
624+ delay *= 2 ;
625+ }
626+ resp => return Ok ( resp) ,
627+ }
628+ }
629+ }
478630 }
479631}
480632
633+ #[ cfg( feature = "async" ) ]
481634fn is_status_retryable ( status : reqwest:: StatusCode ) -> bool {
482635 RETRYABLE_ERROR_CODES . contains ( & status. as_u16 ( ) )
483636}
484637
638+ #[ cfg( all( feature = "async-minreq" , not( feature = "async" ) ) ) ]
639+ fn is_status_retryable ( status : i32 ) -> bool {
640+ RETRYABLE_ERROR_CODES . contains ( & ( status as u16 ) )
641+ }
642+
485643pub trait Sleeper : ' static {
486644 type Sleep : std:: future:: Future < Output = ( ) > ;
487645 fn sleep ( dur : std:: time:: Duration ) -> Self :: Sleep ;
0 commit comments