1111
1212//! Esplora by way of `reqwest` HTTP client.
1313
14- use std:: collections:: HashMap ;
14+ use std:: collections:: { HashMap , HashSet } ;
1515use std:: marker:: PhantomData ;
1616use std:: str:: FromStr ;
1717use std:: time:: Duration ;
1818
1919use bitcoin:: block:: Header as BlockHeader ;
20- use bitcoin:: consensus:: { deserialize, serialize, Decodable , Encodable } ;
20+ use bitcoin:: consensus:: encode:: serialize_hex;
21+ use bitcoin:: consensus:: { deserialize, serialize, Decodable } ;
2122use bitcoin:: hashes:: { sha256, Hash } ;
2223use bitcoin:: hex:: { DisplayHex , FromHex } ;
2324use bitcoin:: { Address , Block , BlockHash , MerkleBlock , Script , Transaction , Txid } ;
2425
2526#[ allow( unused_imports) ]
2627use log:: { debug, error, info, trace} ;
2728
28- use reqwest:: { header, Client , Response } ;
29+ use reqwest:: { header, Body , Client , Response } ;
2930
3031use crate :: {
3132 AddressStats , BlockInfo , BlockStatus , BlockSummary , Builder , Error , MempoolRecentTx ,
32- MempoolStats , MerkleProof , OutputStatus , ScriptHashStats , Tx , TxStatus , Utxo ,
33- BASE_BACKOFF_MILLIS , RETRYABLE_ERROR_CODES ,
33+ MempoolStats , MerkleProof , OutputStatus , ScriptHashStats , SubmitPackageResult , Tx , TxStatus ,
34+ Utxo , BASE_BACKOFF_MILLIS , RETRYABLE_ERROR_CODES ,
3435} ;
3536
3637/// An async client for interacting with an Esplora API server.
@@ -249,21 +250,27 @@ impl<S: Sleeper> AsyncClient<S> {
249250 }
250251 }
251252
252- /// Make an HTTP POST request to given URL, serializing from any `T` that
253- /// implement [`bitcoin::consensus::Encodable`].
254- ///
255- /// It should be used when requesting Esplora endpoints that expected a
256- /// native bitcoin type serialized with [`bitcoin::consensus::Encodable`].
253+ /// Make an HTTP POST request to given URL, converting any `T` that
254+ /// implement [`Into<Body>`] and setting query parameters, if any.
257255 ///
258256 /// # Errors
259257 ///
260258 /// This function will return an error either from the HTTP client, or the
261- /// [`bitcoin::consensus::Encodable`] serialization.
262- async fn post_request_hex < T : Encodable > ( & self , path : & str , body : T ) -> Result < ( ) , Error > {
263- let url = format ! ( "{}{}" , self . url, path) ;
264- let body = serialize :: < T > ( & body) . to_lower_hex_string ( ) ;
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+ }
265272
266- let response = self . client . post ( url ) . body ( body ) . send ( ) . await ?;
273+ let response = request . send ( ) . await ?;
267274
268275 if !response. status ( ) . is_success ( ) {
269276 return Err ( Error :: HttpResponse {
@@ -272,7 +279,7 @@ impl<S: Sleeper> AsyncClient<S> {
272279 } ) ;
273280 }
274281
275- Ok ( ( ) )
282+ Ok ( response )
276283 }
277284
278285 /// Get a [`Transaction`] option given its [`Txid`]
@@ -366,7 +373,49 @@ impl<S: Sleeper> AsyncClient<S> {
366373
367374 /// Broadcast a [`Transaction`] to Esplora
368375 pub async fn broadcast ( & self , transaction : & Transaction ) -> Result < ( ) , Error > {
369- self . post_request_hex ( "/tx" , transaction) . await
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`] 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 ( ) ,
414+ Some ( queryparams) ,
415+ )
416+ . await ?;
417+
418+ Ok ( response. json :: < SubmitPackageResult > ( ) . await ?)
370419 }
371420
372421 /// Get the current height of the blockchain tip
0 commit comments