1use std::collections::HashMap;
15use std::convert::TryFrom;
16use std::str::FromStr;
17use std::thread;
18
19#[allow(unused_imports)]
20use log::{debug, error, info, trace};
21
22use minreq::{Proxy, Request, Response};
23
24use bitcoin::consensus::{deserialize, serialize, Decodable};
25use bitcoin::hex::{DisplayHex, FromHex};
26use bitcoin::Address;
27use bitcoin::{block::Header as BlockHeader, BlockHash, Transaction, Txid};
28
29use crate::{Builder, Error, WaterfallResponse, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES};
30
31#[derive(Debug, Clone)]
32pub struct BlockingClient {
33 url: String,
35 pub proxy: Option<String>,
37 pub timeout: Option<u64>,
39 pub headers: HashMap<String, String>,
41 pub max_retries: usize,
43}
44
45impl BlockingClient {
46 pub fn from_builder(builder: Builder) -> Self {
48 Self {
49 url: builder.base_url,
50 proxy: builder.proxy,
51 timeout: builder.timeout,
52 headers: builder.headers,
53 max_retries: builder.max_retries,
54 }
55 }
56
57 pub fn url(&self) -> &str {
59 &self.url
60 }
61
62 pub fn get_request(&self, path: &str) -> Result<Request, Error> {
64 let mut request = minreq::get(format!("{}{}", self.url, path));
65
66 if let Some(proxy) = &self.proxy {
67 let proxy = Proxy::new(proxy.as_str())?;
68 request = request.with_proxy(proxy);
69 }
70
71 if let Some(timeout) = &self.timeout {
72 request = request.with_timeout(*timeout);
73 }
74
75 if !self.headers.is_empty() {
76 for (key, value) in &self.headers {
77 request = request.with_header(key, value);
78 }
79 }
80
81 Ok(request)
82 }
83
84 fn get_opt_response<T: Decodable>(&self, path: &str) -> Result<Option<T>, Error> {
85 match self.get_with_retry(path) {
86 Ok(resp) if is_status_not_found(resp.status_code) => Ok(None),
87 Ok(resp) if !is_status_ok(resp.status_code) => {
88 let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
89 let message = resp.as_str().unwrap_or_default().to_string();
90 Err(Error::HttpResponse { status, message })
91 }
92 Ok(resp) => Ok(Some(deserialize::<T>(resp.as_bytes())?)),
93 Err(e) => Err(e),
94 }
95 }
96
97 fn get_response_hex<T: Decodable>(&self, path: &str) -> Result<T, Error> {
98 match self.get_with_retry(path) {
99 Ok(resp) if !is_status_ok(resp.status_code) => {
100 let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
101 let message = resp.as_str().unwrap_or_default().to_string();
102 Err(Error::HttpResponse { status, message })
103 }
104 Ok(resp) => {
105 let hex_str = resp.as_str().map_err(Error::Minreq)?;
106 let hex_vec = Vec::from_hex(hex_str).unwrap();
107 deserialize::<T>(&hex_vec).map_err(Error::BitcoinEncoding)
108 }
109 Err(e) => Err(e),
110 }
111 }
112
113 fn get_response_json_with_query<T: serde::de::DeserializeOwned>(
114 &self,
115 path: &str,
116 query_params: &[(&str, &str)],
117 ) -> Result<T, Error> {
118 let mut url = format!("{}{}", self.url, path);
119 if !query_params.is_empty() {
120 url.push('?');
121 for (i, (key, value)) in query_params.iter().enumerate() {
122 if i > 0 {
123 url.push('&');
124 }
125 let encoded_key = urlencoding::encode(key);
127 let encoded_value = urlencoding::encode(value);
128 url.push_str(&format!("{encoded_key}={encoded_value}"));
129 }
130 }
131
132 let mut request = minreq::get(&url);
133
134 if let Some(proxy) = &self.proxy {
135 let proxy = Proxy::new(proxy.as_str())?;
136 request = request.with_proxy(proxy);
137 }
138
139 if let Some(timeout) = &self.timeout {
140 request = request.with_timeout(*timeout);
141 }
142
143 if !self.headers.is_empty() {
144 for (key, value) in &self.headers {
145 request = request.with_header(key, value);
146 }
147 }
148
149 match request.send() {
150 Ok(resp) if !is_status_ok(resp.status_code) => {
151 let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
152 let message = resp.as_str().unwrap_or_default().to_string();
153 Err(Error::HttpResponse { status, message })
154 }
155 Ok(resp) => Ok(resp.json::<T>()?),
156 Err(e) => Err(Error::Minreq(e)),
157 }
158 }
159
160 fn get_response_str(&self, path: &str) -> Result<String, Error> {
161 match self.get_with_retry(path) {
162 Ok(resp) if !is_status_ok(resp.status_code) => {
163 let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
164 let message = resp.as_str().unwrap_or_default().to_string();
165 Err(Error::HttpResponse { status, message })
166 }
167 Ok(resp) => Ok(resp.as_str()?.to_string()),
168 Err(e) => Err(e),
169 }
170 }
171
172 pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
174 self.get_opt_response(&format!("/tx/{txid}/raw"))
175 }
176
177 pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
179 match self.get_tx(txid) {
180 Ok(Some(tx)) => Ok(tx),
181 Ok(None) => Err(Error::TransactionNotFound(*txid)),
182 Err(e) => Err(e),
183 }
184 }
185
186 pub fn waterfalls(&self, descriptor: &str) -> Result<WaterfallResponse, Error> {
188 let path = "/v4/waterfalls";
189 self.get_response_json_with_query(path, &[("descriptor", descriptor)])
190 }
191
192 pub fn waterfalls_addresses(&self, addresses: &[Address]) -> Result<WaterfallResponse, Error> {
194 let addresses_str = addresses
195 .iter()
196 .map(|a| a.to_string())
197 .collect::<Vec<String>>()
198 .join(",");
199 let path = "/v4/waterfalls";
200 self.get_response_json_with_query(path, &[("addresses", &addresses_str)])
201 }
202
203 pub fn waterfalls_version(
205 &self,
206 descriptor: &str,
207 version: u8,
208 page: Option<u32>,
209 to_index: Option<u32>,
210 utxo_only: bool,
211 ) -> Result<WaterfallResponse, Error> {
212 let path = format!("/v{version}/waterfalls");
213 let mut query_params = vec![
214 ("descriptor", descriptor.to_string()),
215 ("utxo_only", utxo_only.to_string()),
216 ];
217
218 if let Some(page) = page {
219 query_params.push(("page", page.to_string()));
220 }
221 if let Some(to_index) = to_index {
222 query_params.push(("to_index", to_index.to_string()));
223 }
224
225 let query_refs: Vec<(&str, &str)> =
226 query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
227 self.get_response_json_with_query(&path, &query_refs)
228 }
229
230 pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
232 self.get_response_hex(&format!("/block/{block_hash}/header"))
233 }
234
235 pub fn server_recipient(&self) -> Result<String, Error> {
237 self.get_response_str("/v1/server_recipient")
238 }
239
240 pub fn server_address(&self) -> Result<String, Error> {
242 self.get_response_str("/v1/server_address")
243 }
244
245 pub fn time_since_last_block(&self) -> Result<String, Error> {
247 self.get_response_str("/v1/time_since_last_block")
248 }
249
250 pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
252 let mut request = minreq::post(format!("{}/tx", self.url)).with_body(
253 serialize(transaction)
254 .to_lower_hex_string()
255 .as_bytes()
256 .to_vec(),
257 );
258
259 if let Some(proxy) = &self.proxy {
260 let proxy = Proxy::new(proxy.as_str())?;
261 request = request.with_proxy(proxy);
262 }
263
264 if let Some(timeout) = &self.timeout {
265 request = request.with_timeout(*timeout);
266 }
267
268 match request.send() {
269 Ok(resp) if !is_status_ok(resp.status_code) => {
270 let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?;
271 let message = resp.as_str().unwrap_or_default().to_string();
272 Err(Error::HttpResponse { status, message })
273 }
274 Ok(_resp) => Ok(()),
275 Err(e) => Err(Error::Minreq(e)),
276 }
277 }
278
279 pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
281 self.get_response_str("/blocks/tip/hash")
282 .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
283 }
284
285 pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
287 self.get_response_str(&format!("/block-height/{block_height}"))
288 .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))?
289 }
290
291 pub fn get_address_txs(&self, address: &Address) -> Result<String, Error> {
293 let path = format!("/address/{address}/txs");
294 self.get_response_str(&path)
295 }
296
297 fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
300 let mut delay = BASE_BACKOFF_MILLIS;
301 let mut attempts = 0;
302
303 loop {
304 match self.get_request(url)?.send()? {
305 resp if attempts < self.max_retries && is_status_retryable(resp.status_code) => {
306 thread::sleep(delay);
307 attempts += 1;
308 delay *= 2;
309 }
310 resp => return Ok(resp),
311 }
312 }
313 }
314}
315
316fn is_status_ok(status: i32) -> bool {
317 status == 200
318}
319
320fn is_status_not_found(status: i32) -> bool {
321 status == 404
322}
323
324fn is_status_retryable(status: i32) -> bool {
325 let status = status as u16;
326 RETRYABLE_ERROR_CODES.contains(&status)
327}