Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6a22216

Browse files
committedJan 23, 2025
feat(client): add async and blocking clients to submit txs package
1 parent 8f49c84 commit 6a22216

File tree

4 files changed

+142
-13
lines changed

4 files changed

+142
-13
lines changed
 

‎Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ path = "src/lib.rs"
1818

1919
[dependencies]
2020
serde = { version = "1.0", features = ["derive"] }
21+
serde_json = "1.0"
2122
bitcoin = { version = "0.32", features = ["serde", "std"], default-features = false }
2223
hex = { version = "0.2", package = "hex-conservative" }
2324
log = "^0.4"
@@ -28,7 +29,6 @@ reqwest = { version = "0.11", features = ["json"], default-features = false, op
2829
tokio = { version = "1", features = ["time"], optional = true }
2930

3031
[dev-dependencies]
31-
serde_json = "1.0"
3232
tokio = { version = "1.20.1", features = ["full"] }
3333
electrsd = { version = "0.28.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }
3434
lazy_static = "1.4.0"

‎src/api.rs

+30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//!
33
//! See: <https://github.com/Blockstream/esplora/blob/master/API.md>
44
5+
use std::collections::HashMap;
6+
57
pub use bitcoin::consensus::{deserialize, serialize};
68
pub use bitcoin::hex::FromHex;
79
use bitcoin::Weight;
@@ -123,6 +125,34 @@ pub struct AddressTxsSummary {
123125
pub tx_count: u32,
124126
}
125127

128+
#[derive(Deserialize, Debug)]
129+
pub struct SubmitPackageResult {
130+
pub package_msg: String,
131+
#[serde(rename = "tx-results")]
132+
pub tx_results: HashMap<String, TxResult>,
133+
#[serde(rename = "replaced-transactions")]
134+
pub replaced_transactions: Option<Vec<String>>,
135+
}
136+
137+
#[derive( Deserialize, Debug)]
138+
pub struct TxResult {
139+
pub txid: String,
140+
#[serde(rename = "other-wtxid")]
141+
pub other_wtxid: Option<String>,
142+
pub vsize: Option<u32>,
143+
pub fees: Option<MempoolFeesSubmitPackage>,
144+
pub error: Option<String>,
145+
}
146+
147+
#[derive(Deserialize, Debug)]
148+
pub struct MempoolFeesSubmitPackage {
149+
pub base: f64,
150+
#[serde(rename = "effective-feerate")]
151+
pub effective_feerate: Option<f64>,
152+
#[serde(rename = "effective-includes")]
153+
pub effective_includes: Option<Vec<String>>,
154+
}
155+
126156
impl Tx {
127157
pub fn to_tx(&self) -> Transaction {
128158
Transaction {

‎src/async.rs

+45-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ use reqwest::{header, Client, Response};
3030

3131
use crate::api::AddressStats;
3232
use crate::{
33-
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
34-
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
33+
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, SubmitPackageResult, Tx, TxStatus, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES
3534
};
3635

3736
#[derive(Debug, Clone)]
@@ -363,6 +362,50 @@ impl<S: Sleeper> AsyncClient<S> {
363362
self.post_request_hex("/tx", transaction).await
364363
}
365364

365+
/// Broadcast a package of [`Transaction`] to Esplora
366+
///
367+
/// if `maxfeerate` is provided, any transaction whose
368+
/// fee is higher will be rejected
369+
///
370+
/// if `maxburnamount` is provided, any transaction
371+
/// with higher provably unspendable outputs amount
372+
/// will be rejected
373+
pub async fn broadcast_package(
374+
&self,
375+
transactions: &[Transaction],
376+
maxfeerate: Option<f64>,
377+
maxburnamount: Option<f64>)
378+
-> Result<SubmitPackageResult, Error> {
379+
let url = format!("{}/txs/package", self.url);
380+
381+
let serialized_txs = transactions
382+
.iter()
383+
.map(|tx| serialize(&tx).to_lower_hex_string())
384+
.collect::<Vec<_>>();
385+
386+
let mut request = self.client.post(url).body(
387+
serde_json::to_string(&serialized_txs).unwrap().as_bytes().to_vec()
388+
);
389+
390+
if let Some(maxfeerate) = maxfeerate {
391+
request = request.query(&[("maxfeerate", maxfeerate.to_string())])
392+
}
393+
394+
if let Some(maxburnamount) = maxburnamount {
395+
request = request.query(&[("maxburnamount", maxburnamount.to_string())])
396+
}
397+
398+
let response = request.send().await?;
399+
if !response.status().is_success() {
400+
return Err(Error::HttpResponse {
401+
status: response.status().as_u16(),
402+
message: response.text().await?,
403+
});
404+
}
405+
406+
response.json::<SubmitPackageResult>().await.map_err(Error::Reqwest)
407+
}
408+
366409
/// Get the current height of the blockchain tip
367410
pub async fn get_height(&self) -> Result<u32, Error> {
368411
self.get_response_text("/blocks/tip/height")

‎src/blocking.rs

+66-10
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use bitcoin::{
3131

3232
use crate::api::AddressStats;
3333
use crate::{
34-
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
35-
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
34+
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, SubmitPackageResult, Tx,
35+
TxStatus, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
3636
};
3737

3838
#[derive(Debug, Clone)]
@@ -88,6 +88,24 @@ impl BlockingClient {
8888
Ok(request)
8989
}
9090

91+
fn post_request<T>(&self, path: &str, body: T) -> Result<Request, Error>
92+
where
93+
T: Into<Vec<u8>>,
94+
{
95+
let mut request = minreq::post(format!("{}/{}", self.url, path)).with_body(body);
96+
97+
if let Some(proxy) = &self.proxy {
98+
let proxy = Proxy::new(proxy.as_str())?;
99+
request = request.with_proxy(proxy);
100+
}
101+
102+
if let Some(timeout) = &self.timeout {
103+
request = request.with_timeout(*timeout);
104+
}
105+
106+
Ok(request)
107+
}
108+
91109
fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
92110
match self.get_with_retry(path) {
93111
Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
@@ -268,20 +286,58 @@ impl BlockingClient {
268286

269287
/// Broadcast a [`Transaction`] to Esplora
270288
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
271-
let mut request = minreq::post(format!("{}/tx", self.url)).with_body(
289+
let request = self.post_request(
290+
"tx",
272291
serialize(transaction)
273292
.to_lower_hex_string()
274293
.as_bytes()
275294
.to_vec(),
276-
);
295+
)?;
277296

278-
if let Some(proxy) = &self.proxy {
279-
let proxy = Proxy::new(proxy.as_str())?;
280-
request = request.with_proxy(proxy);
297+
match request.send() {
298+
Ok(resp) if !is_status_ok(resp.status_code) => {
299+
let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
300+
let message = resp.as_str().unwrap_or_default().to_string();
301+
Err(Error::HttpResponse { status, message })
302+
}
303+
Ok(_resp) => Ok(()),
304+
Err(e) => Err(Error::Minreq(e)),
281305
}
306+
}
282307

283-
if let Some(timeout) = &self.timeout {
284-
request = request.with_timeout(*timeout);
308+
/// Broadcast a package of [`Transaction`] to Esplora
309+
///
310+
/// if `maxfeerate` is provided, any transaction whose
311+
/// fee is higher will be rejected
312+
///
313+
/// if `maxburnamount` is provided, any transaction
314+
/// with higher provably unspendable outputs amount
315+
/// will be rejected
316+
pub fn broadcast_package(
317+
&self,
318+
transactions: &[Transaction],
319+
maxfeerate: Option<f64>,
320+
maxburnamount: Option<f64>,
321+
) -> Result<SubmitPackageResult, Error> {
322+
let serialized_txs = transactions
323+
.iter()
324+
.map(|tx| serialize(&tx).to_lower_hex_string())
325+
.collect::<Vec<_>>();
326+
327+
let mut request = self.post_request(
328+
"txs/package",
329+
serde_json::to_string(&serialized_txs)
330+
.unwrap()
331+
.as_bytes()
332+
.to_vec(),
333+
)?;
334+
335+
if let Some(maxfeerate) = maxfeerate {
336+
request = request.with_param("maxfeerate", maxfeerate.to_string())
337+
}
338+
339+
if let Some(maxburnamount) = maxburnamount {
340+
request = request.with_param("maxburnamount", maxburnamount.to_string())
285341
}
286342

287343
match request.send() {
@@ -290,7 +346,7 @@ impl BlockingClient {
290346
let message = resp.as_str().unwrap_or_default().to_string();
291347
Err(Error::HttpResponse { status, message })
292348
}
293-
Ok(_resp) => Ok(()),
349+
Ok(resp) => Ok(resp.json::<SubmitPackageResult>().map_err(Error::Minreq)?),
294350
Err(e) => Err(Error::Minreq(e)),
295351
}
296352
}

0 commit comments

Comments
 (0)
Please sign in to comment.