Skip to content

Commit 7886297

Browse files
acidbunny21acidbunny21
authored and
acidbunny21
committed
feat(client): add async and blocking clients to submit txs package
1 parent 9c9e7a5 commit 7886297

File tree

4 files changed

+184
-31
lines changed

4 files changed

+184
-31
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 = { version = "1.0", default-features = false }
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

+50-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
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;
7-
use bitcoin::Weight;
89
pub use bitcoin::{
910
transaction, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness,
1011
};
11-
12+
use bitcoin::{FeeRate, Weight, Wtxid};
1213
use serde::Deserialize;
1314

1415
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -123,6 +124,53 @@ pub struct AddressTxsSummary {
123124
pub tx_count: u32,
124125
}
125126

127+
#[derive(Deserialize, Debug)]
128+
pub struct SubmitPackageResult {
129+
/// The transaction package result message. "success" indicates all transactions were accepted
130+
/// into or are already in the mempool.
131+
pub package_msg: String,
132+
/// Transaction results keyed by [`Wtxid`].
133+
#[serde(rename = "tx-results")]
134+
pub tx_results: HashMap<Wtxid, TxResult>,
135+
/// List of txids of replaced transactions.
136+
#[serde(rename = "replaced-transactions")]
137+
pub replaced_transactions: Option<Vec<Txid>>,
138+
}
139+
140+
#[derive(Deserialize, Debug)]
141+
pub struct TxResult {
142+
/// The transaction id.
143+
pub txid: Txid,
144+
/// The [`Wtxid`] of a different transaction with the same [`Txid`] but different witness found
145+
/// in the mempool.
146+
///
147+
/// If set, this means the submitted transaction was ignored.
148+
#[serde(rename = "other-wtxid")]
149+
pub other_wtxid: Option<Wtxid>,
150+
/// Sigops-adjusted virtual transaction size.
151+
pub vsize: Option<u32>,
152+
/// Transaction fees.
153+
pub fees: Option<MempoolFeesSubmitPackage>,
154+
/// The transaction error string, if it was rejected by the mempool
155+
pub error: Option<String>,
156+
}
157+
158+
#[derive(Deserialize, Debug)]
159+
pub struct MempoolFeesSubmitPackage {
160+
/// Transaction fee.
161+
pub base: Amount,
162+
/// The effective feerate.
163+
///
164+
/// Will be `None` if the transaction was already in the mempool. For example, the package
165+
/// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method.
166+
#[serde(rename = "effective-feerate")]
167+
pub effective_feerate: Option<FeeRate>,
168+
/// If [`Self::effective_fee_rate`] is provided, this holds the [`Wtxid`]s of the transactions
169+
/// whose fees and vsizes are included in effective-feerate.
170+
#[serde(rename = "effective-includes")]
171+
pub effective_includes: Option<Vec<Wtxid>>,
172+
}
173+
126174
impl Tx {
127175
pub fn to_tx(&self) -> Transaction {
128176
Transaction {

src/async.rs

+66-18
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111

1212
//! Esplora by way of `reqwest` HTTP client.
1313
14-
use std::collections::HashMap;
14+
use std::collections::{HashMap, HashSet};
1515
use std::marker::PhantomData;
1616
use std::str::FromStr;
1717

18-
use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
18+
use bitcoin::consensus::encode::serialize_hex;
19+
use bitcoin::consensus::{deserialize, serialize, Decodable};
1920
use bitcoin::hashes::{sha256, Hash};
2021
use bitcoin::hex::{DisplayHex, FromHex};
2122
use bitcoin::Address;
@@ -26,12 +27,12 @@ use bitcoin::{
2627
#[allow(unused_imports)]
2728
use log::{debug, error, info, trace};
2829

29-
use reqwest::{header, Client, Response};
30+
use reqwest::{header, Body, Client, Response};
3031

3132
use crate::api::AddressStats;
3233
use crate::{
33-
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
34-
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
34+
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, SubmitPackageResult, Tx,
35+
TxStatus, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
3536
};
3637

3738
#[derive(Debug, Clone)]
@@ -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`]
@@ -359,8 +366,49 @@ impl<S: Sleeper> AsyncClient<S> {
359366
}
360367

361368
/// Broadcast a [`Transaction`] to Esplora
362-
pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
363-
self.post_request_hex("/tx", transaction).await
369+
pub async fn broadcast(&self, transaction: &Transaction) -> Result<Txid, Error> {
370+
let body = serialize::<Transaction>(transaction).to_lower_hex_string();
371+
let response = self.post_request_bytes("/tx", body, None).await?;
372+
let txid = Txid::from_str(&response.text().await?).map_err(|_| Error::InvalidResponse)?;
373+
Ok(txid)
374+
}
375+
376+
/// Broadcast a package of [`Transaction`] to Esplora
377+
///
378+
/// if `maxfeerate` is provided, any transaction whose
379+
/// fee is higher will be rejected
380+
///
381+
/// if `maxburnamount` is provided, any transaction
382+
/// with higher provably unspendable outputs amount
383+
/// will be rejected
384+
pub async fn submit_package(
385+
&self,
386+
transactions: &[Transaction],
387+
maxfeerate: Option<f64>,
388+
maxburnamount: Option<f64>,
389+
) -> Result<SubmitPackageResult, Error> {
390+
let mut queryparams = HashSet::<(&str, String)>::new();
391+
if let Some(maxfeerate) = maxfeerate {
392+
queryparams.insert(("maxfeerate", maxfeerate.to_string()));
393+
}
394+
if let Some(maxburnamount) = maxburnamount {
395+
queryparams.insert(("maxburnamount", maxburnamount.to_string()));
396+
}
397+
398+
let serialized_txs = transactions
399+
.iter()
400+
.map(|tx| serialize_hex(&tx))
401+
.collect::<Vec<_>>();
402+
403+
let response = self
404+
.post_request_bytes(
405+
"/txs/package",
406+
serde_json::to_string(&serialized_txs).unwrap(),
407+
Some(queryparams),
408+
)
409+
.await?;
410+
411+
Ok(response.json::<SubmitPackageResult>().await?)
364412
}
365413

366414
/// Get the current height of the blockchain tip

src/blocking.rs

+67-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::convert::TryFrom;
1616
use std::str::FromStr;
1717
use std::thread;
1818

19+
use bitcoin::consensus::encode::serialize_hex;
1920
#[allow(unused_imports)]
2021
use log::{debug, error, info, trace};
2122

@@ -31,8 +32,8 @@ use bitcoin::{
3132

3233
use crate::api::AddressStats;
3334
use crate::{
34-
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
35-
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
35+
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, SubmitPackageResult, Tx,
36+
TxStatus, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
3637
};
3738

3839
#[derive(Debug, Clone)]
@@ -88,6 +89,24 @@ impl BlockingClient {
8889
Ok(request)
8990
}
9091

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

269288
/// Broadcast a [`Transaction`] to Esplora
270289
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
271-
let mut request = minreq::post(format!("{}/tx", self.url)).with_body(
290+
let request = self.post_request(
291+
"tx",
272292
serialize(transaction)
273293
.to_lower_hex_string()
274294
.as_bytes()
275295
.to_vec(),
276-
);
296+
)?;
277297

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

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

287344
match request.send() {
@@ -290,7 +347,7 @@ impl BlockingClient {
290347
let message = resp.as_str().unwrap_or_default().to_string();
291348
Err(Error::HttpResponse { status, message })
292349
}
293-
Ok(_resp) => Ok(()),
350+
Ok(resp) => Ok(resp.json::<SubmitPackageResult>().map_err(Error::Minreq)?),
294351
Err(e) => Err(Error::Minreq(e)),
295352
}
296353
}

0 commit comments

Comments
 (0)