diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1c949726..8bf6c959 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -146,7 +146,7 @@ interface OnchainPayment { [Throws=NodeError] Txid send_to_address([ByRef]Address address, u64 amount_sats); [Throws=NodeError] - Txid send_all_to_address([ByRef]Address address); + Txid send_all_to_address([ByRef]Address address, boolean retain_reserve); }; interface UnifiedQrPayment { diff --git a/src/error.rs b/src/error.rs index deaf6db3..94dbbc48 100644 --- a/src/error.rs +++ b/src/error.rs @@ -181,6 +181,7 @@ impl From for Error { fn from(e: bdk::Error) -> Self { match e { bdk::Error::Signer(_) => Self::OnchainTxSigningFailed, + bdk::Error::InsufficientFunds { .. } => Self::InsufficientFunds, _ => Self::WalletOperationFailed, } } diff --git a/src/event.rs b/src/event.rs index c4c5034f..d76f0b05 100644 --- a/src/event.rs +++ b/src/event.rs @@ -6,6 +6,7 @@ use crate::{ }; use crate::connection::ConnectionManager; +use crate::fee_estimator::ConfirmationTarget; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -18,7 +19,6 @@ use crate::io::{ }; use crate::logger::{log_debug, log_error, log_info, Logger}; -use lightning::chain::chaininterface::ConfirmationTarget; use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; @@ -398,7 +398,7 @@ where } => { // Construct the raw transaction with the output that is paid the amount of the // channel. - let confirmation_target = ConfirmationTarget::NonAnchorChannelFee; + let confirmation_target = ConfirmationTarget::ChannelFunding; // We set nLockTime to the current height to discourage fee sniping. let cur_height = self.channel_manager.current_best_block().height; diff --git a/src/fee_estimator.rs b/src/fee_estimator.rs index 329cc6e4..b023ae96 100644 --- a/src/fee_estimator.rs +++ b/src/fee_estimator.rs @@ -2,9 +2,9 @@ use crate::config::FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS; use crate::logger::{log_error, log_trace, Logger}; use crate::{Config, Error}; -use lightning::chain::chaininterface::{ - ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW, -}; +use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; +use lightning::chain::chaininterface::FeeEstimator as LdkFeeEstimator; +use lightning::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; use bdk::FeeRate; use esplora_client::AsyncClient as EsploraClient; @@ -17,6 +17,26 @@ use std::ops::Deref; use std::sync::{Arc, RwLock}; use std::time::Duration; +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub(crate) enum ConfirmationTarget { + /// The default target for onchain payments. + OnchainPayment, + /// The target used for funding transactions. + ChannelFunding, + /// Targets used by LDK. + Lightning(LdkConfirmationTarget), +} + +pub(crate) trait FeeEstimator { + fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate; +} + +impl From for ConfirmationTarget { + fn from(value: LdkConfirmationTarget) -> Self { + Self::Lightning(value) + } +} + pub(crate) struct OnchainFeeEstimator where L::Target: Logger, @@ -61,23 +81,30 @@ where } let confirmation_targets = vec![ - ConfirmationTarget::OnChainSweep, - ConfirmationTarget::MinAllowedAnchorChannelRemoteFee, - ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee, - ConfirmationTarget::AnchorChannelFee, - ConfirmationTarget::NonAnchorChannelFee, - ConfirmationTarget::ChannelCloseMinimum, - ConfirmationTarget::OutputSpendingFee, + ConfirmationTarget::OnchainPayment, + ConfirmationTarget::ChannelFunding, + LdkConfirmationTarget::OnChainSweep.into(), + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee.into(), + LdkConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee.into(), + LdkConfirmationTarget::AnchorChannelFee.into(), + LdkConfirmationTarget::NonAnchorChannelFee.into(), + LdkConfirmationTarget::ChannelCloseMinimum.into(), + LdkConfirmationTarget::OutputSpendingFee.into(), ]; + for target in confirmation_targets { let num_blocks = match target { - ConfirmationTarget::OnChainSweep => 6, - ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => 1008, - ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => 144, - ConfirmationTarget::AnchorChannelFee => 1008, - ConfirmationTarget::NonAnchorChannelFee => 12, - ConfirmationTarget::ChannelCloseMinimum => 144, - ConfirmationTarget::OutputSpendingFee => 12, + ConfirmationTarget::OnchainPayment => 6, + ConfirmationTarget::ChannelFunding => 12, + ConfirmationTarget::Lightning(ldk_target) => match ldk_target { + LdkConfirmationTarget::OnChainSweep => 6, + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee => 1008, + LdkConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => 144, + LdkConfirmationTarget::AnchorChannelFee => 1008, + LdkConfirmationTarget::NonAnchorChannelFee => 12, + LdkConfirmationTarget::ChannelCloseMinimum => 144, + LdkConfirmationTarget::OutputSpendingFee => 12, + }, }; let converted_estimates = @@ -96,7 +123,9 @@ where // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that // require some post-estimation adjustments to the fee rates, which we do here. let adjusted_fee_rate = match target { - ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => { + ConfirmationTarget::Lightning( + LdkConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee, + ) => { let slightly_less_than_background = fee_rate.fee_wu(Weight::from_wu(1000)) - 250; FeeRate::from_sat_per_kwu(slightly_less_than_background as f32) @@ -115,33 +144,53 @@ where } Ok(()) } +} - pub(crate) fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate { +impl FeeEstimator for OnchainFeeEstimator +where + L::Target: Logger, +{ + fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate { let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap(); let fallback_sats_kwu = match confirmation_target { - ConfirmationTarget::OnChainSweep => 5000, - ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW, - ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW, - ConfirmationTarget::AnchorChannelFee => 500, - ConfirmationTarget::NonAnchorChannelFee => 1000, - ConfirmationTarget::ChannelCloseMinimum => 500, - ConfirmationTarget::OutputSpendingFee => 1000, + ConfirmationTarget::OnchainPayment => 5000, + ConfirmationTarget::ChannelFunding => 1000, + ConfirmationTarget::Lightning(ldk_target) => match ldk_target { + LdkConfirmationTarget::OnChainSweep => 5000, + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee => { + FEERATE_FLOOR_SATS_PER_KW + }, + LdkConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => { + FEERATE_FLOOR_SATS_PER_KW + }, + LdkConfirmationTarget::AnchorChannelFee => 500, + LdkConfirmationTarget::NonAnchorChannelFee => 1000, + LdkConfirmationTarget::ChannelCloseMinimum => 500, + LdkConfirmationTarget::OutputSpendingFee => 1000, + }, }; // We'll fall back on this, if we really don't have any other information. let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32); - *locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate) + let estimate = *locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate); + + // Currently we assume every transaction needs to at least be relayable, which is why we + // enforce a lower bound of `FEERATE_FLOOR_SATS_PER_KW`. + let weight_units = Weight::from_wu(1000); + FeeRate::from_wu( + estimate.fee_wu(weight_units).max(FEERATE_FLOOR_SATS_PER_KW as u64), + weight_units, + ) } } -impl FeeEstimator for OnchainFeeEstimator +impl LdkFeeEstimator for OnchainFeeEstimator where L::Target: Logger, { - fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { - (self.estimate_fee_rate(confirmation_target).fee_wu(Weight::from_wu(1000)) as u32) - .max(FEERATE_FLOOR_SATS_PER_KW) + fn get_est_sat_per_1000_weight(&self, confirmation_target: LdkConfirmationTarget) -> u32 { + self.estimate_fee_rate(confirmation_target.into()).fee_wu(Weight::from_wu(1000)) as u32 } } diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 5c1365de..ca09c85b 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -2,8 +2,9 @@ use crate::config::Config; use crate::error::Error; -use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::logger::{log_info, FilesystemLogger, Logger}; use crate::types::{ChannelManager, Wallet}; +use crate::wallet::OnchainSendType; use bitcoin::{Address, Txid}; @@ -53,33 +54,39 @@ impl OnchainPayment { let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); - let spendable_amount_sats = - self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); - - if spendable_amount_sats < amount_sats { - log_error!(self.logger, - "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats", - spendable_amount_sats, amount_sats - ); - return Err(Error::InsufficientFunds); - } - self.wallet.send_to_address(address, Some(amount_sats)) + let send_amount = + OnchainSendType::SendRetainingReserve { amount_sats, cur_anchor_reserve_sats }; + self.wallet.send_to_address(address, send_amount) } - /// Send an on-chain payment to the given address, draining all the available funds. + /// Send an on-chain payment to the given address, draining the available funds. /// /// This is useful if you have closed all channels and want to migrate funds to another /// on-chain wallet. /// - /// Please note that this will **not** retain any on-chain reserves, which might be potentially + /// Please note that if `retain_reserves` is set to `false` this will **not** retain any on-chain reserves, which might be potentially /// dangerous if you have open Anchor channels for which you can't trust the counterparty to - /// spend the Anchor output after channel closure. - pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result { + /// spend the Anchor output after channel closure. If `retain_reserves` is set to `true`, this + /// will try to send all spendable onchain funds, i.e., + /// [`BalanceDetails::spendable_onchain_balance_sats`]. + /// + /// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats + pub fn send_all_to_address( + &self, address: &bitcoin::Address, retain_reserves: bool, + ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - self.wallet.send_to_address(address, None) + let send_amount = if retain_reserves { + let cur_anchor_reserve_sats = + crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } + } else { + OnchainSendType::SendAllDrainingReserve + }; + + self.wallet.send_to_address(address, send_amount) } } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 4492bcfc..2db621ed 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -56,11 +56,16 @@ where Err(e) => match e { esplora_client::Error::Reqwest(err) => { if err.status() == StatusCode::from_u16(400).ok() { - // Ignore 400, as this just means bitcoind already knows the + // Log 400 at lesser level, as this often just means bitcoind already knows the // transaction. // FIXME: We can further differentiate here based on the error // message which will be available with rust-esplora-client 0.7 and // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + err + ); } else { log_error!( self.logger, diff --git a/src/wallet.rs b/src/wallet.rs index 0da3f6db..e288cc52 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,9 +1,10 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::config::BDK_WALLET_SYNC_TIMEOUT_SECS; +use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; use crate::Error; -use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::chain::chaininterface::{BroadcasterInterface, FEERATE_FLOOR_SATS_PER_KW}; use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; @@ -18,8 +19,7 @@ use lightning::util::message_signing; use bdk::blockchain::EsploraBlockchain; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; -use bdk::{Balance, FeeRate}; -use bdk::{SignOptions, SyncOptions}; +use bdk::{Balance, SignOptions, SyncOptions}; use bitcoin::address::{Payload, WitnessVersion}; use bitcoin::bech32::u5; @@ -43,7 +43,13 @@ enum WalletSyncStatus { InProgress { subscribers: tokio::sync::broadcast::Sender> }, } -pub struct Wallet +pub(crate) enum OnchainSendType { + SendRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, + SendAllRetainingReserve { cur_anchor_reserve_sats: u64 }, + SendAllDrainingReserve, +} + +pub(crate) struct Wallet where D: BatchDatabase, B::Target: BroadcasterInterface, @@ -153,9 +159,7 @@ where &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, ) -> Result { - let fee_rate = FeeRate::from_sat_per_kwu( - self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, - ); + let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target); let locked_wallet = self.inner.lock().unwrap(); let mut tx_builder = locked_wallet.build_tx(); @@ -233,39 +237,85 @@ where self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) } - /// Send funds to the given address. - /// - /// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be - /// spent. pub(crate) fn send_to_address( - &self, address: &bitcoin::Address, amount_msat_or_drain: Option, + &self, address: &bitcoin::Address, send_amount: OnchainSendType, ) -> Result { - let confirmation_target = ConfirmationTarget::OutputSpendingFee; - let fee_rate = FeeRate::from_sat_per_kwu( - self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, - ); + let confirmation_target = ConfirmationTarget::OnchainPayment; + let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target); let tx = { let locked_wallet = self.inner.lock().unwrap(); let mut tx_builder = locked_wallet.build_tx(); - if let Some(amount_sats) = amount_msat_or_drain { - tx_builder - .add_recipient(address.script_pubkey(), amount_sats) - .fee_rate(fee_rate) - .enable_rbf(); - } else { - tx_builder - .drain_wallet() - .drain_to(address.script_pubkey()) - .fee_rate(fee_rate) - .enable_rbf(); + // Prepare the tx_builder. We properly check the reserve requirements (again) further down. + match send_amount { + OnchainSendType::SendRetainingReserve { amount_sats, .. } => { + tx_builder + .add_recipient(address.script_pubkey(), amount_sats) + .fee_rate(fee_rate) + .enable_rbf(); + }, + OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => { + let change_address_info = + locked_wallet.get_internal_address(AddressIndex::Peek(0)).map_err(|e| { + log_error!(self.logger, "Unable to retrieve address: {}", e); + e + })?; + let spendable_amount_sats = + self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let mut tmp_tx_builder = locked_wallet.build_tx(); + tmp_tx_builder + .drain_wallet() + .drain_to(address.script_pubkey()) + .add_recipient( + change_address_info.address.script_pubkey(), + cur_anchor_reserve_sats, + ) + .fee_rate(fee_rate) + .enable_rbf(); + let tmp_tx_details = match tmp_tx_builder.finish() { + Ok((_, tmp_tx_details)) => tmp_tx_details, + Err(err) => { + log_error!( + self.logger, + "Failed to create temporary transaction: {}", + err + ); + return Err(err.into()); + }, + }; + + let estimated_tx_fee_sats = tmp_tx_details.fee.unwrap_or(0); + let estimated_spendable_amount_sats = + spendable_amount_sats.saturating_sub(estimated_tx_fee_sats); + + if estimated_spendable_amount_sats == 0 { + log_error!(self.logger, + "Unable to send payment without infringing on Anchor reserves. Available: {}sats, estimated fee required: {}sats.", + spendable_amount_sats, + estimated_tx_fee_sats, + ); + return Err(Error::InsufficientFunds); + } + + tx_builder + .add_recipient(address.script_pubkey(), estimated_spendable_amount_sats) + .fee_absolute(estimated_tx_fee_sats) + .enable_rbf(); + }, + OnchainSendType::SendAllDrainingReserve => { + tx_builder + .drain_wallet() + .drain_to(address.script_pubkey()) + .fee_rate(fee_rate) + .enable_rbf(); + }, } - let mut psbt = match tx_builder.finish() { - Ok((psbt, _)) => { + let (mut psbt, tx_details) = match tx_builder.finish() { + Ok((psbt, tx_details)) => { log_trace!(self.logger, "Created PSBT: {:?}", psbt); - psbt + (psbt, tx_details) }, Err(err) => { log_error!(self.logger, "Failed to create transaction: {}", err); @@ -273,6 +323,38 @@ where }, }; + // Check the reserve requirements (again) and return an error if they aren't met. + match send_amount { + OnchainSendType::SendRetainingReserve { amount_sats, cur_anchor_reserve_sats } => { + let spendable_amount_sats = + self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let tx_fee_sats = tx_details.fee.unwrap_or(0); + if spendable_amount_sats < amount_sats + tx_fee_sats { + log_error!(self.logger, + "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats + {}sats fee", + spendable_amount_sats, + amount_sats, + tx_fee_sats, + ); + return Err(Error::InsufficientFunds); + } + }, + OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => { + let spendable_amount_sats = + self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let drain_amount_sats = tx_details.sent - tx_details.received; + if spendable_amount_sats < drain_amount_sats { + log_error!(self.logger, + "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, + drain_amount_sats, + ); + return Err(Error::InsufficientFunds); + } + }, + _ => {}, + } + match locked_wallet.sign(&mut psbt, SignOptions::default()) { Ok(finalized) => { if !finalized { @@ -291,21 +373,33 @@ where let txid = tx.txid(); - if let Some(amount_sats) = amount_msat_or_drain { - log_info!( - self.logger, - "Created new transaction {} sending {}sats on-chain to address {}", - txid, - amount_sats, - address - ); - } else { - log_info!( - self.logger, - "Created new transaction {} sending all available on-chain funds to address {}", - txid, - address - ); + match send_amount { + OnchainSendType::SendRetainingReserve { amount_sats, .. } => { + log_info!( + self.logger, + "Created new transaction {} sending {}sats on-chain to address {}", + txid, + amount_sats, + address + ); + }, + OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => { + log_info!( + self.logger, + "Created new transaction {} sending available on-chain funds retaining a reserve of {}sats to address {}", + txid, + cur_anchor_reserve_sats, + address, + ); + }, + OnchainSendType::SendAllDrainingReserve => { + log_info!( + self.logger, + "Created new transaction {} sending all available on-chain funds to address {}", + txid, + address + ); + }, } Ok(txid) @@ -485,7 +579,7 @@ where /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. -pub struct WalletKeysManager +pub(crate) struct WalletKeysManager where D: BatchDatabase, B::Target: BroadcasterInterface, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index b3788f9d..5197ea6b 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -258,20 +258,39 @@ fn onchain_spend_receive() { let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 1_100_000; premine_and_distribute_funds( &bitcoind.client, &electrsd.client, - vec![addr_b.clone()], - Amount::from_sat(100000), + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), ); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 100000); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let channel_amount_sat = 1_000_000; + let reserve_amount_sat = 25_000; + open_channel(&node_b, &node_a, channel_amount_sat, true, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let node_a_balance = premine_amount_sat - reserve_amount_sat; + let node_b_balance_lower = premine_amount_sat - channel_amount_sat - reserve_amount_sat - 1000; + let node_b_balance_upper = premine_amount_sat - channel_amount_sat - reserve_amount_sat; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, node_a_balance); + assert!(node_b.list_balances().spendable_onchain_balance_sats > node_b_balance_lower); + assert!(node_b.list_balances().spendable_onchain_balance_sats < node_b_balance_upper); assert_eq!( Err(NodeError::InsufficientFunds), - node_a.onchain_payment().send_to_address(&addr_b, 1000) + node_a.onchain_payment().send_to_address(&addr_b, node_a_balance + 1) ); let txid = node_b.onchain_payment().send_to_address(&addr_a, 1000).unwrap(); @@ -281,21 +300,45 @@ fn onchain_spend_receive() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 1000); - assert!(node_b.list_balances().spendable_onchain_balance_sats > 98000); - assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000); + let node_a_balance = node_a_balance + 1000; + let node_b_balance_lower = node_b_balance_lower - 1000; + let node_b_balance_upper = node_b_balance_upper - 1000; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, node_a_balance); + assert!(node_b.list_balances().spendable_onchain_balance_sats > node_b_balance_lower); + assert!(node_b.list_balances().spendable_onchain_balance_sats < node_b_balance_upper); + + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true).unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + wait_for_tx(&electrsd.client, txid); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let node_b_balance_lower = node_b_balance_lower + node_a_balance; + let node_b_balance_upper = node_b_balance_upper + node_a_balance; + let node_a_balance = 0; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, node_a_balance); + assert_eq!(node_a.list_balances().total_onchain_balance_sats, reserve_amount_sat); + assert!(node_b.list_balances().spendable_onchain_balance_sats > node_b_balance_lower); + assert!(node_b.list_balances().spendable_onchain_balance_sats < node_b_balance_upper); let addr_b = node_b.onchain_payment().new_address().unwrap(); - let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().total_onchain_balance_sats, 0); - assert!(node_b.list_balances().spendable_onchain_balance_sats > 99000); - assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000); + let node_b_balance_lower = node_b_balance_lower + reserve_amount_sat; + let node_b_balance_upper = node_b_balance_upper + reserve_amount_sat; + let node_a_balance = 0; + + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, node_a_balance); + assert_eq!(node_a.list_balances().total_onchain_balance_sats, node_a_balance); + assert!(node_b.list_balances().spendable_onchain_balance_sats > node_b_balance_lower); + assert!(node_b.list_balances().spendable_onchain_balance_sats < node_b_balance_upper); } #[test]