breez_sdk_liquid/recover/handlers/
handle_chain_send_swap.rs1use anyhow::Result;
2use log::{debug, error, warn};
3use lwk_wollet::elements::Txid;
4
5use crate::prelude::*;
6use crate::recover::model::*;
7
8pub(crate) struct ChainSendSwapHandler;
10
11impl ChainSendSwapHandler {
12 pub fn should_skip_recovery(
14 chain_swap: &ChainSwap,
15 recovered_data: &RecoveredOnchainDataChainSend,
16 is_within_grace_period: bool,
17 ) -> bool {
18 let swap_id = &chain_swap.id;
19
20 let lockup_is_cleared = chain_swap.user_lockup_tx_id.is_some()
21 && recovered_data.lbtc_user_lockup_tx_id.is_none();
22 let refund_is_cleared =
23 chain_swap.refund_tx_id.is_some() && recovered_data.lbtc_refund_tx_id.is_none();
24 let claim_is_cleared =
25 chain_swap.claim_tx_id.is_some() && recovered_data.btc_claim_tx_id.is_none();
26
27 if is_within_grace_period && (lockup_is_cleared || refund_is_cleared || claim_is_cleared) {
28 warn!(
29 "Local outgoing chain swap {swap_id} was updated recently - skipping recovery \
30 as it would clear a tx that may have been broadcasted by us. Lockup clear: \
31 {lockup_is_cleared} - Refund clear: {refund_is_cleared}"
32 );
33 return true;
34 }
35
36 false
37 }
38
39 pub async fn recover_swap(
41 chain_swap: &mut ChainSwap,
42 context: &ChainSwapRecoveryContext,
43 is_within_grace_period: bool,
44 ) -> Result<()> {
45 let swap_id = &chain_swap.id.clone();
46 debug!("[Recover Chain Send] Recovering data for swap {swap_id}");
47
48 let claim_script = chain_swap
50 .get_claim_swap_script()
51 .ok()
52 .and_then(|script| script.as_bitcoin_script().ok())
53 .and_then(|script| script.funding_addrs.map(|addr| addr.script_pubkey()))
54 .ok_or_else(|| {
55 anyhow::anyhow!("BTC claim script not found for Onchain Send Swap {swap_id}")
56 })?;
57
58 let lockup_script = chain_swap
59 .get_lockup_swap_script()
60 .ok()
61 .and_then(|script| script.as_liquid_script().ok())
62 .and_then(|script| script.funding_addrs.map(|addr| addr.script_pubkey()))
63 .ok_or_else(|| {
64 anyhow::anyhow!("LBTC lockup script not found for Onchain Send Swap {swap_id}")
65 })?;
66
67 let history = &SendChainSwapHistory {
68 lbtc_lockup_script_history: context
69 .lbtc_script_to_history_map
70 .get(&lockup_script)
71 .cloned()
72 .unwrap_or_default(),
73 btc_claim_script_history: context
74 .btc_script_to_history_map
75 .get(&claim_script)
76 .cloned()
77 .unwrap_or_default(),
78 btc_claim_script_txs: context
79 .btc_script_to_txs_map
80 .get(&claim_script)
81 .cloned()
82 .unwrap_or_default(),
83 };
84
85 let recovered_data =
87 Self::recover_onchain_data(&context.tx_map, swap_id, history, &claim_script)?;
88
89 Self::update_swap(
91 chain_swap,
92 &recovered_data,
93 context.liquid_tip_height,
94 context.bitcoin_tip_height,
95 is_within_grace_period,
96 )
97 }
98
99 pub fn update_swap(
101 chain_swap: &mut ChainSwap,
102 recovered_data: &RecoveredOnchainDataChainSend,
103 current_liquid_block_height: u32,
104 current_bitcoin_block_height: u32,
105 is_within_grace_period: bool,
106 ) -> Result<()> {
107 if Self::should_skip_recovery(chain_swap, recovered_data, is_within_grace_period) {
109 return Ok(());
110 }
111
112 let is_expired = current_liquid_block_height >= chain_swap.timeout_block_height
114 || current_bitcoin_block_height >= chain_swap.claim_timeout_block_height;
115 if let Some(new_state) = recovered_data.derive_partial_state(is_expired) {
116 chain_swap.state = new_state;
117 }
118
119 chain_swap.user_lockup_tx_id = recovered_data
121 .lbtc_user_lockup_tx_id
122 .clone()
123 .map(|h| h.txid.to_string());
124 chain_swap.refund_tx_id = recovered_data
125 .lbtc_refund_tx_id
126 .clone()
127 .map(|h| h.txid.to_string());
128 chain_swap.server_lockup_tx_id = recovered_data
129 .btc_server_lockup_tx_id
130 .clone()
131 .map(|h| h.txid.to_string());
132 chain_swap.claim_tx_id = recovered_data
133 .btc_claim_tx_id
134 .clone()
135 .map(|h| h.txid.to_string());
136
137 Ok(())
138 }
139
140 fn recover_onchain_data(
145 tx_map: &TxMap,
146 swap_id: &str,
147 history: &SendChainSwapHistory,
148 claim_script: &BtcScript,
149 ) -> Result<RecoveredOnchainDataChainSend> {
150 let lbtc_user_lockup_tx_id = history
152 .lbtc_lockup_script_history
153 .iter()
154 .find(|&tx| tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
155 .cloned();
156
157 if lbtc_user_lockup_tx_id.is_none() {
158 error!("No lockup tx found when recovering data for Chain Send Swap {swap_id}");
159 }
160
161 let lbtc_refund_tx_id = history
163 .lbtc_lockup_script_history
164 .iter()
165 .find(|&tx| tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
166 .cloned();
167
168 let (btc_server_lockup_tx_id, btc_claim_tx_id) = match history
169 .btc_claim_script_history
170 .len()
171 {
172 1 => (Some(history.btc_claim_script_history[0].clone()), None),
174
175 2 => {
176 let first_tx = history.btc_claim_script_txs[0].clone();
177 let first_tx_id = history.btc_claim_script_history[0].clone();
178 let second_tx_id = history.btc_claim_script_history[1].clone();
179
180 let is_first_tx_lockup_tx = first_tx
182 .output
183 .iter()
184 .any(|out| matches!(&out.script_pubkey, x if x == claim_script));
185
186 match is_first_tx_lockup_tx {
187 true => (Some(first_tx_id), Some(second_tx_id)),
188 false => (Some(second_tx_id), Some(first_tx_id)),
189 }
190 }
191 n => {
192 warn!("BTC script history with length {n} found while recovering data for Chain Send Swap {swap_id}");
193 (None, None)
194 }
195 };
196
197 Ok(RecoveredOnchainDataChainSend {
198 lbtc_user_lockup_tx_id,
199 lbtc_refund_tx_id,
200 btc_server_lockup_tx_id,
201 btc_claim_tx_id,
202 })
203 }
204}
205
206pub(crate) struct RecoveredOnchainDataChainSend {
207 pub(crate) lbtc_user_lockup_tx_id: Option<LBtcHistory>,
209 pub(crate) lbtc_refund_tx_id: Option<LBtcHistory>,
211 pub(crate) btc_server_lockup_tx_id: Option<BtcHistory>,
213 pub(crate) btc_claim_tx_id: Option<BtcHistory>,
215}
216
217impl RecoveredOnchainDataChainSend {
221 pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option<PaymentState> {
222 match &self.lbtc_user_lockup_tx_id {
223 Some(_) => match (&self.btc_claim_tx_id, &self.lbtc_refund_tx_id) {
224 (Some(btc_claim_tx_id), None) => match btc_claim_tx_id.confirmed() {
225 true => Some(PaymentState::Complete),
226 false => Some(PaymentState::Pending),
227 },
228 (None, Some(lbtc_refund_tx_id)) => match lbtc_refund_tx_id.confirmed() {
229 true => Some(PaymentState::Failed),
230 false => Some(PaymentState::RefundPending),
231 },
232 (Some(btc_claim_tx_id), Some(lbtc_refund_tx_id)) => {
233 match btc_claim_tx_id.confirmed() {
234 true => match lbtc_refund_tx_id.confirmed() {
235 true => Some(PaymentState::Complete),
236 false => Some(PaymentState::RefundPending),
237 },
238 false => Some(PaymentState::Pending),
239 }
240 }
241 (None, None) => match is_expired {
242 true => Some(PaymentState::RefundPending),
243 false => Some(PaymentState::Pending),
244 },
245 },
246 None => match is_expired {
247 true => Some(PaymentState::Failed),
248 false => None,
251 },
252 }
253 }
254}
255
256#[derive(Clone)]
257pub(crate) struct SendChainSwapHistory {
258 pub(crate) lbtc_lockup_script_history: Vec<LBtcHistory>,
259 pub(crate) btc_claim_script_history: Vec<BtcHistory>,
260 pub(crate) btc_claim_script_txs: Vec<bitcoin::Transaction>,
261}