1use crate::{
2 error::{
3 BlocktankError, Result, ERR_INIT_HTTP_CLIENT,
4 ERR_INVALID_REQUEST_PARAMS, ERR_INVOICE_SAT_ZERO, ERR_LSP_BALANCE_ZERO, ERR_NODE_ID_EMPTY,
5 ERR_ORDER_ID_CONNECTION_EMPTY,
6 },
7 types::blocktank::*,
8};
9use reqwest::{Client, ClientBuilder, Response, StatusCode};
10use serde_json::{json, Value};
11use std::time::Duration;
12use url::Url;
13
14const DEFAULT_BASE_URL: &str = "https://api1.blocktank.to/api";
15const DEFAULT_NOTIFICATION_URL: &str = "https://api.stag0.blocktank.to/notifications/api";
16const DEFAULT_TIMEOUT_SECS: u64 = 30;
17
18#[derive(Clone, Debug)]
19pub struct BlocktankClient {
20 base_url: Url,
21 client: Client,
22}
23
24impl BlocktankClient {
25 pub fn new(base_url: Option<&str>) -> Result<Self> {
27 let base = base_url.unwrap_or(DEFAULT_BASE_URL);
28 let base_url = Self::normalize_url(base)?;
29 let client = Self::create_http_client()?;
30
31 Ok(Self { base_url, client })
32 }
33
34 fn normalize_url(url_str: &str) -> Result<Url> {
36 let fixed = if !url_str.ends_with('/') {
37 format!("{}/", url_str)
38 } else {
39 url_str.to_string()
40 };
41
42 Url::parse(&fixed).map_err(|e| BlocktankError::InitializationError {
43 message: format!("Invalid URL: {}", e),
44 })
45 }
46
47 fn create_http_client() -> Result<Client> {
49 let builder = ClientBuilder::new().timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
50
51 #[cfg(feature = "rustls-tls")]
52 let builder = builder.use_rustls_tls(); builder
55 .build()
56 .map_err(|e| BlocktankError::InitializationError {
57 message: format!("{}: {}", ERR_INIT_HTTP_CLIENT, e),
58 })
59 }
60
61 fn build_url(&self, path: &str, custom_url: Option<&str>) -> Result<Url> {
63 if custom_url.is_some() {
64 let base = custom_url.unwrap_or(DEFAULT_NOTIFICATION_URL);
65 let base_url = Self::normalize_url(base)?;
66 base_url
67 .join(path)
68 .map_err(|e| BlocktankError::Client(format!("Failed to build URL: {}", e)))
69 } else {
70 self.base_url
71 .join(path)
72 .map_err(|e| BlocktankError::Client(format!("Failed to build URL: {}", e)))
73 }
74 }
75
76 fn create_payload<T>(&self, base_payload: Value, options: Option<T>) -> Result<Value>
78 where
79 T: serde::Serialize,
80 {
81 let mut payload = base_payload;
82 if let Some(opts) = options {
83 let options_json = serde_json::to_value(opts).map_err(|e| {
84 BlocktankError::Client(format!("Failed to serialize options: {}", e))
85 })?;
86 if let Value::Object(options_map) = options_json {
87 if let Value::Object(ref mut payload_map) = payload {
88 payload_map.extend(
90 options_map
91 .into_iter()
92 .filter(|(_, v)| !v.is_null() && !Self::is_default_value(v)),
93 );
94 }
95 }
96 }
97 Ok(payload)
98 }
99
100 fn is_default_value(value: &Value) -> bool {
102 match value {
103 Value::Null => true,
104 Value::String(s) => s.is_empty(),
107 Value::Array(a) => a.is_empty(),
108 Value::Object(o) => o.is_empty(),
109 _ => false,
110 }
111 }
112
113 async fn handle_response<T>(&self, response: Response) -> Result<T>
115 where
116 T: serde::de::DeserializeOwned,
117 {
118 match response.status() {
119 status if status.is_success() => Ok(response.json().await?),
120 StatusCode::BAD_REQUEST => {
121 let error_text = response.text().await?;
123
124 if let Ok(error) = serde_json::from_str::<ApiValidationError>(&error_text) {
126 if let Some(issue) = error.errors.issues.first() {
127 return Err(BlocktankError::InvalidParameter {
128 message: issue.message.clone(),
129 });
130 } else {
131 return Err(BlocktankError::Client(
132 ERR_INVALID_REQUEST_PARAMS.to_string(),
133 ));
134 }
135 }
136
137 if let Ok(simple_error) = serde_json::from_str::<serde_json::Value>(&error_text) {
139 if let Some(message) = simple_error.get("message").and_then(|m| m.as_str()) {
140 return Err(BlocktankError::Client(format!("API error: {}", message)));
141 }
142 }
143
144 Err(BlocktankError::Client(format!(
146 "Client error: Invalid request format {}",
147 error_text
148 )))
149 },
150 status => {
151 let error_details = response.text().await.unwrap_or_else(|_| String::from("No error details available"));
153 Err(BlocktankError::Client(format!(
154 "Request failed with status: {} - {}",
155 status, error_details
156 )))
157 },
158 }
159 }
160
161 async fn wrap_error_handler<F, T>(&self, message: &str, f: F) -> Result<T>
163 where
164 F: std::future::Future<Output = Result<T>>,
165 {
166 match f.await {
167 Ok(result) => Ok(result),
168 Err(e) => Err(BlocktankError::BlocktankClient {
169 message: format!("{}: {}", message, e),
170 data: json!(e.to_string()),
171 }),
172 }
173 }
174
175 pub async fn get_info(&self) -> Result<IBtInfo> {
181 self.wrap_error_handler("Failed to get info", async {
182 let url = self.build_url("info", None)?;
183 let response = self.client.get(url).send().await?;
184 self.handle_response(response).await
185 })
186 .await
187 }
188
189 pub async fn estimate_order_fee(
191 &self,
192 lsp_balance_sat: u64,
193 channel_expiry_weeks: u32,
194 options: Option<CreateOrderOptions>,
195 ) -> Result<IBtEstimateFeeResponse> {
196 self.wrap_error_handler("Failed to estimate channel order fee", async {
197 let base_payload = json!({
198 "lspBalanceSat": lsp_balance_sat,
199 "channelExpiryWeeks": channel_expiry_weeks,
200 });
201
202 let payload = self.create_payload(base_payload, options)?;
203 let url = self.build_url("channels/estimate-fee", None)?;
204
205 let response = self.client.post(url).json(&payload).send().await?;
206
207 self.handle_response(response).await
208 })
209 .await
210 }
211
212 pub async fn estimate_order_fee_full(
215 &self,
216 lsp_balance_sat: u64,
217 channel_expiry_weeks: u32,
218 options: Option<CreateOrderOptions>,
219 ) -> Result<IBtEstimateFeeResponse2> {
220 self.wrap_error_handler("Failed to estimate channel order fee", async {
221 let base_payload = json!({
222 "lspBalanceSat": lsp_balance_sat,
223 "channelExpiryWeeks": channel_expiry_weeks,
224 });
225
226 let payload = self.create_payload(base_payload, options)?;
227 let url = self.build_url("channels/estimate-fee-full", None)?;
228
229 let response = self.client.post(url).json(&payload).send().await?;
230
231 self.handle_response(response).await
232 })
233 .await
234 }
235
236 pub async fn create_order(
238 &self,
239 lsp_balance_sat: u64,
240 channel_expiry_weeks: u32,
241 options: Option<CreateOrderOptions>,
242 ) -> Result<IBtOrder> {
243 if lsp_balance_sat == 0 {
244 return Err(BlocktankError::InvalidParameter {
245 message: ERR_LSP_BALANCE_ZERO.to_string(),
246 });
247 }
248
249 let base_payload = json!({
250 "lspBalanceSat": lsp_balance_sat,
251 "channelExpiryWeeks": channel_expiry_weeks,
252 "clientBalanceSat": 0,
253 });
254
255 let payload = self.create_payload(base_payload, options)?;
256 let url = self.build_url("channels", None)?;
257
258 let response = self
259 .client
260 .post(url)
261 .header("Content-Type", "application/json")
262 .json(&payload)
263 .send()
264 .await?;
265
266 self.handle_response(response).await
267 }
268
269 pub async fn get_order(&self, order_id: &str) -> Result<IBtOrder> {
271 self.wrap_error_handler(&format!("Failed to fetch order {}", order_id), async {
272 let url = self.build_url(&format!("channels/{}", order_id), None)?;
273 let response = self.client.get(url).send().await?;
274 self.handle_response(response).await
275 })
276 .await
277 }
278
279 pub async fn get_orders(&self, order_ids: &[String]) -> Result<Vec<IBtOrder>> {
281 self.wrap_error_handler(&format!("Failed to fetch orders {:?}", order_ids), async {
282 let url = self.build_url("channels", None)?;
283
284 let query_params: Vec<(&str, &str)> =
285 order_ids.iter().map(|id| ("ids[]", id.as_str())).collect();
286
287 let response = self.client.get(url).query(&query_params).send().await?;
288
289 self.handle_response(response).await
290 })
291 .await
292 }
293
294 pub async fn open_channel(
296 &self,
297 order_id: &str,
298 connection_string_or_pubkey: &str,
299 ) -> Result<IBtOrder> {
300 if order_id.is_empty() || connection_string_or_pubkey.is_empty() {
301 return Err(BlocktankError::InvalidParameter {
302 message: ERR_ORDER_ID_CONNECTION_EMPTY.to_string(),
303 });
304 }
305
306 self.wrap_error_handler(
307 &format!("Failed to open the channel for order {}", order_id),
308 async {
309 let payload = json!({
310 "connectionStringOrPubkey": connection_string_or_pubkey,
311 });
312
313 let url = self.build_url(&format!("channels/{}/open", order_id), None)?;
314 let response = self.client.post(url).json(&payload).send().await?;
315
316 self.handle_response(response).await
317 },
318 )
319 .await
320 }
321
322 pub async fn get_min_zero_conf_tx_fee(&self, order_id: &str) -> Result<IBt0ConfMinTxFeeWindow> {
324 self.wrap_error_handler(&format!("Failed to fetch order {}", order_id), async {
325 let url = self.build_url(&format!("channels/{}/min-0conf-tx-fee", order_id), None)?;
326 let response = self.client.get(url).send().await?;
327 self.handle_response(response).await
328 })
329 .await
330 }
331
332 pub async fn create_cjit_entry(
334 &self,
335 channel_size_sat: u64,
336 invoice_sat: u64,
337 invoice_description: &str,
338 node_id: &str,
339 channel_expiry_weeks: u32,
340 options: Option<CreateCjitOptions>,
341 ) -> Result<ICJitEntry> {
342 if channel_size_sat == 0 {
343 return Err(BlocktankError::InvalidParameter {
344 message: ERR_LSP_BALANCE_ZERO.to_string(),
345 });
346 }
347
348 if invoice_sat == 0 {
349 return Err(BlocktankError::InvalidParameter {
350 message: ERR_INVOICE_SAT_ZERO.to_string(),
351 });
352 }
353
354 if node_id.is_empty() {
355 return Err(BlocktankError::InvalidParameter {
356 message: ERR_NODE_ID_EMPTY.to_string(),
357 });
358 }
359
360 let base_payload = json!({
361 "channelSizeSat": channel_size_sat,
362 "invoiceSat": invoice_sat,
363 "invoiceDescription": invoice_description,
364 "channelExpiryWeeks": channel_expiry_weeks,
365 "nodeId": node_id,
366 });
367
368 let payload = self.create_payload(base_payload, options)?;
369 let url = self.build_url("cjit", None)?;
370
371 let response = self.client.post(url).json(&payload).send().await?;
372
373 self.handle_response(response).await
374 }
375
376 pub async fn get_cjit_entry(&self, entry_id: &str) -> Result<ICJitEntry> {
378 self.wrap_error_handler(&format!("Failed to fetch cjit entry {}", entry_id), async {
379 let url = self.build_url(&format!("cjit/{}", entry_id), None)?;
380 let response = self.client.get(url).send().await?;
381 self.handle_response(response).await
382 })
383 .await
384 }
385
386 pub async fn bitkit_log(&self, node_id: &str, message: &str) -> Result<()> {
388 self.wrap_error_handler(&format!("Failed to send log for {}", node_id), async {
389 let payload = json!({
390 "nodeId": node_id,
391 "message": message,
392 });
393
394 let url = self.build_url("bitkit/log", None)?;
395 let response = self.client.post(url).json(&payload).send().await?;
396
397 response.error_for_status()?;
398 Ok(())
399 })
400 .await
401 }
402
403 pub async fn regtest_mine(&self, count: Option<u32>) -> Result<()> {
409 let blocks_to_mine = count.unwrap_or(1);
410 self.wrap_error_handler(
411 &format!("Failed to mine {} blocks", blocks_to_mine),
412 async {
413 let payload = json!({ "count": blocks_to_mine });
414 let url = self.build_url("regtest/chain/mine", None)?;
415 let response = self.client.post(url).json(&payload).send().await?;
416
417 response.error_for_status()?;
418 Ok(())
419 },
420 )
421 .await
422 }
423
424 pub async fn regtest_deposit(&self, address: &str, amount_sat: Option<u64>) -> Result<String> {
427 self.wrap_error_handler(&format!("Failed to deposit to {}", address), async {
428 let mut payload = json!({
429 "address": address,
430 });
431
432 if let Some(amount_sat) = amount_sat {
433 payload
434 .as_object_mut()
435 .unwrap()
436 .insert("amountSat".to_string(), json!(amount_sat));
437 }
438
439 let url = self.build_url("regtest/chain/deposit", None)?;
440 let response = self.client.post(url).json(&payload).send().await?;
441
442 let result = response.error_for_status()?.text().await?;
443 Ok(result)
444 })
445 .await
446 }
447
448 pub async fn regtest_pay(&self, invoice: &str, amount_sat: Option<u64>) -> Result<String> {
451 self.wrap_error_handler("Failed to pay invoice", async {
452 let payload = json!({
453 "invoice": invoice,
454 "amountSat": amount_sat,
455 });
456
457 let url = self.build_url("regtest/channel/pay", None)?;
458 let response = self.client.post(url).json(&payload).send().await?;
459
460 let result = response.error_for_status()?.text().await?;
461 Ok(result)
462 })
463 .await
464 }
465
466 pub async fn regtest_get_payment(&self, payment_id: &str) -> Result<IBtBolt11Invoice> {
468 self.wrap_error_handler(&format!("Failed to get payment {}", payment_id), async {
469 let url = self.build_url(&format!("regtest/channel/pay/{}", payment_id), None)?;
470 let response = self.client.get(url).send().await?;
471 self.handle_response(response).await
472 })
473 .await
474 }
475
476 pub async fn regtest_close_channel(
479 &self,
480 funding_tx_id: &str,
481 vout: u32,
482 force_close_after_s: Option<u64>,
483 ) -> Result<String> {
484 let force_desc = if force_close_after_s.is_some() {
485 " force"
486 } else {
487 ""
488 };
489 self.wrap_error_handler(
490 &format!(
491 "Failed to{} close the channel {}:{}",
492 force_desc, funding_tx_id, vout
493 ),
494 async {
495 let mut payload = json!({
496 "fundingTxId": funding_tx_id,
497 "vout": vout,
498 });
499
500 if let Some(force_close_after_s) = force_close_after_s {
501 payload
502 .as_object_mut()
503 .unwrap()
504 .insert("forceCloseAfterS".to_string(), json!(force_close_after_s));
505 }
506
507 let url = self.build_url("regtest/channel/close", None)?;
508 let response = self.client.post(url).json(&payload).send().await?;
509
510 let result = response.error_for_status()?.text().await?;
511 Ok(result)
512 },
513 )
514 .await
515 }
516
517 pub async fn register_device(
519 &self,
520 device_token: &str,
521 public_key: &str,
522 features: &[String],
523 node_id: &str,
524 iso_timestamp: &str,
525 signature: &str,
526 is_production: Option<bool>,
527 custom_url: Option<&str>,
528 ) -> Result<String> {
529 self.wrap_error_handler("Failed to register device", async {
530 let custom_url = custom_url.or(Some(DEFAULT_NOTIFICATION_URL));
531 let url = self.build_url("device", custom_url)?;
532
533 let mut payload = json!({
534 "deviceToken": device_token,
535 "publicKey": public_key,
536 "features": features,
537 "nodeId": node_id,
538 "isoTimestamp": iso_timestamp,
539 "signature": signature
540 });
541
542 if let Some(is_prod) = is_production {
543 payload
544 .as_object_mut()
545 .unwrap()
546 .insert("isProduction".to_string(), json!(is_prod));
547 }
548
549 let response = self.client.post(url).json(&payload).send().await?;
550
551 let status = response.status();
552 if !status.is_success() {
553 let error_text = response.text().await?;
554 return Err(BlocktankError::Client(format!(
555 "Device registration failed. Status: {}. Response: {}",
556 status, error_text
557 )));
558 }
559
560 response.text().await.map_err(|e| e.into())
561 })
562 .await
563 }
564
565 pub async fn test_notification(
567 &self,
568 device_token: &str,
569 secret_message: &str,
570 notification_type: Option<&str>,
571 custom_url: Option<&str>,
572 ) -> Result<String> {
573 let notification_type = notification_type.unwrap_or("orderPaymentConfirmed");
574 let custom_url = custom_url.or(Some(DEFAULT_NOTIFICATION_URL));
575 self.wrap_error_handler("Failed to send test notification", async {
576 let url = self.build_url(
577 &format!("device/{}/test-notification", device_token),
578 custom_url,
579 )?;
580
581 let payload = json!({
582 "data": {
583 "source": "blocktank",
584 "type": notification_type,
585 "payload": {
586 "secretMessage": secret_message
587 }
588 }
589 });
590
591 let response = self.client.post(url).json(&payload).send().await?;
592
593 let status = response.status();
594 if !status.is_success() {
595 let error_text = response.text().await?;
596 return Err(BlocktankError::Client(format!(
597 "Test notification failed. Status: {}. Response: {}",
598 status, error_text
599 )));
600 }
601
602 response.text().await.map_err(|e| e.into())
603 })
604 .await
605 }
606
607 pub async fn gift_pay(&self, invoice: &str) -> Result<IGift> {
609 self.wrap_error_handler("Failed to pay gift invoice", async {
610 let payload = json!({
611 "invoice": invoice
612 });
613
614 let url = self.build_url("gift/pay", None)?;
615 let response = self.client.post(url).json(&payload).send().await?;
616
617 self.handle_response(response).await
618 })
619 .await
620 }
621
622 pub async fn gift_order(&self, client_node_id: &str, code: &str) -> Result<IGift> {
624 self.wrap_error_handler("Failed to create gift order", async {
625 let payload = json!({
626 "clientNodeId": client_node_id,
627 "code": code
628 });
629
630 let url = self.build_url("gift/order", None)?;
631 let response = self.client.post(url).json(&payload).send().await?;
632
633 self.handle_response(response).await
634 })
635 .await
636 }
637
638 pub async fn get_gift(&self, gift_id: &str) -> Result<IGift> {
640 self.wrap_error_handler("Failed to get gift", async {
641 let url = self.build_url(&format!("gift/{}", gift_id), None)?;
642 let response = self.client.get(url).send().await?;
643 self.handle_response(response).await
644 })
645 .await
646 }
647
648 pub async fn get_payment(&self, payment_id: &str) -> Result<IBtBolt11Invoice> {
650 self.wrap_error_handler("Failed to get payment", async {
651 let url = self.build_url(&format!("payments/{}", payment_id), None)?;
652 let response = self.client.get(url).send().await?;
653 self.handle_response(response).await
654 })
655 .await
656 }
657}