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::{handle_stale_cache_broadcast_error, OnchainWallet},
32};
33use crate::{
34 error::is_txn_already_spent_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 if swap.is_waiting_fee_acceptance() {
357 if let Err(e) = self.handle_amountless_update(swap).await {
358 error!("Failed to accept the quote for swap {}: {e:?}", swap.id);
361 }
362 } else {
363 debug!(
364 "Ignoring repeated TransactionLockupFailed for already-accepted zero-amount swap {}",
365 swap.id
366 );
367 }
368 return Ok(());
369 }
370
371 match swap.refund_tx_id.clone() {
372 None => {
373 warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}");
374 if self
375 .user_lockup_tx_exists(swap)
376 .await
377 .context("Failed to check if user lockup tx exists")?
378 {
379 info!("Chain Swap {id} user lockup tx was broadcast. Setting the swap to refundable.");
380 self.update_swap_info(&ChainSwapUpdate {
381 swap_id: id,
382 to_state: Refundable,
383 ..Default::default()
384 })?;
385 } else {
386 info!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed.");
387 self.update_swap_info(&ChainSwapUpdate {
388 swap_id: id,
389 to_state: Failed,
390 ..Default::default()
391 })?;
392 }
393 }
394 Some(refund_tx_id) => warn!(
395 "Refund for Chain Swap {id} was already broadcast: txid {refund_tx_id}"
396 ),
397 };
398 Ok(())
399 }
400
401 _ => {
402 debug!("Unhandled state for Chain Swap {id}: {swap_state:?}");
403 Ok(())
404 }
405 }
406 }
407
408 async fn handle_amountless_update(&self, swap: &ChainSwap) -> Result<(), PaymentError> {
409 let id = swap.id.clone();
410
411 if swap.accepted_receiver_amount_sat.is_some() {
415 info!("Handling amountless update for swap {id} with existing accepted receiver amount. Erasing the accepted amount now...");
416 self.persister.update_accepted_receiver_amount(&id, None)?;
417 }
418
419 let quote = self
420 .swapper
421 .get_zero_amount_chain_swap_quote(&id)
422 .await
423 .map(|quote| quote.to_sat())?;
424 info!("Got quote of {quote} sat for swap {}", id);
425
426 match self.validate_amountless_swap(swap, quote).await? {
427 ValidateAmountlessSwapResult::ReadyForAccepting {
428 user_lockup_amount_sat,
429 receiver_amount_sat,
430 } => {
431 debug!("Zero-amount swap validated. Auto-accepting...");
432 self.persister
433 .update_actual_payer_amount(&id, user_lockup_amount_sat)?;
434 self.persister
435 .update_accepted_receiver_amount(&id, Some(receiver_amount_sat))?;
436 self.swapper
437 .accept_zero_amount_chain_swap_quote(&id, quote)
438 .inspect_err(|e| {
439 error!("Failed to accept zero-amount swap {id} quote: {e} - trying to erase the accepted receiver amount...");
440 let _ = self.persister.update_accepted_receiver_amount(&id, None);
441 })
442 .await?;
443 self.persister.set_chain_swap_auto_accepted_fees(&id)
444 }
445 ValidateAmountlessSwapResult::RequiresUserAction {
446 user_lockup_amount_sat,
447 } => {
448 debug!("Zero-amount swap validated. Fees are too high for automatic accepting. Moving to WaitingFeeAcceptance");
449 self.persister
450 .update_actual_payer_amount(&id, user_lockup_amount_sat)?;
451 self.update_swap_info(&ChainSwapUpdate {
452 swap_id: id,
453 to_state: WaitingFeeAcceptance,
454 ..Default::default()
455 })
456 }
457 }
458 }
459
460 async fn validate_amountless_swap(
461 &self,
462 swap: &ChainSwap,
463 quote_server_lockup_amount_sat: u64,
464 ) -> Result<ValidateAmountlessSwapResult, PaymentError> {
465 debug!("Validating {swap:?}");
466
467 ensure_sdk!(
468 matches!(swap.direction, Direction::Incoming),
469 PaymentError::generic(format!(
470 "Only an incoming chain swap can be a zero-amount swap. Swap ID: {}",
471 swap.id
472 ))
473 );
474
475 let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
476 let script_balance = self
477 .bitcoin_chain_service
478 .script_get_balance_with_retry(script_pubkey.as_script(), 10)
479 .await?;
480 debug!("Found lockup balance {script_balance:?}");
481 let user_lockup_amount_sat = match script_balance.confirmed > 0 {
482 true => script_balance.confirmed,
483 false => match script_balance.unconfirmed > 0 {
484 true => script_balance.unconfirmed.unsigned_abs(),
485 false => 0,
486 },
487 };
488 ensure_sdk!(
489 user_lockup_amount_sat > 0,
490 PaymentError::generic("Lockup address has no confirmed or unconfirmed balance")
491 );
492
493 let pair = swap.get_boltz_pair()?;
494
495 let server_fees_estimate_sat = pair.fees.server();
497 let service_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
498 let server_lockup_amount_estimate_sat =
499 user_lockup_amount_sat - server_fees_estimate_sat - service_fees_sat;
500
501 let server_fees_leeway_sat = self
503 .config
504 .onchain_fee_rate_leeway_sat
505 .unwrap_or(DEFAULT_ONCHAIN_FEE_RATE_LEEWAY_SAT);
506 let min_auto_accept_server_lockup_amount_sat =
507 server_lockup_amount_estimate_sat.saturating_sub(server_fees_leeway_sat);
508
509 debug!(
510 "user_lockup_amount_sat = {user_lockup_amount_sat}, \
511 service_fees_sat = {service_fees_sat}, \
512 server_fees_estimate_sat = {server_fees_estimate_sat}, \
513 server_fees_leeway_sat = {server_fees_leeway_sat}, \
514 min_auto_accept_server_lockup_amount_sat = {min_auto_accept_server_lockup_amount_sat}, \
515 quote_server_lockup_amount_sat = {quote_server_lockup_amount_sat}",
516 );
517
518 if min_auto_accept_server_lockup_amount_sat > quote_server_lockup_amount_sat {
519 Ok(ValidateAmountlessSwapResult::RequiresUserAction {
520 user_lockup_amount_sat,
521 })
522 } else {
523 let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat;
524 Ok(ValidateAmountlessSwapResult::ReadyForAccepting {
525 user_lockup_amount_sat,
526 receiver_amount_sat,
527 })
528 }
529 }
530
531 async fn on_new_outgoing_status(
532 &self,
533 swap: &ChainSwap,
534 update: &boltz::SwapStatus,
535 ) -> Result<()> {
536 let id = update.id.clone();
537 let status = &update.status;
538 let swap_state = ChainSwapStates::from_str(status)
539 .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?;
540
541 info!("Handling outgoing Chain Swap transition to {status:?} for swap {id}");
542 match swap_state {
544 ChainSwapStates::Created => {
546 match (swap.state, swap.user_lockup_tx_id.clone()) {
547 (TimedOut, _) => warn!("Chain Swap {id} timed out, do not broadcast a lockup tx"),
549
550 (_, None) => {
552 let create_response = swap.get_boltz_create_response()?;
553 let user_lockup_tx = self.lockup_funds(&id, &create_response).await?;
554 let lockup_tx_id = user_lockup_tx.txid().to_string();
555 let lockup_tx_fees_sat: u64 = user_lockup_tx.all_fees().values().sum();
556
557 self.persister.insert_or_update_payment(PaymentTxData {
560 tx_id: lockup_tx_id.clone(),
561 timestamp: Some(utils::now()),
562 fees_sat: lockup_tx_fees_sat,
563 is_confirmed: false,
564 unblinding_data: None,
565 },
566 &[PaymentTxBalance {
567 asset_id: self.config.lbtc_asset_id().to_string(),
568 amount: create_response.lockup_details.amount,
569 payment_type: PaymentType::Send,
570 }],
571 None, false)?;
572
573 self.update_swap_info(&ChainSwapUpdate {
574 swap_id: id,
575 to_state: Pending,
576 user_lockup_tx_id: Some(lockup_tx_id),
577 ..Default::default()
578 })?;
579 },
580
581 (_, Some(lockup_tx_id)) => warn!("User lockup tx for Chain Swap {id} was already broadcast: txid {lockup_tx_id}"),
583 };
584 Ok(())
585 }
586
587 ChainSwapStates::TransactionMempool | ChainSwapStates::TransactionConfirmed => {
589 if let Some(zero_conf_rejected) = update.zero_conf_rejected {
590 info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}");
591 self.persister
592 .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?;
593 }
594 if let Some(transaction) = update.transaction.clone() {
595 self.update_swap_info(&ChainSwapUpdate {
596 swap_id: id,
597 to_state: Pending,
598 user_lockup_tx_id: Some(transaction.id),
599 ..Default::default()
600 })?;
601 }
602 Ok(())
603 }
604
605 ChainSwapStates::TransactionServerMempool => {
608 match swap.claim_tx_id.clone() {
609 None => {
610 let Some(transaction) = update.transaction.clone() else {
611 return Err(anyhow!("Unexpected payload from Boltz status stream"));
612 };
613
614 if let Err(e) = self.verify_user_lockup_tx(swap).await {
615 warn!("User lockup transaction for outgoing Chain Swap {} could not be verified. err: {}", swap.id, e);
616 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
617 }
618
619 if let Err(e) = self
620 .verify_server_lockup_tx(swap, &transaction, false)
621 .await
622 {
623 warn!("Server lockup mempool transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}",
624 swap.id,
625 transaction.id,
626 e);
627 return Err(anyhow!(
628 "Could not verify server lockup transaction {}: {e}",
629 transaction.id
630 ));
631 }
632
633 info!("Server lockup mempool transaction was verified for outgoing Chain Swap {}", swap.id);
634 self.update_swap_info(&ChainSwapUpdate {
635 swap_id: id.clone(),
636 to_state: Pending,
637 server_lockup_tx_id: Some(transaction.id),
638 ..Default::default()
639 })?;
640
641 if swap.accept_zero_conf {
642 maybe_delay_before_claim(swap.metadata.is_local).await;
643 self.claim(&id).await.map_err(|e| {
644 error!("Could not cooperate Chain Swap {id} claim: {e}");
645 anyhow!("Could not post claim details. Err: {e:?}")
646 })?;
647 }
648 }
649 Some(claim_tx_id) => {
650 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
651 }
652 };
653 Ok(())
654 }
655
656 ChainSwapStates::TransactionServerConfirmed => {
659 match swap.claim_tx_id.clone() {
660 None => {
661 let Some(transaction) = update.transaction.clone() else {
662 return Err(anyhow!("Unexpected payload from Boltz status stream"));
663 };
664
665 if let Err(e) = self.verify_user_lockup_tx(swap).await {
666 warn!("User lockup transaction for outgoing Chain Swap {} could not be verified. err: {}", swap.id, e);
667 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
668 }
669
670 if let Err(e) = self.verify_server_lockup_tx(swap, &transaction, true).await
671 {
672 warn!("Server lockup transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}",
673 swap.id,
674 transaction.id,
675 e);
676 return Err(anyhow!(
677 "Could not verify server lockup transaction {}: {e}",
678 transaction.id
679 ));
680 }
681
682 info!(
683 "Server lockup transaction was verified for outgoing Chain Swap {}",
684 swap.id
685 );
686 self.update_swap_info(&ChainSwapUpdate {
687 swap_id: id.clone(),
688 to_state: Pending,
689 server_lockup_tx_id: Some(transaction.id),
690 ..Default::default()
691 })?;
692
693 maybe_delay_before_claim(swap.metadata.is_local).await;
694 self.claim(&id).await.map_err(|e| {
695 error!("Could not cooperate Chain Swap {id} claim: {e}");
696 anyhow!("Could not post claim details. Err: {e:?}")
697 })?;
698 }
699 Some(claim_tx_id) => {
700 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
701 }
702 };
703 Ok(())
704 }
705
706 ChainSwapStates::TransactionFailed
713 | ChainSwapStates::TransactionLockupFailed
714 | ChainSwapStates::TransactionRefunded
715 | ChainSwapStates::SwapExpired => {
716 match &swap.refund_tx_id {
717 None => {
718 warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}");
719 match swap.user_lockup_tx_id {
720 Some(_) => {
721 warn!("Chain Swap {id} user lockup tx has been broadcast.");
722 let refund_tx_id = match self.refund_outgoing_swap(swap, true).await
723 {
724 Ok(refund_tx_id) => Some(refund_tx_id),
725 Err(e) => {
726 warn!(
727 "Could not refund Send swap {id} cooperatively: {e:?}"
728 );
729 None
730 }
731 };
732 self.update_swap_info(&ChainSwapUpdate {
736 swap_id: id,
737 to_state: RefundPending,
738 refund_tx_id,
739 ..Default::default()
740 })?;
741 }
742 None => {
743 warn!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed.");
744 self.update_swap_info(&ChainSwapUpdate {
745 swap_id: id,
746 to_state: Failed,
747 ..Default::default()
748 })?;
749 }
750 }
751 }
752 Some(refund_tx_id) => warn!(
753 "Refund tx for Chain Swap {id} was already broadcast: txid {refund_tx_id}"
754 ),
755 };
756 Ok(())
757 }
758
759 _ => {
760 debug!("Unhandled state for Chain Swap {id}: {swap_state:?}");
761 Ok(())
762 }
763 }
764 }
765
766 async fn lockup_funds(
767 &self,
768 swap_id: &str,
769 create_response: &CreateChainResponse,
770 ) -> Result<Transaction, PaymentError> {
771 let lockup_details = create_response.lockup_details.clone();
772
773 debug!(
774 "Initiated Chain Swap: send {} sats to liquid address {}",
775 lockup_details.amount, lockup_details.lockup_address
776 );
777
778 let lockup_tx = self
779 .onchain_wallet
780 .build_tx_or_drain_tx(
781 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
782 &lockup_details.lockup_address,
783 &self.config.lbtc_asset_id().to_string(),
784 lockup_details.amount,
785 )
786 .await?;
787
788 let lockup_tx_id = match self.liquid_chain_service.broadcast(&lockup_tx).await {
789 Ok(tx_id) => tx_id.to_string(),
790 Err(err) => {
791 return Err(handle_stale_cache_broadcast_error(&*self.onchain_wallet, err).await)
792 }
793 };
794
795 self.onchain_wallet.apply_broadcast_tx(&lockup_tx).await;
796
797 debug!(
798 "Successfully broadcast lockup transaction for Chain Swap {swap_id}. Lockup tx id: {lockup_tx_id}"
799 );
800 Ok(lockup_tx)
801 }
802
803 fn fetch_chain_swap_by_id(&self, swap_id: &str) -> Result<ChainSwap, PaymentError> {
804 self.persister
805 .fetch_chain_swap_by_id(swap_id)
806 .map_err(|e| {
807 error!("Failed to fetch chain swap by id: {e:?}");
808 PaymentError::PersistError
809 })?
810 .ok_or(PaymentError::Generic {
811 err: format!("Chain Swap not found {swap_id}"),
812 })
813 }
814
815 pub(crate) fn update_swap(&self, updated_swap: ChainSwap) -> Result<(), PaymentError> {
817 let swap = self.fetch_chain_swap_by_id(&updated_swap.id)?;
818 if updated_swap != swap {
819 info!(
820 "Updating Chain swap {} to {:?} (user_lockup_tx_id = {:?}, server_lockup_tx_id = {:?}, claim_tx_id = {:?}, refund_tx_id = {:?})",
821 updated_swap.id,
822 updated_swap.state,
823 updated_swap.user_lockup_tx_id,
824 updated_swap.server_lockup_tx_id,
825 updated_swap.claim_tx_id,
826 updated_swap.refund_tx_id
827 );
828 self.persister.insert_or_update_chain_swap(&updated_swap)?;
829 let _ = self.subscription_notifier.send(updated_swap.id);
830 }
831 Ok(())
832 }
833
834 pub(crate) fn update_swap_info(
836 &self,
837 swap_update: &ChainSwapUpdate,
838 ) -> Result<(), PaymentError> {
839 info!("Updating Chain swap {swap_update:?}");
840 let swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
841 Self::validate_state_transition(swap.state, swap_update.to_state)?;
842 self.persister.try_handle_chain_swap_update(swap_update)?;
843 let updated_swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
844 if updated_swap != swap {
845 let _ = self.subscription_notifier.send(updated_swap.id);
846 }
847 Ok(())
848 }
849
850 async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> {
851 {
852 let mut claiming_guard = self.claiming_swaps.lock().await;
853 if claiming_guard.contains(swap_id) {
854 debug!("Claim for swap {swap_id} already in progress, skipping.");
855 return Ok(());
856 }
857 claiming_guard.insert(swap_id.to_string());
858 }
859
860 let result = self.claim_inner(swap_id).await;
861
862 {
863 let mut claiming_guard = self.claiming_swaps.lock().await;
864 claiming_guard.remove(swap_id);
865 }
866
867 result
868 }
869
870 async fn claim_inner(&self, swap_id: &str) -> Result<(), PaymentError> {
871 let swap = self.fetch_chain_swap_by_id(swap_id)?;
872 ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed);
873
874 if !swap.user_lockup_spent {
877 match swap.direction {
878 Direction::Incoming => {
879 let liquid_tip = self.liquid_chain_service.tip().await?;
880 if liquid_tip > swap.claim_timeout_block_height - 10 {
881 return Err(PaymentError::Generic {
882 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),
883 });
884 }
885 }
886 Direction::Outgoing => {
887 let bitcoin_tip = self.bitcoin_chain_service.tip().await?;
888 if bitcoin_tip > swap.claim_timeout_block_height - 2 {
889 return Err(PaymentError::Generic {
890 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),
891 });
892 }
893 }
894 }
895 }
896
897 debug!("Initiating claim for Chain Swap {swap_id}");
898 let claim_address = match (swap.direction, swap.claim_address.clone()) {
901 (Direction::Incoming, None) => {
902 Some(self.onchain_wallet.next_unused_address().await?.to_string())
903 }
904 _ => swap.claim_address.clone(),
905 };
906 let claim_tx = self
907 .swapper
908 .create_claim_tx(Swap::Chain(swap.clone()), claim_address.clone(), true)
909 .await?;
910
911 let tx_id = claim_tx.txid();
914 match self
915 .persister
916 .set_chain_swap_claim(swap_id, claim_address, &tx_id)
917 {
918 Ok(_) => {
919 let broadcast_res = match claim_tx {
920 SdkTransaction::Liquid(tx) => {
922 match self.liquid_chain_service.broadcast(&tx).await {
923 Ok(tx_id) => Ok(tx_id.to_hex()),
924 Err(e) if is_txn_already_spent_error(&e) => {
925 Err(PaymentError::AlreadyClaimed)
926 }
927 Err(err) => {
928 debug!(
929 "Could not broadcast claim tx via chain service for Chain swap {swap_id}: {err:?}"
930 );
931 let claim_tx_hex = tx.serialize().to_lower_hex_string();
932 self.swapper
933 .broadcast_tx(self.config.network.into(), &claim_tx_hex)
934 .await
935 }
936 }
937 }
938 SdkTransaction::Bitcoin(tx) => self
939 .bitcoin_chain_service
940 .broadcast(&tx)
941 .await
942 .map(|tx_id| tx_id.to_hex())
943 .map_err(|err| PaymentError::Generic {
944 err: err.to_string(),
945 }),
946 };
947
948 match broadcast_res {
949 Ok(claim_tx_id) => {
950 let payment_id = match swap.direction {
951 Direction::Incoming => {
952 self.persister.insert_or_update_payment(
955 PaymentTxData {
956 tx_id: claim_tx_id.clone(),
957 timestamp: Some(utils::now()),
958 fees_sat: 0,
959 is_confirmed: false,
960 unblinding_data: None,
961 },
962 &[PaymentTxBalance {
963 asset_id: self.config.lbtc_asset_id().to_string(),
964 amount: swap
965 .accepted_receiver_amount_sat
966 .unwrap_or(swap.receiver_amount_sat),
967 payment_type: PaymentType::Receive,
968 }],
969 None,
970 false,
971 )?;
972 Some(claim_tx_id.clone())
973 }
974 Direction::Outgoing => swap.user_lockup_tx_id,
975 };
976
977 info!("Successfully broadcast claim tx {claim_tx_id} for Chain Swap {swap_id}");
978 payment_id.and_then(|payment_id| {
981 self.subscription_notifier.send(payment_id).ok()
982 });
983 Ok(())
984 }
985 Err(err) => {
986 debug!(
988 "Could not broadcast claim tx via swapper for Chain swap {swap_id}: {err:?}"
989 );
990 self.persister
991 .unset_chain_swap_claim_tx_id(swap_id, &tx_id)?;
992 Err(err)
993 }
994 }
995 }
996 Err(err) => {
997 debug!(
998 "Failed to set claim_tx_id after creating tx for Chain swap {swap_id}: txid {tx_id}"
999 );
1000 Err(err)
1001 }
1002 }
1003 }
1004
1005 pub(crate) async fn prepare_refund(
1006 &self,
1007 lockup_address: &str,
1008 refund_address: &str,
1009 fee_rate_sat_per_vb: u32,
1010 ) -> SdkResult<(u32, u64, Option<String>)> {
1011 let swap = self
1012 .persister
1013 .fetch_chain_swap_by_lockup_address(lockup_address)?
1014 .ok_or(SdkError::generic(format!(
1015 "Chain Swap with lockup address {lockup_address} not found"
1016 )))?;
1017
1018 let refund_tx_id = swap.refund_tx_id.clone();
1019 if let Some(refund_tx_id) = &refund_tx_id {
1020 warn!(
1021 "A refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
1022 swap.id
1023 );
1024 }
1025
1026 let (refund_tx_size, refund_tx_fees_sat) = self
1027 .swapper
1028 .estimate_refund_broadcast(
1029 Swap::Chain(swap),
1030 refund_address,
1031 Some(fee_rate_sat_per_vb as f64),
1032 true,
1033 )
1034 .await?;
1035
1036 Ok((refund_tx_size, refund_tx_fees_sat, refund_tx_id))
1037 }
1038
1039 pub(crate) async fn refund_incoming_swap(
1040 &self,
1041 lockup_address: &str,
1042 refund_address: &str,
1043 broadcast_fee_rate_sat_per_vb: u32,
1044 is_cooperative: bool,
1045 ) -> Result<String, PaymentError> {
1046 let swap = self
1047 .persister
1048 .fetch_chain_swap_by_lockup_address(lockup_address)?
1049 .ok_or(PaymentError::Generic {
1050 err: format!("Swap for lockup address {lockup_address} not found"),
1051 })?;
1052 let id = &swap.id;
1053
1054 ensure_sdk!(
1055 swap.state.is_refundable(),
1056 PaymentError::Generic {
1057 err: format!("Chain Swap {id} was not in refundable state")
1058 }
1059 );
1060
1061 info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1062
1063 let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else {
1064 return Err(PaymentError::Generic {
1065 err: "Unexpected swap script type found".to_string(),
1066 });
1067 };
1068
1069 let script_pk = swap_script
1070 .to_address(self.config.network.as_bitcoin_chain())
1071 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1072 .script_pubkey();
1073 let utxos = self
1074 .bitcoin_chain_service
1075 .get_script_utxos(&script_pk)
1076 .await?;
1077
1078 let SdkTransaction::Bitcoin(refund_tx) = self
1079 .swapper
1080 .create_refund_tx(
1081 Swap::Chain(swap.clone()),
1082 refund_address,
1083 utxos,
1084 Some(broadcast_fee_rate_sat_per_vb as f64),
1085 is_cooperative,
1086 )
1087 .await?
1088 else {
1089 return Err(PaymentError::Generic {
1090 err: format!("Unexpected refund tx type returned for incoming Chain swap {id}",),
1091 });
1092 };
1093 let refund_tx_id = self
1094 .bitcoin_chain_service
1095 .broadcast(&refund_tx)
1096 .await?
1097 .to_string();
1098
1099 info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1100
1101 self.update_swap_info(&ChainSwapUpdate {
1105 swap_id: swap.id,
1106 to_state: RefundPending,
1107 refund_tx_id: Some(refund_tx_id.clone()),
1108 ..Default::default()
1109 })?;
1110
1111 Ok(refund_tx_id)
1112 }
1113
1114 pub(crate) async fn refund_outgoing_swap(
1115 &self,
1116 swap: &ChainSwap,
1117 is_cooperative: bool,
1118 ) -> Result<String, PaymentError> {
1119 ensure_sdk!(
1120 swap.refund_tx_id.is_none(),
1121 PaymentError::Generic {
1122 err: format!(
1123 "A refund tx for outgoing Chain Swap {} was already broadcast",
1124 swap.id
1125 )
1126 }
1127 );
1128
1129 info!(
1130 "Initiating refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1131 swap.id
1132 );
1133
1134 let SwapScriptV2::Liquid(swap_script) = swap.get_lockup_swap_script()? else {
1135 return Err(PaymentError::Generic {
1136 err: "Unexpected swap script type found".to_string(),
1137 });
1138 };
1139
1140 let script_pk = swap_script
1141 .to_address(self.config.network.into())
1142 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1143 .to_unconfidential()
1144 .script_pubkey();
1145 let utxos = self
1146 .liquid_chain_service
1147 .get_script_utxos(&script_pk)
1148 .await?;
1149
1150 let refund_address = match swap.refund_address {
1151 Some(ref refund_address) => refund_address.clone(),
1152 None => {
1153 let address = self.onchain_wallet.next_unused_address().await?.to_string();
1155 self.persister
1156 .set_chain_swap_refund_address(&swap.id, &address)?;
1157 address
1158 }
1159 };
1160
1161 let SdkTransaction::Liquid(refund_tx) = self
1162 .swapper
1163 .create_refund_tx(
1164 Swap::Chain(swap.clone()),
1165 &refund_address,
1166 utxos,
1167 None,
1168 is_cooperative,
1169 )
1170 .await?
1171 else {
1172 return Err(PaymentError::Generic {
1173 err: format!(
1174 "Unexpected refund tx type returned for outgoing Chain swap {}",
1175 swap.id
1176 ),
1177 });
1178 };
1179 let refund_tx_id = self
1180 .liquid_chain_service
1181 .broadcast(&refund_tx)
1182 .await?
1183 .to_string();
1184
1185 info!(
1186 "Successfully broadcast refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1187 swap.id
1188 );
1189
1190 Ok(refund_tx_id)
1191 }
1192
1193 async fn refund_outgoing(&self, height: u32) -> Result<(), PaymentError> {
1194 let pending_swaps: Vec<ChainSwap> = self
1196 .persister
1197 .list_pending_chain_swaps()?
1198 .into_iter()
1199 .filter(|s| s.direction == Direction::Outgoing && s.refund_tx_id.is_none())
1200 .collect();
1201 for swap in pending_swaps {
1202 let swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?;
1203 let locktime_from_height = ElementsLockTime::from_height(height)
1204 .map_err(|e| PaymentError::Generic { err: e.to_string() })?;
1205 info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime);
1206 let has_swap_expired =
1207 utils::is_locktime_expired(locktime_from_height, swap_script.locktime);
1208 if has_swap_expired || swap.state == RefundPending {
1209 let refund_tx_id_res = match swap.state {
1210 Pending => self.refund_outgoing_swap(&swap, false).await,
1211 RefundPending => match has_swap_expired {
1212 true => {
1213 self.refund_outgoing_swap(&swap, true)
1214 .or_else(|e| {
1215 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
1216 self.refund_outgoing_swap(&swap, false)
1217 })
1218 .await
1219 }
1220 false => self.refund_outgoing_swap(&swap, true).await,
1221 },
1222 _ => {
1223 continue;
1224 }
1225 };
1226
1227 if let Ok(refund_tx_id) = refund_tx_id_res {
1228 let update_swap_info_res = self.update_swap_info(&ChainSwapUpdate {
1229 swap_id: swap.id.clone(),
1230 to_state: RefundPending,
1231 refund_tx_id: Some(refund_tx_id),
1232 ..Default::default()
1233 });
1234 if let Err(err) = update_swap_info_res {
1235 warn!(
1236 "Could not update outgoing Chain swap {} information: {err:?}",
1237 swap.id
1238 );
1239 };
1240 }
1241 }
1242 }
1243 Ok(())
1244 }
1245
1246 fn validate_state_transition(
1247 from_state: PaymentState,
1248 to_state: PaymentState,
1249 ) -> Result<(), PaymentError> {
1250 match (from_state, to_state) {
1251 (_, Created) => Err(PaymentError::Generic {
1252 err: "Cannot transition to Created state".to_string(),
1253 }),
1254
1255 (Created | Pending | WaitingFeeAcceptance, Pending) => Ok(()),
1256 (_, Pending) => Err(PaymentError::Generic {
1257 err: format!("Cannot transition from {from_state:?} to Pending state"),
1258 }),
1259
1260 (Created | Pending | WaitingFeeAcceptance, WaitingFeeAcceptance) => Ok(()),
1261 (_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
1262 err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
1263 }),
1264
1265 (Created | Pending | WaitingFeeAcceptance | RefundPending, Complete) => Ok(()),
1266 (_, Complete) => Err(PaymentError::Generic {
1267 err: format!("Cannot transition from {from_state:?} to Complete state"),
1268 }),
1269
1270 (Created, TimedOut) => Ok(()),
1271 (_, TimedOut) => Err(PaymentError::Generic {
1272 err: format!("Cannot transition from {from_state:?} to TimedOut state"),
1273 }),
1274
1275 (
1276 Created | Pending | WaitingFeeAcceptance | RefundPending | Failed | Complete,
1277 Refundable,
1278 ) => Ok(()),
1279 (_, Refundable) => Err(PaymentError::Generic {
1280 err: format!("Cannot transition from {from_state:?} to Refundable state"),
1281 }),
1282
1283 (Pending | WaitingFeeAcceptance | Refundable | RefundPending, RefundPending) => Ok(()),
1284 (_, RefundPending) => Err(PaymentError::Generic {
1285 err: format!("Cannot transition from {from_state:?} to RefundPending state"),
1286 }),
1287
1288 (Complete, Failed) => Err(PaymentError::Generic {
1289 err: format!("Cannot transition from {from_state:?} to Failed state"),
1290 }),
1291
1292 (_, Failed) => Ok(()),
1293 }
1294 }
1295
1296 async fn fetch_incoming_swap_actual_payer_amount(&self, chain_swap: &ChainSwap) -> Result<u64> {
1297 let swap_script = chain_swap.get_lockup_swap_script()?;
1298 let script_pubkey = swap_script
1299 .as_bitcoin_script()?
1300 .to_address(self.config.network.as_bitcoin_chain())
1301 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1302 .script_pubkey();
1303
1304 let history = self.fetch_bitcoin_script_history(&swap_script).await?;
1305
1306 let first_tx_id = history
1308 .first()
1309 .ok_or(anyhow!(
1310 "No history found for user lockup script for swap {}",
1311 chain_swap.id
1312 ))?
1313 .txid
1314 .to_raw_hash()
1315 .into();
1316
1317 let txs = self
1319 .bitcoin_chain_service
1320 .get_transactions_with_retry(&[first_tx_id], 3)
1321 .await?;
1322 let user_lockup_tx = txs.first().ok_or(anyhow!(
1323 "No transactions found for user lockup script for swap {}",
1324 chain_swap.id
1325 ))?;
1326
1327 user_lockup_tx
1329 .output
1330 .iter()
1331 .find(|out| out.script_pubkey == script_pubkey)
1332 .map(|out| out.value.to_sat())
1333 .ok_or(anyhow!("No output found paying to user lockup script"))
1334 }
1335
1336 async fn verify_server_lockup_tx(
1337 &self,
1338 chain_swap: &ChainSwap,
1339 swap_update_tx: &TransactionInfo,
1340 verify_confirmation: bool,
1341 ) -> Result<()> {
1342 match chain_swap.direction {
1343 Direction::Incoming => {
1344 self.verify_incoming_server_lockup_tx(
1345 chain_swap,
1346 swap_update_tx,
1347 verify_confirmation,
1348 )
1349 .await
1350 }
1351 Direction::Outgoing => {
1352 self.verify_outgoing_server_lockup_tx(
1353 chain_swap,
1354 swap_update_tx,
1355 verify_confirmation,
1356 )
1357 .await
1358 }
1359 }
1360 }
1361
1362 async fn verify_incoming_server_lockup_tx(
1363 &self,
1364 chain_swap: &ChainSwap,
1365 swap_update_tx: &TransactionInfo,
1366 verify_confirmation: bool,
1367 ) -> Result<()> {
1368 let swap_script = chain_swap.get_claim_swap_script()?;
1369 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1370 let liquid_swap_script = swap_script.as_liquid_script()?;
1372 let address = liquid_swap_script
1373 .to_address(self.config.network.into())
1374 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1375 let tx_hex = swap_update_tx
1376 .hex
1377 .as_ref()
1378 .ok_or(anyhow!("Transaction info without hex"))?;
1379 let tx = self
1380 .liquid_chain_service
1381 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1382 .await?;
1383 let rbf_explicit = tx.input.iter().any(|tx_in| tx_in.sequence.is_rbf());
1385 if !verify_confirmation && rbf_explicit {
1386 bail!("Transaction signals RBF");
1387 }
1388 let secp = Secp256k1::new();
1390 let to_address_output = tx
1391 .output
1392 .iter()
1393 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey());
1394 let mut value = 0;
1395 for tx_out in to_address_output {
1396 value += tx_out
1397 .unblind(&secp, liquid_swap_script.blinding_key.secret_key())?
1398 .value;
1399 }
1400 match chain_swap.accepted_receiver_amount_sat {
1401 None => {
1402 if value < claim_details.amount {
1403 bail!(
1404 "Transaction value {value} sats is less than {} sats",
1405 claim_details.amount
1406 );
1407 }
1408 }
1409 Some(accepted_receiver_amount_sat) => {
1410 let expected_server_lockup_amount_sat =
1411 accepted_receiver_amount_sat + chain_swap.claim_fees_sat;
1412 if value < expected_server_lockup_amount_sat {
1413 bail!(
1414 "Transaction value {value} sats is less than accepted {} sats",
1415 expected_server_lockup_amount_sat
1416 );
1417 }
1418 }
1419 }
1420
1421 Ok(())
1422 }
1423
1424 async fn verify_outgoing_server_lockup_tx(
1425 &self,
1426 chain_swap: &ChainSwap,
1427 swap_update_tx: &TransactionInfo,
1428 verify_confirmation: bool,
1429 ) -> Result<()> {
1430 let swap_script = chain_swap.get_claim_swap_script()?;
1431 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1432 let address = swap_script
1434 .as_bitcoin_script()?
1435 .to_address(self.config.network.as_bitcoin_chain())
1436 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1437 let tx_hex = swap_update_tx
1438 .hex
1439 .as_ref()
1440 .ok_or(anyhow!("Transaction info without hex"))?;
1441 let tx = self
1442 .bitcoin_chain_service
1443 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1444 .await?;
1445 let rbf_explicit = tx.input.iter().any(|input| input.sequence.is_rbf());
1447 if !verify_confirmation && rbf_explicit {
1448 return Err(anyhow!("Transaction signals RBF"));
1449 }
1450 let value: u64 = tx
1452 .output
1453 .iter()
1454 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey())
1455 .map(|tx_out| tx_out.value.to_sat())
1456 .sum();
1457 if value < claim_details.amount {
1458 return Err(anyhow!(
1459 "Transaction value {value} sats is less than {} sats",
1460 claim_details.amount
1461 ));
1462 }
1463 Ok(())
1464 }
1465
1466 async fn user_lockup_tx_exists(&self, chain_swap: &ChainSwap) -> Result<bool> {
1467 let lockup_script = chain_swap.get_lockup_swap_script()?;
1468 let script_history = self.fetch_script_history(&lockup_script).await?;
1469
1470 match chain_swap.user_lockup_tx_id.clone() {
1471 Some(user_lockup_tx_id) => {
1472 if !script_history.iter().any(|h| h.0 == user_lockup_tx_id) {
1473 return Ok(false);
1474 }
1475 }
1476 None => {
1477 let (txid, _tx_height) = match script_history.into_iter().nth(0) {
1478 Some(h) => h,
1479 None => {
1480 return Ok(false);
1481 }
1482 };
1483 self.update_swap_info(&ChainSwapUpdate {
1484 swap_id: chain_swap.id.clone(),
1485 to_state: Pending,
1486 user_lockup_tx_id: Some(txid.clone()),
1487 ..Default::default()
1488 })?;
1489 }
1490 }
1491
1492 Ok(true)
1493 }
1494
1495 async fn verify_user_lockup_tx(&self, chain_swap: &ChainSwap) -> Result<()> {
1496 if !self.user_lockup_tx_exists(chain_swap).await? {
1497 bail!("User lockup tx not found in script history");
1498 }
1499
1500 if chain_swap.direction == Direction::Incoming {
1502 let actual_payer_amount_sat = match chain_swap.actual_payer_amount_sat {
1503 Some(amount) => amount,
1504 None => {
1505 let actual_payer_amount_sat = self
1506 .fetch_incoming_swap_actual_payer_amount(chain_swap)
1507 .await?;
1508 self.persister
1509 .update_actual_payer_amount(&chain_swap.id, actual_payer_amount_sat)?;
1510 actual_payer_amount_sat
1511 }
1512 };
1513 if chain_swap.payer_amount_sat > 0
1515 && chain_swap.payer_amount_sat != actual_payer_amount_sat
1516 {
1517 bail!("Invalid user lockup tx - user lockup amount ({actual_payer_amount_sat} sat) differs from agreed ({} sat)", chain_swap.payer_amount_sat);
1518 }
1519 }
1520
1521 Ok(())
1522 }
1523
1524 async fn fetch_bitcoin_script_history(
1525 &self,
1526 swap_script: &SwapScriptV2,
1527 ) -> Result<Vec<BtcHistory>> {
1528 let address = swap_script
1529 .as_bitcoin_script()?
1530 .to_address(self.config.network.as_bitcoin_chain())
1531 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1532 let script_pubkey = address.script_pubkey();
1533 let script = script_pubkey.as_script();
1534 self.bitcoin_chain_service
1535 .get_script_history_with_retry(script, 10)
1536 .await
1537 }
1538
1539 async fn fetch_liquid_script_history(
1540 &self,
1541 swap_script: &SwapScriptV2,
1542 ) -> Result<Vec<LBtcHistory>> {
1543 let address = swap_script
1544 .as_liquid_script()?
1545 .to_address(self.config.network.into())
1546 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1547 .to_unconfidential();
1548 let script = Script::from_hex(hex::encode(address.script_pubkey().as_bytes()).as_str())
1549 .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
1550 self.liquid_chain_service
1551 .get_script_history_with_retry(&script, 10)
1552 .await
1553 }
1554}
1555
1556enum ValidateAmountlessSwapResult {
1557 ReadyForAccepting {
1558 user_lockup_amount_sat: u64,
1559 receiver_amount_sat: u64,
1560 },
1561 RequiresUserAction {
1562 user_lockup_amount_sat: u64,
1563 },
1564}
1565
1566async fn maybe_delay_before_claim(is_swap_local: bool) {
1567 if !is_swap_local {
1572 info!("Waiting 5 seconds before claim to reduce likelihood of concurrent claims");
1573 tokio::time::sleep(Duration::from_secs(5)).await;
1574 }
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use anyhow::Result;
1580 use std::collections::{HashMap, HashSet};
1581
1582 use crate::{
1583 model::{
1584 ChainSwapUpdate, Direction,
1585 PaymentState::{self, *},
1586 },
1587 test_utils::{
1588 chain_swap::{new_chain_swap, new_chain_swap_handler},
1589 persist::create_persister,
1590 },
1591 };
1592
1593 #[cfg(feature = "browser-tests")]
1594 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1595
1596 #[sdk_macros::async_test_all]
1597 async fn test_chain_swap_state_transitions() -> Result<()> {
1598 create_persister!(persister);
1599
1600 let chain_swap_handler = new_chain_swap_handler(persister.clone())?;
1601
1602 let all_states = HashSet::from([
1604 Created,
1605 Pending,
1606 WaitingFeeAcceptance,
1607 Complete,
1608 TimedOut,
1609 Failed,
1610 ]);
1611 let valid_combinations = HashMap::from([
1612 (
1613 Created,
1614 HashSet::from([
1615 Pending,
1616 WaitingFeeAcceptance,
1617 Complete,
1618 TimedOut,
1619 Refundable,
1620 Failed,
1621 ]),
1622 ),
1623 (
1624 Pending,
1625 HashSet::from([
1626 Pending,
1627 WaitingFeeAcceptance,
1628 Complete,
1629 Refundable,
1630 RefundPending,
1631 Failed,
1632 ]),
1633 ),
1634 (
1635 WaitingFeeAcceptance,
1636 HashSet::from([
1637 Pending,
1638 WaitingFeeAcceptance,
1639 Complete,
1640 Refundable,
1641 RefundPending,
1642 Failed,
1643 ]),
1644 ),
1645 (TimedOut, HashSet::from([Failed])),
1646 (Complete, HashSet::from([Refundable])),
1647 (Refundable, HashSet::from([RefundPending, Failed])),
1648 (
1649 RefundPending,
1650 HashSet::from([Refundable, Complete, Failed, RefundPending]),
1651 ),
1652 (Failed, HashSet::from([Failed, Refundable])),
1653 ]);
1654
1655 for (first_state, allowed_states) in valid_combinations.iter() {
1656 for allowed_state in allowed_states {
1657 let chain_swap = new_chain_swap(
1658 Direction::Incoming,
1659 Some(*first_state),
1660 false,
1661 None,
1662 false,
1663 false,
1664 None,
1665 );
1666 persister.insert_or_update_chain_swap(&chain_swap)?;
1667
1668 assert!(chain_swap_handler
1669 .update_swap_info(&ChainSwapUpdate {
1670 swap_id: chain_swap.id,
1671 to_state: *allowed_state,
1672 ..Default::default()
1673 })
1674 .is_ok());
1675 }
1676 }
1677
1678 let invalid_combinations: HashMap<PaymentState, HashSet<PaymentState>> = valid_combinations
1680 .iter()
1681 .map(|(first_state, allowed_states)| {
1682 (
1683 *first_state,
1684 all_states.difference(allowed_states).cloned().collect(),
1685 )
1686 })
1687 .collect();
1688
1689 for (first_state, disallowed_states) in invalid_combinations.iter() {
1690 for disallowed_state in disallowed_states {
1691 let chain_swap = new_chain_swap(
1692 Direction::Incoming,
1693 Some(*first_state),
1694 false,
1695 None,
1696 false,
1697 false,
1698 None,
1699 );
1700 persister.insert_or_update_chain_swap(&chain_swap)?;
1701
1702 assert!(chain_swap_handler
1703 .update_swap_info(&ChainSwapUpdate {
1704 swap_id: chain_swap.id,
1705 to_state: *disallowed_state,
1706 ..Default::default()
1707 })
1708 .is_err());
1709 }
1710 }
1711
1712 Ok(())
1713 }
1714}