breez_sdk_liquid/swapper/boltz/
liquid.rs1use 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}