diff --git a/cli/Cargo.lock b/cli/Cargo.lock index ec2f811ee..e1ad147e8 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -539,7 +539,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/hydra-yse/boltz-rust?branch=yse-fee-calculation#09710fb508a2a188978f37c091ebb488331d627d" +source = "git+https://github.com/ok300/boltz-rust?branch=ok300-chain-swap-refund-check-all-utoxs#368d31c3ea34027e6e8f07daa5e9a43c5fa91dde" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 3fc6e85e1..b4ee40723 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/hydra-yse/boltz-rust?branch=yse-fee-calculation#09710fb508a2a188978f37c091ebb488331d627d" +source = "git+https://github.com/ok300/boltz-rust?branch=ok300-chain-swap-refund-check-all-utoxs#5934ec921ac3240cabcab2d98be29b2b4652af56" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 5fbce3d50..66493b3c6 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -14,7 +14,7 @@ frb = ["dep:flutter_rust_bridge"] [dependencies] anyhow = { workspace = true } bip39 = "2.0.0" -boltz-client = { git = "https://github.com/hydra-yse/boltz-rust", branch = "yse-fee-calculation" } +boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-chain-swap-refund-check-all-utoxs" } chrono = "0.4" env_logger = "0.11" flutter_rust_bridge = { version = "=2.4.0", features = [ diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 488621c8e..73e785856 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -76,7 +76,7 @@ impl ChainSwapHandler { loop { tokio::select! { _ = rescan_interval.tick() => { - if let Err(e) = cloned.rescan_incoming_chain_swaps().await { + if let Err(e) = cloned.rescan_incoming_chain_swaps(false).await { error!("Error checking incoming chain swaps: {e:?}"); } if let Err(e) = cloned.rescan_outgoing_chain_swaps().await { @@ -110,7 +110,10 @@ impl ChainSwapHandler { } } - pub(crate) async fn rescan_incoming_chain_swaps(&self) -> Result<()> { + pub(crate) async fn rescan_incoming_chain_swaps( + &self, + ignore_monitoring_block_height: bool, + ) -> Result<()> { let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; let chain_swaps: Vec = self .persister @@ -124,22 +127,34 @@ impl ChainSwapHandler { current_height ); for swap in chain_swaps { - if let Err(e) = self.rescan_incoming_chain_swap(&swap, current_height).await { + if let Err(e) = self + .rescan_incoming_chain_swap(&swap, current_height, ignore_monitoring_block_height) + .await + { error!("Error rescanning incoming Chain Swap {}: {e:?}", swap.id); } } Ok(()) } + /// ### Arguments + /// - `swap`: the swap being rescanned + /// - `current_height`: the tip + /// - `ignore_monitoring_block_height`: if true, it rescans an expired swap even after the + /// cutoff monitoring block height async fn rescan_incoming_chain_swap( &self, swap: &ChainSwap, current_height: u32, + ignore_monitoring_block_height: bool, ) -> Result<()> { let monitoring_block_height = swap.timeout_block_height + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS; let is_swap_expired = current_height > swap.timeout_block_height; - let is_monitoring_expired = current_height > monitoring_block_height; + let is_monitoring_expired = match ignore_monitoring_block_height { + true => false, + false => current_height > monitoring_block_height, + }; if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending { let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?; @@ -730,7 +745,7 @@ impl ChainSwapHandler { Ok(()) } - pub async fn prepare_refund( + pub(crate) async fn prepare_refund( &self, lockup_address: &str, refund_address: &str, @@ -771,30 +786,25 @@ impl ChainSwapHandler { .persister .fetch_chain_swap_by_lockup_address(lockup_address)? .ok_or(PaymentError::Generic { - err: format!("Swap {} not found", lockup_address), + err: format!("Swap for lockup address {} not found", lockup_address), })?; + let id = &swap.id; ensure_sdk!( swap.state == Refundable, PaymentError::Generic { - err: format!("Chain Swap {} was not marked as `Refundable`", swap.id) + err: format!("Chain Swap {id} was not marked as `Refundable`") } ); ensure_sdk!( swap.refund_tx_id.is_none(), PaymentError::Generic { - err: format!( - "A refund tx for incoming Chain Swap {} was already broadcast", - swap.id - ) + err: format!("A refund tx for incoming Chain Swap {id} was already broadcast",) } ); - info!( - "Initiating refund for incoming Chain Swap {}, is_cooperative: {is_cooperative}", - swap.id - ); + info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}",); let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else { return Err(PaymentError::Generic { @@ -818,18 +828,25 @@ impl ChainSwapHandler { )? else { return Err(PaymentError::Generic { - err: format!( - "Unexpected refund tx type returned for incoming Chain swap {}", - swap.id - ), + err: format!("Unexpected refund tx type returned for incoming Chain swap {id}",), }); }; let refund_tx_id = bitcoin_chain_service.broadcast(&refund_tx)?.to_string(); - info!( - "Successfully broadcast refund for incoming Chain Swap {}, is_cooperative: {is_cooperative}", - swap.id - ); + info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}"); + + // After refund tx is broadcasted, set the payment state to `RefundPending`. This ensures: + // - the swap is not shown in `list-refundables` anymore + // - the background thread will move it to Failed once the refund tx confirms + self.update_swap_info( + &swap.id, + RefundPending, + None, + None, + None, + Some(&refund_tx_id), + ) + .await?; Ok(refund_tx_id) } diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index cec87701e..7ff94392e 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1862,14 +1862,20 @@ impl LiquidSdk { ) }) .await?; + Ok(RefundResponse { refund_tx_id }) } - /// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] within - /// the monitoring period to check if there are any confirmed funds available to refund. + /// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] to check + /// if there are any confirmed funds available to refund. + /// + /// Since it bypasses the monitoring period, this should be called rarely or when the caller + /// expects there is a very old refundable chain swap. Otherwise, for relatively recent swaps + /// (within last [CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS] blocks = ~30 days), calling this + /// is not necessary as it happens automatically in the background. pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> { self.chain_swap_handler - .rescan_incoming_chain_swaps() + .rescan_incoming_chain_swaps(true) .await?; Ok(()) } diff --git a/lib/core/src/swapper/boltz/bitcoin.rs b/lib/core/src/swapper/boltz/bitcoin.rs index 65244d592..8954bfedc 100644 --- a/lib/core/src/swapper/boltz/bitcoin.rs +++ b/lib/core/src/swapper/boltz/bitcoin.rs @@ -71,17 +71,17 @@ impl BoltzSwapper { SdkError::generic("Address network validation failed") ); - let utxo = utxos - .first() - .and_then(|utxo| utxo.as_bitcoin().cloned()) - .ok_or(SdkError::generic("No UTXO found"))?; + let utxos = utxos + .iter() + .filter_map(|utxo| utxo.as_bitcoin().cloned()) + .collect(); let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; let refund_tx = BtcSwapTx { kind: SwapTxKind::Refund, swap_script, output_address: address.assume_checked(), - utxo, + utxos, }; let refund_keypair = swap.get_refund_keypair()?;