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#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
58#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
59pub trait EsploraClientExt {
60	fn _client(&self) -> &reqwest::Client;
61	fn _base_url(&self) -> &str;
62
63	/// Make an HTTP POST request to given URL, converting any `T` that
64	/// implement [`Into<Body>`] and setting query parameters, if any.
65	///
66	/// # Errors
67	///
68	/// This function will return an error either from the HTTP client, or the
69	/// response's [`serde_json`] deserialization.
70	// implementation borrowed from esplora-client crate
71	async fn post_request_bytes<T: Into<Body> + Send>(
72		&self,
73		path: &str,
74		body: T,
75		query_params: Option<HashSet<(&str, String)>>,
76	) -> Result<Response, bdk_esplora::esplora_client::Error> {
77		let url: String = format!("{}{}", self._base_url(), path);
78		let mut request = self._client().post(url).body(body);
79
80		for param in query_params.unwrap_or_default() {
81			request = request.query(&param);
82		}
83
84		let response = request.send().await?;
85
86		if !response.status().is_success() {
87			return Err(bdk_esplora::esplora_client::Error::HttpResponse {
88				status: response.status().as_u16(),
89				message: response.text().await?,
90			});
91		}
92
93		Ok(response)
94	}
95
96	/// Broadcast a package of [`Transaction`] to Esplora
97	///
98	/// if `maxfeerate` is provided, any transaction whose
99	/// fee is higher will be rejected
100	///
101	/// if  `maxburnamount` is provided, any transaction
102	/// with higher provably unspendable outputs amount
103	/// will be rejected
104	async fn submit_package(
105		&self,
106		transactions: &[Transaction],
107		maxfeerate: Option<f64>,
108		maxburnamount: Option<f64>,
109	) -> Result<SubmitPackageResult, bdk_esplora::esplora_client::Error> {
110		let mut queryparams = HashSet::<(&str, String)>::new();
111		if let Some(maxfeerate) = maxfeerate {
112			queryparams.insert(("maxfeerate", maxfeerate.to_string()));
113		}
114		if let Some(maxburnamount) = maxburnamount {
115			queryparams.insert(("maxburnamount", maxburnamount.to_string()));
116		}
117
118		let serialized_txs = transactions
119			.iter()
120			.map(|tx| serialize_hex(&tx))
121			.collect::<Vec<_>>();
122
123		let response = self
124			.post_request_bytes(
125				"/txs/package",
126				serde_json::to_string(&serialized_txs).unwrap(),
127				Some(queryparams),
128			)
129			.await?;
130
131		Ok(response.json::<SubmitPackageResult>().await?)
132	}
133}
134
135impl EsploraClientExt for bdk_esplora::esplora_client::AsyncClient {
136	fn _client(&self) -> &reqwest::Client { self.client() }
137	fn _base_url(&self) -> &str { self.url() }
138}
139
140fn deserialize_feerate<'de, D>(d: D) -> Result<Option<FeeRate>, D::Error>
141where
142	D: serde::de::Deserializer<'de>,
143{
144	use serde::de::Error;
145
146	let btc_per_kvb = match Option::<f64>::deserialize(d)? {
147		Some(v) => v,
148		None => return Ok(None),
149	};
150	let sat_per_kwu = btc_per_kvb * 25_000_000.0;
151	if sat_per_kwu.is_infinite() {
152		return Err(D::Error::custom("feerate overflow"));
153	}
154	Ok(Some(FeeRate::from_sat_per_kwu(sat_per_kwu as u64)))
155}