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#[async_trait::async_trait]
58pub trait EsploraClientExt {
59 fn _client(&self) -> &reqwest::Client;
60 fn _base_url(&self) -> &str;
61
62 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(¶m);
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 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}