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 pub package_msg: String,
14 #[serde(rename = "tx-results")]
16 pub tx_results: HashMap<Wtxid, TxResult>,
17 #[serde(rename = "replaced-transactions")]
19 pub replaced_transactions: Option<Vec<Txid>>,
20}
21
22#[derive(Deserialize, Debug)]
23pub struct TxResult {
24 pub txid: Txid,
26 #[serde(rename = "other-wtxid")]
31 pub other_wtxid: Option<Wtxid>,
32 pub vsize: Option<u32>,
34 pub fees: Option<MempoolFeesSubmitPackage>,
36 pub error: Option<String>,
38}
39
40#[derive(Deserialize, Debug)]
41pub struct MempoolFeesSubmitPackage {
42 #[serde(with = "bitcoin::amount::serde::as_btc")]
44 pub base: Amount,
45 #[serde(rename = "effective-feerate", default, deserialize_with = "deserialize_feerate")]
50 pub effective_feerate: Option<FeeRate>,
51 #[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 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(¶m);
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 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}