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 ;
1717
1818use bitcoin:: block:: Header as BlockHeader ;
19- use bitcoin:: consensus:: { deserialize, serialize, Decodable , Encodable } ;
19+ use bitcoin:: consensus:: encode:: serialize_hex;
20+ use bitcoin:: consensus:: { deserialize, serialize, Decodable } ;
2021use bitcoin:: hashes:: { sha256, Hash } ;
2122use bitcoin:: hex:: { DisplayHex , FromHex } ;
2223use bitcoin:: { Address , Block , BlockHash , MerkleBlock , Script , Transaction , Txid } ;
2324
2425#[ allow( unused_imports) ]
2526use log:: { debug, error, info, trace} ;
2627
27- use reqwest:: { header, Client , Response } ;
28+ use reqwest:: { header, Body , Client , Response } ;
2829
2930use crate :: {
3031 AddressStats , BlockInfo , BlockStatus , BlockSummary , Builder , Error , MempoolRecentTx ,
31- MempoolStats , MerkleProof , OutputStatus , ScriptHashStats , Tx , TxStatus , Utxo ,
32- BASE_BACKOFF_MILLIS , RETRYABLE_ERROR_CODES ,
32+ MempoolStats , MerkleProof , OutputStatus , ScriptHashStats , SubmitPackageResult , Tx , TxStatus ,
33+ Utxo , BASE_BACKOFF_MILLIS , RETRYABLE_ERROR_CODES ,
3334} ;
3435
3536#[ derive( Debug , Clone ) ]
@@ -247,21 +248,27 @@ impl<S: Sleeper> AsyncClient<S> {
247248 }
248249 }
249250
250- /// Make an HTTP POST request to given URL, serializing from any `T` that
251- /// implement [`bitcoin::consensus::Encodable`].
252- ///
253- /// It should be used when requesting Esplora endpoints that expected a
254- /// native bitcoin type serialized with [`bitcoin::consensus::Encodable`].
251+ /// Make an HTTP POST request to given URL, converting any `T` that
252+ /// implement [`Into<Body>`] and setting query parameters, if any.
255253 ///
256254 /// # Errors
257255 ///
258256 /// This function will return an error either from the HTTP client, or the
259- /// [`bitcoin::consensus::Encodable`] serialization.
260- async fn post_request_hex < T : Encodable > ( & self , path : & str , body : T ) -> Result < ( ) , Error > {
261- let url = format ! ( "{}{}" , self . url, path) ;
262- let body = serialize :: < T > ( & body) . to_lower_hex_string ( ) ;
257+ /// response's [`serde_json`] deserialization.
258+ async fn post_request_bytes < T : Into < Body > > (
259+ & self ,
260+ path : & str ,
261+ body : T ,
262+ query_params : Option < HashSet < ( & str , String ) > > ,
263+ ) -> Result < Response , Error > {
264+ let url: String = format ! ( "{}{}" , self . url, path) ;
265+ let mut request = self . client . post ( url) . body ( body) ;
266+
267+ for param in query_params. unwrap_or_default ( ) {
268+ request = request. query ( & param) ;
269+ }
263270
264- let response = self . client . post ( url ) . body ( body ) . send ( ) . await ?;
271+ let response = request . send ( ) . await ?;
265272
266273 if !response. status ( ) . is_success ( ) {
267274 return Err ( Error :: HttpResponse {
@@ -270,7 +277,7 @@ impl<S: Sleeper> AsyncClient<S> {
270277 } ) ;
271278 }
272279
273- Ok ( ( ) )
280+ Ok ( response )
274281 }
275282
276283 /// Get a [`Transaction`] option given its [`Txid`]
@@ -363,8 +370,49 @@ impl<S: Sleeper> AsyncClient<S> {
363370 }
364371
365372 /// Broadcast a [`Transaction`] to Esplora
366- pub async fn broadcast ( & self , transaction : & Transaction ) -> Result < ( ) , Error > {
367- self . post_request_hex ( "/tx" , transaction) . await
373+ pub async fn broadcast ( & self , transaction : & Transaction ) -> Result < Txid , Error > {
374+ let body = serialize :: < Transaction > ( transaction) . to_lower_hex_string ( ) ;
375+ let response = self . post_request_bytes ( "/tx" , body, None ) . await ?;
376+ let txid = Txid :: from_str ( & response. text ( ) . await ?) . map_err ( |_| Error :: InvalidResponse ) ?;
377+ Ok ( txid)
378+ }
379+
380+ /// Broadcast a package of [`Transaction`] to Esplora
381+ ///
382+ /// if `maxfeerate` is provided, any transaction whose
383+ /// fee is higher will be rejected
384+ ///
385+ /// if `maxburnamount` is provided, any transaction
386+ /// with higher provably unspendable outputs amount
387+ /// will be rejected
388+ pub async fn submit_package (
389+ & self ,
390+ transactions : & [ Transaction ] ,
391+ maxfeerate : Option < f64 > ,
392+ maxburnamount : Option < f64 > ,
393+ ) -> Result < SubmitPackageResult , Error > {
394+ let mut queryparams = HashSet :: < ( & str , String ) > :: new ( ) ;
395+ if let Some ( maxfeerate) = maxfeerate {
396+ queryparams. insert ( ( "maxfeerate" , maxfeerate. to_string ( ) ) ) ;
397+ }
398+ if let Some ( maxburnamount) = maxburnamount {
399+ queryparams. insert ( ( "maxburnamount" , maxburnamount. to_string ( ) ) ) ;
400+ }
401+
402+ let serialized_txs = transactions
403+ . iter ( )
404+ . map ( |tx| serialize_hex ( & tx) )
405+ . collect :: < Vec < _ > > ( ) ;
406+
407+ let response = self
408+ . post_request_bytes (
409+ "/txs/package" ,
410+ serde_json:: to_string ( & serialized_txs) . unwrap ( ) ,
411+ Some ( queryparams) ,
412+ )
413+ . await ?;
414+
415+ Ok ( response. json :: < SubmitPackageResult > ( ) . await ?)
368416 }
369417
370418 /// Get the current height of the blockchain tip
0 commit comments