Skip to content

Commit f33a69c

Browse files
committed
Allow honoring reserve in send_all_to_address
Previously, `OnchainPayment::send_all_to_address` could only be used to fully drain the onchain wallet, i.e., would not retain any reserves. Here, we try to introduce a `retain_reserves` bool that allows users to send all funds while honoring the configured on-chain reserves. While we're at it, we move the reserve checks for `send_to_address` also to the internal wallet's method, which makes the checks more accurate as they now are checked against the final transaction value, including transaction fees.
1 parent 51c9b4d commit f33a69c

File tree

5 files changed

+213
-59
lines changed

5 files changed

+213
-59
lines changed

bindings/ldk_node.udl

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ interface OnchainPayment {
161161
[Throws=NodeError]
162162
Txid send_to_address([ByRef]Address address, u64 amount_sats);
163163
[Throws=NodeError]
164-
Txid send_all_to_address([ByRef]Address address);
164+
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve);
165165
};
166166

167167
interface UnifiedQrPayment {

src/error.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use bdk_chain::bitcoin::psbt::ExtractTxError as BdkExtractTxError;
99
use bdk_chain::local_chain::CannotConnectError as BdkChainConnectionError;
10+
use bdk_chain::tx_graph::CalculateFeeError as BdkChainCalculateFeeError;
1011
use bdk_wallet::error::CreateTxError as BdkCreateTxError;
1112
use bdk_wallet::signer::SignerError as BdkSignerError;
1213

@@ -196,8 +197,11 @@ impl From<BdkSignerError> for Error {
196197
}
197198

198199
impl From<BdkCreateTxError> for Error {
199-
fn from(_: BdkCreateTxError) -> Self {
200-
Self::OnchainTxCreationFailed
200+
fn from(e: BdkCreateTxError) -> Self {
201+
match e {
202+
BdkCreateTxError::CoinSelection(_) => Self::InsufficientFunds,
203+
_ => Self::OnchainTxCreationFailed,
204+
}
201205
}
202206
}
203207

@@ -213,6 +217,12 @@ impl From<BdkChainConnectionError> for Error {
213217
}
214218
}
215219

220+
impl From<BdkChainCalculateFeeError> for Error {
221+
fn from(_: BdkChainCalculateFeeError) -> Self {
222+
Self::WalletOperationFailed
223+
}
224+
}
225+
216226
impl From<lightning_transaction_sync::TxSyncError> for Error {
217227
fn from(_e: lightning_transaction_sync::TxSyncError) -> Self {
218228
Self::TxSyncFailed

src/payment/onchain.rs

+25-20
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
1010
use crate::config::Config;
1111
use crate::error::Error;
12-
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
12+
use crate::logger::{log_info, FilesystemLogger, Logger};
1313
use crate::types::{ChannelManager, Wallet};
14+
use crate::wallet::OnchainSendAmount;
1415

15-
use bitcoin::{Address, Amount, Txid};
16+
use bitcoin::{Address, Txid};
1617

1718
use std::sync::{Arc, RwLock};
1819

@@ -60,35 +61,39 @@ impl OnchainPayment {
6061

6162
let cur_anchor_reserve_sats =
6263
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
63-
let spendable_amount_sats =
64-
self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
65-
66-
if spendable_amount_sats < amount_sats {
67-
log_error!(self.logger,
68-
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats",
69-
spendable_amount_sats, amount_sats
70-
);
71-
return Err(Error::InsufficientFunds);
72-
}
73-
74-
let amount = Amount::from_sat(amount_sats);
75-
self.wallet.send_to_address(address, Some(amount))
64+
let send_amount =
65+
OnchainSendAmount::ExactRetainingReserve { amount_sats, cur_anchor_reserve_sats };
66+
self.wallet.send_to_address(address, send_amount)
7667
}
7768

78-
/// Send an on-chain payment to the given address, draining all the available funds.
69+
/// Send an on-chain payment to the given address, draining the available funds.
7970
///
8071
/// This is useful if you have closed all channels and want to migrate funds to another
8172
/// on-chain wallet.
8273
///
83-
/// Please note that this will **not** retain any on-chain reserves, which might be potentially
74+
/// Please note that if `retain_reserves` is set to `false` this will **not** retain any on-chain reserves, which might be potentially
8475
/// dangerous if you have open Anchor channels for which you can't trust the counterparty to
85-
/// spend the Anchor output after channel closure.
86-
pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
76+
/// spend the Anchor output after channel closure. If `retain_reserves` is set to `true`, this
77+
/// will try to send all spendable onchain funds, i.e.,
78+
/// [`BalanceDetails::spendable_onchain_balance_sats`].
79+
///
80+
/// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats
81+
pub fn send_all_to_address(
82+
&self, address: &bitcoin::Address, retain_reserves: bool,
83+
) -> Result<Txid, Error> {
8784
let rt_lock = self.runtime.read().unwrap();
8885
if rt_lock.is_none() {
8986
return Err(Error::NotRunning);
9087
}
9188

92-
self.wallet.send_to_address(address, None)
89+
let send_amount = if retain_reserves {
90+
let cur_anchor_reserve_sats =
91+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
92+
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats }
93+
} else {
94+
OnchainSendAmount::AllDrainingReserve
95+
};
96+
97+
self.wallet.send_to_address(address, send_amount)
9398
}
9499
}

src/wallet/mod.rs

+174-35
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use lightning_invoice::RawBolt11Invoice;
2828

2929
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
3030
use bdk_chain::ChainPosition;
31-
use bdk_wallet::{KeychainKind, PersistedWallet, SignOptions, Update};
31+
use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update};
3232

