bitcoin_ext/
esplora.rs

1use std::collections::{HashMap, HashSet};
2
3use bdk_esplora::esplora_client::Amount;
4use bitcoin::{FeeRate, Transaction, Txid, Wtxid};
5use bitcoin::consensus::encode::serialize_hex;
6use reqwest::{Body, Response};
7use serde::Deserialize;
8
9#[derive(Deserialize, Debug)]
10pub struct SubmitPackageResult {
11	/// The transaction package result message. "success" indicates all transactions were accepted
12	/// into or are already in the mempool.
13	pub package_msg: String,
14	/// Transaction results keyed by [`Wtxid`].
15	#[serde(rename = "tx-results")]
16	pub tx_results: HashMap<Wtxid, TxResult>,
17	/// List of txids of replaced transactions.
18	#[serde(rename = "replaced-transactions")]
19	pub replaced_transactions: Option<Vec<Txid>>,
20}
21
22#[derive(Deserialize, Debug)]
23pub struct TxResult {
24	/// The transaction id.
25	pub txid: Txid,
26	/// The [`Wtxid`] of a different transaction with the same [`Txid`] but different witness found
27	/// in the mempool.
28	///
29	/// If set, this means the submitted transaction was ignored.
30	#[serde(rename = "other-wtxid")]
31	pub other_wtxid: Option<Wtxid>,
32	/// Sigops-adjusted virtual transaction size.
33	pub vsize: Option<u32>,
34	/// Transaction fees.
35	pub fees: Option<MempoolFeesSubmitPackage>,
36	/// The transaction error string, if it was rejected by the mempool
37	pub error: Option<String>,
38}
39
40#[derive(Deserialize, Debug)]
41pub struct MempoolFeesSubmitPackage {
42	/// Transaction fee.
43	#[serde(with = "bitcoin::amount::serde::as_btc")]
44	pub base: Amount,
45	/// The effective feerate.
46	///
47	/// Will be `None` if the transaction was already in the mempool. For example, the package
48	/// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method.
49	#[serde(rename = "effective-feerate", default, deserialize_with = "deserialize_feerate")]
50	pub effective_feerate: Option<FeeRate>,
51	/// If [Self::effective_feerate] is provided, this holds the [`Wtxid`]s of the transactions
52	/// whose fees and vsizes are included in effective-feerate.
53	#[serde(rename = "effective-includes")]
54	pub effective_includes: Option<Vec<Wtxid>>,
55}
56
57#[async_trait::async_trait]
58pub trait EsploraClientExt {
59	fn _client(&self) -> &reqwest::Client;
60	fn _base_url(&self) -> &str;
61
62	/// Make an HTTP POST request to given URL, converting any `T` that
63	/// implement [`Into<Body>`] and setting query parameters, if any.
64	///
65	/// # Errors
66	///
67	/// This function will return an error either from the HTTP client, or the
68	/// response's [`serde_json`] deserialization.
69	// implementation borrowed from esplora-client crate
70	async fn post_request_bytes<T: Into<Body> + Send>(
71		&self,
72		path: &str,
73		body: T,
74		query_params: Option<HashSet<(&str, String)>>,
75	) -> Result<Response, bdk_esplora::esplora_client::Error> {
76		let url: String = format!("{}{}", self._base_url(), path);
77		let mut request = self._client().post(url).body(body);
78
79		for param in query_params.unwrap_or_default() {
80			request = request.query(&param);
81		}
82
83		let response = request.send().await?;
84
85		if !response.status().is_success() {
86			return Err(bdk_esplora::esplora_client::Error::HttpResponse {
87				status: response.status().as_u16(),
88				message: response.text().await?,
89			});
90		}
91
92		Ok(response)
93	}
94
95	/// Broadcast a package of [`Transaction`] to Esplora
96	///
97	/// if `maxfeerate` is provided, any transaction whose
98	/// fee is higher will be rejected
99	///
100	/// if  `maxburnamount` is provided, any transaction
101	/// with higher provably unspendable outputs amount
102	/// will be rejected
103	async fn submit_package(
104		&self,
105		transactions: &[Transaction],
106		maxfeerate: Option<f64>,
107		maxburnamount: Option<f64>,
108	) -> Result<SubmitPackageResult, bdk_esplora::esplora_client::Error> {
109		let mut queryparams = HashSet::<(&str, String)>::new();
110		if let Some(maxfeerate) = maxfeerate {
111			queryparams.insert(("maxfeerate", maxfeerate.to_string()));
112		}
113		if let Some(maxburnamount) = maxburnamount {
114			queryparams.insert(("maxburnamount", maxburnamount.to_string()));
115		}
116
117		let serialized_txs = transactions
118			.iter()
119			.map(|tx| serialize_hex(&tx))
120			.collect::<Vec<_>>();
121
122		let response = self
123			.post_request_bytes(
124				"/txs/package",
125				serde_json::to_string(&serialized_txs).unwrap(),
126				Some(queryparams),
127			)
128			.await?;
129
130		Ok(response.json::<SubmitPackageResult>().await?)
131	}
132}
133
134impl EsploraClientExt for bdk_esplora::esplora_client::AsyncClient {
135	fn _client(&self) -> &reqwest::Client { self.client() }
136	fn _base_url(&self) -> &str { self.url() }
137}
138
139fn deserialize_feerate<'de, D>(d: D) -> Result<Option<FeeRate>, D::Error>
140where
141	D: serde::de::Deserializer<'de>,
142{
143	use serde::de::Error;
144
145	let btc_per_kvb = match Option::<f64>::deserialize(d)? {
146		Some(v) => v,
147		None => return Ok(None),
148	};
149	let sat_per_kwu = btc_per_kvb * 25_000_000.0;
150	if sat_per_kwu.is_infinite() {
151		return Err(D::Error::custom("feerate overflow"));
152	}
153	Ok(Some(FeeRate::from_sat_per_kwu(sat_per_kwu as u64)))
154}