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