breez_sdk_liquid/recover/handlers/
handle_send_swap.rs1use std::sync::Arc;
2
3use anyhow::Result;
4use boltz_client::ToHex;
5use log::{debug, error, warn};
6use lwk_wollet::elements::Txid;
7
8use crate::prelude::*;
9use crate::recover::model::*;
10use crate::swapper::Swapper;
11use crate::utils;
12
13pub(crate) struct SendSwapHandler;
15
16impl SendSwapHandler {
17 pub fn should_skip_recovery(
19 send_swap: &SendSwap,
20 recovered_data: &RecoveredOnchainDataSend,
21 is_within_grace_period: bool,
22 ) -> bool {
23 let swap_id = &send_swap.id;
24 let lockup_is_cleared =
25 send_swap.lockup_tx_id.is_some() && recovered_data.lockup_tx_id.is_none();
26 let refund_is_cleared =
27 send_swap.refund_tx_id.is_some() && recovered_data.refund_tx_id.is_none();
28
29 if is_within_grace_period && (lockup_is_cleared || refund_is_cleared) {
30 warn!(
31 "Local send swap {swap_id} was updated recently - skipping recovery \
32 as it would clear a tx that may have been broadcasted by us. Lockup clear: \
33 {lockup_is_cleared} - Refund clear: {refund_is_cleared}"
34 );
35 return true;
36 }
37
38 false
39 }
40
41 pub async fn recover_swap(
43 send_swap: &mut SendSwap,
44 context: &ReceiveOrSendSwapRecoveryContext,
45 is_within_grace_period: bool,
46 ) -> Result<()> {
47 let swap_id = send_swap.id.clone();
48 debug!("[Recover Send] Recovering data for swap {swap_id}");
49 let swap_script = send_swap.get_swap_script()?;
50 let lockup_script = swap_script
51 .funding_addrs
52 .ok_or(anyhow::anyhow!("no funding address found"))?
53 .script_pubkey();
54
55 let empty_history = Vec::<LBtcHistory>::new();
56 let history = context
57 .lbtc_script_to_history_map
58 .get(&lockup_script)
59 .unwrap_or(&empty_history);
60
61 let mut recovered_data = Self::recover_onchain_data(&context.tx_map, &swap_id, history)?;
63
64 if recovered_data.lockup_tx_id.is_some() && send_swap.preimage.is_none() {
66 match Self::recover_preimage(
70 context,
71 recovered_data.claim_tx_id.clone(),
72 &swap_id,
73 context.swapper.clone(),
74 )
75 .await
76 {
77 Ok(Some(preimage)) => {
78 recovered_data.preimage = Some(preimage);
79 }
80 Ok(None) => {
81 warn!("No preimage found for Send Swap {swap_id}");
82 recovered_data.claim_tx_id = None;
83 }
84 Err(e) => {
85 error!("Failed to recover preimage for swap {swap_id}: {e}");
86 recovered_data.claim_tx_id = None
87 }
88 }
89 }
90
91 Self::update_swap(
93 send_swap,
94 &swap_id,
95 &recovered_data,
96 context.liquid_tip_height,
97 is_within_grace_period,
98 )
99 }
100
101 pub fn update_swap(
103 send_swap: &mut SendSwap,
104 swap_id: &str,
105 recovered_data: &RecoveredOnchainDataSend,
106 current_block_height: u32,
107 is_within_grace_period: bool,
108 ) -> Result<()> {
109 if Self::should_skip_recovery(send_swap, recovered_data, is_within_grace_period) {
111 return Ok(());
112 }
113
114 send_swap.lockup_tx_id = recovered_data
116 .lockup_tx_id
117 .clone()
118 .map(|h| h.txid.to_string());
119 send_swap.refund_tx_id = recovered_data
120 .refund_tx_id
121 .clone()
122 .map(|h| h.txid.to_string());
123
124 if let Some(preimage) = &recovered_data.preimage {
126 match utils::verify_payment_hash(preimage, &send_swap.invoice) {
127 Ok(_) => send_swap.preimage = Some(preimage.clone()),
128 Err(e) => {
129 error!("Failed to verify recovered preimage for swap {swap_id}: {e}");
130 }
131 }
132 }
133
134 let timeout_block_height = send_swap.timeout_block_height as u32;
136 let is_expired = current_block_height >= timeout_block_height;
137 if let Some(new_state) =
138 recovered_data.derive_partial_state(send_swap.preimage.clone(), is_expired)
139 {
140 send_swap.state = new_state;
141 }
142
143 Ok(())
144 }
145
146 fn recover_onchain_data(
151 tx_map: &TxMap,
152 swap_id: &str,
153 wallet_history: &[LBtcHistory],
154 ) -> Result<RecoveredOnchainDataSend> {
155 let lockup_tx_id = wallet_history
157 .iter()
158 .find(|&tx| tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
159 .cloned();
160
161 let claim_tx_id = if lockup_tx_id.is_some() {
162 wallet_history
167 .iter()
168 .filter(|&tx| !tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
169 .find(|&tx| !tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
170 .cloned()
171 } else {
172 error!("No lockup tx found when recovering data for Send Swap {swap_id}");
173 None
174 };
175
176 let refund_tx_id = wallet_history
178 .iter()
179 .find(|&tx| tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
180 .cloned();
181
182 Ok(RecoveredOnchainDataSend {
183 lockup_tx_id,
184 claim_tx_id,
185 refund_tx_id,
186 preimage: None,
187 })
188 }
189
190 async fn recover_preimage(
192 context: &ReceiveOrSendSwapRecoveryContext,
193 claim_tx_id: Option<LBtcHistory>,
194 swap_id: &str,
195 swapper: Arc<dyn Swapper>,
196 ) -> Result<Option<String>> {
197 if let Ok(preimage) = swapper.get_submarine_preimage(swap_id).await {
199 log::debug!("Fetched Send Swap {swap_id} preimage cooperatively: {preimage}");
200 return Ok(Some(preimage));
201 }
202 warn!("Could not recover Send swap {swap_id} preimage cooperatively");
203 match claim_tx_id {
204 Some(claim_tx_id) => {
206 let claim_txs = context
207 .liquid_chain_service
208 .get_transactions(&[claim_tx_id.txid])
209 .await?;
210 match claim_txs.is_empty() {
211 false => Self::extract_preimage_from_claim_tx(&claim_txs[0], swap_id).map(Some),
212 true => {
213 warn!("Could not recover Send swap {swap_id} preimage non cooperatively");
214 Ok(None)
215 }
216 }
217 }
218 None => Ok(None),
219 }
220 }
221
222 pub fn extract_preimage_from_claim_tx(
224 claim_tx: &lwk_wollet::elements::Transaction,
225 swap_id: &str,
226 ) -> Result<String> {
227 use lwk_wollet::bitcoin::Witness;
228 use lwk_wollet::hashes::{sha256, Hash as _};
229
230 let input = claim_tx
231 .input
232 .first()
233 .ok_or_else(|| anyhow::anyhow!("Found no input for claim tx"))?;
234
235 let script_witness_bytes = input.clone().witness.script_witness;
236 log::debug!("Found Send Swap {swap_id} claim tx witness: {script_witness_bytes:?}");
237 let script_witness = Witness::from(script_witness_bytes);
238
239 let preimage_bytes = script_witness
240 .nth(1)
241 .ok_or_else(|| anyhow::anyhow!("Claim tx witness has no preimage"))?;
242 let preimage = sha256::Hash::from_slice(preimage_bytes)
243 .map_err(|e| anyhow::anyhow!("Claim tx witness has invalid preimage: {e}"))?;
244 let preimage_hex = preimage.to_hex();
245 log::debug!("Found Send Swap {swap_id} claim tx preimage: {preimage_hex}");
246
247 Ok(preimage_hex)
248 }
249}
250
251pub(crate) struct RecoveredOnchainDataSend {
252 pub(crate) lockup_tx_id: Option<LBtcHistory>,
253 pub(crate) claim_tx_id: Option<LBtcHistory>,
254 pub(crate) refund_tx_id: Option<LBtcHistory>,
255 pub(crate) preimage: Option<String>,
256}
257
258impl RecoveredOnchainDataSend {
259 pub(crate) fn derive_partial_state(
260 &self,
261 preimage: Option<String>,
262 is_expired: bool,
263 ) -> Option<PaymentState> {
264 match &self.lockup_tx_id {
265 Some(_) => match preimage {
266 Some(_) => Some(PaymentState::Complete),
267 None => match &self.refund_tx_id {
268 Some(refund_tx_id) => match refund_tx_id.confirmed() {
269 true => Some(PaymentState::Failed),
270 false => Some(PaymentState::RefundPending),
271 },
272 None => match is_expired {
273 true => Some(PaymentState::RefundPending),
274 false => Some(PaymentState::Pending),
275 },
276 },
277 },
278 None => match is_expired {
279 true => Some(PaymentState::Failed),
280 false => None,
283 },
284 }
285 }
286}