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