1use crate::network::Network;
21use crate::{error::Error, network::Chain, util::secrets::Preimage};
22use crate::{BtcSwapScript, LBtcSwapScript};
23use bitcoin::secp256k1;
24use bitcoin::{hashes::sha256, hex::DisplayHex, PublicKey};
25use lightning_invoice::Bolt11Invoice;
26use reqwest::Method;
27use secp256k1_musig::musig;
28use serde::{Deserialize, Serialize};
29use serde_json::{json, Value};
30use std::collections::HashMap;
31use std::fmt::{Display, Formatter};
32use std::str::FromStr;
33use std::time::Duration;
34
35pub const BOLTZ_TESTNET_URL_V2: &str = "https://api.testnet.boltz.exchange/v2";
36pub const BOLTZ_MAINNET_URL_V2: &str = "https://api.boltz.exchange/v2";
37pub const BOLTZ_REGTEST: &str = "http://localhost:9001/v2";
38
39#[cfg(feature = "ws")]
40pub use crate::swaps::status_stream::{BoltzWsApi, BoltzWsConfig};
41use reqwest::RequestBuilder;
42#[cfg(feature = "ws")]
43pub use tokio_tungstenite_wasm;
44#[cfg(feature = "ws")]
45use tokio_tungstenite_wasm::{connect, connect_with_protocols, WebSocketStream};
46
47#[derive(Serialize, Deserialize, Debug)]
48pub struct HeightResponse {
49 #[serde(rename = "BTC")]
50 pub btc: u32,
51 #[serde(rename = "L-BTC")]
52 pub lbtc: u32,
53}
54
55fn check_limits_within(maximal: u64, minimal: u64, output_amount: u64) -> Result<(), Error> {
56 if output_amount < minimal {
57 return Err(Error::Protocol(format!(
58 "Output amount is below minimum {minimal}"
59 )));
60 }
61 if output_amount > maximal {
62 return Err(Error::Protocol(format!(
63 "Output amount is above maximum {maximal}"
64 )));
65 }
66 Ok(())
67}
68
69#[derive(Serialize, Deserialize, Debug, Clone)]
71#[serde(rename_all = "camelCase")]
72pub struct PairLimits {
73 pub maximal: u64,
75 pub minimal: u64,
77 pub maximal_zero_conf: u64,
79}
80
81impl PairLimits {
82 pub fn within(&self, output_amount: u64) -> Result<(), Error> {
84 check_limits_within(self.maximal, self.minimal, output_amount)
85 }
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone)]
89#[serde(rename_all = "camelCase")]
90pub struct SubmarinePairLimits {
91 pub maximal: u64,
93 pub minimal: u64,
95 pub maximal_zero_conf: u64,
97 pub minimal_batched: Option<u64>,
99}
100
101impl SubmarinePairLimits {
102 pub fn within(&self, output_amount: u64) -> Result<(), Error> {
104 let minimal = self.minimal_batched.unwrap_or(self.minimal);
105 check_limits_within(self.maximal, minimal, output_amount)
106 }
107}
108
109#[derive(Serialize, Deserialize, Debug, Clone)]
110#[serde(rename_all = "camelCase")]
111pub struct ReverseLimits {
112 pub maximal: u64,
114 pub minimal: u64,
116}
117
118impl ReverseLimits {
119 pub fn within(&self, output_amount: u64) -> Result<(), Error> {
121 check_limits_within(self.maximal, self.minimal, output_amount)
122 }
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone)]
126#[serde(rename_all = "camelCase")]
127pub struct PairMinerFees {
128 pub lockup: u64,
129 pub claim: u64,
130}
131
132#[derive(Serialize, Deserialize, Debug, Clone)]
133#[serde(rename_all = "camelCase")]
134pub struct ChainMinerFees {
135 pub server: u64,
136 pub user: PairMinerFees,
137}
138
139#[derive(Serialize, Deserialize, Debug, Clone)]
140#[serde(rename_all = "camelCase")]
141pub struct ChainFees {
142 pub percentage: f64,
143 pub miner_fees: ChainMinerFees,
144}
145
146impl ChainFees {
147 pub fn total(&self, amount_sat: u64) -> u64 {
148 self.boltz(amount_sat) + self.claim_estimate() + self.lockup() + self.server()
149 }
150
151 pub fn boltz(&self, amount_sat: u64) -> u64 {
152 ((self.percentage / 100.0) * amount_sat as f64).ceil() as u64
153 }
154
155 pub fn claim_estimate(&self) -> u64 {
156 self.miner_fees.user.claim
157 }
158
159 pub fn lockup(&self) -> u64 {
160 self.miner_fees.user.lockup
161 }
162
163 pub fn server(&self) -> u64 {
164 self.miner_fees.server
165 }
166}
167
168#[derive(Serialize, Deserialize, Debug, Clone)]
169#[serde(rename_all = "camelCase")]
170pub struct ReverseFees {
171 pub percentage: f64,
172 pub miner_fees: PairMinerFees,
173}
174
175impl ReverseFees {
176 pub fn total(&self, invoice_amount_sat: u64) -> u64 {
177 self.boltz(invoice_amount_sat) + self.claim_estimate() + self.lockup()
178 }
179
180 pub fn boltz(&self, invoice_amount_sat: u64) -> u64 {
181 ((self.percentage / 100.0) * invoice_amount_sat as f64).ceil() as u64
182 }
183
184 pub fn claim_estimate(&self) -> u64 {
185 self.miner_fees.claim
186 }
187
188 pub fn lockup(&self) -> u64 {
189 self.miner_fees.lockup
190 }
191}
192
193#[derive(Serialize, Deserialize, Debug, Clone)]
194#[serde(rename_all = "camelCase")]
195pub struct SubmarineFees {
196 pub percentage: f64,
198 pub miner_fees: u64,
200}
201
202impl SubmarineFees {
203 pub fn total(&self, invoice_amount_sat: u64) -> u64 {
204 self.boltz(invoice_amount_sat) + self.network()
205 }
206
207 pub fn boltz(&self, invoice_amount_sat: u64) -> u64 {
208 ((self.percentage / 100.0) * invoice_amount_sat as f64).ceil() as u64
209 }
210
211 pub fn network(&self) -> u64 {
212 self.miner_fees
213 }
214}
215
216#[derive(Serialize, Deserialize, Debug, Clone)]
217#[serde(rename_all = "camelCase")]
218pub struct ChainPair {
219 pub hash: String,
221 pub rate: f64,
223 pub limits: PairLimits,
225 pub fees: ChainFees,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone)]
230#[serde(rename_all = "camelCase")]
231pub struct ReversePair {
232 pub hash: String,
234 pub rate: f64,
236 pub limits: ReverseLimits,
238 pub fees: ReverseFees,
240}
241
242#[derive(Serialize, Deserialize, Debug, Clone)]
243#[serde(rename_all = "camelCase")]
244pub struct SubmarinePair {
245 pub hash: String,
247 pub rate: f64,
249 pub limits: SubmarinePairLimits,
251 pub fees: SubmarineFees,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct GetSubmarinePairsResponse {
257 #[serde(rename = "BTC")]
258 pub btc: HashMap<String, SubmarinePair>,
259 #[serde(rename = "L-BTC")]
260 pub lbtc: HashMap<String, SubmarinePair>,
261}
262
263impl GetSubmarinePairsResponse {
264 pub fn get_btc_to_btc_pair(&self) -> Option<SubmarinePair> {
267 self.btc.get("BTC").cloned()
268 }
269
270 pub fn get_btc_to_lbtc_pair(&self) -> Option<SubmarinePair> {
273 self.btc.get("L-BTC").cloned()
274 }
275
276 pub fn get_lbtc_to_btc_pair(&self) -> Option<SubmarinePair> {
279 self.lbtc.get("BTC").cloned()
280 }
281
282 pub fn get_lbtc_to_lbtc_pair(&self) -> Option<SubmarinePair> {
285 self.lbtc.get("L-BTC").cloned()
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct GetReversePairsResponse {
291 #[serde(rename = "BTC")]
292 pub btc: HashMap<String, ReversePair>,
293}
294
295impl GetReversePairsResponse {
296 pub fn get_btc_to_btc_pair(&self) -> Option<ReversePair> {
299 self.btc.get("BTC").cloned()
300 }
301
302 pub fn get_btc_to_lbtc_pair(&self) -> Option<ReversePair> {
305 self.btc.get("L-BTC").cloned()
306 }
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct GetChainPairsResponse {
311 #[serde(rename = "BTC")]
312 pub btc: HashMap<String, ChainPair>,
313 #[serde(rename = "L-BTC")]
314 pub lbtc: HashMap<String, ChainPair>,
315}
316
317impl GetChainPairsResponse {
318 pub fn get_btc_to_lbtc_pair(&self) -> Option<ChainPair> {
321 self.btc.get("L-BTC").cloned()
322 }
323
324 pub fn get_lbtc_to_btc_pair(&self) -> Option<ChainPair> {
327 self.lbtc.get("BTC").cloned()
328 }
329}
330
331#[derive(Debug, Clone)]
333pub struct BoltzApiClientV2 {
334 base_url: String,
335 http_client: reqwest::Client,
336 timeout: Option<Duration>,
337}
338
339impl BoltzApiClientV2 {
340 pub fn new(base_url: String, timeout: Option<Duration>) -> Self {
341 let http_client = reqwest::Client::new();
342 Self {
343 base_url,
344 http_client,
345 timeout,
346 }
347 }
348
349 pub fn default(network: Network) -> Self {
350 let base_url = match network {
351 Network::Mainnet => BOLTZ_MAINNET_URL_V2.to_string(),
352 Network::Testnet => BOLTZ_TESTNET_URL_V2.to_string(),
353 Network::Regtest => BOLTZ_REGTEST.to_string(),
354 };
355 Self::new(base_url, None)
356 }
357
358 pub fn with_client(
359 base_url: String,
360 http_client: reqwest::Client,
361 timeout: Option<Duration>,
362 ) -> Self {
363 Self {
364 base_url,
365 http_client,
366 timeout,
367 }
368 }
369
370 pub fn get_ws_url(&self) -> String {
372 self.base_url.clone().replace("http", "ws") + "/ws"
373 }
374
375 #[cfg(feature = "ws")]
377 pub async fn connect_ws(&self) -> Result<WebSocketStream, Error> {
378 Ok(connect(self.get_ws_url()).await?)
379 }
380
381 #[cfg(feature = "ws")]
383 pub async fn connect_ws_with_protocols(
384 &self,
385 protocols: &[&str],
386 ) -> Result<WebSocketStream, Error> {
387 Ok(connect_with_protocols(self.get_ws_url(), protocols).await?)
388 }
389
390 #[cfg(feature = "ws")]
391 pub fn ws(&self, config: BoltzWsConfig) -> BoltzWsApi {
392 BoltzWsApi::new(self.get_ws_url(), config)
393 }
394
395 async fn get_response(&self, end_point: &str) -> Result<reqwest::Response, Error> {
397 let url = format!("{}/{}", self.base_url, end_point);
398 let req_builder = self.http_client.get(url);
399 let req_builder = self.maybe_add_timeout(req_builder);
400 Ok(req_builder.send().await?)
401 }
402
403 async fn get(&self, end_point: &str) -> Result<String, Error> {
405 Ok(self.get_response(end_point).await?.text().await?)
406 }
407
408 async fn post(&self, end_point: &str, data: impl Serialize) -> Result<String, Error> {
410 let url = format!("{}/{}", self.base_url, end_point);
411
412 self.request(Method::POST, url, data).await
413 }
414
415 async fn patch(&self, end_point: &str, data: impl Serialize) -> Result<String, Error> {
417 let url = format!("{}/{}", self.base_url, end_point);
418
419 self.request(Method::PATCH, url, data).await
420 }
421
422 async fn request(
423 &self,
424 method: Method,
425 url: String,
426 data: impl Serialize,
427 ) -> Result<String, Error> {
428 let method_str = method.to_string();
429 let req_builder = self.http_client.request(method, url).json(&data);
430 let req_builder = self.maybe_add_timeout(req_builder);
431 match req_builder.send().await {
432 Ok(r) => {
433 if r.status().is_success() {
434 log::debug!("{method_str} response: {r:#?}");
435 Ok(r.text().await?)
436 } else {
437 log::error!("{} error: HTTP {}", method_str, r.status());
438 let err_resp = r.text().await.unwrap_or("Unknown error".to_string());
439 let e_val: Value = serde_json::from_str(&err_resp).unwrap_or(Value::Null);
440 let e_str = e_val.get("error").unwrap_or(&Value::Null).to_string();
441 Err(Error::HTTP(e_str))
442 }
443 }
444 Err(e) => {
445 log::error!("{method_str} error: {e:#?}");
446 Err(e.into())
447 }
448 }
449 }
450
451 fn maybe_add_timeout(&self, req_builder: RequestBuilder) -> RequestBuilder {
452 if let Some(timeout) = self.timeout {
453 req_builder.timeout(timeout)
454 } else {
455 req_builder
456 }
457 }
458
459 pub async fn get_fee_estimation(&self) -> Result<GetFeeEstimationResponse, Error> {
460 Ok(serde_json::from_str(&self.get("chain/fees").await?)?)
461 }
462
463 pub async fn get_height(&self) -> Result<HeightResponse, Error> {
464 Ok(serde_json::from_str(&self.get("chain/heights").await?)?)
465 }
466
467 pub async fn get_submarine_pairs(&self) -> Result<GetSubmarinePairsResponse, Error> {
468 Ok(serde_json::from_str(&self.get("swap/submarine").await?)?)
469 }
470
471 pub async fn get_reverse_pairs(&self) -> Result<GetReversePairsResponse, Error> {
472 Ok(serde_json::from_str(&self.get("swap/reverse").await?)?)
473 }
474
475 pub async fn get_chain_pairs(&self) -> Result<GetChainPairsResponse, Error> {
476 Ok(serde_json::from_str(&self.get("swap/chain").await?)?)
477 }
478
479 pub async fn post_swap_req(
480 &self,
481 swap_request: &CreateSubmarineRequest,
482 ) -> Result<CreateSubmarineResponse, Error> {
483 let data = serde_json::to_value(swap_request)?;
484 Ok(serde_json::from_str(
485 &self.post("swap/submarine", data).await?,
486 )?)
487 }
488
489 pub async fn post_reverse_req(
490 &self,
491 req: CreateReverseRequest,
492 ) -> Result<CreateReverseResponse, Error> {
493 Ok(serde_json::from_str(
494 &self.post("swap/reverse", req).await?,
495 )?)
496 }
497
498 pub async fn post_chain_req(
499 &self,
500 req: CreateChainRequest,
501 ) -> Result<CreateChainResponse, Error> {
502 Ok(serde_json::from_str(&self.post("swap/chain", req).await?)?)
503 }
504
505 pub async fn get_submarine_claim_tx_details(
506 &self,
507 id: &String,
508 ) -> Result<SubmarineClaimTxResponse, Error> {
509 let endpoint = format!("swap/submarine/{id}/claim");
510 let response = self.get_response(&endpoint).await?;
511 let status = response.status();
512 if status.is_success() {
513 let body = response.text().await?;
514 Ok(serde_json::from_str(&body)?)
515 } else {
516 let body = serde_json::from_str(&response.text().await?)?;
517 Err(Error::HTTPStatusNotSuccess(status, body))
518 }
519 }
520
521 pub async fn get_chain_claim_tx_details(
522 &self,
523 id: &String,
524 ) -> Result<Option<ChainClaimTxResponse>, Error> {
525 let endpoint = format!("swap/chain/{id}/claim");
526 let res = self.get(&endpoint).await?;
527
528 match serde_json::from_str(&res) {
529 Ok(response) => Ok(response),
530 Err(e) => {
531 let error: ErrorResponse = serde_json::from_str(&res)?;
532 if error.error == "server claim succeeded already" {
533 Ok(None)
534 } else {
535 Err(Error::JSON(e))
536 }
537 }
538 }
539 }
540
541 pub async fn post_submarine_claim_tx_details(
542 &self,
543 id: &String,
544 pub_nonce: musig::PublicNonce,
545 partial_sig: musig::PartialSignature,
546 ) -> Result<Value, Error> {
547 let data = json!(
548 {
549 "pubNonce": pub_nonce.serialize().to_lower_hex_string(),
550 "partialSignature": partial_sig.serialize().to_lower_hex_string()
551 }
552 );
553 let endpoint = format!("swap/submarine/{id}/claim");
554 Ok(serde_json::from_str(&self.post(&endpoint, data).await?)?)
555 }
556
557 pub async fn post_chain_claim_tx_details(
558 &self,
559 id: &String,
560 preimage: &Preimage,
561 signature: Option<(musig::PartialSignature, musig::PublicNonce)>,
562 to_sign: ToSign,
563 ) -> Result<PartialSig, Error> {
564 let data = match signature {
565 Some((partial_sig, pub_nonce)) => json!(
566 {
567 "preimage": preimage.bytes.ok_or(Error::Protocol("Preimage bytes not available to post chain claim".to_string()))?.to_lower_hex_string(),
568 "signature": PartialSig {
569 pub_nonce: pub_nonce.serialize().to_lower_hex_string(),
570 partial_signature: partial_sig.serialize().to_lower_hex_string(),
571 },
572 "toSign": to_sign,
573 }
574 ),
575 None => json!(
576 {
577 "preimage": preimage.bytes.ok_or(Error::Protocol("Preimage bytes not available to post chain claim".to_string()))?.to_lower_hex_string(),
578 "toSign": to_sign,
579 }
580 ),
581 };
582 let endpoint = format!("swap/chain/{id}/claim");
583 Ok(serde_json::from_str(&self.post(&endpoint, data).await?)?)
584 }
585
586 pub async fn get_reverse_tx(&self, id: &str) -> Result<ReverseSwapTxResp, Error> {
587 Ok(serde_json::from_str(
588 &self.get(&format!("swap/reverse/{id}/transaction")).await?,
589 )?)
590 }
591
592 pub async fn get_submarine_tx(&self, id: &str) -> Result<SubmarineSwapTxResp, Error> {
593 Ok(serde_json::from_str(
594 &self
595 .get(&format!("swap/submarine/{id}/transaction"))
596 .await?,
597 )?)
598 }
599
600 pub async fn get_submarine_preimage(
601 &self,
602 id: &str,
603 ) -> Result<SubmarineSwapPreimageResp, Error> {
604 Ok(serde_json::from_str(
605 &self.get(&format!("swap/submarine/{id}/preimage")).await?,
606 )?)
607 }
608
609 pub async fn get_chain_txs(&self, id: &str) -> Result<ChainSwapTxResp, Error> {
610 Ok(serde_json::from_str(
611 &self.get(&format!("swap/chain/{id}/transactions")).await?,
612 )?)
613 }
614
615 pub async fn get_reverse_partial_sig(
616 &self,
617 id: &String,
618 preimage: &Preimage,
619 pub_nonce: &musig::PublicNonce,
620 claim_tx_hex: &String,
621 ) -> Result<PartialSig, Error> {
622 let data = json!(
623 {
624 "preimage": preimage.bytes.ok_or(Error::Protocol("Preimage bytes not available to post chain claim".to_string()))?.to_lower_hex_string(),
625 "pubNonce": pub_nonce.serialize().to_lower_hex_string(),
626 "transaction": claim_tx_hex,
627 "index": 0
628 }
629 );
630
631 let endpoint = format!("swap/reverse/{id}/claim");
632 Ok(serde_json::from_str(&self.post(&endpoint, data).await?)?)
633 }
634
635 pub async fn get_submarine_partial_sig(
636 &self,
637 id: &String,
638 input_index: usize,
639 pub_nonce: &musig::PublicNonce,
640 refund_tx_hex: &String,
641 ) -> Result<PartialSig, Error> {
642 let data = json!(
643 {
644 "pubNonce": pub_nonce.serialize().to_lower_hex_string(),
645 "transaction": refund_tx_hex,
646 "index": input_index
647 }
648 );
649
650 let endpoint = format!("swap/submarine/{id}/refund");
651 Ok(serde_json::from_str(&self.post(&endpoint, data).await?)?)
652 }
653
654 pub async fn get_chain_partial_sig(
655 &self,
656 id: &String,
657 input_index: usize,
658 pub_nonce: &musig::PublicNonce,
659 refund_tx_hex: &String,
660 ) -> Result<PartialSig, Error> {
661 let data = json!(
662 {
663 "pubNonce": pub_nonce.serialize().to_lower_hex_string(),
664 "transaction": refund_tx_hex,
665 "index": input_index
666 }
667 );
668
669 let endpoint = format!("swap/chain/{id}/refund");
670 Ok(serde_json::from_str(&self.post(&endpoint, data).await?)?)
671 }
672
673 pub async fn get_mrh_bip21(&self, invoice: &str) -> Result<MrhResponse, Error> {
674 let request = format!("swap/reverse/{invoice}/bip21");
675 Ok(serde_json::from_str(&self.get(&request).await?)?)
676 }
677
678 pub async fn broadcast_tx(&self, chain: Chain, tx_hex: &String) -> Result<Value, Error> {
679 let data = json!(
680 {
681 "hex": tx_hex
682 }
683 );
684
685 let chain = match chain {
686 Chain::Bitcoin(_) => "BTC",
687 Chain::Liquid(_) => "L-BTC",
688 };
689
690 let end_point = format!("chain/{chain}/transaction");
691 Ok(serde_json::from_str(&self.post(&end_point, data).await?)?)
692 }
693
694 pub async fn post_bolt12_offer(&self, req: CreateBolt12OfferRequest) -> Result<(), Error> {
696 let data = serde_json::to_value(req)?;
697 let end_point = "lightning/BTC/bolt12".to_string();
698 self.post(&end_point, data).await?;
699 Ok(())
700 }
701
702 pub async fn patch_bolt12_offer(&self, req: UpdateBolt12OfferRequest) -> Result<(), Error> {
710 let data = serde_json::to_value(req)?;
711 let end_point = "lightning/BTC/bolt12".to_string();
712 self.patch(&end_point, data).await?;
713 Ok(())
714 }
715
716 pub async fn delete_bolt12_offer(&self, offer: &str, signature: &str) -> Result<(), Error> {
722 let data = json!(
723 {
724 "offer": offer,
725 "signature": signature,
726 }
727 );
728
729 let end_point = "lightning/BTC/bolt12/delete".to_string();
730 self.post(&end_point, data).await?;
731 Ok(())
732 }
733
734 pub async fn get_bolt12_invoice(
736 &self,
737 req: GetBolt12FetchRequest,
738 ) -> Result<GetBolt12FetchResponse, Error> {
739 let data = serde_json::to_value(req)?;
740 let end_point = "lightning/BTC/bolt12/fetch".to_string();
741 Ok(serde_json::from_str(&self.post(&end_point, data).await?)?)
742 }
743
744 pub async fn get_bolt12_params(&self) -> Result<GetBolt12ParamsResponse, Error> {
746 let end_point = "lightning/BTC/bolt12/L-BTC".to_string();
747 Ok(serde_json::from_str(&self.get(&end_point).await?)?)
748 }
749
750 pub async fn get_nodes(&self) -> Result<GetNodesResponse, Error> {
752 let end_point = "nodes".to_string();
753 Ok(serde_json::from_str(&self.get(&end_point).await?)?)
754 }
755
756 pub async fn get_quote(&self, swap_id: &str) -> Result<GetQuoteResponse, Error> {
761 let end_point = format!("swap/chain/{swap_id}/quote");
762 Ok(serde_json::from_str(&self.get(&end_point).await?)?)
763 }
764
765 pub async fn accept_quote(&self, swap_id: &str, amount_sat: u64) -> Result<(), Error> {
767 let data = json!(
768 {
769 "amount": amount_sat
770 }
771 );
772
773 let end_point = format!("swap/chain/{swap_id}/quote");
774 self.post(&end_point, data).await?;
775 Ok(())
776 }
777
778 pub async fn get_swap(&self, swap_id: &str) -> Result<GetSwapResponse, Error> {
780 let end_point = format!("swap/{swap_id}");
781 Ok(serde_json::from_str(&self.get(&end_point).await?)?)
782 }
783
784 pub async fn post_swap_restore(
786 &self,
787 xpub: &String,
788 ) -> Result<Vec<SwapRestoreResponse>, Error> {
789 let data = json!(
790 {
791 "xpub": xpub,
792 }
793 );
794
795 Ok(serde_json::from_str::<Vec<SwapRestoreResponse>>(
796 &self.post("swap/restore", data).await?,
797 )?)
798 }
799
800 pub async fn post_swap_restore_index(
802 &self,
803 xpub: &String,
804 ) -> Result<SwapRestoreIndexResponse, Error> {
805 let data = json!(
806 {
807 "xpub": xpub,
808 }
809 );
810
811 Ok(serde_json::from_str::<SwapRestoreIndexResponse>(
812 &self.post("swap/restore/index", data).await?,
813 )?)
814 }
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
818#[serde(rename_all = "camelCase")]
819pub struct ChainClaimTxResponse {
820 pub pub_nonce: String,
821 pub public_key: PublicKey,
822 pub transaction_hash: String,
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize)]
826#[serde(rename_all = "camelCase")]
827pub struct SubmarineClaimTxResponse {
828 pub preimage: String,
829 pub pub_nonce: String,
830 pub public_key: PublicKey,
831 pub transaction_hash: String,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
835#[serde(rename_all = "camelCase")]
836pub struct MrhResponse {
837 pub bip21: String,
838 pub signature: String,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
842#[serde(rename_all = "camelCase")]
843pub struct Webhook<T> {
844 pub url: String,
845 #[serde(skip_serializing_if = "Option::is_none")]
846 pub hash_swap_id: Option<bool>,
847 #[serde(skip_serializing_if = "Option::is_none")]
848 pub status: Option<Vec<T>>,
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853pub struct CreateSubmarineRequest {
854 pub from: String,
855 pub to: String,
856 pub invoice: String,
857 pub refund_public_key: PublicKey,
858 #[serde(skip_serializing_if = "Option::is_none")]
859 pub pair_hash: Option<String>,
860 #[serde(skip_serializing_if = "Option::is_none")]
861 pub referral_id: Option<String>,
862 #[serde(skip_serializing_if = "Option::is_none")]
863 pub webhook: Option<Webhook<SubSwapStates>>,
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
867#[serde(rename_all = "camelCase")]
868pub struct CreateSubmarineResponse {
869 pub accept_zero_conf: bool,
870 pub address: String,
871 pub bip21: String,
872 pub claim_public_key: PublicKey,
873 pub expected_amount: u64,
874 pub id: String,
875 #[serde(skip_serializing_if = "Option::is_none")]
876 pub referral_id: Option<String>,
877 pub swap_tree: SwapTree,
878 pub timeout_block_height: u64,
879 #[serde(skip_serializing_if = "Option::is_none")]
880 pub blinding_key: Option<String>,
881}
882impl CreateSubmarineResponse {
883 pub fn validate(
885 &self,
886 invoice: &str,
887 our_pubkey: &PublicKey,
888 chain: Chain,
889 ) -> Result<(), Error> {
890 let preimage = Preimage::from_invoice_str(invoice)?;
891
892 match chain {
893 Chain::Bitcoin(bitcoin_chain) => {
894 let boltz_sub_script = BtcSwapScript::submarine_from_swap_resp(self, *our_pubkey)?;
895 boltz_sub_script.validate_address(bitcoin_chain, self.address.clone())
896 }
897 Chain::Liquid(liquid_chain) => {
898 let boltz_sub_script = LBtcSwapScript::submarine_from_swap_resp(self, *our_pubkey)?;
899 if boltz_sub_script.hashlock != preimage.hash160 {
900 return Err(Error::Protocol(format!(
901 "Hash160 mismatch: {},{}",
902 boltz_sub_script.hashlock, preimage.hash160
903 )));
904 }
905
906 boltz_sub_script.validate_address(liquid_chain, self.address.clone())
907 }
908 }
909 }
910}
911#[derive(Debug, Clone, Serialize, Deserialize)]
912#[serde(rename_all = "camelCase")]
913pub struct SwapTree {
914 pub claim_leaf: Leaf,
915 pub refund_leaf: Leaf,
916}
917
918#[derive(Debug, Clone, Serialize, Deserialize)]
919#[serde(rename_all = "camelCase")]
920pub struct Leaf {
921 pub output: String,
922 pub version: u8,
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct ClaimDetails {
928 pub tree: SwapTree,
929 pub amount: Option<u64>,
930 pub key_index: u32,
931 pub lockup_address: String,
932 pub server_public_key: String,
933 pub timeout_block_height: u32,
934 pub blinding_key: Option<String>,
935 pub preimage_hash: String,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
939#[serde(rename_all = "camelCase")]
940pub struct RefundDetails {
941 pub tree: SwapTree,
942 pub key_index: u32,
943 pub lockup_address: String,
944 pub server_public_key: String,
945 pub timeout_block_height: u32,
946 pub blinding_key: Option<String>,
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
950#[serde(rename_all = "lowercase")]
951pub enum SwapRestoreType {
952 Reverse,
953 Submarine,
954 Chain,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
958#[serde(rename_all = "camelCase")]
959pub struct SwapRestoreResponse {
960 pub id: String,
961 #[serde(rename = "type")]
962 pub swap_type: SwapRestoreType,
963 pub status: String,
964 pub created_at: u64,
965 pub from: String,
966 pub to: String,
967 pub claim_details: Option<ClaimDetails>,
968 pub refund_details: Option<RefundDetails>,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
972#[serde(rename_all = "camelCase")]
973pub struct SwapRestoreIndexResponse {
974 pub index: i64,
975}
976
977#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
978pub enum SubscriptionChannel {
979 #[serde(rename = "swap.update")]
980 SwapUpdate,
981 #[serde(rename = "invoice.request")]
982 InvoiceRequest,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
986pub struct InvoiceRequestParams {
987 pub offer: String,
988 pub signature: String,
989}
990
991#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
992#[serde(tag = "channel")]
993pub enum SubscribeRequest {
994 #[serde(rename = "swap.update")]
995 SwapUpdate { args: Vec<String> },
996 #[serde(rename = "invoice.request")]
997 InvoiceRequest { args: Vec<InvoiceRequestParams> },
998}
999
1000#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1001pub struct UnsubscribeRequest {
1002 pub channel: SubscriptionChannel,
1003 pub args: Vec<String>,
1004}
1005
1006#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1007pub struct InvoiceCreated {
1008 pub id: String,
1009 pub invoice: String,
1010}
1011
1012#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1013pub struct InvoiceError {
1014 pub id: String,
1015 pub error: String,
1016}
1017
1018#[derive(Clone, Debug, Serialize, Deserialize)]
1019#[serde(tag = "op")]
1020pub enum WsRequest {
1021 #[serde(rename = "subscribe")]
1022 Subscribe(SubscribeRequest),
1023 #[serde(rename = "unsubscribe")]
1024 Unsubscribe(UnsubscribeRequest),
1025 #[serde(rename = "invoice")]
1026 Invoice(InvoiceCreated),
1027 #[serde(rename = "invoice.error")]
1028 InvoiceError(InvoiceError),
1029 #[serde(rename = "ping")]
1030 Ping,
1031}
1032
1033impl WsRequest {
1034 pub fn subscribe_swap_request(swap_id: &str) -> Self {
1035 Self::subscribe_swaps_request(vec![swap_id.to_string()])
1036 }
1037
1038 pub fn subscribe_swaps_request(swap_ids: Vec<String>) -> Self {
1039 Self::Subscribe(SubscribeRequest::SwapUpdate { args: swap_ids })
1040 }
1041
1042 pub fn subscribe_invoice_request(params: InvoiceRequestParams) -> Self {
1043 Self::subscribe_invoice_requests(vec![params])
1044 }
1045
1046 pub fn subscribe_invoice_requests(params: Vec<InvoiceRequestParams>) -> Self {
1047 Self::Subscribe(SubscribeRequest::InvoiceRequest { args: params })
1048 }
1049}
1050
1051#[derive(Deserialize, Serialize, Debug, PartialEq)]
1052pub struct SubscribeResponse {
1053 pub channel: SubscriptionChannel,
1054 pub args: Vec<String>,
1055
1056 pub timestamp: String,
1057}
1058
1059#[derive(Deserialize, Serialize, Debug, PartialEq)]
1060pub struct UnsubscribeResponse {
1061 pub channel: SubscriptionChannel,
1062 pub args: Vec<String>,
1063
1064 pub timestamp: String,
1065}
1066
1067#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
1068pub struct TransactionInfo {
1069 pub id: String,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub hex: Option<String>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1073 pub eta: Option<u64>,
1074}
1075
1076#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
1077pub struct FailureReasonIncorrectAmounts {
1078 pub expected: u64,
1079 pub actual: u64,
1080}
1081
1082#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
1083pub struct ChannelInfo {
1084 #[serde(rename = "fundingTransactionId")]
1085 pub funding_transaction_id: String,
1086 #[serde(rename = "fundingTransactionVout")]
1087 pub funding_transaction_vout: u64,
1088}
1089
1090#[derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq)]
1091pub struct SwapStatus {
1092 pub id: String,
1093 pub status: String,
1094
1095 #[serde(rename = "zeroConfRejected", skip_serializing_if = "Option::is_none")]
1096 pub zero_conf_rejected: Option<bool>,
1097 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub transaction: Option<TransactionInfo>,
1099
1100 #[serde(rename = "failureReason", skip_serializing_if = "Option::is_none")]
1101 pub failure_reason: Option<String>,
1102 #[serde(rename = "failureDetails", skip_serializing_if = "Option::is_none")]
1103 pub failure_details: Option<FailureReasonIncorrectAmounts>,
1104
1105 #[serde(rename = "channel", skip_serializing_if = "Option::is_none")]
1106 pub channel_info: Option<ChannelInfo>,
1107}
1108
1109#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
1110pub struct InvoiceRequest {
1111 pub id: String,
1112
1113 pub offer: String,
1114 #[serde(rename = "invoiceRequest")]
1115 pub invoice_request: String,
1116}
1117
1118#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
1119pub struct ErrorResponse {
1120 pub error: String,
1121}
1122
1123#[derive(Deserialize, Serialize, Debug, PartialEq)]
1124pub struct UpdateResponse<T> {
1125 pub channel: SubscriptionChannel,
1126 pub args: Vec<T>,
1127
1128 pub timestamp: String,
1129}
1130
1131#[derive(Deserialize, Serialize, Debug, PartialEq)]
1132#[serde(tag = "event")]
1133pub enum WsResponse {
1134 #[serde(rename = "subscribe")]
1135 Subscribe(SubscribeResponse),
1136 #[serde(rename = "unsubscribe")]
1137 Unsubscribe(UnsubscribeResponse),
1138 #[serde(rename = "update")]
1139 Update(UpdateResponse<SwapStatus>),
1140 #[serde(rename = "request")]
1141 InvoiceRequest(UpdateResponse<InvoiceRequest>),
1142 #[serde(rename = "error")]
1143 Error(ErrorResponse),
1144 #[serde(rename = "pong")]
1145 Pong,
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150pub struct CreateReverseRequest {
1151 pub from: String,
1152 pub to: String,
1153 pub claim_public_key: PublicKey,
1154 #[serde(skip_serializing_if = "Option::is_none")]
1156 pub invoice: Option<String>,
1157 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub invoice_amount: Option<u64>,
1160 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub preimage_hash: Option<sha256::Hash>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1164 pub description: Option<String>,
1165 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub description_hash: Option<String>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1168 pub address: Option<String>,
1169 #[serde(skip_serializing_if = "Option::is_none")]
1170 pub address_signature: Option<String>,
1171 #[serde(skip_serializing_if = "Option::is_none")]
1172 pub referral_id: Option<String>,
1173 #[serde(skip_serializing_if = "Option::is_none")]
1174 pub webhook: Option<Webhook<RevSwapStates>>,
1175}
1176
1177#[derive(Debug, Clone, Serialize, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub struct CreateReverseResponse {
1180 pub id: String,
1181 pub invoice: Option<String>,
1182 pub swap_tree: SwapTree,
1183 pub lockup_address: String,
1184 pub refund_public_key: PublicKey,
1185 pub timeout_block_height: u32,
1186 pub onchain_amount: u64,
1187 #[serde(skip_serializing_if = "Option::is_none")]
1188 pub blinding_key: Option<String>,
1189}
1190impl CreateReverseResponse {
1191 pub fn validate(
1195 &self,
1196 preimage: &Preimage,
1197 our_pubkey: &PublicKey,
1198 chain: Chain,
1199 ) -> Result<(), Error> {
1200 if let Some(invoice) = &self.invoice {
1201 let invoice = Bolt11Invoice::from_str(invoice)?;
1203 if invoice.payment_hash().to_string() != preimage.sha256.to_string() {
1204 return Err(Error::Protocol(format!(
1205 "Preimage hash mismatch : {},{}",
1206 &invoice.payment_hash().to_string(),
1207 preimage.sha256
1208 )));
1209 }
1210 }
1211
1212 match chain {
1213 Chain::Bitcoin(bitcoin_chain) => {
1214 let boltz_rev_script = BtcSwapScript::reverse_from_swap_resp(self, *our_pubkey)?;
1215 boltz_rev_script.validate_address(bitcoin_chain, self.lockup_address.clone())
1216 }
1217 Chain::Liquid(liquid_chain) => {
1218 let boltz_rev_script = LBtcSwapScript::reverse_from_swap_resp(self, *our_pubkey)?;
1219 boltz_rev_script.validate_address(liquid_chain, self.lockup_address.clone())
1220 }
1221 }
1222 }
1223}
1224
1225#[derive(Debug, Clone, PartialEq)]
1226pub enum Side {
1227 Lockup,
1228 Claim,
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[serde(rename_all = "camelCase")]
1233pub struct ChainSwapDetails {
1234 pub swap_tree: SwapTree,
1235 pub lockup_address: String,
1236 pub server_public_key: PublicKey,
1237 pub timeout_block_height: u32,
1238 pub amount: u64,
1239 #[serde(skip_serializing_if = "Option::is_none")]
1240 pub blinding_key: Option<String>,
1241 #[serde(skip_serializing_if = "Option::is_none")]
1242 pub refund_address: Option<String>,
1243 #[serde(skip_serializing_if = "Option::is_none")]
1244 pub claim_address: Option<String>,
1245 #[serde(skip_serializing_if = "Option::is_none")]
1246 pub bip21: Option<String>,
1247}
1248
1249#[derive(Debug, Clone, Serialize, Deserialize)]
1250#[serde(rename_all = "camelCase")]
1251pub struct CreateChainRequest {
1252 pub from: String,
1253 pub to: String,
1254 pub preimage_hash: sha256::Hash,
1255 #[serde(skip_serializing_if = "Option::is_none")]
1256 pub claim_public_key: Option<PublicKey>,
1257 #[serde(skip_serializing_if = "Option::is_none")]
1258 pub refund_public_key: Option<PublicKey>,
1259 #[serde(skip_serializing_if = "Option::is_none")]
1260 pub user_lock_amount: Option<u64>,
1261 #[serde(skip_serializing_if = "Option::is_none")]
1262 pub server_lock_amount: Option<u64>,
1263 #[serde(skip_serializing_if = "Option::is_none")]
1264 pub pair_hash: Option<String>,
1265 #[serde(skip_serializing_if = "Option::is_none")]
1266 pub referral_id: Option<String>,
1267 #[serde(skip_serializing_if = "Option::is_none")]
1268 pub webhook: Option<Webhook<ChainSwapStates>>,
1269}
1270
1271#[derive(Debug, Clone, Serialize, Deserialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct CreateChainResponse {
1274 pub id: String,
1275 pub claim_details: ChainSwapDetails,
1276 pub lockup_details: ChainSwapDetails,
1277}
1278impl CreateChainResponse {
1279 pub fn validate(
1281 &self,
1282 claim_pubkey: &PublicKey,
1283 refund_pubkey: &PublicKey,
1284 from_chain: Chain,
1285 to_chain: Chain,
1286 ) -> Result<(), Error> {
1287 self.validate_side(
1288 Side::Lockup,
1289 from_chain,
1290 &self.lockup_details,
1291 refund_pubkey,
1292 )?;
1293 self.validate_side(Side::Claim, to_chain, &self.claim_details, claim_pubkey)
1294 }
1295
1296 fn validate_side(
1297 &self,
1298 side: Side,
1299 chain: Chain,
1300 details: &ChainSwapDetails,
1301 our_pubkey: &PublicKey,
1302 ) -> Result<(), Error> {
1303 match chain {
1304 Chain::Bitcoin(bitcoin_chain) => {
1305 let boltz_chain_script =
1306 BtcSwapScript::chain_from_swap_resp(side, details.clone(), *our_pubkey)?;
1307 boltz_chain_script.validate_address(bitcoin_chain, details.lockup_address.clone())
1308 }
1309 Chain::Liquid(liquid_chain) => {
1310 let boltz_chain_script =
1311 LBtcSwapScript::chain_from_swap_resp(side, details.clone(), *our_pubkey)?;
1312 boltz_chain_script.validate_address(liquid_chain, details.lockup_address.clone())
1313 }
1314 }
1315 }
1316}
1317
1318#[derive(Debug, Clone, Serialize, Deserialize)]
1319#[serde(rename_all = "camelCase")]
1320pub struct ChainSwapTx {
1321 pub id: String,
1322 #[serde(skip_serializing_if = "Option::is_none")]
1323 pub hex: Option<String>,
1324}
1325
1326#[derive(Debug, Clone, Serialize, Deserialize)]
1327#[serde(rename_all = "camelCase")]
1328pub struct ChainSwapTxTimeout {
1329 pub block_height: u32,
1330 #[serde(skip_serializing_if = "Option::is_none")]
1331 pub eta: Option<u32>,
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct ChainSwapTxLock {
1337 pub transaction: ChainSwapTx,
1338 pub timeout: ChainSwapTxTimeout,
1339}
1340
1341#[derive(Debug, Clone, Serialize, Deserialize)]
1342#[serde(rename_all = "camelCase")]
1343pub struct ChainSwapTxResp {
1344 #[serde(skip_serializing_if = "Option::is_none")]
1345 pub user_lock: Option<ChainSwapTxLock>,
1346 #[serde(skip_serializing_if = "Option::is_none")]
1347 pub server_lock: Option<ChainSwapTxLock>,
1348}
1349
1350#[derive(Debug, Clone, Serialize, Deserialize)]
1351#[serde(rename_all = "camelCase")]
1352pub struct ReverseSwapTxResp {
1353 pub id: String,
1354 #[serde(skip_serializing_if = "Option::is_none")]
1355 pub hex: Option<String>,
1356 pub timeout_block_height: u32,
1357}
1358
1359#[derive(Debug, Clone, Serialize, Deserialize)]
1360#[serde(rename_all = "camelCase")]
1361pub struct SubmarineSwapTxResp {
1362 pub id: String,
1363 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub hex: Option<String>,
1365 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub timeout_block_height: Option<u32>,
1367 #[serde(skip_serializing_if = "Option::is_none")]
1368 pub timeout_eta: Option<u32>,
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize)]
1372#[serde(rename_all = "camelCase")]
1373pub struct SubmarineSwapPreimageResp {
1374 pub preimage: String,
1375}
1376
1377#[derive(Debug, Clone, Serialize, Deserialize)]
1378#[serde(rename_all = "camelCase")]
1379pub struct PartialSig {
1380 pub pub_nonce: String,
1381 pub partial_signature: String,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize)]
1385#[serde(rename_all = "camelCase")]
1386pub struct ToSign {
1387 pub pub_nonce: String,
1388 pub transaction: String,
1389 pub index: u32,
1390}
1391
1392#[derive(Debug, Clone)]
1393pub struct Cooperative<'a> {
1394 pub boltz_api: &'a BoltzApiClientV2,
1395 pub swap_id: String,
1396 pub signature: Option<(musig::PartialSignature, musig::PublicNonce)>,
1399}
1400
1401#[derive(Debug, Clone, Serialize, Deserialize)]
1402pub struct SwapUpdateTxDetails {
1403 pub id: String,
1404 pub hex: String,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize)]
1408pub struct RespError {
1409 pub id: String,
1410 pub error: String,
1411}
1412
1413#[derive(Debug, Clone, PartialEq)]
1414pub enum SwapTxKind {
1415 Claim,
1416 Refund,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub enum SubSwapStates {
1424 #[serde(rename = "swap.created")]
1427 Created,
1428 #[serde(rename = "transaction.mempool")]
1431 TransactionMempool,
1432 #[serde(rename = "transaction.confirmed")]
1434 TransactionConfirmed,
1435 #[serde(rename = "invoice.set")]
1438 InvoiceSet,
1439 #[serde(rename = "invoice.paid")]
1441 InvoicePaid,
1442 #[serde(rename = "invoice.pending")]
1444 InvoicePending,
1445 #[serde(rename = "invoice.failedToPay")]
1448 InvoiceFailedToPay,
1449 #[serde(rename = "transaction.claimed")]
1452 TransactionClaimed,
1453 #[serde(rename = "transaction.claim.pending")]
1458 TransactionClaimPending,
1459 #[serde(rename = "transaction.lockupFailed")]
1461 TransactionLockupFailed,
1462 #[serde(rename = "swap.expired")]
1465 SwapExpired,
1466}
1467
1468impl Display for SubSwapStates {
1469 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1470 let str = match self {
1471 SubSwapStates::Created => "swap.created".to_string(),
1472 SubSwapStates::TransactionMempool => "transaction.mempool".to_string(),
1473 SubSwapStates::TransactionConfirmed => "transaction.confirmed".to_string(),
1474 SubSwapStates::InvoiceSet => "invoice.set".to_string(),
1475 SubSwapStates::InvoicePaid => "invoice.paid".to_string(),
1476 SubSwapStates::InvoicePending => "invoice.pending".to_string(),
1477 SubSwapStates::InvoiceFailedToPay => "invoice.failedToPay".to_string(),
1478 SubSwapStates::TransactionClaimed => "transaction.claimed".to_string(),
1479 SubSwapStates::TransactionClaimPending => "transaction.claim.pending".to_string(),
1480 SubSwapStates::TransactionLockupFailed => "transaction.lockupFailed".to_string(),
1481 SubSwapStates::SwapExpired => "swap.expired".to_string(),
1482 };
1483 write!(f, "{str}")
1484 }
1485}
1486
1487impl FromStr for SubSwapStates {
1488 type Err = ();
1489
1490 fn from_str(s: &str) -> Result<Self, Self::Err> {
1491 match s {
1492 "swap.created" => Ok(SubSwapStates::Created),
1493 "transaction.mempool" => Ok(SubSwapStates::TransactionMempool),
1494 "transaction.confirmed" => Ok(SubSwapStates::TransactionConfirmed),
1495 "invoice.set" => Ok(SubSwapStates::InvoiceSet),
1496 "invoice.paid" => Ok(SubSwapStates::InvoicePaid),
1497 "invoice.pending" => Ok(SubSwapStates::InvoicePending),
1498 "invoice.failedToPay" => Ok(SubSwapStates::InvoiceFailedToPay),
1499 "transaction.claimed" => Ok(SubSwapStates::TransactionClaimed),
1500 "transaction.claim.pending" => Ok(SubSwapStates::TransactionClaimPending),
1501 "transaction.lockupFailed" => Ok(SubSwapStates::TransactionLockupFailed),
1502 "swap.expired" => Ok(SubSwapStates::SwapExpired),
1503 _ => Err(()),
1504 }
1505 }
1506}
1507
1508#[derive(Debug, Clone, Serialize, Deserialize)]
1512pub enum RevSwapStates {
1513 #[serde(rename = "swap.created")]
1515 Created,
1516 #[serde(rename = "minerfee.paid")]
1519 MinerFeePaid,
1520 #[serde(rename = "transaction.mempool")]
1523 TransactionMempool,
1524 #[serde(rename = "transaction.confirmed")]
1528 TransactionConfirmed,
1529 #[serde(rename = "invoice.settled")]
1533 InvoiceSettled,
1534 #[serde(rename = "invoice.expired")]
1537 InvoiceExpired,
1538 #[serde(rename = "swap.expired")]
1540 SwapExpired,
1541 #[serde(rename = "transaction.failed")]
1546 TransactionFailed,
1547 #[serde(rename = "transaction.refunded")]
1552 TransactionRefunded,
1553}
1554
1555impl Display for RevSwapStates {
1556 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1557 let str = match self {
1558 RevSwapStates::Created => "swap.created".to_string(),
1559 RevSwapStates::MinerFeePaid => "minerfee.paid".to_string(),
1560 RevSwapStates::TransactionMempool => "transaction.mempool".to_string(),
1561 RevSwapStates::TransactionConfirmed => "transaction.confirmed".to_string(),
1562 RevSwapStates::InvoiceSettled => "invoice.settled".to_string(),
1563 RevSwapStates::InvoiceExpired => "invoice.expired".to_string(),
1564 RevSwapStates::SwapExpired => "swap.expired".to_string(),
1565 RevSwapStates::TransactionFailed => "transaction.failed".to_string(),
1566 RevSwapStates::TransactionRefunded => "transaction.refunded".to_string(),
1567 };
1568 write!(f, "{str}")
1569 }
1570}
1571
1572impl FromStr for RevSwapStates {
1573 type Err = ();
1574
1575 fn from_str(s: &str) -> Result<Self, Self::Err> {
1576 match s {
1577 "swap.created" => Ok(RevSwapStates::Created),
1578 "minerfee.paid" => Ok(RevSwapStates::MinerFeePaid),
1579 "transaction.mempool" => Ok(RevSwapStates::TransactionMempool),
1580 "transaction.confirmed" => Ok(RevSwapStates::TransactionConfirmed),
1581 "invoice.settled" => Ok(RevSwapStates::InvoiceSettled),
1582 "invoice.expired" => Ok(RevSwapStates::InvoiceExpired),
1583 "swap.expired" => Ok(RevSwapStates::SwapExpired),
1584 "transaction.failed" => Ok(RevSwapStates::TransactionFailed),
1585 "transaction.refunded" => Ok(RevSwapStates::TransactionRefunded),
1586 _ => Err(()),
1587 }
1588 }
1589}
1590
1591#[derive(Debug, Clone, Serialize, Deserialize)]
1592pub enum ChainSwapStates {
1593 #[serde(rename = "swap.created")]
1595 Created,
1596 #[serde(rename = "transaction.zeroconf.rejected")]
1598 TransactionZeroConfRejected,
1599 #[serde(rename = "transaction.mempool")]
1601 TransactionMempool,
1602 #[serde(rename = "transaction.confirmed")]
1605 TransactionConfirmed,
1606 #[serde(rename = "transaction.server.mempool")]
1608 TransactionServerMempool,
1609 #[serde(rename = "transaction.server.confirmed")]
1611 TransactionServerConfirmed,
1612 #[serde(rename = "transaction.claimed")]
1614 TransactionClaimed,
1615 #[serde(rename = "transaction.lockupFailed")]
1617 TransactionLockupFailed,
1618 #[serde(rename = "swap.expired")]
1620 SwapExpired,
1621 #[serde(rename = "transaction.failed")]
1625 TransactionFailed,
1626 #[serde(rename = "transaction.refunded")]
1630 TransactionRefunded,
1631}
1632
1633impl Display for ChainSwapStates {
1634 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1635 let str = match self {
1636 ChainSwapStates::Created => "swap.created".to_string(),
1637 ChainSwapStates::TransactionZeroConfRejected => {
1638 "transaction.zeroconf.rejected".to_string()
1639 }
1640 ChainSwapStates::TransactionMempool => "transaction.mempool".to_string(),
1641 ChainSwapStates::TransactionConfirmed => "transaction.confirmed".to_string(),
1642 ChainSwapStates::TransactionServerMempool => "transaction.server.mempool".to_string(),
1643 ChainSwapStates::TransactionServerConfirmed => {
1644 "transaction.server.confirmed".to_string()
1645 }
1646 ChainSwapStates::TransactionClaimed => "transaction.claimed".to_string(),
1647 ChainSwapStates::TransactionLockupFailed => "transaction.lockupFailed".to_string(),
1648 ChainSwapStates::SwapExpired => "swap.expired".to_string(),
1649 ChainSwapStates::TransactionFailed => "transaction.failed".to_string(),
1650 ChainSwapStates::TransactionRefunded => "transaction.refunded".to_string(),
1651 };
1652 write!(f, "{str}")
1653 }
1654}
1655
1656impl FromStr for ChainSwapStates {
1657 type Err = ();
1658
1659 fn from_str(s: &str) -> Result<Self, Self::Err> {
1660 match s {
1661 "swap.created" => Ok(ChainSwapStates::Created),
1662 "transaction.zeroconf.rejected" => Ok(ChainSwapStates::TransactionZeroConfRejected),
1663 "transaction.mempool" => Ok(ChainSwapStates::TransactionMempool),
1664 "transaction.confirmed" => Ok(ChainSwapStates::TransactionConfirmed),
1665 "transaction.server.mempool" => Ok(ChainSwapStates::TransactionServerMempool),
1666 "transaction.server.confirmed" => Ok(ChainSwapStates::TransactionServerConfirmed),
1667 "transaction.claimed" => Ok(ChainSwapStates::TransactionClaimed),
1668 "transaction.lockupFailed" => Ok(ChainSwapStates::TransactionLockupFailed),
1669 "swap.expired" => Ok(ChainSwapStates::SwapExpired),
1670 "transaction.failed" => Ok(ChainSwapStates::TransactionFailed),
1671 "transaction.refunded" => Ok(ChainSwapStates::TransactionRefunded),
1672 _ => Err(()),
1673 }
1674 }
1675}
1676
1677#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
1678#[serde(rename_all = "lowercase")]
1679pub enum SwapType {
1680 Submarine,
1681 ReverseSubmarine,
1682 Chain,
1683}
1684
1685#[derive(Serialize, Deserialize, Debug)]
1686#[serde(rename_all = "lowercase")]
1687pub enum OrderSide {
1688 Buy,
1689 Sell,
1690}
1691
1692impl Display for OrderSide {
1693 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1694 let str = match self {
1695 OrderSide::Buy => "buy",
1696 OrderSide::Sell => "sell",
1697 };
1698 f.write_str(str)
1699 }
1700}
1701
1702impl FromStr for OrderSide {
1703 type Err = ();
1704
1705 fn from_str(s: &str) -> Result<Self, Self::Err> {
1706 match s {
1707 "buy" => Ok(OrderSide::Buy),
1708 "sell" => Ok(OrderSide::Sell),
1709 _ => Err(()),
1710 }
1711 }
1712}
1713
1714#[derive(Serialize, Deserialize, Debug)]
1715pub struct GetFeeEstimationResponse {
1716 #[serde(rename = "BTC")]
1717 pub btc: f64,
1718 #[serde(rename = "L-BTC")]
1719 pub lbtc: f64,
1720}
1721
1722#[derive(Debug, Clone, Serialize, Deserialize)]
1723#[serde(rename_all = "camelCase")]
1724pub struct CreateBolt12OfferRequest {
1725 pub offer: String,
1726 #[serde(skip_serializing_if = "Option::is_none")]
1727 pub url: Option<String>,
1728}
1729
1730#[derive(Debug, Clone, Serialize, Deserialize)]
1731#[serde(rename_all = "camelCase")]
1732pub struct UpdateBolt12OfferRequest {
1733 pub offer: String,
1734 #[serde(skip_serializing_if = "Option::is_none")]
1737 pub url: Option<String>,
1738 pub signature: String,
1740}
1741
1742#[derive(Clone, Debug, Deserialize, Serialize)]
1743#[serde(rename_all = "camelCase")]
1744pub struct MagicRoutingHint {
1745 pub bip21: String,
1746 pub signature: String,
1747}
1748
1749#[derive(Debug, Clone, Serialize, Deserialize)]
1750#[serde(rename_all = "camelCase")]
1751pub struct GetBolt12FetchRequest {
1752 pub offer: String,
1754 pub amount: u64,
1756 #[serde(skip_serializing_if = "Option::is_none")]
1758 pub note: Option<String>,
1759}
1760
1761#[derive(Debug, Clone, Serialize, Deserialize)]
1762#[serde(rename_all = "camelCase")]
1763pub struct GetBolt12FetchResponse {
1764 pub invoice: String,
1766 #[serde(skip_serializing_if = "Option::is_none")]
1768 pub magic_routing_hint: Option<MagicRoutingHint>,
1769}
1770
1771#[derive(Debug, Clone, Serialize, Deserialize)]
1772#[serde(rename_all = "camelCase")]
1773pub struct GetBolt12ParamsResponse {
1774 pub min_cltv: u64,
1776}
1777
1778#[derive(Serialize, Deserialize, Debug, Clone)]
1779#[serde(rename_all = "camelCase")]
1780pub struct Node {
1781 pub public_key: secp256k1::PublicKey,
1783 pub uris: Vec<String>,
1785}
1786
1787#[derive(Debug, Clone, Serialize, Deserialize)]
1788pub struct GetNodesResponse {
1789 #[serde(rename = "BTC")]
1790 pub btc: HashMap<String, Node>,
1791}
1792
1793impl GetNodesResponse {
1794 pub fn get_btc_lnd_node(&self) -> Option<Node> {
1797 self.btc.get("LND").cloned()
1798 }
1799
1800 pub fn get_btc_cln_node(&self) -> Option<Node> {
1803 self.btc.get("CLN").cloned()
1804 }
1805}
1806
1807#[derive(Debug, Clone, Serialize, Deserialize)]
1808#[serde(rename_all = "camelCase")]
1809pub struct GetQuoteResponse {
1810 pub amount: u64,
1812}
1813
1814#[derive(Serialize, Deserialize, Debug)]
1815#[serde(rename_all = "camelCase")]
1816pub struct TransactionResponse {
1817 pub id: String,
1818 pub hex: String,
1819}
1820
1821#[derive(Serialize, Deserialize, Debug)]
1822#[serde(rename_all = "camelCase")]
1823pub struct GetSwapResponse {
1824 pub status: String,
1825 pub zero_conf_rejected: Option<bool>,
1826 pub transaction: Option<TransactionResponse>,
1827}
1828
1829#[cfg(test)]
1830mod tests {
1831 use super::*;
1832
1833 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1834 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1835
1836 #[macros::async_test_all]
1837 async fn test_get_fee_estimation() {
1838 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1839 let result = client.get_fee_estimation().await;
1840 assert!(result.is_ok(), "Failed to get fee estimation");
1841 }
1842
1843 #[macros::async_test_all]
1844 async fn test_get_height() {
1845 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1846 let result = client.get_height().await;
1847 assert!(result.is_ok(), "Failed to get height");
1848 }
1849
1850 #[macros::async_test_all]
1851 async fn test_get_submarine_pairs() {
1852 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1853 let result = client.get_submarine_pairs().await;
1854 assert!(result.is_ok(), "Failed to get submarine pairs");
1855 }
1856
1857 #[macros::async_test_all]
1858 async fn test_get_reverse_pairs() {
1859 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1860 let result = client.get_reverse_pairs().await;
1861 assert!(result.is_ok(), "Failed to get reverse pairs");
1862 }
1863
1864 #[macros::async_test_all]
1865 async fn test_get_chain_pairs() {
1866 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1867 let result = client.get_chain_pairs().await;
1868 assert!(result.is_ok(), "Failed to get chain pairs");
1869 }
1870
1871 #[macros::async_test_all]
1872 #[ignore]
1873 async fn test_get_submarine_claim_tx_details() {
1874 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1875 let id = "G6c6GJJY8eXz".to_string();
1876 let result = client.get_submarine_claim_tx_details(&id).await;
1877 assert!(
1878 result.is_ok(),
1879 "Failed to get submarine claim transaction details"
1880 );
1881 }
1882
1883 #[macros::async_test_all]
1884 #[ignore]
1885 async fn test_get_chain_claim_tx_details() {
1886 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1887 let id = "3BIJf8UqGaSC".to_string();
1888 let result = client.get_chain_claim_tx_details(&id).await;
1889 assert!(
1890 result.is_ok(),
1891 "Failed to get chain claim transaction details"
1892 );
1893 }
1894
1895 #[macros::async_test_all]
1896 #[ignore]
1897 async fn test_get_reverse_tx() {
1898 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1899 let id = "G6c6GJJY8eXz";
1900 let result = client.get_reverse_tx(id).await;
1901 assert!(result.is_ok(), "Failed to get reverse transaction");
1902 }
1903
1904 #[macros::async_test_all]
1905 #[ignore]
1906 async fn test_get_submarine_tx() {
1907 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1908 let id = "G6c6GJJY8eXz";
1909 let result = client.get_submarine_tx(id).await;
1910 assert!(result.is_ok(), "Failed to get submarine transaction");
1911 }
1912
1913 #[macros::async_test_all]
1914 async fn test_get_chain_txs() {
1915 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1916 let id = "G6c6GJJY8eXz";
1917 let result = client.get_chain_txs(id).await;
1918 assert!(result.is_ok(), "Failed to get chain transactions");
1919 }
1920
1921 #[macros::async_test_all]
1922 async fn test_get_swap() {
1923 let client = BoltzApiClientV2::new(BOLTZ_MAINNET_URL_V2.to_string(), None);
1924 let id = "G6c6GJJY8eXz";
1925 let result = client.get_swap(id).await;
1926 assert!(result.is_ok(), "Failed to get swap status");
1927 }
1928}