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