breez_sdk_liquid/swapper/boltz/
liquid.rs

1use std::str::FromStr;
2
3use boltz_client::{
4    boltz::SwapTxKind, elements::Transaction, fees::Fee, network::LiquidClient,
5    util::secrets::Preimage, ElementsAddress as Address, LBtcSwapTx,
6};
7use log::info;
8
9use crate::{
10    ensure_sdk,
11    error::{PaymentError, SdkError},
12    prelude::{ChainSwap, Direction, ReceiveSwap, Swap, Utxo, LIQUID_FEE_RATE_SAT_PER_VBYTE},
13    utils,
14};
15
16use super::{BoltzSwapper, ProxyUrlFetcher};
17
18impl<P: ProxyUrlFetcher> BoltzSwapper<P> {
19    pub(crate) fn validate_send_swap_preimage(
20        &self,
21        swap_id: &str,
22        invoice: &str,
23        preimage: &str,
24    ) -> Result<(), PaymentError> {
25        utils::verify_payment_hash(preimage, invoice)?;
26        info!("Preimage is valid for Send Swap {swap_id}");
27        Ok(())
28    }
29
30    pub(crate) async fn new_receive_claim_tx(
31        &self,
32        swap: &ReceiveSwap,
33        claim_address: String,
34    ) -> Result<Transaction, PaymentError> {
35        let liquid_client = self.get_liquid_client()?;
36        let swap_script = swap.get_swap_script()?;
37
38        let claim_tx_wrapper = LBtcSwapTx::new_claim(
39            swap_script,
40            claim_address,
41            liquid_client,
42            &self.get_boltz_client().await?.inner,
43            swap.id.clone(),
44        )
45        .await?;
46
47        let signed_tx = claim_tx_wrapper
48            .sign_claim(
49                &swap.get_claim_keypair()?,
50                &Preimage::from_str(&swap.preimage)?,
51                Fee::Absolute(swap.claim_fees_sat),
52                self.get_cooperative_details(swap.id.clone(), None).await?,
53                true,
54            )
55            .await?;
56
57        Ok(signed_tx)
58    }
59
60    pub(crate) async fn new_incoming_chain_claim_tx(
61        &self,
62        swap: &ChainSwap,
63        claim_address: String,
64    ) -> Result<Transaction, PaymentError> {
65        let liquid_client = self.get_liquid_client()?;
66        let claim_keypair = swap.get_claim_keypair()?;
67        let swap_script = swap.get_claim_swap_script()?.as_liquid_script()?;
68        let claim_tx_wrapper = LBtcSwapTx::new_claim(
69            swap_script,
70            claim_address,
71            liquid_client,
72            &self.get_boltz_client().await?.inner,
73            swap.id.clone(),
74        )
75        .await?;
76
77        let signature = self.get_claim_partial_sig(swap).await?;
78
79        let signed_tx = claim_tx_wrapper
80            .sign_claim(
81                &claim_keypair,
82                &Preimage::from_str(&swap.preimage)?,
83                Fee::Absolute(swap.claim_fees_sat),
84                self.get_cooperative_details(swap.id.clone(), signature)
85                    .await?,
86                true,
87            )
88            .await?;
89
90        Ok(signed_tx)
91    }
92
93    fn calculate_refund_fees(&self, refund_tx_size: usize) -> u64 {
94        (refund_tx_size as f64 * LIQUID_FEE_RATE_SAT_PER_VBYTE).ceil() as u64
95    }
96
97    pub(crate) async fn new_lbtc_refund_wrapper(
98        &self,
99        swap: &Swap,
100        refund_address: &str,
101    ) -> Result<LBtcSwapTx, SdkError> {
102        let liquid_client = self.get_liquid_client()?;
103        let refund_wrapper = match swap {
104            Swap::Chain(swap) => match swap.direction {
105                Direction::Incoming => {
106                    return Err(SdkError::generic(format!(
107                        "Cannot create Liquid refund wrapper for incoming Chain swap {}",
108                        swap.id
109                    )));
110                }
111                Direction::Outgoing => {
112                    let swap_script = swap.get_lockup_swap_script()?;
113                    LBtcSwapTx::new_refund(
114                        swap_script.as_liquid_script()?,
115                        refund_address,
116                        liquid_client,
117                        &self.get_boltz_client().await?.inner,
118                        swap.id.clone(),
119                    )
120                    .await
121                }
122            },
123            Swap::Send(swap) => {
124                let swap_script = swap.get_swap_script()?;
125                LBtcSwapTx::new_refund(
126                    swap_script,
127                    refund_address,
128                    liquid_client,
129                    &self.get_boltz_client().await?.inner,
130                    swap.id.clone(),
131                )
132                .await
133            }
134            Swap::Receive(swap) => {
135                return Err(SdkError::generic(format!(
136                    "Cannot create Liquid refund wrapper for Receive swap {}",
137                    swap.id
138                )));
139            }
140        }?;
141        Ok(refund_wrapper)
142    }
143
144    pub(crate) async fn new_lbtc_refund_tx(
145        &self,
146        swap: &Swap,
147        refund_address: &str,
148        utxos: Vec<Utxo>,
149        is_cooperative: bool,
150    ) -> Result<Transaction, SdkError> {
151        let liquid_client = self.get_liquid_client()?;
152
153        let (swap_script, refund_keypair) = match swap {
154            Swap::Chain(swap) => {
155                ensure_sdk!(
156                    swap.direction == Direction::Outgoing,
157                    SdkError::generic("Cannot create LBTC refund tx for incoming Chain swaps")
158                );
159
160                (
161                    swap.get_lockup_swap_script()?.as_liquid_script()?,
162                    swap.get_refund_keypair()?,
163                )
164            }
165            Swap::Send(swap) => (swap.get_swap_script()?, swap.get_refund_keypair()?),
166            Swap::Receive(_) => {
167                return Err(SdkError::generic(
168                    "Cannot create LBTC refund tx for Receive swaps.",
169                ));
170            }
171        };
172        let swap_id = swap.id();
173
174        let address = Address::from_str(refund_address)
175            .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?;
176
177        let genesis_hash = liquid_client.get_genesis_hash().await?;
178
179        let (funding_outpoint, funding_tx_out) =
180            *utxos
181                .first()
182                .and_then(|utxo| utxo.as_liquid())
183                .ok_or(SdkError::generic("No refundable UTXOs found"))?;
184
185        let refund_tx = LBtcSwapTx {
186            kind: SwapTxKind::Refund,
187            swap_script,
188            output_address: address,
189            funding_outpoint,
190            funding_utxo: funding_tx_out,
191            genesis_hash,
192        };
193
194        let refund_tx_size = refund_tx.size(&refund_keypair, is_cooperative, true)?;
195        let broadcast_fees_sat = self.calculate_refund_fees(refund_tx_size);
196
197        let cooperative = match is_cooperative {
198            true => self.get_cooperative_details(swap_id.clone(), None).await?,
199            false => None,
200        };
201
202        let signed_tx = refund_tx
203            .sign_refund(
204                &refund_keypair,
205                Fee::Absolute(broadcast_fees_sat),
206                cooperative,
207                true,
208            )
209            .await?;
210        Ok(signed_tx)
211    }
212}