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        is_cooperative: bool,
35    ) -> Result<Transaction, PaymentError> {
36        let liquid_client = self.get_liquid_client()?;
37        let swap_script = swap.get_swap_script()?;
38
39        let claim_tx_wrapper = LBtcSwapTx::new_claim(
40            swap_script,
41            claim_address,
42            liquid_client,
43            &self.get_boltz_client().await?.inner,
44            swap.id.clone(),
45        )
46        .await?;
47
48        let cooperative_details = if is_cooperative {
49            self.get_cooperative_details(swap.id.clone(), None).await?
50        } else {
51            None
52        };
53
54        let fee = if is_cooperative {
55            Fee::Absolute(swap.claim_fees_sat)
56        } else {
57            Fee::Relative(LIQUID_FEE_RATE_SAT_PER_VBYTE)
58        };
59
60        let signed_tx = claim_tx_wrapper
61            .sign_claim(
62                &swap.get_claim_keypair()?,
63                &Preimage::from_str(&swap.preimage)?,
64                fee,
65                cooperative_details,
66                true,
67            )
68            .await?;
69
70        Ok(signed_tx)
71    }
72
73    pub(crate) async fn new_incoming_chain_claim_tx(
74        &self,
75        swap: &ChainSwap,
76        claim_address: String,
77        is_cooperative: bool,
78    ) -> Result<Transaction, PaymentError> {
79        let liquid_client = self.get_liquid_client()?;
80        let claim_keypair = swap.get_claim_keypair()?;
81        let swap_script = swap.get_claim_swap_script()?.as_liquid_script()?;
82        let claim_tx_wrapper = LBtcSwapTx::new_claim(
83            swap_script,
84            claim_address,
85            liquid_client,
86            &self.get_boltz_client().await?.inner,
87            swap.id.clone(),
88        )
89        .await?;
90
91        let cooperative_details = if is_cooperative {
92            let signature = self.get_claim_partial_sig(swap).await?;
93            self.get_cooperative_details(swap.id.clone(), signature)
94                .await?
95        } else {
96            None
97        };
98
99        let fee = if is_cooperative {
100            Fee::Absolute(swap.claim_fees_sat)
101        } else {
102            Fee::Relative(LIQUID_FEE_RATE_SAT_PER_VBYTE)
103        };
104
105        let signed_tx = claim_tx_wrapper
106            .sign_claim(
107                &claim_keypair,
108                &Preimage::from_str(&swap.preimage)?,
109                fee,
110                cooperative_details,
111                true,
112            )
113            .await?;
114
115        Ok(signed_tx)
116    }
117
118    fn calculate_refund_fees(&self, refund_tx_size: usize) -> u64 {
119        (refund_tx_size as f64 * LIQUID_FEE_RATE_SAT_PER_VBYTE).ceil() as u64
120    }
121
122    pub(crate) async fn new_lbtc_refund_wrapper(
123        &self,
124        swap: &Swap,
125        refund_address: &str,
126    ) -> Result<LBtcSwapTx, SdkError> {
127        let liquid_client = self.get_liquid_client()?;
128        let refund_wrapper = match swap {
129            Swap::Chain(swap) => match swap.direction {
130                Direction::Incoming => {
131                    return Err(SdkError::generic(format!(
132                        "Cannot create Liquid refund wrapper for incoming Chain swap {}",
133                        swap.id
134                    )));
135                }
136                Direction::Outgoing => {
137                    let swap_script = swap.get_lockup_swap_script()?;
138                    LBtcSwapTx::new_refund(
139                        swap_script.as_liquid_script()?,
140                        refund_address,
141                        liquid_client,
142                        &self.get_boltz_client().await?.inner,
143                        swap.id.clone(),
144                    )
145                    .await
146                }
147            },
148            Swap::Send(swap) => {
149                let swap_script = swap.get_swap_script()?;
150                LBtcSwapTx::new_refund(
151                    swap_script,
152                    refund_address,
153                    liquid_client,
154                    &self.get_boltz_client().await?.inner,
155                    swap.id.clone(),
156                )
157                .await
158            }
159            Swap::Receive(swap) => {
160                return Err(SdkError::generic(format!(
161                    "Cannot create Liquid refund wrapper for Receive swap {}",
162                    swap.id
163                )));
164            }
165        }?;
166        Ok(refund_wrapper)
167    }
168
169    pub(crate) async fn new_lbtc_refund_tx(
170        &self,
171        swap: &Swap,
172        refund_address: &str,
173        utxos: Vec<Utxo>,
174        is_cooperative: bool,
175    ) -> Result<Transaction, SdkError> {
176        let liquid_client = self.get_liquid_client()?;
177
178        let (swap_script, refund_keypair) = match swap {
179            Swap::Chain(swap) => {
180                ensure_sdk!(
181                    swap.direction == Direction::Outgoing,
182                    SdkError::generic("Cannot create LBTC refund tx for incoming Chain swaps")
183                );
184
185                (
186                    swap.get_lockup_swap_script()?.as_liquid_script()?,
187                    swap.get_refund_keypair()?,
188                )
189            }
190            Swap::Send(swap) => (swap.get_swap_script()?, swap.get_refund_keypair()?),
191            Swap::Receive(_) => {
192                return Err(SdkError::generic(
193                    "Cannot create LBTC refund tx for Receive swaps.",
194                ));
195            }
196        };
197        let swap_id = swap.id();
198
199        let address = Address::from_str(refund_address)
200            .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?;
201
202        let genesis_hash = liquid_client.get_genesis_hash().await?;
203
204        let (funding_outpoint, funding_tx_out) =
205            *utxos
206                .first()
207                .and_then(|utxo| utxo.as_liquid())
208                .ok_or(SdkError::generic("No refundable UTXOs found"))?;
209
210        let refund_tx = LBtcSwapTx {
211            kind: SwapTxKind::Refund,
212            swap_script,
213            output_address: address,
214            funding_outpoint,
215            funding_utxo: funding_tx_out,
216            genesis_hash,
217        };
218
219        let refund_tx_size = refund_tx.size(&refund_keypair, is_cooperative, true)?;
220        let broadcast_fees_sat = self.calculate_refund_fees(refund_tx_size);
221
222        let cooperative = match is_cooperative {
223            true => self.get_cooperative_details(swap_id.clone(), None).await?,
224            false => None,
225        };
226
227        let signed_tx = refund_tx
228            .sign_refund(
229                &refund_keypair,
230                Fee::Absolute(broadcast_fees_sat),
231                cooperative,
232                true,
233            )
234            .await?;
235        Ok(signed_tx)
236    }
237}