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: &RecoveryContext,
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 is_within_grace_period,
95 )
96 }
97
98 pub fn update_swap(
100 chain_swap: &mut ChainSwap,
101 recovered_data: &RecoveredOnchainDataChainSend,
102 current_block_height: u32,
103 is_within_grace_period: bool,
104 ) -> Result<()> {
105 if Self::should_skip_recovery(chain_swap, recovered_data, is_within_grace_period) {
107 return Ok(());
108 }
109
110 let is_expired = current_block_height >= chain_swap.timeout_block_height;
112 if let Some(new_state) = recovered_data.derive_partial_state(is_expired) {
113 chain_swap.state = new_state;
114 }
115
116 chain_swap.user_lockup_tx_id = recovered_data
118 .lbtc_user_lockup_tx_id
119 .clone()
120 .map(|h| h.txid.to_string());
121 chain_swap.refund_tx_id = recovered_data
122 .lbtc_refund_tx_id
123 .clone()
124 .map(|h| h.txid.to_string());
125 chain_swap.server_lockup_tx_id = recovered_data
126 .btc_server_lockup_tx_id
127 .clone()
128 .map(|h| h.txid.to_string());
129 chain_swap.claim_tx_id = recovered_data
130 .btc_claim_tx_id
131 .clone()
132 .map(|h| h.txid.to_string());
133
134 Ok(())
135 }
136
137 fn recover_onchain_data(
142 tx_map: &TxMap,
143 swap_id: &str,
144 history: &SendChainSwapHistory,
145 claim_script: &BtcScript,
146 ) -> Result<RecoveredOnchainDataChainSend> {
147 let lbtc_user_lockup_tx_id = history
149 .lbtc_lockup_script_history
150 .iter()
151 .find(|&tx| tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
152 .cloned();
153
154 if lbtc_user_lockup_tx_id.is_none() {
155 error!("No lockup tx found when recovering data for Chain Send Swap {swap_id}");
156 }
157
158 let lbtc_refund_tx_id = history
160 .lbtc_lockup_script_history
161 .iter()
162 .find(|&tx| tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
163 .cloned();
164
165 let (btc_server_lockup_tx_id, btc_claim_tx_id) = match history
166 .btc_claim_script_history
167 .len()
168 {
169 1 => (Some(history.btc_claim_script_history[0].clone()), None),
171
172 2 => {
173 let first_tx = history.btc_claim_script_txs[0].clone();
174 let first_tx_id = history.btc_claim_script_history[0].clone();
175 let second_tx_id = history.btc_claim_script_history[1].clone();
176
177 let is_first_tx_lockup_tx = first_tx
179 .output
180 .iter()
181 .any(|out| matches!(&out.script_pubkey, x if x == claim_script));
182
183 match is_first_tx_lockup_tx {
184 true => (Some(first_tx_id), Some(second_tx_id)),
185 false => (Some(second_tx_id), Some(first_tx_id)),
186 }
187 }
188 n => {
189 warn!("BTC script history with length {n} found while recovering data for Chain Send Swap {swap_id}");
190 (None, None)
191 }
192 };
193
194 Ok(RecoveredOnchainDataChainSend {
195 lbtc_user_lockup_tx_id,
196 lbtc_refund_tx_id,
197 btc_server_lockup_tx_id,
198 btc_claim_tx_id,
199 })
200 }
201}
202
203pub(crate) struct RecoveredOnchainDataChainSend {
204 pub(crate) lbtc_user_lockup_tx_id: Option<LBtcHistory>,
206 pub(crate) lbtc_refund_tx_id: Option<LBtcHistory>,
208 pub(crate) btc_server_lockup_tx_id: Option<BtcHistory>,
210 pub(crate) btc_claim_tx_id: Option<BtcHistory>,
212}
213
214impl RecoveredOnchainDataChainSend {
218 pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option<PaymentState> {
219 match &self.lbtc_user_lockup_tx_id {
220 Some(_) => match (&self.btc_claim_tx_id, &self.lbtc_refund_tx_id) {
221 (Some(btc_claim_tx_id), None) => match btc_claim_tx_id.confirmed() {
222 true => Some(PaymentState::Complete),
223 false => Some(PaymentState::Pending),
224 },
225 (None, Some(lbtc_refund_tx_id)) => match lbtc_refund_tx_id.confirmed() {
226 true => Some(PaymentState::Failed),
227 false => Some(PaymentState::RefundPending),
228 },
229 (Some(btc_claim_tx_id), Some(lbtc_refund_tx_id)) => {
230 match btc_claim_tx_id.confirmed() {
231 true => match lbtc_refund_tx_id.confirmed() {
232 true => Some(PaymentState::Complete),
233 false => Some(PaymentState::RefundPending),
234 },
235 false => Some(PaymentState::Pending),
236 }
237 }
238 (None, None) => match is_expired {
239 true => Some(PaymentState::RefundPending),
240 false => Some(PaymentState::Pending),
241 },
242 },
243 None => match is_expired {
244 true => Some(PaymentState::Failed),
245 false => None,
248 },
249 }
250 }
251}
252
253#[derive(Clone)]
254pub(crate) struct SendChainSwapHistory {
255 pub(crate) lbtc_lockup_script_history: Vec<LBtcHistory>,
256 pub(crate) btc_claim_script_history: Vec<BtcHistory>,
257 pub(crate) btc_claim_script_txs: Vec<bitcoin::Transaction>,
258}