3333
use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
3434
use bitcoin::blockdata::locktime::absolute::LockTime;
@@ -45,6 +45,12 @@ use bitcoin::{
4545
use std::ops::Deref;
4646
use std::sync::{Arc, Mutex};
4747

48+
pub(crate) enum OnchainSendAmount {
49+
ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 },
50+
AllRetainingReserve { cur_anchor_reserve_sats: u64 },
51+
AllDrainingReserve,
52+
}
53+
4854
pub(crate) mod persist;
4955
pub(crate) mod ser;
5056

@@ -215,6 +221,12 @@ where
215221
);
216222
}
217223

224+
self.get_balances_inner(balance, total_anchor_channels_reserve_sats)
225+
}
226+
227+
fn get_balances_inner(
228+
&self, balance: Balance, total_anchor_channels_reserve_sats: u64,
229+
) -> Result<(u64, u64), Error> {
218230
let (total, spendable) = (
219231
balance.total().to_sat(),
220232
balance.trusted_spendable().to_sat().saturating_sub(total_anchor_channels_reserve_sats),
@@ -229,32 +241,95 @@ where
229241
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
230242
}
231243

232-
/// Send funds to the given address.
233-
///
234-
/// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be
235-
/// spent.
236244
pub(crate) fn send_to_address(
237-
&self, address: &bitcoin::Address, amount_or_drain: Option<Amount>,
245+
&self, address: &bitcoin::Address, send_amount: OnchainSendAmount,
238246
) -> Result<Txid, Error> {
239247
let confirmation_target = ConfirmationTarget::OnchainPayment;
240248
let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
241249

242250
let tx = {
243251
let mut locked_wallet = self.inner.lock().unwrap();
244-
let mut tx_builder = locked_wallet.build_tx();
245-
246-
if let Some(amount) = amount_or_drain {
247-
tx_builder
248-
.add_recipient(address.script_pubkey(), amount)
249-
.fee_rate(fee_rate)
250-
.enable_rbf();
251-
} else {
252-
tx_builder
253-
.drain_wallet()
254-
.drain_to(address.script_pubkey())
255-
.fee_rate(fee_rate)
256-
.enable_rbf();
257-
}
252+
253+
// Prepare the tx_builder. We properly check the reserve requirements (again) further down.
254+
let tx_builder = match send_amount {
255+
OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => {
256+
let mut tx_builder = locked_wallet.build_tx();
257+
let amount = Amount::from_sat(amount_sats);
258+
tx_builder
259+
.add_recipient(address.script_pubkey(), amount)
260+
.fee_rate(fee_rate)
261+
.enable_rbf();
262+
tx_builder
263+
},
264+
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => {
265+
let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0);
266+
let balance = locked_wallet.balance();
267+
let spendable_amount_sats = self
268+
.get_balances_inner(balance, cur_anchor_reserve_sats)
269+
.map(|(_, s)| s)
270+
.unwrap_or(0);
271+
let tmp_tx = {
272+
let mut tmp_tx_builder = locked_wallet.build_tx();
273+
tmp_tx_builder
274+
.drain_wallet()
275+
.drain_to(address.script_pubkey())
276+
.add_recipient(
277+
change_address_info.address.script_pubkey(),
278+
Amount::from_sat(cur_anchor_reserve_sats),
279+
)
280+
.fee_rate(fee_rate)
281+
.enable_rbf();
282+
match tmp_tx_builder.finish() {
283+
Ok(psbt) => psbt.unsigned_tx,
284+
Err(err) => {
285+
log_error!(
286+
self.logger,
287+
"Failed to create temporary transaction: {}",
288+
err
289+
);
290+
return Err(err.into());
291+
},
292+
}
293+
};
294+
295+
let estimated_tx_fee = locked_wallet.calculate_fee(&tmp_tx).map_err(|e| {
296+
log_error!(
297+
self.logger,
298+
"Failed to calculate fee of temporary transaction: {}",
299+
e
300+
);
301+
e
302+
})?;
303+
let estimated_spendable_amount = Amount::from_sat(
304+
spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat()),
305+
);
306+
307+
if estimated_spendable_amount == Amount::ZERO {
308+
log_error!(self.logger,
309+
"Unable to send payment without infringing on Anchor reserves. Available: {}sats, estimated fee required: {}sats.",
310+
spendable_amount_sats,
311+
estimated_tx_fee,
312+
);
313+
return Err(Error::InsufficientFunds);
314+
}
315+
316+
let mut tx_builder = locked_wallet.build_tx();
317+
tx_builder
318+
.add_recipient(address.script_pubkey(), estimated_spendable_amount)
319+
.fee_absolute(estimated_tx_fee)
320+
.enable_rbf();
321+
tx_builder
322+
},
323+
OnchainSendAmount::AllDrainingReserve => {
324+
let mut tx_builder = locked_wallet.build_tx();
325+
tx_builder
326+
.drain_wallet()
327+
.drain_to(address.script_pubkey())
328+
.fee_rate(fee_rate)
329+
.enable_rbf();
330+
tx_builder
331+
},
332+
};
258333

259334
let mut psbt = match tx_builder.finish() {
260335
Ok(psbt) => {
@@ -267,6 +342,58 @@ where
267342
},
268343
};
269344

345+
// Check the reserve requirements (again) and return an error if they aren't met.
346+
match send_amount {
347+
OnchainSendAmount::ExactRetainingReserve {
348+
amount_sats,
349+
cur_anchor_reserve_sats,
350+
} => {
351+
let balance = locked_wallet.balance();
352+
let spendable_amount_sats = self
353+
.get_balances_inner(balance, cur_anchor_reserve_sats)
354+
.map(|(_, s)| s)
355+
.unwrap_or(0);
356+
let tx_fee_sats = locked_wallet
357+
.calculate_fee(&psbt.unsigned_tx)
358+
.map_err(|e| {
359+
log_error!(
360+
self.logger,
361+
"Failed to calculate fee of candidate transaction: {}",
362+
e
363+
);
364+
e
365+
})?
366+
.to_sat();
367+
if spendable_amount_sats < amount_sats.saturating_add(tx_fee_sats) {
368+
log_error!(self.logger,
369+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats + {}sats fee",
370+
spendable_amount_sats,
371+
amount_sats,
372+
tx_fee_sats,
373+
);
374+
return Err(Error::InsufficientFunds);
375+
}
376+
},
377+
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => {
378+
let balance = locked_wallet.balance();
379+
let spendable_amount_sats = self
380+
.get_balances_inner(balance, cur_anchor_reserve_sats)
381+
.map(|(_, s)| s)
382+
.unwrap_or(0);
383+
let (sent, received) = locked_wallet.sent_and_received(&psbt.unsigned_tx);
384+
let drain_amount = sent - received;
385+
if spendable_amount_sats < drain_amount.to_sat() {
386+
log_error!(self.logger,
387+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}",
388+
spendable_amount_sats,
389+
drain_amount,
390+
);
391+
return Err(Error::InsufficientFunds);
392+
}
393+
},
394+
_ => {},
395+
}
396+
270397
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
271398
Ok(finalized) => {
272399
if !finalized {
@@ -295,21 +422,33 @@ where
295422

296423
let txid = tx.compute_txid();
297424

298-
if let Some(amount) = amount_or_drain {
299-
log_info!(
300-
self.logger,
301-
"Created new transaction {} sending {}sats on-chain to address {}",
302-
txid,
303-
amount.to_sat(),
304-
address
305-
);
306-
} else {
307-
log_info!(
308-
self.logger,
309-
"Created new transaction {} sending all available on-chain funds to address {}",
310-
txid,
311-
address
312-
);
425+
match send_amount {
426+
OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => {
427+
log_info!(
428+
self.logger,
429+
"Created new transaction {} sending {}sats on-chain to address {}",
430+
txid,
431+
amount_sats,
432+
address
433+
);
434+
},
435+
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => {
436+
log_info!(
437+
self.logger,
438+
"Created new transaction {} sending available on-chain funds retaining a reserve of {}sats to address {}",
439+
txid,
440+
cur_anchor_reserve_sats,
441+
address,
442+
);
443+
},
444+
OnchainSendAmount::AllDrainingReserve => {
445+
log_info!(
446+
self.logger,
447+
"Created new transaction {} sending all available on-chain funds to address {}",
448+
txid,
449+
address
450+
);
451+
},
313452
}
314453

315454
Ok(txid)

tests/integration_tests_rust.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ fn onchain_spend_receive() {
305305
assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000);
306306

307307
let addr_b = node_b.onchain_payment().new_address().unwrap();
308-
let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap();
308+
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false).unwrap();
309309
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
310310
wait_for_tx(&electrsd.client, txid);
311311

0 commit comments

Comments
 (0)