boltz_client/swaps/
boltz.rs

1//!
2//! ### Boltz v2 API
3//! ## Estimate fees
4//!
5//! ### Example
6//! ```
7//! let client = BoltzApiClient::new(BOLTZ_MAINNET_URL);
8//! let pairs = client.get_pairs()?;
9//! let btc_pair = pairs.get_btc_pair();
10//! let output_amount = 75_000;
11//! let base_fees = btc_pair.fees.reverse_base(output_amount)?;
12//! let claim_fee = btc_pair.fees.reverse_claim_estimate();
13//! println!("CALCULATED FEES: {}", base_fees);
14//! println!("ONCHAIN LOCKUP: {}", output_amount - base_fees);
15//! println!(
16//!     "ONCHAIN RECIEVABLE: {}",
17//!     output_amount - base_fees - claim_fee
18//! );
19
20use 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/// Various limits of swap parameters
70#[derive(Serialize, Deserialize, Debug, Clone)]
71#[serde(rename_all = "camelCase")]
72pub struct PairLimits {
73    /// Maximum swap amount
74    pub maximal: u64,
75    /// Minimum swap amount
76    pub minimal: u64,
77    /// Maximum amount allowed for zero-conf
78    pub maximal_zero_conf: u64,
79}
80
81impl PairLimits {
82    /// Check whether the output amount intended is within the Limits
83    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    /// Maximum swap amount
92    pub maximal: u64,
93    /// Minimum swap amount
94    pub minimal: u64,
95    /// Maximum amount allowed for zero-conf
96    pub maximal_zero_conf: u64,
97    /// Minimum batch swap amount
98    pub minimal_batched: Option<u64>,
99}
100
101impl SubmarinePairLimits {
102    /// Check whether the output amount intended is within the Limits
103    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    /// Maximum swap amount
113    pub maximal: u64,
114    /// Minimum swap amount
115    pub minimal: u64,
116}
117
118impl ReverseLimits {
119    /// Check whether the output amount intended is within the Limits
120    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    /// The percentage of the "send amount" that is charged by Boltz as "Boltz Fee".
197    pub percentage: f64,
198    /// The network fees charged for locking up and claiming funds onchain. These values are absolute, denominated in 10 ** -8 of the quote asset.
199    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    /// Pair hash, representing an id for an asset-pair swap
220    pub hash: String,
221    /// The exchange rate of the pair
222    pub rate: f64,
223    /// The swap limits
224    pub limits: PairLimits,
225    /// Total fees required for the swap
226    pub fees: ChainFees,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone)]
230#[serde(rename_all = "camelCase")]
231pub struct ReversePair {
232    /// Pair hash, representing an id for an asset-pair swap
233    pub hash: String,
234    /// The exchange rate of the pair
235    pub rate: f64,
236    /// The swap limits
237    pub limits: ReverseLimits,
238    /// Total fees required for the swap
239    pub fees: ReverseFees,
240}
241
242#[derive(Serialize, Deserialize, Debug, Clone)]
243#[serde(rename_all = "camelCase")]
244pub struct SubmarinePair {
245    /// Pair hash, representing an id for an asset-pair swap
246    pub hash: String,
247    /// The exchange rate of the pair
248    pub rate: f64,
249    /// The swap limits
250    pub limits: SubmarinePairLimits,
251    /// Total fees required for the swap
252    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    /// Get the BtcBtc Pair data from the response.
265    /// Returns None if not found.
266    pub fn get_btc_to_btc_pair(&self) -> Option<SubmarinePair> {
267        self.btc.get("BTC").cloned()
268    }
269
270    /// Get the BtcLBtc Pair data from the response.
271    /// Returns None if not found.
272    pub fn get_btc_to_lbtc_pair(&self) -> Option<SubmarinePair> {
273        self.btc.get("L-BTC").cloned()
274    }
275
276    /// Get the LBtcBtc Pair data from the response.
277    /// Returns None if not found.
278    pub fn get_lbtc_to_btc_pair(&self) -> Option<SubmarinePair> {
279        self.lbtc.get("BTC").cloned()
280    }
281
282    /// Get the LBtcLBtc Pair data from the response.
283    /// Returns None if not found.
284    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    /// Get the BtcBtc Pair data from the response.
297    /// Returns None if not found.
298    pub fn get_btc_to_btc_pair(&self) -> Option<ReversePair> {
299        self.btc.get("BTC").cloned()
300    }
301
302    /// Get the BtcLBtc Pair data from the response.
303    /// Returns None if not found.
304    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    /// Get the BtcLBtc Pair data from the response.
319    /// Returns None if not found.
320    pub fn get_btc_to_lbtc_pair(&self) -> Option<ChainPair> {
321        self.btc.get("L-BTC").cloned()
322    }
323
324    /// Get the LBtcBtc Pair data from the response.
325    /// Returns None if not found.
326    pub fn get_lbtc_to_btc_pair(&self) -> Option<ChainPair> {
327        self.lbtc.get("BTC").cloned()
328    }
329}
330
331/// Reference Documnetation: <https://api.boltz.exchange/swagger>
332#[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    /// Returns the WebSocket URL for the Boltz server
371    pub fn get_ws_url(&self) -> String {
372        self.base_url.clone().replace("http", "ws") + "/ws"
373    }
374
375    /// Returns the web socket connection to the boltz server
376    #[cfg(feature = "ws")]
377    pub async fn connect_ws(&self) -> Result<WebSocketStream, Error> {
378        Ok(connect(self.get_ws_url()).await?)
379    }
380
381    /// Same as `connect_ws` but with protocols
382    #[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    /// Make a GET request. Returns the Response
396    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    /// Make a GET request. Returns the Response as text
404    async fn get(&self, end_point: &str) -> Result<String, Error> {
405        Ok(self.get_response(end_point).await?.text().await?)
406    }
407
408    /// Make a POST request. Returns the Response
409    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    /// Make a PATCH request. Returns the Response
416    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    /// Creates a BOLT12 offer
695    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    /// Updates the webhook URL for a BOLT12 offer
703    ///
704    /// # Arguments
705    ///   * `req` - The request object containing the offer and the new webhook URL
706    ///     * `offer` - The BOLT12 offer
707    ///     * `url` - The updated webhook URL. Setting to None will remove the webhook URL from the registered offer
708    ///     * `signature` - The schnorr signature of the SHA256 hash of the webhook URL or "UPDATE" when not set
709    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    /// Deletes a BOLT12 offer
717    ///
718    /// # Arguments
719    ///    * `offer` - The BOLT12 offer
720    ///    * `signature` - This schnorr signature of the SHA256 hash of "DELETE"
721    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    /// Fetch an invoice for the specified BOLT12 offer
735    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    /// Gets parameters for a BOLT12 offer
745    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    /// Fetch information about the Lightning nodes the backend is connected to
751    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    /// Gets a quote for a Zero-Amount or over- or underpaid Chain Swap.
757    ///
758    /// If the user locked up a valid amount, it will return the server lockup amount. In all other
759    /// cases, it will return an error.
760    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    /// Accepts a specific quote for a Zero-Amount or over- or underpaid Chain Swap.
766    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    /// Gets the latest status of the Swap
779    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    /// Restore swaps from an xpub
785    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    /// Restore swaps from an xpub
801    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    /// Ensure submarine swap redeem script uses the preimage hash used in the invoice
884    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    /// The BOLT12 invoice
1155    #[serde(skip_serializing_if = "Option::is_none")]
1156    pub invoice: Option<String>,
1157    /// The invoice amount if the invoice is not provided
1158    #[serde(skip_serializing_if = "Option::is_none")]
1159    pub invoice_amount: Option<u64>,
1160    /// The preimage hash if the invoice is not provided
1161    #[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    /// Validate reverse swap response
1192    /// Ensure reverse swap invoice uses the provided preimage
1193    /// Ensure reverse swap redeem script matches locally constructured SwapScript
1194    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            // Boltz will only return a BOLT11 invoice if the invoice is not provided
1202            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    /// Validate chain swap response
1280    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    /// The signature (partial_sig + pub_nonce) is needed to post the claim tx details of the Chain swap
1397    /// It may be omitted for a chain swap if we've already sent the signature to Boltz
1398    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/// States for a submarine swap.
1420///
1421/// See <https://docs.boltz.exchange/v/api/lifecycle#normal-submarine-swaps>
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub enum SubSwapStates {
1424    /// Initial state of the swap; optionally the initial state can also be `invoice.set` in case
1425    /// the invoice was already specified in the request that created the swap.
1426    #[serde(rename = "swap.created")]
1427    Created,
1428    /// The lockup transaction was found in the mempool, meaning the user sent funds to the
1429    /// lockup address.
1430    #[serde(rename = "transaction.mempool")]
1431    TransactionMempool,
1432    /// The lockup transaction was included in a block.
1433    #[serde(rename = "transaction.confirmed")]
1434    TransactionConfirmed,
1435    /// The swap has an invoice that should be paid.
1436    /// Can be the initial state when the invoice was specified in the request that created the swap
1437    #[serde(rename = "invoice.set")]
1438    InvoiceSet,
1439    /// Boltz successfully paid the invoice.
1440    #[serde(rename = "invoice.paid")]
1441    InvoicePaid,
1442    /// Boltz started paying the invoice.
1443    #[serde(rename = "invoice.pending")]
1444    InvoicePending,
1445    /// Boltz failed to pay the invoice. In this case the user needs to broadcast a refund
1446    /// transaction to reclaim the locked up onchain coins.
1447    #[serde(rename = "invoice.failedToPay")]
1448    InvoiceFailedToPay,
1449    /// Indicates that after the invoice was successfully paid, the onchain were successfully
1450    /// claimed by Boltz. This is the final status of a successful Normal Submarine Swap.
1451    #[serde(rename = "transaction.claimed")]
1452    TransactionClaimed,
1453    /// Indicates that Boltz is ready for the creation of a cooperative signature for a key path
1454    /// spend. Taproot Swaps are not claimed immediately by Boltz after the invoice has been paid,
1455    /// but instead Boltz waits for the API client to post a signature for a key path spend. If the
1456    /// API client does not cooperate in a key path spend, Boltz will eventually claim via the script path.
1457    #[serde(rename = "transaction.claim.pending")]
1458    TransactionClaimPending,
1459    /// Indicates the lockup failed, which is usually because the user sent too little.
1460    #[serde(rename = "transaction.lockupFailed")]
1461    TransactionLockupFailed,
1462    /// Indicates the user didn't send onchain (lockup) and the swap expired (approximately 24h).
1463    /// This means that it was cancelled and chain L-BTC shouldn't be sent anymore.
1464    #[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/// States for a reverse swap.
1509///
1510/// See <https://docs.boltz.exchange/v/api/lifecycle#reverse-submarine-swaps>
1511#[derive(Debug, Clone, Serialize, Deserialize)]
1512pub enum RevSwapStates {
1513    /// Initial state of a newly created Reverse Submarine Swap.
1514    #[serde(rename = "swap.created")]
1515    Created,
1516    /// Optional and currently not enabled on Boltz. If Boltz requires prepaying miner fees via a
1517    /// separate Lightning invoice, this state is set when the miner fee invoice was successfully paid.
1518    #[serde(rename = "minerfee.paid")]
1519    MinerFeePaid,
1520    /// Boltz's lockup transaction is found in the mempool which will only happen after the user
1521    /// paid the Lightning hold invoice.
1522    #[serde(rename = "transaction.mempool")]
1523    TransactionMempool,
1524    /// The lockup transaction was included in a block. This state is skipped, if the client
1525    /// optionally accepts the transaction without confirmation. Boltz broadcasts chain transactions
1526    /// non-RBF only.
1527    #[serde(rename = "transaction.confirmed")]
1528    TransactionConfirmed,
1529    /// The transaction claiming onchain was broadcast by the user's client and Boltz used the
1530    /// preimage of this transaction to settle the Lightning invoice. This is the final status of a
1531    /// successful Reverse Submarine Swap.
1532    #[serde(rename = "invoice.settled")]
1533    InvoiceSettled,
1534    /// Set when the invoice of Boltz expired and pending HTLCs are cancelled. Boltz invoices
1535    /// currently expire after 50% of the swap timeout window.
1536    #[serde(rename = "invoice.expired")]
1537    InvoiceExpired,
1538    /// This is the final status of a swap, if the swap expires without the lightning invoice being paid.
1539    #[serde(rename = "swap.expired")]
1540    SwapExpired,
1541    /// Set in the unlikely event that Boltz is unable to send the agreed amount of onchain coins
1542    /// after the user set up the payment to the provided Lightning invoice. If this happens, the
1543    /// pending Lightning HTLC will also be cancelled. The Lightning bitcoin automatically bounce
1544    /// back to the user, no further action or refund is required and the user didn't pay any fees.
1545    #[serde(rename = "transaction.failed")]
1546    TransactionFailed,
1547    /// This is the final status of a swap, if the user successfully set up the Lightning payment
1548    /// and Boltz successfully locked up coins onchain, but the Boltz API Client did not claim
1549    /// the locked oncahin coins before swap expiry. In this case, Boltz will also automatically refund
1550    /// its own locked onchain coins and the Lightning payment is cancelled.
1551    #[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    /// The initial state of the chain swap.
1594    #[serde(rename = "swap.created")]
1595    Created,
1596    /// The server has rejected a 0-conf transaction for this swap.
1597    #[serde(rename = "transaction.zeroconf.rejected")]
1598    TransactionZeroConfRejected,
1599    /// The lockup transaction of the client was found in the mempool.
1600    #[serde(rename = "transaction.mempool")]
1601    TransactionMempool,
1602    /// The lockup transaction of the client was confirmed in a block. When the server accepts 0-conf,
1603    /// for the lockup transaction, this state is skipped.
1604    #[serde(rename = "transaction.confirmed")]
1605    TransactionConfirmed,
1606    /// The lockup transaction of the server has been broadcast.
1607    #[serde(rename = "transaction.server.mempool")]
1608    TransactionServerMempool,
1609    /// The lockup transaction of the server has been included in a block.
1610    #[serde(rename = "transaction.server.confirmed")]
1611    TransactionServerConfirmed,
1612    /// The server claimed the coins that the client locked.
1613    #[serde(rename = "transaction.claimed")]
1614    TransactionClaimed,
1615    /// Indicates the lockup failed, which is usually because the user sent too little.
1616    #[serde(rename = "transaction.lockupFailed")]
1617    TransactionLockupFailed,
1618    /// This is the final status of a swap, if the swap expires without a chain bitcoin transaction.
1619    #[serde(rename = "swap.expired")]
1620    SwapExpired,
1621    /// Set in the unlikely event that Boltz is unable to lock the agreed amount of chain bitcoin.
1622    /// The user needs to submit a refund transaction to reclaim the chain bitcoin if bitcoin were
1623    /// already sent.
1624    #[serde(rename = "transaction.failed")]
1625    TransactionFailed,
1626    /// If the user and Boltz both successfully locked up bitcoin on the chain, but the user did not
1627    /// claim the locked chain bitcoin until swap expiry, Boltz will automatically refund its own locked
1628    /// chain bitcoin.
1629    #[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    /// The updated webhook URL.
1735    /// Setting to None will remove the webhook URL from the registered Offer.
1736    #[serde(skip_serializing_if = "Option::is_none")]
1737    pub url: Option<String>,
1738    /// The schnorr signature of the SHA256 hash of the webhook URL or "UPDATE" when None
1739    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    /// The offer to fetch an invoice for
1753    pub offer: String,
1754    /// The amount to pay, in satoshi
1755    pub amount: u64,
1756    /// The optional payer note
1757    #[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    /// BOLT12 invoice
1765    pub invoice: String,
1766    /// The invoice magic routing hint
1767    #[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    /// Minimum CLTV value
1775    pub min_cltv: u64,
1776}
1777
1778#[derive(Serialize, Deserialize, Debug, Clone)]
1779#[serde(rename_all = "camelCase")]
1780pub struct Node {
1781    /// The public key
1782    pub public_key: secp256k1::PublicKey,
1783    /// The public URIs
1784    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    /// Get the BTC LND node data from the response.
1795    /// Returns None if not found.
1796    pub fn get_btc_lnd_node(&self) -> Option<Node> {
1797        self.btc.get("LND").cloned()
1798    }
1799
1800    /// Get the BTC CLN node data from the response.
1801    /// Returns None if not found.
1802    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    /// Server lockup amount, in sat
1811    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}