1use std::str::FromStr;
2use std::time::Duration;
3use std::{collections::HashSet, sync::Arc};
4
5use anyhow::{anyhow, bail, Context, Result};
6use boltz_client::{
7 boltz::{self},
8 swaps::boltz::{ChainSwapStates, CreateChainResponse, TransactionInfo},
9 ElementsLockTime, Secp256k1, Serialize, ToHex,
10};
11use elements::{hex::FromHex, Script, Transaction};
12use futures_util::TryFutureExt;
13use log::{debug, error, info, warn};
14use lwk_wollet::hashes::hex::DisplayHex;
15use tokio::sync::{broadcast, Mutex};
16use tokio_with_wasm::alias as tokio;
17
18use crate::{
19 chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
20 elements, ensure_sdk,
21 error::{PaymentError, SdkError, SdkResult},
22 model::{
23 BlockListener, BtcHistory, ChainSwap, ChainSwapUpdate, Config, Direction, LBtcHistory,
24 PaymentState::{self, *},
25 PaymentTxData, PaymentType, Swap, SwapScriptV2, Transaction as SdkTransaction,
26 LIQUID_FEE_RATE_MSAT_PER_VBYTE,
27 },
28 persist::Persister,
29 swapper::Swapper,
30 utils,
31 wallet::OnchainWallet,
32};
33use crate::{
34 error::is_txn_mempool_conflict_error, model::DEFAULT_ONCHAIN_FEE_RATE_LEEWAY_SAT,
35 persist::model::PaymentTxBalance,
36};
37
38pub const ESTIMATED_BTC_CLAIM_TX_VSIZE: u64 = 111;
40
41pub(crate) struct ChainSwapHandler {
42 config: Config,
43 onchain_wallet: Arc<dyn OnchainWallet>,
44 persister: std::sync::Arc<Persister>,
45 swapper: Arc<dyn Swapper>,
46 liquid_chain_service: Arc<dyn LiquidChainService>,
47 bitcoin_chain_service: Arc<dyn BitcoinChainService>,
48 subscription_notifier: broadcast::Sender<String>,
49 claiming_swaps: Arc<Mutex<HashSet<String>>>,
50}
51
52#[sdk_macros::async_trait]
53impl BlockListener for ChainSwapHandler {
54 async fn on_bitcoin_block(&self, height: u32) {
55 if let Err(e) = self.claim_outgoing(height).await {
56 error!("Error claiming outgoing: {e:?}");
57 }
58 }
59
60 async fn on_liquid_block(&self, height: u32) {
61 if let Err(e) = self.refund_outgoing(height).await {
62 warn!("Error refunding outgoing: {e:?}");
63 }
64 if let Err(e) = self.claim_incoming(height).await {
65 error!("Error claiming incoming: {e:?}");
66 }
67 }
68}
69
70impl ChainSwapHandler {
71 pub(crate) fn new(
72 config: Config,
73 onchain_wallet: Arc<dyn OnchainWallet>,
74 persister: std::sync::Arc<Persister>,
75 swapper: Arc<dyn Swapper>,
76 liquid_chain_service: Arc<dyn LiquidChainService>,
77 bitcoin_chain_service: Arc<dyn BitcoinChainService>,
78 ) -> Result<Self> {
79 let (subscription_notifier, _) = broadcast::channel::<String>(30);
80 Ok(Self {
81 config,
82 onchain_wallet,
83 persister,
84 swapper,
85 liquid_chain_service,
86 bitcoin_chain_service,
87 subscription_notifier,
88 claiming_swaps: Arc::new(Mutex::new(HashSet::new())),
89 })
90 }
91
92 pub(crate) fn subscribe_payment_updates(&self) -> broadcast::Receiver<String> {
93 self.subscription_notifier.subscribe()
94 }
95
96 pub(crate) async fn on_new_status(&self, update: &boltz::SwapStatus) -> Result<()> {
98 let id = &update.id;
99 let swap = self.fetch_chain_swap_by_id(id)?;
100
101 match swap.direction {
102 Direction::Incoming => self.on_new_incoming_status(&swap, update).await,
103 Direction::Outgoing => self.on_new_outgoing_status(&swap, update).await,
104 }
105 }
106
107 async fn claim_incoming(&self, height: u32) -> Result<()> {
108 let chain_swaps: Vec<ChainSwap> = self
109 .persister
110 .list_chain_swaps()?
111 .into_iter()
112 .filter(|s| {
113 s.direction == Direction::Incoming && s.state == Pending && s.claim_tx_id.is_none()
114 })
115 .collect();
116 info!(
117 "Rescanning {} incoming Chain Swap(s) server lockup txs at height {}",
118 chain_swaps.len(),
119 height
120 );
121 for swap in chain_swaps {
122 if let Err(e) = self.claim_confirmed_server_lockup(&swap).await {
123 error!(
124 "Error rescanning server lockup of incoming Chain Swap {}: {e:?}",
125 swap.id,
126 );
127 }
128 }
129 Ok(())
130 }
131
132 async fn claim_outgoing(&self, height: u32) -> Result<()> {
133 let chain_swaps: Vec<ChainSwap> = self
134 .persister
135 .list_chain_swaps()?
136 .into_iter()
137 .filter(|s| {
138 s.direction == Direction::Outgoing && s.state == Pending && s.claim_tx_id.is_none()
139 })
140 .collect();
141 info!(
142 "Rescanning {} outgoing Chain Swap(s) server lockup txs at height {}",
143 chain_swaps.len(),
144 height
145 );
146 for swap in chain_swaps {
147 if let Err(e) = self.claim_confirmed_server_lockup(&swap).await {
148 error!(
149 "Error rescanning server lockup of outgoing Chain Swap {}: {e:?}",
150 swap.id
151 );
152 }
153 }
154 Ok(())
155 }
156
157 async fn fetch_script_history(&self, swap_script: &SwapScriptV2) -> Result<Vec<(String, i32)>> {
158 let history = match swap_script {
159 SwapScriptV2::Liquid(_) => self
160 .fetch_liquid_script_history(swap_script)
161 .await?
162 .into_iter()
163 .map(|h| (h.txid.to_hex(), h.height))
164 .collect(),
165 SwapScriptV2::Bitcoin(_) => self
166 .fetch_bitcoin_script_history(swap_script)
167 .await?
168 .into_iter()
169 .map(|h| (h.txid.to_hex(), h.height))
170 .collect(),
171 };
172 Ok(history)
173 }
174
175 async fn claim_confirmed_server_lockup(&self, swap: &ChainSwap) -> Result<()> {
176 let Some(tx_id) = swap.server_lockup_tx_id.clone() else {
177 return Ok(());
179 };
180 let swap_id = &swap.id;
181 let swap_script = swap.get_claim_swap_script()?;
182 let script_history = self.fetch_script_history(&swap_script).await?;
183 let (_tx_history, tx_height) =
184 script_history
185 .iter()
186 .find(|h| h.0.eq(&tx_id))
187 .ok_or(anyhow!(
188 "Server lockup tx for Chain Swap {swap_id} was not found, txid={tx_id}"
189 ))?;
190 if *tx_height > 0 {
191 info!("Chain Swap {swap_id} server lockup tx is confirmed");
192 self.claim(swap_id)
193 .await
194 .map_err(|e| anyhow!("Could not claim Chain Swap {swap_id}: {e:?}"))?;
195 }
196 Ok(())
197 }
198
199 async fn on_new_incoming_status(
200 &self,
201 swap: &ChainSwap,
202 update: &boltz::SwapStatus,
203 ) -> Result<()> {
204 let id = update.id.clone();
205 let status = &update.status;
206 let swap_state = ChainSwapStates::from_str(status)
207 .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?;
208
209 info!("Handling incoming Chain Swap transition to {status:?} for swap {id}");
210 match swap_state {
212 ChainSwapStates::TransactionMempool | ChainSwapStates::TransactionConfirmed => {
214 if let Some(zero_conf_rejected) = update.zero_conf_rejected {
215 info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}");
216 self.persister
217 .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?;
218 }
219 if let Some(transaction) = update.transaction.clone() {
220 let actual_payer_amount =
221 self.fetch_incoming_swap_actual_payer_amount(swap).await?;
222 self.persister
223 .update_actual_payer_amount(&swap.id, actual_payer_amount)?;
224
225 self.update_swap_info(&ChainSwapUpdate {
226 swap_id: id,
227 to_state: Pending,
228 user_lockup_tx_id: Some(transaction.id),
229 ..Default::default()
230 })?;
231 }
232 Ok(())
233 }
234
235 ChainSwapStates::TransactionServerMempool => {
238 match swap.claim_tx_id.clone() {
239 None => {
240 let Some(transaction) = update.transaction.clone() else {
241 return Err(anyhow!("Unexpected payload from Boltz status stream"));
242 };
243
244 if let Err(e) = self.verify_user_lockup_tx(swap).await {
245 warn!("User lockup transaction for incoming Chain Swap {} could not be verified. err: {}", swap.id, e);
246 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
247 }
248
249 if let Err(e) = self
250 .verify_server_lockup_tx(swap, &transaction, false)
251 .await
252 {
253 warn!("Server lockup mempool transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}",
254 swap.id,
255 transaction.id,
256 e);
257 return Err(anyhow!(
258 "Could not verify server lockup transaction {}: {e}",
259 transaction.id
260 ));
261 }
262
263 info!("Server lockup mempool transaction was verified for incoming Chain Swap {}", swap.id);
264 self.update_swap_info(&ChainSwapUpdate {
265 swap_id: id.clone(),
266 to_state: Pending,
267 server_lockup_tx_id: Some(transaction.id),
268 ..Default::default()
269 })?;
270
271 if swap.accept_zero_conf {
272 maybe_delay_before_claim(swap.metadata.is_local).await;
273 self.claim(&id).await.map_err(|e| {
274 error!("Could not cooperate Chain Swap {id} claim: {e}");
275 anyhow!("Could not post claim details. Err: {e:?}")
276 })?;
277 }
278 }
279 Some(claim_tx_id) => {
280 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
281 }
282 };
283 Ok(())
284 }
285
286 ChainSwapStates::TransactionServerConfirmed => {
289 match swap.claim_tx_id.clone() {
290 None => {
291 let Some(transaction) = update.transaction.clone() else {
292 return Err(anyhow!("Unexpected payload from Boltz status stream"));
293 };
294
295 if let Err(e) = self.verify_user_lockup_tx(swap).await {
296 warn!("User lockup transaction for incoming Chain Swap {} could not be verified. err: {}", swap.id, e);
297 return Err(anyhow!("Could not verify user lockup transaction: {e}",));
298 }
299
300 let verify_res =
301 self.verify_server_lockup_tx(swap, &transaction, true).await;
302
303 self.update_swap_info(&ChainSwapUpdate {
307 swap_id: id.clone(),
308 to_state: Pending,
309 server_lockup_tx_id: Some(transaction.id.clone()),
310 ..Default::default()
311 })?;
312
313 match verify_res {
314 Ok(_) => {
315 info!("Server lockup transaction was verified for incoming Chain Swap {}", swap.id);
316
317 maybe_delay_before_claim(swap.metadata.is_local).await;
318 self.claim(&id).await.map_err(|e| {
319 error!("Could not cooperate Chain Swap {id} claim: {e}");
320 anyhow!("Could not post claim details. Err: {e:?}")
321 })?;
322 }
323 Err(e) => {
324 warn!("Server lockup transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e);
325 return Err(anyhow!(
326 "Could not verify server lockup transaction {}: {e}",
327 transaction.id
328 ));
329 }
330 }
331 }
332 Some(claim_tx_id) => {
333 warn!("Claim tx for Chain Swap {id} was already broadcast: txid {claim_tx_id}")
334 }
335 };
336 Ok(())
337 }
338
339 ChainSwapStates::TransactionFailed
346 | ChainSwapStates::TransactionLockupFailed
347 | ChainSwapStates::TransactionRefunded
348 | ChainSwapStates::SwapExpired => {
349 let is_zero_amount = swap.payer_amount_sat == 0;
351 if matches!(swap_state, ChainSwapStates::TransactionLockupFailed) && is_zero_amount
352 {
353 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 = self
789 .liquid_chain_service
790 .broadcast(&lockup_tx)
791 .await?
792 .to_string();
793
794 debug!(
795 "Successfully broadcast lockup transaction for Chain Swap {swap_id}. Lockup tx id: {lockup_tx_id}"
796 );
797 Ok(lockup_tx)
798 }
799
800 fn fetch_chain_swap_by_id(&self, swap_id: &str) -> Result<ChainSwap, PaymentError> {
801 self.persister
802 .fetch_chain_swap_by_id(swap_id)
803 .map_err(|e| {
804 error!("Failed to fetch chain swap by id: {e:?}");
805 PaymentError::PersistError
806 })?
807 .ok_or(PaymentError::Generic {
808 err: format!("Chain Swap not found {swap_id}"),
809 })
810 }
811
812 pub(crate) fn update_swap(&self, updated_swap: ChainSwap) -> Result<(), PaymentError> {
814 let swap = self.fetch_chain_swap_by_id(&updated_swap.id)?;
815 if updated_swap != swap {
816 info!(
817 "Updating Chain swap {} to {:?} (user_lockup_tx_id = {:?}, server_lockup_tx_id = {:?}, claim_tx_id = {:?}, refund_tx_id = {:?})",
818 updated_swap.id,
819 updated_swap.state,
820 updated_swap.user_lockup_tx_id,
821 updated_swap.server_lockup_tx_id,
822 updated_swap.claim_tx_id,
823 updated_swap.refund_tx_id
824 );
825 self.persister.insert_or_update_chain_swap(&updated_swap)?;
826 let _ = self.subscription_notifier.send(updated_swap.id);
827 }
828 Ok(())
829 }
830
831 pub(crate) fn update_swap_info(
833 &self,
834 swap_update: &ChainSwapUpdate,
835 ) -> Result<(), PaymentError> {
836 info!("Updating Chain swap {swap_update:?}");
837 let swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
838 Self::validate_state_transition(swap.state, swap_update.to_state)?;
839 self.persister.try_handle_chain_swap_update(swap_update)?;
840 let updated_swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?;
841 if updated_swap != swap {
842 let _ = self.subscription_notifier.send(updated_swap.id);
843 }
844 Ok(())
845 }
846
847 async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> {
848 {
849 let mut claiming_guard = self.claiming_swaps.lock().await;
850 if claiming_guard.contains(swap_id) {
851 debug!("Claim for swap {swap_id} already in progress, skipping.");
852 return Ok(());
853 }
854 claiming_guard.insert(swap_id.to_string());
855 }
856
857 let result = self.claim_inner(swap_id).await;
858
859 {
860 let mut claiming_guard = self.claiming_swaps.lock().await;
861 claiming_guard.remove(swap_id);
862 }
863
864 result
865 }
866
867 async fn claim_inner(&self, swap_id: &str) -> Result<(), PaymentError> {
868 let swap = self.fetch_chain_swap_by_id(swap_id)?;
869 ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed);
870
871 if !swap.user_lockup_spent {
874 match swap.direction {
875 Direction::Incoming => {
876 let liquid_tip = self.liquid_chain_service.tip().await?;
877 if liquid_tip > swap.claim_timeout_block_height - 10 {
878 return Err(PaymentError::Generic {
879 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),
880 });
881 }
882 }
883 Direction::Outgoing => {
884 let bitcoin_tip = self.bitcoin_chain_service.tip().await?;
885 if bitcoin_tip > swap.claim_timeout_block_height - 2 {
886 return Err(PaymentError::Generic {
887 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),
888 });
889 }
890 }
891 }
892 }
893
894 debug!("Initiating claim for Chain Swap {swap_id}");
895 let claim_address = match (swap.direction, swap.claim_address.clone()) {
898 (Direction::Incoming, None) => {
899 Some(self.onchain_wallet.next_unused_address().await?.to_string())
900 }
901 _ => swap.claim_address.clone(),
902 };
903 let claim_tx = self
904 .swapper
905 .create_claim_tx(Swap::Chain(swap.clone()), claim_address.clone(), true)
906 .await?;
907
908 let tx_id = claim_tx.txid();
911 match self
912 .persister
913 .set_chain_swap_claim(swap_id, claim_address, &tx_id)
914 {
915 Ok(_) => {
916 let broadcast_res = match claim_tx {
917 SdkTransaction::Liquid(tx) => {
919 match self.liquid_chain_service.broadcast(&tx).await {
920 Ok(tx_id) => Ok(tx_id.to_hex()),
921 Err(e) if is_txn_mempool_conflict_error(&e) => {
922 Err(PaymentError::AlreadyClaimed)
923 }
924 Err(err) => {
925 debug!(
926 "Could not broadcast claim tx via chain service for Chain swap {swap_id}: {err:?}"
927 );
928 let claim_tx_hex = tx.serialize().to_lower_hex_string();
929 self.swapper
930 .broadcast_tx(self.config.network.into(), &claim_tx_hex)
931 .await
932 }
933 }
934 }
935 SdkTransaction::Bitcoin(tx) => self
936 .bitcoin_chain_service
937 .broadcast(&tx)
938 .await
939 .map(|tx_id| tx_id.to_hex())
940 .map_err(|err| PaymentError::Generic {
941 err: err.to_string(),
942 }),
943 };
944
945 match broadcast_res {
946 Ok(claim_tx_id) => {
947 let payment_id = match swap.direction {
948 Direction::Incoming => {
949 self.persister.insert_or_update_payment(
952 PaymentTxData {
953 tx_id: claim_tx_id.clone(),
954 timestamp: Some(utils::now()),
955 fees_sat: 0,
956 is_confirmed: false,
957 unblinding_data: None,
958 },
959 &[PaymentTxBalance {
960 asset_id: self.config.lbtc_asset_id().to_string(),
961 amount: swap
962 .accepted_receiver_amount_sat
963 .unwrap_or(swap.receiver_amount_sat),
964 payment_type: PaymentType::Receive,
965 }],
966 None,
967 false,
968 )?;
969 Some(claim_tx_id.clone())
970 }
971 Direction::Outgoing => swap.user_lockup_tx_id,
972 };
973
974 info!("Successfully broadcast claim tx {claim_tx_id} for Chain Swap {swap_id}");
975 payment_id.and_then(|payment_id| {
978 self.subscription_notifier.send(payment_id).ok()
979 });
980 Ok(())
981 }
982 Err(err) => {
983 debug!(
985 "Could not broadcast claim tx via swapper for Chain swap {swap_id}: {err:?}"
986 );
987 self.persister
988 .unset_chain_swap_claim_tx_id(swap_id, &tx_id)?;
989 Err(err)
990 }
991 }
992 }
993 Err(err) => {
994 debug!(
995 "Failed to set claim_tx_id after creating tx for Chain swap {swap_id}: txid {tx_id}"
996 );
997 Err(err)
998 }
999 }
1000 }
1001
1002 pub(crate) async fn prepare_refund(
1003 &self,
1004 lockup_address: &str,
1005 refund_address: &str,
1006 fee_rate_sat_per_vb: u32,
1007 ) -> SdkResult<(u32, u64, Option<String>)> {
1008 let swap = self
1009 .persister
1010 .fetch_chain_swap_by_lockup_address(lockup_address)?
1011 .ok_or(SdkError::generic(format!(
1012 "Chain Swap with lockup address {lockup_address} not found"
1013 )))?;
1014
1015 let refund_tx_id = swap.refund_tx_id.clone();
1016 if let Some(refund_tx_id) = &refund_tx_id {
1017 warn!(
1018 "A refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
1019 swap.id
1020 );
1021 }
1022
1023 let (refund_tx_size, refund_tx_fees_sat) = self
1024 .swapper
1025 .estimate_refund_broadcast(
1026 Swap::Chain(swap),
1027 refund_address,
1028 Some(fee_rate_sat_per_vb as f64),
1029 true,
1030 )
1031 .await?;
1032
1033 Ok((refund_tx_size, refund_tx_fees_sat, refund_tx_id))
1034 }
1035
1036 pub(crate) async fn refund_incoming_swap(
1037 &self,
1038 lockup_address: &str,
1039 refund_address: &str,
1040 broadcast_fee_rate_sat_per_vb: u32,
1041 is_cooperative: bool,
1042 ) -> Result<String, PaymentError> {
1043 let swap = self
1044 .persister
1045 .fetch_chain_swap_by_lockup_address(lockup_address)?
1046 .ok_or(PaymentError::Generic {
1047 err: format!("Swap for lockup address {lockup_address} not found"),
1048 })?;
1049 let id = &swap.id;
1050
1051 ensure_sdk!(
1052 swap.state.is_refundable(),
1053 PaymentError::Generic {
1054 err: format!("Chain Swap {id} was not in refundable state")
1055 }
1056 );
1057
1058 info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1059
1060 let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else {
1061 return Err(PaymentError::Generic {
1062 err: "Unexpected swap script type found".to_string(),
1063 });
1064 };
1065
1066 let script_pk = swap_script
1067 .to_address(self.config.network.as_bitcoin_chain())
1068 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1069 .script_pubkey();
1070 let utxos = self
1071 .bitcoin_chain_service
1072 .get_script_utxos(&script_pk)
1073 .await?;
1074
1075 let SdkTransaction::Bitcoin(refund_tx) = self
1076 .swapper
1077 .create_refund_tx(
1078 Swap::Chain(swap.clone()),
1079 refund_address,
1080 utxos,
1081 Some(broadcast_fee_rate_sat_per_vb as f64),
1082 is_cooperative,
1083 )
1084 .await?
1085 else {
1086 return Err(PaymentError::Generic {
1087 err: format!("Unexpected refund tx type returned for incoming Chain swap {id}",),
1088 });
1089 };
1090 let refund_tx_id = self
1091 .bitcoin_chain_service
1092 .broadcast(&refund_tx)
1093 .await?
1094 .to_string();
1095
1096 info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
1097
1098 self.update_swap_info(&ChainSwapUpdate {
1102 swap_id: swap.id,
1103 to_state: RefundPending,
1104 refund_tx_id: Some(refund_tx_id.clone()),
1105 ..Default::default()
1106 })?;
1107
1108 Ok(refund_tx_id)
1109 }
1110
1111 pub(crate) async fn refund_outgoing_swap(
1112 &self,
1113 swap: &ChainSwap,
1114 is_cooperative: bool,
1115 ) -> Result<String, PaymentError> {
1116 ensure_sdk!(
1117 swap.refund_tx_id.is_none(),
1118 PaymentError::Generic {
1119 err: format!(
1120 "A refund tx for outgoing Chain Swap {} was already broadcast",
1121 swap.id
1122 )
1123 }
1124 );
1125
1126 info!(
1127 "Initiating refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1128 swap.id
1129 );
1130
1131 let SwapScriptV2::Liquid(swap_script) = swap.get_lockup_swap_script()? else {
1132 return Err(PaymentError::Generic {
1133 err: "Unexpected swap script type found".to_string(),
1134 });
1135 };
1136
1137 let script_pk = swap_script
1138 .to_address(self.config.network.into())
1139 .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
1140 .to_unconfidential()
1141 .script_pubkey();
1142 let utxos = self
1143 .liquid_chain_service
1144 .get_script_utxos(&script_pk)
1145 .await?;
1146
1147 let refund_address = match swap.refund_address {
1148 Some(ref refund_address) => refund_address.clone(),
1149 None => {
1150 let address = self.onchain_wallet.next_unused_address().await?.to_string();
1152 self.persister
1153 .set_chain_swap_refund_address(&swap.id, &address)?;
1154 address
1155 }
1156 };
1157
1158 let SdkTransaction::Liquid(refund_tx) = self
1159 .swapper
1160 .create_refund_tx(
1161 Swap::Chain(swap.clone()),
1162 &refund_address,
1163 utxos,
1164 None,
1165 is_cooperative,
1166 )
1167 .await?
1168 else {
1169 return Err(PaymentError::Generic {
1170 err: format!(
1171 "Unexpected refund tx type returned for outgoing Chain swap {}",
1172 swap.id
1173 ),
1174 });
1175 };
1176 let refund_tx_id = self
1177 .liquid_chain_service
1178 .broadcast(&refund_tx)
1179 .await?
1180 .to_string();
1181
1182 info!(
1183 "Successfully broadcast refund for outgoing Chain Swap {}, is_cooperative: {is_cooperative}",
1184 swap.id
1185 );
1186
1187 Ok(refund_tx_id)
1188 }
1189
1190 async fn refund_outgoing(&self, height: u32) -> Result<(), PaymentError> {
1191 let pending_swaps: Vec<ChainSwap> = self
1193 .persister
1194 .list_pending_chain_swaps()?
1195 .into_iter()
1196 .filter(|s| s.direction == Direction::Outgoing && s.refund_tx_id.is_none())
1197 .collect();
1198 for swap in pending_swaps {
1199 let swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?;
1200 let locktime_from_height = ElementsLockTime::from_height(height)
1201 .map_err(|e| PaymentError::Generic { err: e.to_string() })?;
1202 info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime);
1203 let has_swap_expired =
1204 utils::is_locktime_expired(locktime_from_height, swap_script.locktime);
1205 if has_swap_expired || swap.state == RefundPending {
1206 let refund_tx_id_res = match swap.state {
1207 Pending => self.refund_outgoing_swap(&swap, false).await,
1208 RefundPending => match has_swap_expired {
1209 true => {
1210 self.refund_outgoing_swap(&swap, true)
1211 .or_else(|e| {
1212 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
1213 self.refund_outgoing_swap(&swap, false)
1214 })
1215 .await
1216 }
1217 false => self.refund_outgoing_swap(&swap, true).await,
1218 },
1219 _ => {
1220 continue;
1221 }
1222 };
1223
1224 if let Ok(refund_tx_id) = refund_tx_id_res {
1225 let update_swap_info_res = self.update_swap_info(&ChainSwapUpdate {
1226 swap_id: swap.id.clone(),
1227 to_state: RefundPending,
1228 refund_tx_id: Some(refund_tx_id),
1229 ..Default::default()
1230 });
1231 if let Err(err) = update_swap_info_res {
1232 warn!(
1233 "Could not update outgoing Chain swap {} information: {err:?}",
1234 swap.id
1235 );
1236 };
1237 }
1238 }
1239 }
1240 Ok(())
1241 }
1242
1243 fn validate_state_transition(
1244 from_state: PaymentState,
1245 to_state: PaymentState,
1246 ) -> Result<(), PaymentError> {
1247 match (from_state, to_state) {
1248 (_, Created) => Err(PaymentError::Generic {
1249 err: "Cannot transition to Created state".to_string(),
1250 }),
1251
1252 (Created | Pending | WaitingFeeAcceptance, Pending) => Ok(()),
1253 (_, Pending) => Err(PaymentError::Generic {
1254 err: format!("Cannot transition from {from_state:?} to Pending state"),
1255 }),
1256
1257 (Created | Pending | WaitingFeeAcceptance, WaitingFeeAcceptance) => Ok(()),
1258 (_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
1259 err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
1260 }),
1261
1262 (Created | Pending | WaitingFeeAcceptance | RefundPending, Complete) => Ok(()),
1263 (_, Complete) => Err(PaymentError::Generic {
1264 err: format!("Cannot transition from {from_state:?} to Complete state"),
1265 }),
1266
1267 (Created, TimedOut) => Ok(()),
1268 (_, TimedOut) => Err(PaymentError::Generic {
1269 err: format!("Cannot transition from {from_state:?} to TimedOut state"),
1270 }),
1271
1272 (
1273 Created | Pending | WaitingFeeAcceptance | RefundPending | Failed | Complete,
1274 Refundable,
1275 ) => Ok(()),
1276 (_, Refundable) => Err(PaymentError::Generic {
1277 err: format!("Cannot transition from {from_state:?} to Refundable state"),
1278 }),
1279
1280 (Pending | WaitingFeeAcceptance | Refundable | RefundPending, RefundPending) => Ok(()),
1281 (_, RefundPending) => Err(PaymentError::Generic {
1282 err: format!("Cannot transition from {from_state:?} to RefundPending state"),
1283 }),
1284
1285 (Complete, Failed) => Err(PaymentError::Generic {
1286 err: format!("Cannot transition from {from_state:?} to Failed state"),
1287 }),
1288
1289 (_, Failed) => Ok(()),
1290 }
1291 }
1292
1293 async fn fetch_incoming_swap_actual_payer_amount(&self, chain_swap: &ChainSwap) -> Result<u64> {
1294 let swap_script = chain_swap.get_lockup_swap_script()?;
1295 let script_pubkey = swap_script
1296 .as_bitcoin_script()?
1297 .to_address(self.config.network.as_bitcoin_chain())
1298 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1299 .script_pubkey();
1300
1301 let history = self.fetch_bitcoin_script_history(&swap_script).await?;
1302
1303 let first_tx_id = history
1305 .first()
1306 .ok_or(anyhow!(
1307 "No history found for user lockup script for swap {}",
1308 chain_swap.id
1309 ))?
1310 .txid
1311 .to_raw_hash()
1312 .into();
1313
1314 let txs = self
1316 .bitcoin_chain_service
1317 .get_transactions_with_retry(&[first_tx_id], 3)
1318 .await?;
1319 let user_lockup_tx = txs.first().ok_or(anyhow!(
1320 "No transactions found for user lockup script for swap {}",
1321 chain_swap.id
1322 ))?;
1323
1324 user_lockup_tx
1326 .output
1327 .iter()
1328 .find(|out| out.script_pubkey == script_pubkey)
1329 .map(|out| out.value.to_sat())
1330 .ok_or(anyhow!("No output found paying to user lockup script"))
1331 }
1332
1333 async fn verify_server_lockup_tx(
1334 &self,
1335 chain_swap: &ChainSwap,
1336 swap_update_tx: &TransactionInfo,
1337 verify_confirmation: bool,
1338 ) -> Result<()> {
1339 match chain_swap.direction {
1340 Direction::Incoming => {
1341 self.verify_incoming_server_lockup_tx(
1342 chain_swap,
1343 swap_update_tx,
1344 verify_confirmation,
1345 )
1346 .await
1347 }
1348 Direction::Outgoing => {
1349 self.verify_outgoing_server_lockup_tx(
1350 chain_swap,
1351 swap_update_tx,
1352 verify_confirmation,
1353 )
1354 .await
1355 }
1356 }
1357 }
1358
1359 async fn verify_incoming_server_lockup_tx(
1360 &self,
1361 chain_swap: &ChainSwap,
1362 swap_update_tx: &TransactionInfo,
1363 verify_confirmation: bool,
1364 ) -> Result<()> {
1365 let swap_script = chain_swap.get_claim_swap_script()?;
1366 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1367 let liquid_swap_script = swap_script.as_liquid_script()?;
1369 let address = liquid_swap_script
1370 .to_address(self.config.network.into())
1371 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1372 let tx_hex = swap_update_tx
1373 .hex
1374 .as_ref()
1375 .ok_or(anyhow!("Transaction info without hex"))?;
1376 let tx = self
1377 .liquid_chain_service
1378 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1379 .await?;
1380 let rbf_explicit = tx.input.iter().any(|tx_in| tx_in.sequence.is_rbf());
1382 if !verify_confirmation && rbf_explicit {
1383 bail!("Transaction signals RBF");
1384 }
1385 let secp = Secp256k1::new();
1387 let to_address_output = tx
1388 .output
1389 .iter()
1390 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey());
1391 let mut value = 0;
1392 for tx_out in to_address_output {
1393 value += tx_out
1394 .unblind(&secp, liquid_swap_script.blinding_key.secret_key())?
1395 .value;
1396 }
1397 match chain_swap.accepted_receiver_amount_sat {
1398 None => {
1399 if value < claim_details.amount {
1400 bail!(
1401 "Transaction value {value} sats is less than {} sats",
1402 claim_details.amount
1403 );
1404 }
1405 }
1406 Some(accepted_receiver_amount_sat) => {
1407 let expected_server_lockup_amount_sat =
1408 accepted_receiver_amount_sat + chain_swap.claim_fees_sat;
1409 if value < expected_server_lockup_amount_sat {
1410 bail!(
1411 "Transaction value {value} sats is less than accepted {} sats",
1412 expected_server_lockup_amount_sat
1413 );
1414 }
1415 }
1416 }
1417
1418 Ok(())
1419 }
1420
1421 async fn verify_outgoing_server_lockup_tx(
1422 &self,
1423 chain_swap: &ChainSwap,
1424 swap_update_tx: &TransactionInfo,
1425 verify_confirmation: bool,
1426 ) -> Result<()> {
1427 let swap_script = chain_swap.get_claim_swap_script()?;
1428 let claim_details = chain_swap.get_boltz_create_response()?.claim_details;
1429 let address = swap_script
1431 .as_bitcoin_script()?
1432 .to_address(self.config.network.as_bitcoin_chain())
1433 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1434 let tx_hex = swap_update_tx
1435 .hex
1436 .as_ref()
1437 .ok_or(anyhow!("Transaction info without hex"))?;
1438 let tx = self
1439 .bitcoin_chain_service
1440 .verify_tx(&address, &swap_update_tx.id, tx_hex, verify_confirmation)
1441 .await?;
1442 let rbf_explicit = tx.input.iter().any(|input| input.sequence.is_rbf());
1444 if !verify_confirmation && rbf_explicit {
1445 return Err(anyhow!("Transaction signals RBF"));
1446 }
1447 let value: u64 = tx
1449 .output
1450 .iter()
1451 .filter(|tx_out| tx_out.script_pubkey == address.script_pubkey())
1452 .map(|tx_out| tx_out.value.to_sat())
1453 .sum();
1454 if value < claim_details.amount {
1455 return Err(anyhow!(
1456 "Transaction value {value} sats is less than {} sats",
1457 claim_details.amount
1458 ));
1459 }
1460 Ok(())
1461 }
1462
1463 async fn user_lockup_tx_exists(&self, chain_swap: &ChainSwap) -> Result<bool> {
1464 let lockup_script = chain_swap.get_lockup_swap_script()?;
1465 let script_history = self.fetch_script_history(&lockup_script).await?;
1466
1467 match chain_swap.user_lockup_tx_id.clone() {
1468 Some(user_lockup_tx_id) => {
1469 if !script_history.iter().any(|h| h.0 == user_lockup_tx_id) {
1470 return Ok(false);
1471 }
1472 }
1473 None => {
1474 let (txid, _tx_height) = match script_history.into_iter().nth(0) {
1475 Some(h) => h,
1476 None => {
1477 return Ok(false);
1478 }
1479 };
1480 self.update_swap_info(&ChainSwapUpdate {
1481 swap_id: chain_swap.id.clone(),
1482 to_state: Pending,
1483 user_lockup_tx_id: Some(txid.clone()),
1484 ..Default::default()
1485 })?;
1486 }
1487 }
1488
1489 Ok(true)
1490 }
1491
1492 async fn verify_user_lockup_tx(&self, chain_swap: &ChainSwap) -> Result<()> {
1493 if !self.user_lockup_tx_exists(chain_swap).await? {
1494 bail!("User lockup tx not found in script history");
1495 }
1496
1497 if chain_swap.direction == Direction::Incoming {
1499 let actual_payer_amount_sat = match chain_swap.actual_payer_amount_sat {
1500 Some(amount) => amount,
1501 None => {
1502 let actual_payer_amount_sat = self
1503 .fetch_incoming_swap_actual_payer_amount(chain_swap)
1504 .await?;
1505 self.persister
1506 .update_actual_payer_amount(&chain_swap.id, actual_payer_amount_sat)?;
1507 actual_payer_amount_sat
1508 }
1509 };
1510 if chain_swap.payer_amount_sat > 0
1512 && chain_swap.payer_amount_sat != actual_payer_amount_sat
1513 {
1514 bail!("Invalid user lockup tx - user lockup amount ({actual_payer_amount_sat} sat) differs from agreed ({} sat)", chain_swap.payer_amount_sat);
1515 }
1516 }
1517
1518 Ok(())
1519 }
1520
1521 async fn fetch_bitcoin_script_history(
1522 &self,
1523 swap_script: &SwapScriptV2,
1524 ) -> Result<Vec<BtcHistory>> {
1525 let address = swap_script
1526 .as_bitcoin_script()?
1527 .to_address(self.config.network.as_bitcoin_chain())
1528 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
1529 let script_pubkey = address.script_pubkey();
1530 let script = script_pubkey.as_script();
1531 self.bitcoin_chain_service
1532 .get_script_history_with_retry(script, 10)
1533 .await
1534 }
1535
1536 async fn fetch_liquid_script_history(
1537 &self,
1538 swap_script: &SwapScriptV2,
1539 ) -> Result<Vec<LBtcHistory>> {
1540 let address = swap_script
1541 .as_liquid_script()?
1542 .to_address(self.config.network.into())
1543 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?
1544 .to_unconfidential();
1545 let script = Script::from_hex(hex::encode(address.script_pubkey().as_bytes()).as_str())
1546 .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
1547 self.liquid_chain_service
1548 .get_script_history_with_retry(&script, 10)
1549 .await
1550 }
1551}
1552
1553enum ValidateAmountlessSwapResult {
1554 ReadyForAccepting {
1555 user_lockup_amount_sat: u64,
1556 receiver_amount_sat: u64,
1557 },
1558 RequiresUserAction {
1559 user_lockup_amount_sat: u64,
1560 },
1561}
1562
1563async fn maybe_delay_before_claim(is_swap_local: bool) {
1564 if !is_swap_local {
1569 info!("Waiting 5 seconds before claim to reduce likelihood of concurrent claims");
1570 tokio::time::sleep(Duration::from_secs(5)).await;
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use anyhow::Result;
1577 use std::collections::{HashMap, HashSet};
1578
1579 use crate::{
1580 model::{
1581 ChainSwapUpdate, Direction,
1582 PaymentState::{self, *},
1583 },
1584 test_utils::{
1585 chain_swap::{new_chain_swap, new_chain_swap_handler},
1586 persist::create_persister,
1587 },
1588 };
1589
1590 #[cfg(feature = "browser-tests")]
1591 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1592
1593 #[sdk_macros::async_test_all]
1594 async fn test_chain_swap_state_transitions() -> Result<()> {
1595 create_persister!(persister);
1596
1597 let chain_swap_handler = new_chain_swap_handler(persister.clone())?;
1598
1599 let all_states = HashSet::from([
1601 Created,
1602 Pending,
1603 WaitingFeeAcceptance,
1604 Complete,
1605 TimedOut,
1606 Failed,
1607 ]);
1608 let valid_combinations = HashMap::from([
1609 (
1610 Created,
1611 HashSet::from([
1612 Pending,
1613 WaitingFeeAcceptance,
1614 Complete,
1615 TimedOut,
1616 Refundable,
1617 Failed,
1618 ]),
1619 ),
1620 (
1621 Pending,
1622 HashSet::from([
1623 Pending,
1624 WaitingFeeAcceptance,
1625 Complete,
1626 Refundable,
1627 RefundPending,
1628 Failed,
1629 ]),
1630 ),
1631 (
1632 WaitingFeeAcceptance,
1633 HashSet::from([
1634 Pending,
1635 WaitingFeeAcceptance,
1636 Complete,
1637 Refundable,
1638 RefundPending,
1639 Failed,
1640 ]),
1641 ),
1642 (TimedOut, HashSet::from([Failed])),
1643 (Complete, HashSet::from([Refundable])),
1644 (Refundable, HashSet::from([RefundPending, Failed])),
1645 (
1646 RefundPending,
1647 HashSet::from([Refundable, Complete, Failed, RefundPending]),
1648 ),
1649 (Failed, HashSet::from([Failed, Refundable])),
1650 ]);
1651
1652 for (first_state, allowed_states) in valid_combinations.iter() {
1653 for allowed_state in allowed_states {
1654 let chain_swap = new_chain_swap(
1655 Direction::Incoming,
1656 Some(*first_state),
1657 false,
1658 None,
1659 false,
1660 false,
1661 None,
1662 );
1663 persister.insert_or_update_chain_swap(&chain_swap)?;
1664
1665 assert!(chain_swap_handler
1666 .update_swap_info(&ChainSwapUpdate {
1667 swap_id: chain_swap.id,
1668 to_state: *allowed_state,
1669 ..Default::default()
1670 })
1671 .is_ok());
1672 }
1673 }
1674
1675 let invalid_combinations: HashMap<PaymentState, HashSet<PaymentState>> = valid_combinations
1677 .iter()
1678 .map(|(first_state, allowed_states)| {
1679 (
1680 *first_state,
1681 all_states.difference(allowed_states).cloned().collect(),
1682 )
1683 })
1684 .collect();
1685
1686 for (first_state, disallowed_states) in invalid_combinations.iter() {
1687 for disallowed_state in disallowed_states {
1688 let chain_swap = new_chain_swap(
1689 Direction::Incoming,
1690 Some(*first_state),
1691 false,
1692 None,
1693 false,
1694 false,
1695 None,
1696 );
1697 persister.insert_or_update_chain_swap(&chain_swap)?;
1698
1699 assert!(chain_swap_handler
1700 .update_swap_info(&ChainSwapUpdate {
1701 swap_id: chain_swap.id,
1702 to_state: *disallowed_state,
1703 ..Default::default()
1704 })
1705 .is_err());
1706 }
1707 }
1708
1709 Ok(())
1710 }
1711}