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 let status = response.status();
119 let response_text = response.text().await?;
120
121 match status {
122 s if s.is_success() => {
123 if response_text.is_empty() {
125 return Err(BlocktankError::Client(
127 "Unexpected empty response body".to_string()
128 ));
129 }
130
131 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
133 let is_error_structure = json_value.get("message").is_some()
135 && (json_value.get("type").is_some() || json_value.get("name").is_some());
136
137 if is_error_structure {
138 return self.parse_structured_error(&json_value);
139 }
140
141 serde_json::from_value::<T>(json_value).map_err(|e| {
143 BlocktankError::Client(format!(
144 "Failed to deserialize response: {}. Response: {}",
145 e, response_text
146 ))
147 })
148 } else {
149 Err(BlocktankError::Client(format!(
152 "Expected JSON response but got: {}",
153 response_text
154 )))
155 }
156 },
157 StatusCode::BAD_REQUEST => {
158 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
160 if json_value.get("message").is_some() {
162 return self.parse_structured_error(&json_value);
163 }
164
165 if let Ok(error) = serde_json::from_value::<ApiValidationError>(json_value) {
167 if let Some(issue) = error.errors.issues.first() {
168 return Err(BlocktankError::InvalidParameter {
169 message: issue.message.clone(),
170 });
171 }
172 }
173 }
174
175 Err(BlocktankError::Client(format!("Bad request: {}", response_text)))
177 },
178 StatusCode::NOT_FOUND => {
179 Err(BlocktankError::Client(format!("Not found: {}", response_text)))
180 },
181 StatusCode::UNAUTHORIZED => {
182 Err(BlocktankError::Client(format!("Unauthorized: {}", response_text)))
183 },
184 StatusCode::INTERNAL_SERVER_ERROR => {
185 Err(BlocktankError::Client(format!("Server error: {}", response_text)))
186 },
187 status => {
188 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
190 if let Some(msg) = json_value.get("message").and_then(|m| m.as_str()) {
191 return Err(BlocktankError::Client(format!(
192 "Request failed ({}): {}", status, msg
193 )));
194 }
195 }
196
197 Err(BlocktankError::Client(format!(
198 "Request failed with status {}: {}",
199 status, response_text
200 )))
201 },
202 }
203 }
204
205 fn parse_structured_error<T>(&self, json_value: &serde_json::Value) -> Result<T> {
207 let message = json_value.get("message")
208 .and_then(|m| m.as_str())
209 .unwrap_or("Unknown error");
210
211 let error_type = json_value.get("type")
212 .and_then(|t| t.as_str());
213
214 let error_name = json_value.get("name")
215 .and_then(|n| n.as_str())
216 .unwrap_or("Error");
217
218 let full_message = if let Some(err_type) = error_type {
220 format!("{}: {} (type: {})", error_name, message, err_type)
221 } else {
222 format!("{}: {}", error_name, message)
223 };
224
225 match (error_type, error_name) {
227 (Some("ValidationError"), _) | (_, "ValidationError") => {
228 Err(BlocktankError::InvalidParameter { message: full_message })
229 },
230 (Some("WRONG_ORDER_STATE"), _) | (_, "ChannelOpenError") => {
231 Err(BlocktankError::InvalidParameter { message: full_message })
232 },
233 _ => {
234 Err(BlocktankError::Client(full_message))
235 },
236 }
237 }
238
239 async fn wrap_error_handler<F, T>(&self, message: &str, f: F) -> Result<T>
241 where
242 F: std::future::Future<Output = Result<T>>,
243 {
244 match f.await {
245 Ok(result) => Ok(result),
246 Err(e) => Err(BlocktankError::BlocktankClient {
247 message: format!("{}: {}", message, e),
248 data: json!(e.to_string()),
249 }),
250 }
251 }
252
253 pub async fn get_info(&self) -> Result<IBtInfo> {
259 self.wrap_error_handler("Failed to get info", async {
260 let url = self.build_url("info", None)?;
261 let response = self.client.get(url).send().await?;
262 self.handle_response(response).await
263 })
264 .await
265 }
266
267 pub async fn estimate_order_fee(
269 &self,
270 lsp_balance_sat: u64,
271 channel_expiry_weeks: u32,
272 options: Option<CreateOrderOptions>,
273 ) -> Result<IBtEstimateFeeResponse> {
274 self.wrap_error_handler("Failed to estimate channel order fee", async {
275 let base_payload = json!({
276 "lspBalanceSat": lsp_balance_sat,
277 "channelExpiryWeeks": channel_expiry_weeks,
278 });
279
280 let payload = self.create_payload(base_payload, options)?;
281 let url = self.build_url("channels/estimate-fee", None)?;
282
283 let response = self.client.post(url).json(&payload).send().await?;
284
285 self.handle_response(response).await
286 })
287 .await
288 }
289
290 pub async fn estimate_order_fee_full(
293 &self,
294 lsp_balance_sat: u64,
295 channel_expiry_weeks: u32,
296 options: Option<CreateOrderOptions>,
297 ) -> Result<IBtEstimateFeeResponse2> {
298 self.wrap_error_handler("Failed to estimate channel order fee", async {
299 let base_payload = json!({
300 "lspBalanceSat": lsp_balance_sat,
301 "channelExpiryWeeks": channel_expiry_weeks,
302 });
303
304 let payload = self.create_payload(base_payload, options)?;
305 let url = self.build_url("channels/estimate-fee-full", None)?;
306
307 let response = self.client.post(url).json(&payload).send().await?;
308
309 self.handle_response(response).await
310 })
311 .await
312 }
313
314 pub async fn create_order(
316 &self,
317 lsp_balance_sat: u64,
318 channel_expiry_weeks: u32,
319 options: Option<CreateOrderOptions>,
320 ) -> Result<IBtOrder> {
321 if lsp_balance_sat == 0 {
322 return Err(BlocktankError::InvalidParameter {
323 message: ERR_LSP_BALANCE_ZERO.to_string(),
324 });
325 }
326
327 let base_payload = json!({
328 "lspBalanceSat": lsp_balance_sat,
329 "channelExpiryWeeks": channel_expiry_weeks,
330 "clientBalanceSat": 0,
331 });
332
333 let payload = self.create_payload(base_payload, options)?;
334 let url = self.build_url("channels", None)?;
335
336 let response = self
337 .client
338 .post(url)
339 .json(&payload)
340 .send()
341 .await?;
342
343 self.handle_response(response).await
344 }
345
346 pub async fn get_order(&self, order_id: &str) -> Result<IBtOrder> {
348 self.wrap_error_handler(&format!("Failed to fetch order {}", order_id), async {
349 let url = self.build_url(&format!("channels/{}", order_id), None)?;
350 let response = self.client.get(url).send().await?;
351 self.handle_response(response).await
352 })
353 .await
354 }
355
356 pub async fn get_orders(&self, order_ids: &[String]) -> Result<Vec<IBtOrder>> {
358 self.wrap_error_handler(&format!("Failed to fetch orders {:?}", order_ids), async {
359 let url = self.build_url("channels", None)?;
360
361 let query_params: Vec<(&str, &str)> =
362 order_ids.iter().map(|id| ("ids[]", id.as_str())).collect();
363
364 let response = self.client.get(url).query(&query_params).send().await?;
365
366 self.handle_response(response).await
367 })
368 .await
369 }
370
371 pub async fn open_channel(
373 &self,
374 order_id: &str,
375 connection_string_or_pubkey: &str,
376 ) -> Result<IBtOrder> {
377 if order_id.is_empty() || connection_string_or_pubkey.is_empty() {
378 return Err(BlocktankError::InvalidParameter {
379 message: ERR_ORDER_ID_CONNECTION_EMPTY.to_string(),
380 });
381 }
382
383 self.wrap_error_handler(
384 &format!("Failed to open the channel for order {}", order_id),
385 async {
386 let payload = json!({
387 "connectionStringOrPubkey": connection_string_or_pubkey,
388 });
389
390 let url = self.build_url(&format!("channels/{}/open", order_id), None)?;
391 let response = self.client.post(url).json(&payload).send().await?;
392
393 self.handle_response(response).await
394 },
395 )
396 .await
397 }
398
399 pub async fn get_min_zero_conf_tx_fee(&self, order_id: &str) -> Result<IBt0ConfMinTxFeeWindow> {
401 self.wrap_error_handler(&format!("Failed to fetch order {}", order_id), async {
402 let url = self.build_url(&format!("channels/{}/min-0conf-tx-fee", order_id), None)?;
403 let response = self.client.get(url).send().await?;
404 self.handle_response(response).await
405 })
406 .await
407 }
408
409 pub async fn create_cjit_entry(
411 &self,
412 channel_size_sat: u64,
413 invoice_sat: u64,
414 invoice_description: &str,
415 node_id: &str,
416 channel_expiry_weeks: u32,
417 options: Option<CreateCjitOptions>,
418 ) -> Result<ICJitEntry> {
419 if channel_size_sat == 0 {
420 return Err(BlocktankError::InvalidParameter {
421 message: ERR_LSP_BALANCE_ZERO.to_string(),
422 });
423 }
424
425 if invoice_sat == 0 {
426 return Err(BlocktankError::InvalidParameter {
427 message: ERR_INVOICE_SAT_ZERO.to_string(),
428 });
429 }
430
431 if node_id.is_empty() {
432 return Err(BlocktankError::InvalidParameter {
433 message: ERR_NODE_ID_EMPTY.to_string(),
434 });
435 }
436
437 let base_payload = json!({
438 "channelSizeSat": channel_size_sat,
439 "invoiceSat": invoice_sat,
440 "invoiceDescription": invoice_description,
441 "channelExpiryWeeks": channel_expiry_weeks,
442 "nodeId": node_id,
443 });
444
445 let payload = self.create_payload(base_payload, options)?;
446 let url = self.build_url("cjit", None)?;
447
448 let response = self.client.post(url).json(&payload).send().await?;
449
450 self.handle_response(response).await
451 }
452
453 pub async fn get_cjit_entry(&self, entry_id: &str) -> Result<ICJitEntry> {
455 self.wrap_error_handler(&format!("Failed to fetch cjit entry {}", entry_id), async {
456 let url = self.build_url(&format!("cjit/{}", entry_id), None)?;
457 let response = self.client.get(url).send().await?;
458 self.handle_response(response).await
459 })
460 .await
461 }
462
463 pub async fn bitkit_log(&self, node_id: &str, message: &str) -> Result<()> {
465 self.wrap_error_handler(&format!("Failed to send log for {}", node_id), async {
466 let payload = json!({
467 "nodeId": node_id,
468 "message": message,
469 });
470
471 let url = self.build_url("bitkit/log", None)?;
472 let response = self.client.post(url).json(&payload).send().await?;
473
474 response.error_for_status()?;
475 Ok(())
476 })
477 .await
478 }
479
480 pub async fn regtest_mine(&self, count: Option<u32>) -> Result<()> {
486 let blocks_to_mine = count.unwrap_or(1);
487 self.wrap_error_handler(
488 &format!("Failed to mine {} blocks", blocks_to_mine),
489 async {
490 let payload = json!({ "count": blocks_to_mine });
491 let url = self.build_url("regtest/chain/mine", None)?;
492 let response = self.client.post(url).json(&payload).send().await?;
493
494 response.error_for_status()?;
495 Ok(())
496 },
497 )
498 .await
499 }
500
501 pub async fn regtest_deposit(&self, address: &str, amount_sat: Option<u64>) -> Result<String> {
504 self.wrap_error_handler(&format!("Failed to deposit to {}", address), async {
505 let mut payload = json!({
506 "address": address,
507 });
508
509 if let Some(amount_sat) = amount_sat {
510 payload
511 .as_object_mut()
512 .unwrap()
513 .insert("amountSat".to_string(), json!(amount_sat));
514 }
515
516 let url = self.build_url("regtest/chain/deposit", None)?;
517 let response = self.client.post(url).json(&payload).send().await?;
518
519 let result = response.error_for_status()?.text().await?;
520 Ok(result)
521 })
522 .await
523 }
524
525 pub async fn regtest_pay(&self, invoice: &str, amount_sat: Option<u64>) -> Result<String> {
528 self.wrap_error_handler("Failed to pay invoice", async {
529 let payload = json!({
530 "invoice": invoice,
531 "amountSat": amount_sat,
532 });
533
534 let url = self.build_url("regtest/channel/pay", None)?;
535 let response = self.client.post(url).json(&payload).send().await?;
536
537 let result = response.error_for_status()?.text().await?;
538 Ok(result)
539 })
540 .await
541 }
542
543 pub async fn regtest_get_payment(&self, payment_id: &str) -> Result<IBtBolt11Invoice> {
545 self.wrap_error_handler(&format!("Failed to get payment {}", payment_id), async {
546 let url = self.build_url(&format!("regtest/channel/pay/{}", payment_id), None)?;
547 let response = self.client.get(url).send().await?;
548 self.handle_response(response).await
549 })
550 .await
551 }
552
553 pub async fn regtest_close_channel(
556 &self,
557 funding_tx_id: &str,
558 vout: u32,
559 force_close_after_s: Option<u64>,
560 ) -> Result<String> {
561 let force_desc = if force_close_after_s.is_some() {
562 " force"
563 } else {
564 ""
565 };
566 self.wrap_error_handler(
567 &format!(
568 "Failed to{} close the channel {}:{}",
569 force_desc, funding_tx_id, vout
570 ),
571 async {
572 let mut payload = json!({
573 "fundingTxId": funding_tx_id,
574 "vout": vout,
575 });
576
577 if let Some(force_close_after_s) = force_close_after_s {
578 payload
579 .as_object_mut()
580 .unwrap()
581 .insert("forceCloseAfterSec".to_string(), json!(force_close_after_s));
582 }
583
584 let url = self.build_url("regtest/channel/close", None)?;
585 let response = self.client.post(url).json(&payload).send().await?;
586
587 let result = response.error_for_status()?.text().await?;
588 Ok(result)
589 },
590 )
591 .await
592 }
593
594 pub async fn register_device(
596 &self,
597 device_token: &str,
598 public_key: &str,
599 features: &[String],
600 node_id: &str,
601 iso_timestamp: &str,
602 signature: &str,
603 is_production: Option<bool>,
604 custom_url: Option<&str>,
605 ) -> Result<String> {
606 self.wrap_error_handler("Failed to register device", async {
607 let custom_url = custom_url.or(Some(DEFAULT_NOTIFICATION_URL));
608 let url = self.build_url("device", custom_url)?;
609
610 let mut payload = json!({
611 "deviceToken": device_token,
612 "publicKey": public_key,
613 "features": features,
614 "nodeId": node_id,
615 "isoTimestamp": iso_timestamp,
616 "signature": signature
617 });
618
619 if let Some(is_prod) = is_production {
620 payload
621 .as_object_mut()
622 .unwrap()
623 .insert("isProduction".to_string(), json!(is_prod));
624 }
625
626 let response = self.client.post(url).json(&payload).send().await?;
627
628 let status = response.status();
629 if !status.is_success() {
630 let error_text = response.text().await?;
631 return Err(BlocktankError::Client(format!(
632 "Device registration failed. Status: {}. Response: {}",
633 status, error_text
634 )));
635 }
636
637 response.text().await.map_err(|e| e.into())
638 })
639 .await
640 }
641
642 pub async fn test_notification(
644 &self,
645 device_token: &str,
646 secret_message: &str,
647 notification_type: Option<&str>,
648 custom_url: Option<&str>,
649 ) -> Result<String> {
650 let notification_type = notification_type.unwrap_or("orderPaymentConfirmed");
651 let custom_url = custom_url.or(Some(DEFAULT_NOTIFICATION_URL));
652 self.wrap_error_handler("Failed to send test notification", async {
653 let url = self.build_url(
654 &format!("device/{}/test-notification", device_token),
655 custom_url,
656 )?;
657
658 let payload = json!({
659 "data": {
660 "source": "blocktank",
661 "type": notification_type,
662 "payload": {
663 "secretMessage": secret_message
664 }
665 }
666 });
667
668 let response = self.client.post(url).json(&payload).send().await?;
669
670 let status = response.status();
671 if !status.is_success() {
672 let error_text = response.text().await?;
673 return Err(BlocktankError::Client(format!(
674 "Test notification failed. Status: {}. Response: {}",
675 status, error_text
676 )));
677 }
678
679 response.text().await.map_err(|e| e.into())
680 })
681 .await
682 }
683
684 pub async fn gift_pay(&self, invoice: &str) -> Result<IGift> {
686 self.wrap_error_handler("Failed to pay gift invoice", async {
687 let payload = json!({
688 "invoice": invoice
689 });
690
691 let url = self.build_url("gift/pay", None)?;
692 let response = self.client.post(url).json(&payload).send().await?;
693
694 self.handle_response(response).await
695 })
696 .await
697 }
698
699 pub async fn gift_order(&self, client_node_id: &str, code: &str) -> Result<IGift> {
701 self.wrap_error_handler("Failed to create gift order", async {
702 let payload = json!({
703 "clientNodeId": client_node_id,
704 "code": code
705 });
706
707 let url = self.build_url("gift/order", None)?;
708 let response = self.client.post(url).json(&payload).send().await?;
709
710 self.handle_response(response).await
711 })
712 .await
713 }
714
715 pub async fn get_gift(&self, gift_id: &str) -> Result<IGift> {
717 self.wrap_error_handler("Failed to get gift", async {
718 let url = self.build_url(&format!("gift/{}", gift_id), None)?;
719 let response = self.client.get(url).send().await?;
720 self.handle_response(response).await
721 })
722 .await
723 }
724
725 pub async fn get_payment(&self, payment_id: &str) -> Result<IBtBolt11Invoice> {
727 self.wrap_error_handler("Failed to get payment", async {
728 let url = self.build_url(&format!("payments/{}", payment_id), None)?;
729 let response = self.client.get(url).send().await?;
730 self.handle_response(response).await
731 })
732 .await
733 }
734}