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