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