1use std::str::FromStr;
2use std::time::Duration;
3use std::{collections::HashSet, sync::Arc};
4
5use anyhow::{anyhow, bail, Context, Result};
6use boltz_client::{
7 boltz::{self},
8 swaps::boltz::{ChainSwapStates, CreateChainResponse, TransactionInfo},
9 ElementsLockTime, Secp256k1, Serialize, ToHex,
10};
11use elements::{hex::FromHex, Script, Transaction};
12use futures_util::TryFutureExt;
13use log::{debug, error, info, warn};
14use lwk_wollet::hashes::hex::DisplayHex;
15use tokio::sync::{broadcast, Mutex};
16use tokio_with_wasm::alias as tokio;
17
18use crate::{
19 chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
20 elements, ensure_sdk,
21 error::{PaymentError, SdkError, SdkResult},
22 model::{
23 BlockListener, BtcHistory, ChainSwap, ChainSwapUpdate, Config, Direction, LBtcHistory,
24 PaymentState::{self, *},
25 PaymentTxData, PaymentType, Swap, SwapScriptV2, Transaction as SdkTransaction,
26 LIQUID_FEE_RATE_MSAT_PER_VBYTE,
27 },
28 persist::Persister,
29 swapper::Swapper,
30 utils,
31 wallet::OnchainWallet,
32};
33use crate::{
34 error::is_txn_mempool_conflict_error, model::DEFAULT_ONCHAIN_FEE_RATE_LEEWAY_SAT,
35 persist::model::PaymentTxBalance,
36};
37
38pub const ESTIMATED_BTC_CLAIM_TX_VSIZE: u64 = 111;
40
41pub(crate) struct ChainSwapHandler {
42 config: Config,
43 onchain_wallet: Arc<dyn OnchainWallet>,
44 persister: std::sync::Arc<Persister>,
45 swapper: Arc<dyn Swapper>,
46 liquid_chain_service: Arc<dyn LiquidChainService>,
47 bitcoin_chain_service: Arc<dyn BitcoinChainService>,
48 subscription_notifier: broadcast::Sender<String>,
49 claiming_swaps: Arc<Mutex<HashSet<String>>>,
50}
51
52#[sdk_macros::async_trait]
53impl BlockListener for ChainSwapHandler {
54 async fn on_bitcoin_block(&self, height: u32) {
55 if let Err(e) = self.claim_outgoing(height).await {
56 error!("Error claiming outgoing: {e:?}");
57 }
58 }
59
60 async fn on_liquid_block(&self, height: u32) {
61 if let Err(e) = self.refund_outgoing(height).await {
62 warn!("Error refunding outgoing: {e:?}");
63 }
64 if let Err(e) = self.claim_incoming(height).await {
65 error!("Error claiming incoming: {e:?}");
66 }
67 }
68}
69
70impl ChainSwapHandler {
71 pub(crate) fn new(
72 config: Config,
73 onchain_wallet: Arc<dyn OnchainWallet>,
74 persister: std::sync::Arc<Persister>,
75 swapper: Arc<dyn Swapper>,
76 liquid_chain_service: Arc<dyn LiquidChainService>,
77 bitcoin_chain_service: Arc<dyn BitcoinChainService>,
78 ) -> Result<Self> {
79 let (subscription_notifier, _) = broadcast::channel::<String>(30);
80 Ok(Self {
81 config,
82 onchain_wallet,
83 persister,
84 swapper,
85 liquid_chain_service,
86 bitcoin_chain_service,
87 subscription_notifier,
88 claiming_swaps: Arc::new(Mutex::new(HashSet::new())),
89 })
90 }
91
92 pub(crate) fn subscribe_payment_updates(&self) -> broadcast::Receiver<String> {
93 self.subscription_notifier.subscribe()
94 }
95
96 pub(crate) async fn on_new_status(&self, update: &boltz::SwapStatus) -> Result<()> {
98 let id = &update.id;
99 let swap = self.fetch_chain_swap_by_id(id)?;
100
101 match swap.direction {
102 Direction::Incoming => self.on_new_incoming_status(&swap, update).await,
103 Direction::Outgoing => self.on_new_outgoing_status(&swap, update).await,
104 }
105 }
106
107 async fn claim_incoming(&self, height: u32) -> Result<()> {
108 let chain_swaps: Vec<ChainSwap> = self
109 .persister
110 .list_chain_swaps()?
111 .into_iter()
112 .filter(|s| {
113 s.direction == Direction::Incoming && s.state == Pending && s.claim_tx_id.is_none()
114 })
115 .collect();
116 info!(
117 "Rescanning {} incoming Chain Swap(s) server lockup txs at height {}",
118 chain_swaps.len(),
119 height
120 );
121 for swap in chain_swaps {
122 if let Err(e) = self.claim_confirmed_server_lockup(&swap).await {
123 error!(
124 "Error rescanning server lockup of incoming Chain Swap {}: {e:?}",
125 swap.id,
126 );
127 }
128 }
129 Ok(())
130 }
131
132 async fn claim_outgoing(&self, height: u32) -> Result<()> {
133 let chain_swaps: Vec<ChainSwap> = self
134 .persister
135 .list_chain_swaps()?
136 .into_iter()
137 .filter(|s| {
138 s.direction == Direction::Outgoing && s.state == Pending && s.claim_tx_id.is_none()
139 })
140 .collect();
141 info!(
142 "Rescanning {} outgoing Chain Swap(s) server lockup txs at height {}",
143 chain_swaps.len(),
144 height
145 );
146 for swap in chain_swaps {
147 if let Err(e) = self.claim_confirmed_server_lockup(&swap).await {
148 error!(
149 "Error rescanning server lockup of outgoing Chain Swap {}: {e:?}",
150 swap.id
151 );
152 }
153 }
154 Ok(())
155 }
156
157 async fn fetch_script_history(&self, swap_script: &SwapScriptV2) -> Result<Vec<(String, i32)>> {
158 let history = match swap_script {
159 SwapScriptV2::Liquid(_) => self
160 .fetch_liquid_script_history(swap_script)
161 .await?
162 .into_iter()
163 .map(|h| (h.txid.to_hex(), h.height))
164 .collect(),
165 SwapScriptV2::Bitcoin(_) => self
166 .fetch_bitcoin_script_history(swap_script)
167 .await?
168 .into_iter()
169 .map(|h| (h.txid.to_hex(), h.height))
170 .collect(),
171 };
172 Ok(history)
173 }
174
175 async fn claim_confirmed_server_lockup(&self, swap: &ChainSwap) -> Result<()> {
176 let Some(tx_id) = swap.server_lockup_tx_id.clone() else {
177 return Ok(());
179 };
180 let swap_id = &swap.id;
181 let swap_script = swap.get_claim_swap_script()?;
182 let script_history = self.fetch_script_history(&swap_script).await?;
183 let (_tx_history, tx_height) =
184 script_history
185 .iter()
186 .find(|h| h.0.eq(&tx_id))
187 .ok_or(anyhow!(
188 "Server lockup tx for Chain Swap {swap_id} was not found, txid={tx_id}"
189 ))?;
190 if *tx_height > 0 {
191 info!("Chain Swap {swap_id} server lockup tx is confirmed");
192 self.claim(swap_id)
193 .await
194 .map_err(|e| anyhow!("Could not claim Chain Swap {swap_id}: {e:?}"))?;
195 }
196 Ok(())
197 }
198
199 async fn on_new_incoming_status(
200 &self,
201 swap: &ChainSwap,
202 update: &boltz::SwapStatus,
203 ) -> Result<()> {
204 let id = update.id.clone();
205 let status = &update.status;
206 let swap_state = ChainSwapStates::from_str(status)
207 .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?;
208
209 info!("Handling incoming Chain Swap transition to {status:?} for swap {id}");
210 match swap_state {
212 ChainSwapStates::TransactionMempool | ChainSwapStates::TransactionConfirmed => {
214 if let Some(zero_conf_rejected) = update.zero_conf_rejected {
215 info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}");
216 self.persister
217 .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?;
218 }
219 if let Some(transaction) = update.transaction.clone() {
220 let actual_payer_amount =
221 self.fetch_incoming_swap_actual_payer_amount(swap).await?;
222 self.persister
223 .update_actual_payer_amount(&swap.id, actual_payer_amount)?;
224
225 self.update_swap_info(&ChainSwapUpdate {
226 swap_id: id,
227 to_state: Pending,
228 user_lockup_tx_id: Some(transaction.id),
229 ..Default::default()
230 })?;
231 }
232 Ok(())
233 }
234
235 ChainSwapStates::TransactionServerMempool => {
238 match swap.claim_tx_id.clone() {
239 None => {
240 let Some(transaction) = update.transaction.clone() else {
241 return Err(anyhow!("Unexpected payload from Boltz status stream"));
242 };
243
244 if let Err(e) = self.verify_user_lockup_tx(swap).await {
245 warn!("User lockup transaction for incoming Chain Swap {} could not be verified. err: {}", swap.id, e);
246 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
247 }
248
249 if let Err(e) = self
250 .verify_server_lockup_tx(swap, &transaction, false)
251 .await
252 {
253 warn!("Server lockup mempool transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}",
254 swap.id,
255 transaction.id,
256 e);
257 return Err(anyhow!(
258 "Could not verify server lockup transaction {}: {e}",
259 transaction.id
260 ));
261 }
262
263 info!("Server lockup mempool transaction was verified for incoming Chain Swap {}", swap.id);
264 self.update_swap_info(&ChainSwapUpdate {
265 swap_id: id.clone(),
266 to_state: Pending,
267 server_lockup_tx_id: Some(transaction.id),
268 ..Default::default()
269 })?;
270
271 if swap.accept_zero_conf {
272 maybe_delay_before_claim(swap.metadata.is_local).await;
273 self.claim(&id).await.map_err(|e| {
274 error!("Could not cooperate Chain Swap {id} claim: {e}");
275 anyhow!("Could not post claim details. Err: {e:?}")
276 })?;
277 }
278 }
279 Some(claim_tx_id) => {
280 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
281 }
282 };
283 Ok(())
284 }
285
286 ChainSwapStates::TransactionServerConfirmed => {
289 match swap.claim_tx_id.clone() {
290 None => {
291 let Some(transaction) = update.transaction.clone() else {
292 return Err(anyhow!("Unexpected payload from Boltz status stream"));
293 };
294
295 if let Err(e) = self.verify_user_lockup_tx(swap).await {
296 warn!("User lockup transaction for incoming Chain Swap {} could not be verified. err: {}", swap.id, e);
297 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
298 }
299
300 let verify_res =
301 self.verify_server_lockup_tx(swap, &transaction, true).await;
302
303 self.update_swap_info(&ChainSwapUpdate {
307 swap_id: id.clone(),
308 to_state: Pending,
309 server_lockup_tx_id: Some(transaction.id.clone()),
310 ..Default::default()
311 })?;
312
313 match verify_res {
314 Ok(_) => {
315 info!("Server lockup transaction was verified for incoming Chain Swap {}", swap.id);
316
317 maybe_delay_before_claim(swap.metadata.is_local).await;
318 self.claim(&id).await.map_err(|e| {
319 error!("Could not cooperate Chain Swap {id} claim: {e}");
320 anyhow!("Could not post claim details. Err: {e:?}")
321 })?;
322 }
323 Err(e) => {
324 warn!("Server lockup transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e);
325 return Err(anyhow!(
326 "Could not verify server lockup transaction {}: {e}",
327 transaction.id
328 ));
329 }
330 }
331 }
332 Some(claim_tx_id) => {
333 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
334 }
335 };
336 Ok(())
337 }
338
339 ChainSwapStates::TransactionFailed
346 | ChainSwapStates::TransactionLockupFailed
347 | ChainSwapStates::TransactionRefunded
348 | ChainSwapStates::SwapExpired => {
349 let is_zero_amount = swap.payer_amount_sat == 0;
351 if matches!(swap_state, ChainSwapStates::TransactionLockupFailed) && is_zero_amount
352 {
353 match self.handle_amountless_update(swap).await {
354 Ok(_) => {
355 return Ok(()); }
358 Err(e) => error!("Failed to accept the quote for swap {}: {e:?}", &swap.id),
360 }
361 }
362
363 match swap.refund_tx_id.clone() {
364 None => {
365 warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}");
366 if self
367 .user_lockup_tx_exists(swap)
368 .await
369 .context("Failed to check if user lockup tx exists")?
370 {
371 info!("Chain Swap {id} user lockup tx was broadcast. Setting the swap to refundable.");
372 self.update_swap_info(&ChainSwapUpdate {
373 swap_id: id,
374 to_state: Refundable,
375 ..Default::default()
376 })?;
377 } else {
378 info!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed.");
379 self.update_swap_info(&ChainSwapUpdate {
380 swap_id: id,
381 to_state: Failed,
382 ..Default::default()
383 })?;
384 }
385 }
386 Some(refund_tx_id) => warn!(
387 "Refund for Chain Swap {id} was already broadcast: txid {refund_tx_id}"
388 ),
389 };
390 Ok(())
391 }
392
393 _ => {
394 debug!("Unhandled state for Chain Swap {id}: {swap_state:?}");
395 Ok(())
396 }
397 }
398 }
399
400 async fn handle_amountless_update(&self, swap: &ChainSwap) -> Result<(), PaymentError> {
401 let id = swap.id.clone();
402
403 if swap.accepted_receiver_amount_sat.is_some() {
407 info!("Handling amountless update for swap {id} with existing accepted receiver amount. Erasing the accepted amount now...");
408 self.persister.update_accepted_receiver_amount(&id, None)?;
409 }
410
411 let quote = self
412 .swapper
413 .get_zero_amount_chain_swap_quote(&id)
414 .await
415 .map(|quote| quote.to_sat())?;
416 info!("Got quote of {quote} sat for swap {}", &id);
417
418 match self.validate_amountless_swap(swap, quote).await? {
419 ValidateAmountlessSwapResult::ReadyForAccepting {
420 user_lockup_amount_sat,
421 receiver_amount_sat,
422 } => {
423 debug!("Zero-amount swap validated. Auto-accepting...");
424 self.persister
425 .update_actual_payer_amount(&id, user_lockup_amount_sat)?;
426 self.persister
427 .update_accepted_receiver_amount(&id, Some(receiver_amount_sat))?;
428 self.swapper
429 .accept_zero_amount_chain_swap_quote(&id, quote)
430 .inspect_err(|e| {
431 error!("Failed to accept zero-amount swap {id} quote: {e} - trying to erase the accepted receiver amount...");
432 let _ = self.persister.update_accepted_receiver_amount(&id, None);
433 })
434 .await?;
435 self.persister.set_chain_swap_auto_accepted_fees(&id)
436 }
437 ValidateAmountlessSwapResult::RequiresUserAction {
438 user_lockup_amount_sat,
439 } => {
440 debug!("Zero-amount swap validated. Fees are too high for automatic accepting. Moving to WaitingFeeAcceptance");
441 self.persister
442 .update_actual_payer_amount(&id, user_lockup_amount_sat)?;
443 self.update_swap_info(&ChainSwapUpdate {
444 swap_id: id,
445 to_state: WaitingFeeAcceptance,
446 ..Default::default()
447 })
448 }
449 }
450 }
451
452 async fn validate_amountless_swap(
453 &self,
454 swap: &ChainSwap,
455 quote_server_lockup_amount_sat: u64,
456 ) -> Result<ValidateAmountlessSwapResult, PaymentError> {
457 debug!("Validating {swap:?}");
458
459 ensure_sdk!(
460 matches!(swap.direction, Direction::Incoming),
461 PaymentError::generic(format!(
462 "Only an incoming chain swap can be a zero-amount swap. Swap ID: {}",
463 &swap.id
464 ))
465 );
466
467 let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
468 let script_balance = self
469 .bitcoin_chain_service
470 .script_get_balance_with_retry(script_pubkey.as_script(), 10)
471 .await?;
472 debug!("Found lockup balance {script_balance:?}");
473 let user_lockup_amount_sat = match script_balance.confirmed > 0 {
474 true => script_balance.confirmed,
475 false => match script_balance.unconfirmed > 0 {
476 true => script_balance.unconfirmed.unsigned_abs(),
477 false => 0,
478 },
479 };
480 ensure_sdk!(
481 user_lockup_amount_sat > 0,
482 PaymentError::generic("Lockup address has no confirmed or unconfirmed balance")
483 );
484
485 let pair = swap.get_boltz_pair()?;
486
487 let server_fees_estimate_sat = pair.fees.server();
489 let service_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
490 let server_lockup_amount_estimate_sat =
491 user_lockup_amount_sat - server_fees_estimate_sat - service_fees_sat;
492
493 let server_fees_leeway_sat = self
495 .config
496 .onchain_fee_rate_leeway_sat
497 .unwrap_or(DEFAULT_ONCHAIN_FEE_RATE_LEEWAY_SAT);
498 let min_auto_accept_server_lockup_amount_sat =
499 server_lockup_amount_estimate_sat.saturating_sub(server_fees_leeway_sat);
500
501 debug!(
502 "user_lockup_amount_sat = {user_lockup_amount_sat}, \
503 service_fees_sat = {service_fees_sat}, \
504 server_fees_estimate_sat = {server_fees_estimate_sat}, \
505 server_fees_leeway_sat = {server_fees_leeway_sat}, \
506 min_auto_accept_server_lockup_amount_sat = {min_auto_accept_server_lockup_amount_sat}, \
507 quote_server_lockup_amount_sat = {quote_server_lockup_amount_sat}",
508 );
509
510 if min_auto_accept_server_lockup_amount_sat > quote_server_lockup_amount_sat {
511 Ok(ValidateAmountlessSwapResult::RequiresUserAction {
512 user_lockup_amount_sat,
513 })
514 } else {
515 let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat;
516 Ok(ValidateAmountlessSwapResult::ReadyForAccepting {
517 user_lockup_amount_sat,
518 receiver_amount_sat,
519 })
520 }
521 }
522
523 async fn on_new_outgoing_status(
524 &self,
525 swap: &ChainSwap,
526 update: &boltz::SwapStatus,
527 ) -> Result<()> {
528 let id = update.id.clone();
529 let status = &update.status;
530 let swap_state = ChainSwapStates::from_str(status)
531 .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?;
532
533 info!("Handling outgoing Chain Swap transition to {status:?} for swap {id}");
534 match swap_state {
536 ChainSwapStates::Created => {
538 match (swap.state, swap.user_lockup_tx_id.clone()) {
539 (TimedOut, _) => warn!("Chain Swap {id} timed out, do not broadcast a lockup tx"),
541
542 (_, None) => {
544 let create_response = swap.get_boltz_create_response()?;
545 let user_lockup_tx = self.lockup_funds(&id, &create_response).await?;
546 let lockup_tx_id = user_lockup_tx.txid().to_string();
547 let lockup_tx_fees_sat: u64 = user_lockup_tx.all_fees().values().sum();
548
549 self.persister.insert_or_update_payment(PaymentTxData {
552 tx_id: lockup_tx_id.clone(),
553 timestamp: Some(utils::now()),
554 fees_sat: lockup_tx_fees_sat,
555 is_confirmed: false,
556 unblinding_data: None,
557 },
558 &[PaymentTxBalance {
559 asset_id: self.config.lbtc_asset_id().to_string(),
560 amount: create_response.lockup_details.amount,
561 payment_type: PaymentType::Send,
562 }],
563 None, false)?;
564
565 self.update_swap_info(&ChainSwapUpdate {
566 swap_id: id,
567 to_state: Pending,
568 user_lockup_tx_id: Some(lockup_tx_id),
569 ..Default::default()
570 })?;
571 },
572
573 (_, Some(lockup_tx_id)) => warn!("User lockup tx for Chain Swap {id} was already broadcast: txid {lockup_tx_id}"),
575 };
576 Ok(())
577 }
578
579 ChainSwapStates::TransactionMempool | ChainSwapStates::TransactionConfirmed => {
581 if let Some(zero_conf_rejected) = update.zero_conf_rejected {
582 info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}");
583 self.persister
584 .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?;
585 }
586 if let Some(transaction) = update.transaction.clone() {
587 self.update_swap_info(&ChainSwapUpdate {
588 swap_id: id,
589 to_state: Pending,
590 user_lockup_tx_id: Some(transaction.id),
591 ..Default::default()
592 })?;
593 }
594 Ok(())
595 }
596
597 ChainSwapStates::TransactionServerMempool => {
600 match swap.claim_tx_id.clone() {
601 None => {
602 let Some(transaction) = update.transaction.clone() else {
603 return Err(anyhow!("Unexpected payload from Boltz status stream"));
604 };
605
606 if let Err(e) = self.verify_user_lockup_tx(swap).await {
607 warn!("User lockup transaction for outgoing Chain Swap {} could not be verified. err: {}", swap.id, e);
608 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
609 }
610
611 if let Err(e) = self
612 .verify_server_lockup_tx(swap, &transaction, false)
613 .await
614 {
615 warn!("Server lockup mempool transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}",
616 swap.id,
617 transaction.id,
618 e);
619 return Err(anyhow!(
620 "Could not verify server lockup transaction {}: {e}",
621 transaction.id
622 ));
623 }
624
625 info!("Server lockup mempool transaction was verified for outgoing Chain Swap {}", swap.id);
626 self.update_swap_info(&ChainSwapUpdate {
627 swap_id: id.clone(),
628 to_state: Pending,
629 server_lockup_tx_id: Some(transaction.id),
630 ..Default::default()
631 })?;
632
633 if swap.accept_zero_conf {
634 maybe_delay_before_claim(swap.metadata.is_local).await;
635 self.claim(&id).await.map_err(|e| {
636 error!("Could not cooperate Chain Swap {id} claim: {e}");
637 anyhow!("Could not post claim details. Err: {e:?}")
638 })?;
639 }
640 }
641 Some(claim_tx_id) => {
642 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
643 }
644 };
645 Ok(())
646 }
647
648 ChainSwapStates::TransactionServerConfirmed => {
651 match swap.claim_tx_id.clone() {
652 None => {
653 let Some(transaction) = update.transaction.clone() else {
654 return Err(anyhow!("Unexpected payload from Boltz status stream"));
655 };
656
657 if let Err(e) = self.verify_user_lockup_tx(swap).await {
658 warn!("User lockup transaction for outgoing Chain Swap {} could not be verified. err: {}", swap.id, e);
659 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
660 }
661
662 if let Err(e) = self.verify_server_lockup_tx(swap, &transaction, true).await
663 {
664 warn!("Server lockup transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}",
665 swap.id,
666 transaction.id,
667 e);
668 return Err(anyhow!(
669 "Could not verify server lockup transaction {}: {e}",
670 transaction.id
671 ));
672 }
673
674 info!(
675 "Server lockup transaction was verified for outgoing Chain Swap {}",
676 swap.id
677 );
678 self.update_swap_info(&ChainSwapUpdate {
679 swap_id: id.clone(),
680 to_state: Pending,
681 server_lockup_tx_id: Some(transaction.id),
682 ..Default::default()
683 })?;
684
685 maybe_delay_before_claim(swap.metadata.is_local).await;
686 self.claim(&id).await.map_err(|e| {
687 error!("Could not cooperate Chain Swap {id} claim: {e}");
688 anyhow!("Could not post claim details. Err: {e:?}")
689 })?;
690 }
691 Some(claim_tx_id) => {
692 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
693 }
694 };
695 Ok(())
696 }
697
698 ChainSwapStates::TransactionFailed
705 | ChainSwapStates::TransactionLockupFailed
706 | ChainSwapStates::TransactionRefunded
707 | ChainSwapStates::SwapExpired => {
708 match &swap.refund_tx_id {
709 None => {
710 warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}");
711 match swap.user_lockup_tx_id {
712 Some(_) => {
713 warn!("Chain Swap {id} user lockup tx has been broadcast.");
714 let refund_tx_id = match self.refund_outgoing_swap(swap, true).await
715 {
716 Ok(refund_tx_id) => Some(refund_tx_id),
717 Err(e) => {
718 warn!(
719 "Could not refund Send swap {id} cooperatively: {e:?}"
720 );
721 None
722 }
723 };
724 self.update_swap_info(&ChainSwapUpdate {
728 swap_id: id,
729 to_state: RefundPending,
730 refund_tx_id,
731 ..Default::default()
732 })?;
733 }
734 None => {
735 warn!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed.");
736 self.update_swap_info(&ChainSwapUpdate {
737 swap_id: id,
738 to_state: Failed,
739 ..Default::default()
740 })?;
741 }
742 }
743 }
744 Some(refund_tx_id) => warn!(
745 "Refund tx for Chain Swap {id} was already broadcast: txid {refund_tx_id}"
746 ),
747 };
748 Ok(())
749 }
750
751 _ => {
752 debug!("Unhandled state for Chain Swap {id}: {swap_state:?}");
753 Ok(())
754 }
755 }
756 }
757
758 async fn lockup_funds(
759 &self,
760 swap_id: &str,
761 create_response: &CreateChainResponse,
762 ) -> Result<Transaction, PaymentError> {
763 let lockup_details = create_response.lockup_details.clone();
764
765 debug!(
766 "Initiated Chain Swap: send {} sats to liquid address {}",
767 lockup_details.amount, lockup_details.lockup_address
768 );
769
770 let lockup_tx = self
771 .onchain_wallet
772 .build_tx_or_drain_tx(
773 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
774 &lockup_details.lockup_address,
775 &self.config.lbtc_asset_id().to_string(),
776 lockup_details.amount,
777 )
778 .await?;
779
780 let lockup_tx_id = self
781 .liquid_chain_service
782 .broadcast(&lockup_tx)
783 .await?
784 .to_string();
785
786 debug!(
787 "Successfully broadcast lockup transaction for Chain Swap {swap_id}. Lockup tx id: {lockup_tx_id}"
788 );
789 Ok(lockup_tx)
790 }
791
792 fn fetch_chain_swap_by_id(&self, swap_id: &str) -> Result<ChainSwap, PaymentError> {
793 self.persister
794 .fetch_chain_swap_by_id(swap_id)
795 .map_err(|_| PaymentError::PersistError)?
796 .ok_or(PaymentError::Generic {
797 err: format!("Chain Swap not found {swap_id}"),
798 })
799 }
800
801 pub(crate) fn update_swap(&self, updated_swap: ChainSwap) -> Result<(), PaymentError> {
803 let swap = self.fetch_chain_swap_by_id(&updated_swap.id)?;
804 if updated_swap != swap {
805 info!(
806 "Updating Chain swap {} to {:?} (user_lockup_tx_id = {:?}, server_lockup_tx_id = {:?}, claim_tx_id = {:?}, refund_tx_id = {:?})",
807 updated_swap.id,
808 updated_swap.state,
809 updated_swap.user_lockup_tx_id,
810 updated_swap.server_lockup_tx_id,
811 updated_swap.claim_tx_id,
812 updated_swap.refund_tx_id
813 );
814 self.persister.insert_or_update_chain_swap(&updated_swap)?;
815 let _ = self.subscription_notifier.send(updated_swap.id);
816 }
817 Ok(())
818 }
819
820 pub(crate) fn update_swap_info(
822 &self,
823 swap_update: &ChainSwapUpdate,
824 ) -> Result<(), PaymentError> {
825 info!("Updating Chain swap {swap_update:?}");
826 let swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
827 Self::validate_state_transition(swap.state, swap_update.to_state)?;
828 self.persister.try_handle_chain_swap_update(swap_update)?;
829 let updated_swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
830 if updated_swap != swap {
831 let _ = self.subscription_notifier.send(updated_swap.id);
832 }
833 Ok(())
834 }
835
836 async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> {
837 {
838 let mut claiming_guard = self.claiming_swaps.lock().await;
839 if claiming_guard.contains(swap_id) {
840 debug!("Claim for swap {swap_id} already in progress, skipping.");
841 return Ok(());
842 }
843 claiming_guard.insert(swap_id.to_string());
844 }
845
846 let result = self.claim_inner(swap_id).await;
847
848 {
849 let mut claiming_guard = self.claiming_swaps.lock().await;
850 claiming_guard.remove(swap_id);
851 }
852
853 result
854 }
855
856 async fn claim_inner(&self, swap_id: &str) -> Result<(), PaymentError> {
857 let swap = self.fetch_chain_swap_by_id(swap_id)?;
858 ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed);
859
860 match swap.direction {
862 Direction::Incoming => {
863 let liquid_tip = self.liquid_chain_service.tip().await?;
864 if liquid_tip > swap.claim_timeout_block_height - 10 {
865 return Err(PaymentError::Generic {
866 err: format!("Preventing claim for incoming chain swap {swap_id} as timeout block height {} has been/will soon be reached (liquid tip: {liquid_tip})", swap.claim_timeout_block_height),
867 });
868 }
869 }
870 Direction::Outgoing => {
871 let bitcoin_tip = self.bitcoin_chain_service.tip().await?;
872 if bitcoin_tip > swap.claim_timeout_block_height - 2 {
873 return Err(PaymentError::Generic {
874 err: format!("Preventing claim for outgoing chain swap {swap_id} as timeout block height {} has been/will soon be reached (bitcoin tip: {bitcoin_tip})", swap.claim_timeout_block_height),
875 });
876 }
877 }
878 }
879
880 debug!("Initiating claim for Chain Swap {swap_id}");
881 let claim_address = match (swap.direction, swap.claim_address.clone()) {
884 (Direction::Incoming, None) => {
885 Some(self.onchain_wallet.next_unused_address().await?.to_string())
886 }
887 _ => swap.claim_address.clone(),
888 };
889 let claim_tx = self
890 .swapper
891 .create_claim_tx(Swap::Chain(swap.clone()), claim_address.clone())
892 .await?;
893
894 let tx_id = claim_tx.txid();
897 match self
898 .persister
899 .set_chain_swap_claim(swap_id, claim_address, &tx_id)
900 {
901 Ok(_) => {
902 let broadcast_res = match claim_tx {
903 SdkTransaction::Liquid(tx) => {
905 match self.liquid_chain_service.broadcast(&tx).await {
906 Ok(tx_id) => Ok(tx_id.to_hex()),
907 Err(e) if is_txn_mempool_conflict_error(&e) => {
908 Err(PaymentError::AlreadyClaimed)
909 }
910 Err(err) => {
911 debug!(
912 "Could not broadcast claim tx via chain service for Chain swap {swap_id}: {err:?}"
913 );
914 let claim_tx_hex = tx.serialize().to_lower_hex_string();
915 self.swapper
916 .broadcast_tx(self.config.network.into(), &claim_tx_hex)
917 .await
918 }
919 }
920 }
921 SdkTransaction::Bitcoin(tx) => self
922 .bitcoin_chain_service
923 .broadcast(&tx)
924 .await
925 .map(|tx_id| tx_id.to_hex())
926 .map_err(|err| PaymentError::Generic {
927 err: err.to_string(),
928 }),
929 };
930
931 match broadcast_res {
932 Ok(claim_tx_id) => {
933 let payment_id = match swap.direction {
934 Direction::Incoming => {
935 self.persister.insert_or_update_payment(
938 PaymentTxData {
939 tx_id: claim_tx_id.clone(),
940 timestamp: Some(utils::now()),
941 fees_sat: 0,
942 is_confirmed: false,
943 unblinding_data: None,
944 },
945 &[PaymentTxBalance {
946 asset_id: self.config.lbtc_asset_id().to_string(),
947 amount: swap
948 .accepted_receiver_amount_sat
949 .unwrap_or(swap.receiver_amount_sat),
950 payment_type: PaymentType::Receive,
951 }],
952 None,
953 false,
954 )?;
955 Some(claim_tx_id.clone())
956 }
957 Direction::Outgoing => swap.user_lockup_tx_id,
958 };
959
960 info!("Successfully broadcast claim tx {claim_tx_id} for Chain Swap {swap_id}");
961 payment_id.and_then(|payment_id| {
964 self.subscription_notifier.send(payment_id).ok()
965 });
966 Ok(())
967 }
968 Err(err) => {
969 debug!(
971 "Could not broadcast claim tx via swapper for Chain swap {swap_id}: {err:?}"
972 );
973 self.persister
974 .unset_chain_swap_claim_tx_id(swap_id, &tx_id)?;
975 Err(err)
976 }
977 }
978 }
979 Err(err) => {
980 debug!(
981 "Failed to set claim_tx_id after creating tx for Chain swap {swap_id}: txid {tx_id}"
982 );
983 Err(err)
984 }
985 }
986 }
987
988 pub(crate) async fn prepare_refund(
989 &self,
990 lockup_address: &str,
991 refund_address: &str,
992 fee_rate_sat_per_vb: u32,
993 ) -> SdkResult<(u32, u64, Option<String>)> {
994 let swap = self
995 .persister
996 .fetch_chain_swap_by_lockup_address(lockup_address)?
997 .ok_or(SdkError::generic(format!(
998 "Chain Swap with lockup address {lockup_address} not found"
999 )))?;
1000
1001 let refund_tx_id = swap.refund_tx_id.clone();
1002 if let Some(refund_tx_id) = &refund_tx_id {
1003 warn!(
1004 "A refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
1005 swap.id
1006 );
1007 }
1008
1009 let (refund_tx_size, refund_tx_fees_sat) = self
1010 .swapper
1011 .estimate_refund_broadcast(
1012 Swap::Chain(swap),
1013 refund_address,
1014 Some(fee_rate_sat_per_vb as f64),
1015 true,
1016 )
1017 .await?;
1018
1019 Ok((refund_tx_size, refund_tx_fees_sat, refund_tx_id))
1020 }
1021
1022 pub(crate) async fn refund_incoming_swap(
1023 &self,
1024 lockup_address: &str,
1025 refund_address: &str,
1026 broadcast_fee_rate_sat_per_vb: u32,
1027 is_cooperative: bool,
1028 ) -> Result<String, PaymentError> {
1029 let swap = self
1030 .persister
1031 .fetch_chain_swap_by_lockup_address(lockup_address)?
1032 .ok_or(PaymentError::Generic {
1033 err: format!("Swap for lockup address {lockup_address} not found"),
1034 })?;
1035 let id = &swap.id;
1036
1037 ensure_sdk!(
1038 swap.state.is_refundable(),
1039 PaymentError::Generic {
1040 err: format!("Chain Swap {id} was not in refundable state")
1041 }
1042 );
1043
1044 info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1045
1046 let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else {
1047 return Err(PaymentError::Generic {
1048 err: "Unexpected swap script type found".to_string(),
1049 });
1050 };
1051
1052 let script_pk = swap_script
1053 .to_address(self.config.network.as_bitcoin_chain())
1054 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1055 .script_pubkey();
1056 let utxos = self
1057 .bitcoin_chain_service
1058 .get_script_utxos(&script_pk)
1059 .await?;
1060
1061 let SdkTransaction::Bitcoin(refund_tx) = self
1062 .swapper
1063 .create_refund_tx(
1064 Swap::Chain(swap.clone()),
1065 refund_address,
1066 utxos,
1067 Some(broadcast_fee_rate_sat_per_vb as f64),
1068 is_cooperative,
1069 )
1070 .await?
1071 else {
1072 return Err(PaymentError::Generic {
1073 err: format!("Unexpected refund tx type returned for incoming Chain swap {id}",),
1074 });
1075 };
1076 let refund_tx_id = self
1077 .bitcoin_chain_service
1078 .broadcast(&refund_tx)
1079 .await?
1080 .to_string();
1081
1082 info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1083
1084 self.update_swap_info(&ChainSwapUpdate {
1088 swap_id: swap.id,
1089 to_state: RefundPending,
1090 refund_tx_id: Some(refund_tx_id.clone()),
1091 ..Default::default()
1092 })?;
1093
1094 Ok(refund_tx_id)
1095 }
1096
1097 pub(crate) async fn refund_outgoing_swap(
1098 &self,
1099 swap: &ChainSwap,
1100 is_cooperative: bool,
1101 ) -> Result<String, PaymentError> {
1102 ensure_sdk!(
1103 swap.refund_tx_id.is_none(),
1104 PaymentError::Generic {
1105 err: format!(
1106 "A refund tx for outgoing Chain Swap {} was already broadcast",
1107 swap.id
1108 )
1109 }
1110 );
1111
1112 info!(
1113 "Initiating refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1114 swap.id
1115 );
1116
1117 let SwapScriptV2::Liquid(swap_script) = swap.get_lockup_swap_script()? else {
1118 return Err(PaymentError::Generic {
1119 err: "Unexpected swap script type found".to_string(),
1120 });
1121 };
1122
1123 let script_pk = swap_script
1124 .to_address(self.config.network.into())
1125 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1126 .to_unconfidential()
1127 .script_pubkey();
1128 let utxos = self
1129 .liquid_chain_service
1130 .get_script_utxos(&script_pk)
1131 .await?;
1132
1133 let refund_address = match swap.refund_address {
1134 Some(ref refund_address) => refund_address.clone(),
1135 None => {
1136 let address = self.onchain_wallet.next_unused_address().await?.to_string();
1138 self.persister
1139 .set_chain_swap_refund_address(&swap.id, &address)?;
1140 address
1141 }
1142 };
1143
1144 let SdkTransaction::Liquid(refund_tx) = self
1145 .swapper
1146 .create_refund_tx(
1147 Swap::Chain(swap.clone()),
1148 &refund_address,
1149 utxos,
1150 None,
1151 is_cooperative,
1152 )
1153 .await?
1154 else {
1155 return Err(PaymentError::Generic {
1156 err: format!(
1157 "Unexpected refund tx type returned for outgoing Chain swap {}",
1158 swap.id
1159 ),
1160 });
1161 };
1162 let refund_tx_id = self
1163 .liquid_chain_service
1164 .broadcast(&refund_tx)
1165 .await?
1166 .to_string();
1167
1168 info!(
1169 "Successfully broadcast refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1170 swap.id
1171 );
1172
1173 Ok(refund_tx_id)
1174 }
1175
1176 async fn refund_outgoing(&self, height: u32) -> Result<(), PaymentError> {
1177 let pending_swaps: Vec<ChainSwap> = self
1179 .persister
1180 .list_pending_chain_swaps()?
1181 .into_iter()
1182 .filter(|s| s.direction == Direction::Outgoing && s.refund_tx_id.is_none())
1183 .collect();
1184 for swap in pending_swaps {
1185 let swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?;
1186 let locktime_from_height = ElementsLockTime::from_height(height)
1187 .map_err(|e| PaymentError::Generic { err: e.to_string() })?;
1188 info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime);
1189 let has_swap_expired =
1190 utils::is_locktime_expired(locktime_from_height, swap_script.locktime);
1191 if has_swap_expired || swap.state == RefundPending {
1192 let refund_tx_id_res = match swap.state {
1193 Pending => self.refund_outgoing_swap(&swap, false).await,
1194 RefundPending => match has_swap_expired {
1195 true => {
1196 self.refund_outgoing_swap(&swap, true)
1197 .or_else(|e| {
1198 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
1199 self.refund_outgoing_swap(&swap, false)
1200 })
1201 .await
1202 }
1203 false => self.refund_outgoing_swap(&swap, true).await,
1204 },
1205 _ => {
1206 continue;
1207 }
1208 };
1209
1210 if let Ok(refund_tx_id) = refund_tx_id_res {
1211 let update_swap_info_res = self.update_swap_info(&ChainSwapUpdate {
1212 swap_id: swap.id.clone(),
1213 to_state: RefundPending,
1214 refund_tx_id: Some(refund_tx_id),
1215 ..Default::default()
1216 });
1217 if let Err(err) = update_swap_info_res {
1218 warn!(
1219 "Could not update outgoing Chain swap {} information: {err:?}",
1220 swap.id
1221 );
1222 };
1223 }
1224 }
1225 }
1226 Ok(())
1227 }
1228
1229 fn validate_state_transition(
1230 from_state: PaymentState,
1231 to_state: PaymentState,
1232 ) -> Result<(), PaymentError> {
1233 match (from_state, to_state) {
1234 (_, Created) => Err(PaymentError::Generic {
1235 err: "Cannot transition to Created state".to_string(),
1236 }),
1237
1238 (Created | Pending | WaitingFeeAcceptance, Pending) => Ok(()),
1239 (_, Pending) => Err(PaymentError::Generic {
1240 err: format!("Cannot transition from {from_state:?} to Pending state"),
1241 }),
1242
1243 (Created | Pending | WaitingFeeAcceptance, WaitingFeeAcceptance) => Ok(()),
1244 (_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
1245 err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
1246 }),
1247
1248 (Created | Pending | WaitingFeeAcceptance | RefundPending, Complete) => Ok(()),
1249 (_, Complete) => Err(PaymentError::Generic {
1250 err: format!("Cannot transition from {from_state:?} to Complete state"),
1251 }),
1252
1253 (Created, TimedOut) => Ok(()),
1254 (_, TimedOut) => Err(PaymentError::Generic {
1255 err: format!("Cannot transition from {from_state:?} to TimedOut state"),
1256 }),
1257
1258 (
1259 Created | Pending | WaitingFeeAcceptance | RefundPending | Failed | Complete,
1260 Refundable,
1261 ) => Ok(()),
1262 (_, Refundable) => Err(PaymentError::Generic {
1263 err: format!("Cannot transition from {from_state:?} to Refundable state"),
1264 }),
1265
1266 (Pending | WaitingFeeAcceptance | Refundable | RefundPending, RefundPending) => Ok(()),
1267 (_, RefundPending) => Err(PaymentError::Generic {
1268 err: format!("Cannot transition from {from_state:?} to RefundPending state"),
1269 }),
1270
1271 (Complete, Failed) => Err(PaymentError::Generic {
1272 err: format!("Cannot transition from {from_state:?} to Failed state"),
1273 }),
1274
1275 (_, Failed) => Ok(()),
1276 }
1277 }
1278
1279 async fn fetch_incoming_swap_actual_payer_amount(&self, chain_swap: &ChainSwap) -> Result<u64> {
1280 let swap_script = chain_swap.get_lockup_swap_script()?;
1281 let script_pubkey = swap_script
1282 .as_bitcoin_script()?
1283 .to_address(self.config.network.as_bitcoin_chain())
1284 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1285 .script_pubkey();
1286
1287 let history = self.fetch_bitcoin_script_history(&swap_script).await?;
1288
1289 let first_tx_id = history
1291 .first()
1292 .ok_or(anyhow!(
1293 "No history found for user lockup script for swap {}",
1294 chain_swap.id
1295 ))?
1296 .txid
1297 .to_raw_hash()
1298 .into();
1299
1300 let txs = self
1302 .bitcoin_chain_service
1303 .get_transactions(&[first_tx_id])
1304 .await?;
1305 let user_lockup_tx = txs.first().ok_or(anyhow!(
1306 "No transactions found for user lockup script for swap {}",
1307 chain_swap.id
1308 ))?;
1309
1310 user_lockup_tx
1312 .output
1313 .iter()
1314 .find(|out| out.script_pubkey == script_pubkey)
1315 .map(|out| out.value.to_sat())
1316 .ok_or(anyhow!("No output found paying to user lockup script"))
1317 }
1318
1319 async fn verify_server_lockup_tx(
1320 &self,
1321 chain_swap: &ChainSwap,
1322 swap_update_tx: &TransactionInfo,
1323 verify_confirmation: bool,
1324 ) -> Result<()> {
1325 match chain_swap.direction {
1326 Direction::Incoming => {
1327 self.verify_incoming_server_lockup_tx(
1328 chain_swap,
1329 swap_update_tx,
1330 verify_confirmation,
1331 )
1332 .await
1333 }
1334 Direction::Outgoing => {
1335 self.verify_outgoing_server_lockup_tx(
1336 chain_swap,
1337 swap_update_tx,
1338 verify_confirmation,
1339 )
1340 .await
1341 }
1342 }
1343 }
1344
1345 async fn verify_incoming_server_lockup_tx(
1346 &self,
1347 chain_swap: &ChainSwap,
1348 swap_update_tx: &TransactionInfo,
1349 verify_confirmation: bool,
1350 ) -> Result<()> {
1351 let swap_script = chain_swap.get_claim_swap_script()?;
1352 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1353 let liquid_swap_script = swap_script.as_liquid_script()?;
1355 let address = liquid_swap_script
1356 .to_address(self.config.network.into())
1357 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1358 let tx_hex = swap_update_tx
1359 .hex
1360 .as_ref()
1361 .ok_or(anyhow!("Transaction info without hex"))?;
1362 let tx = self
1363 .liquid_chain_service
1364 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1365 .await?;
1366 let rbf_explicit = tx.input.iter().any(|tx_in| tx_in.sequence.is_rbf());
1368 if !verify_confirmation && rbf_explicit {
1369 bail!("Transaction signals RBF");
1370 }
1371 let secp = Secp256k1::new();
1373 let to_address_output = tx
1374 .output
1375 .iter()
1376 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey());
1377 let mut value = 0;
1378 for tx_out in to_address_output {
1379 value += tx_out
1380 .unblind(&secp, liquid_swap_script.blinding_key.secret_key())?
1381 .value;
1382 }
1383 match chain_swap.accepted_receiver_amount_sat {
1384 None => {
1385 if value < claim_details.amount {
1386 bail!(
1387 "Transaction value {value} sats is less than {} sats",
1388 claim_details.amount
1389 );
1390 }
1391 }
1392 Some(accepted_receiver_amount_sat) => {
1393 let expected_server_lockup_amount_sat =
1394 accepted_receiver_amount_sat + chain_swap.claim_fees_sat;
1395 if value < expected_server_lockup_amount_sat {
1396 bail!(
1397 "Transaction value {value} sats is less than accepted {} sats",
1398 expected_server_lockup_amount_sat
1399 );
1400 }
1401 }
1402 }
1403
1404 Ok(())
1405 }
1406
1407 async fn verify_outgoing_server_lockup_tx(
1408 &self,
1409 chain_swap: &ChainSwap,
1410 swap_update_tx: &TransactionInfo,
1411 verify_confirmation: bool,
1412 ) -> Result<()> {
1413 let swap_script = chain_swap.get_claim_swap_script()?;
1414 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1415 let address = swap_script
1417 .as_bitcoin_script()?
1418 .to_address(self.config.network.as_bitcoin_chain())
1419 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1420 let tx_hex = swap_update_tx
1421 .hex
1422 .as_ref()
1423 .ok_or(anyhow!("Transaction info without hex"))?;
1424 let tx = self
1425 .bitcoin_chain_service
1426 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1427 .await?;
1428 let rbf_explicit = tx.input.iter().any(|input| input.sequence.is_rbf());
1430 if !verify_confirmation && rbf_explicit {
1431 return Err(anyhow!("Transaction signals RBF"));
1432 }
1433 let value: u64 = tx
1435 .output
1436 .iter()
1437 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey())
1438 .map(|tx_out| tx_out.value.to_sat())
1439 .sum();
1440 if value < claim_details.amount {
1441 return Err(anyhow!(
1442 "Transaction value {value} sats is less than {} sats",
1443 claim_details.amount
1444 ));
1445 }
1446 Ok(())
1447 }
1448
1449 async fn user_lockup_tx_exists(&self, chain_swap: &ChainSwap) -> Result<bool> {
1450 let lockup_script = chain_swap.get_lockup_swap_script()?;
1451 let script_history = self.fetch_script_history(&lockup_script).await?;
1452
1453 match chain_swap.user_lockup_tx_id.clone() {
1454 Some(user_lockup_tx_id) => {
1455 if !script_history.iter().any(|h| h.0 == user_lockup_tx_id) {
1456 return Ok(false);
1457 }
1458 }
1459 None => {
1460 let (txid, _tx_height) = match script_history.into_iter().nth(0) {
1461 Some(h) => h,
1462 None => {
1463 return Ok(false);
1464 }
1465 };
1466 self.update_swap_info(&ChainSwapUpdate {
1467 swap_id: chain_swap.id.clone(),
1468 to_state: Pending,
1469 user_lockup_tx_id: Some(txid.clone()),
1470 ..Default::default()
1471 })?;
1472 }
1473 }
1474
1475 Ok(true)
1476 }
1477
1478 async fn verify_user_lockup_tx(&self, chain_swap: &ChainSwap) -> Result<()> {
1479 if !self.user_lockup_tx_exists(chain_swap).await? {
1480 bail!("User lockup tx not found in script history");
1481 }
1482
1483 if chain_swap.direction == Direction::Incoming {
1485 let actual_payer_amount_sat = match chain_swap.actual_payer_amount_sat {
1486 Some(amount) => amount,
1487 None => {
1488 let actual_payer_amount_sat = self
1489 .fetch_incoming_swap_actual_payer_amount(chain_swap)
1490 .await?;
1491 self.persister
1492 .update_actual_payer_amount(&chain_swap.id, actual_payer_amount_sat)?;
1493 actual_payer_amount_sat
1494 }
1495 };
1496 if chain_swap.payer_amount_sat > 0
1498 && chain_swap.payer_amount_sat != actual_payer_amount_sat
1499 {
1500 bail!("Invalid user lockup tx - user lockup amount ({actual_payer_amount_sat} sat) differs from agreed ({} sat)", chain_swap.payer_amount_sat);
1501 }
1502 }
1503
1504 Ok(())
1505 }
1506
1507 async fn fetch_bitcoin_script_history(
1508 &self,
1509 swap_script: &SwapScriptV2,
1510 ) -> Result<Vec<BtcHistory>> {
1511 let address = swap_script
1512 .as_bitcoin_script()?
1513 .to_address(self.config.network.as_bitcoin_chain())
1514 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1515 let script_pubkey = address.script_pubkey();
1516 let script = script_pubkey.as_script();
1517 self.bitcoin_chain_service
1518 .get_script_history_with_retry(script, 10)
1519 .await
1520 }
1521
1522 async fn fetch_liquid_script_history(
1523 &self,
1524 swap_script: &SwapScriptV2,
1525 ) -> Result<Vec<LBtcHistory>> {
1526 let address = swap_script
1527 .as_liquid_script()?
1528 .to_address(self.config.network.into())
1529 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1530 .to_unconfidential();
1531 let script = Script::from_hex(hex::encode(address.script_pubkey().as_bytes()).as_str())
1532 .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
1533 self.liquid_chain_service
1534 .get_script_history_with_retry(&script, 10)
1535 .await
1536 }
1537}
1538
1539enum ValidateAmountlessSwapResult {
1540 ReadyForAccepting {
1541 user_lockup_amount_sat: u64,
1542 receiver_amount_sat: u64,
1543 },
1544 RequiresUserAction {
1545 user_lockup_amount_sat: u64,
1546 },
1547}
1548
1549async fn maybe_delay_before_claim(is_swap_local: bool) {
1550 if !is_swap_local {
1555 info!("Waiting 5 seconds before claim to reduce likelihood of concurrent claims");
1556 tokio::time::sleep(Duration::from_secs(5)).await;
1557 }
1558}
1559
1560#[cfg(test)]
1561mod tests {
1562 use anyhow::Result;
1563 use std::collections::{HashMap, HashSet};
1564
1565 use crate::{
1566 model::{
1567 ChainSwapUpdate, Direction,
1568 PaymentState::{self, *},
1569 },
1570 test_utils::{
1571 chain_swap::{new_chain_swap, new_chain_swap_handler},
1572 persist::create_persister,
1573 },
1574 };
1575
1576 #[cfg(feature = "browser-tests")]
1577 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1578
1579 #[sdk_macros::async_test_all]
1580 async fn test_chain_swap_state_transitions() -> Result<()> {
1581 create_persister!(persister);
1582
1583 let chain_swap_handler = new_chain_swap_handler(persister.clone())?;
1584
1585 let all_states = HashSet::from([
1587 Created,
1588 Pending,
1589 WaitingFeeAcceptance,
1590 Complete,
1591 TimedOut,
1592 Failed,
1593 ]);
1594 let valid_combinations = HashMap::from([
1595 (
1596 Created,
1597 HashSet::from([
1598 Pending,
1599 WaitingFeeAcceptance,
1600 Complete,
1601 TimedOut,
1602 Refundable,
1603 Failed,
1604 ]),
1605 ),
1606 (
1607 Pending,
1608 HashSet::from([
1609 Pending,
1610 WaitingFeeAcceptance,
1611 Complete,
1612 Refundable,
1613 RefundPending,
1614 Failed,
1615 ]),
1616 ),
1617 (
1618 WaitingFeeAcceptance,
1619 HashSet::from([
1620 Pending,
1621 WaitingFeeAcceptance,
1622 Complete,
1623 Refundable,
1624 RefundPending,
1625 Failed,
1626 ]),
1627 ),
1628 (TimedOut, HashSet::from([Failed])),
1629 (Complete, HashSet::from([Refundable])),
1630 (Refundable, HashSet::from([RefundPending, Failed])),
1631 (
1632 RefundPending,
1633 HashSet::from([Refundable, Complete, Failed, RefundPending]),
1634 ),
1635 (Failed, HashSet::from([Failed, Refundable])),
1636 ]);
1637
1638 for (first_state, allowed_states) in valid_combinations.iter() {
1639 for allowed_state in allowed_states {
1640 let chain_swap = new_chain_swap(
1641 Direction::Incoming,
1642 Some(*first_state),
1643 false,
1644 None,
1645 false,
1646 false,
1647 None,
1648 );
1649 persister.insert_or_update_chain_swap(&chain_swap)?;
1650
1651 assert!(chain_swap_handler
1652 .update_swap_info(&ChainSwapUpdate {
1653 swap_id: chain_swap.id,
1654 to_state: *allowed_state,
1655 ..Default::default()
1656 })
1657 .is_ok());
1658 }
1659 }
1660
1661 let invalid_combinations: HashMap<PaymentState, HashSet<PaymentState>> = valid_combinations
1663 .iter()
1664 .map(|(first_state, allowed_states)| {
1665 (
1666 *first_state,
1667 all_states.difference(allowed_states).cloned().collect(),
1668 )
1669 })
1670 .collect();
1671
1672 for (first_state, disallowed_states) in invalid_combinations.iter() {
1673 for disallowed_state in disallowed_states {
1674 let chain_swap = new_chain_swap(
1675 Direction::Incoming,
1676 Some(*first_state),
1677 false,
1678 None,
1679 false,
1680 false,
1681 None,
1682 );
1683 persister.insert_or_update_chain_swap(&chain_swap)?;
1684
1685 assert!(chain_swap_handler
1686 .update_swap_info(&ChainSwapUpdate {
1687 swap_id: chain_swap.id,
1688 to_state: *disallowed_state,
1689 ..Default::default()
1690 })
1691 .is_err());
1692 }
1693 }
1694
1695 Ok(())
1696 }
1697}