Skip to content

Commit 0223698

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 01c499e commit 0223698

File tree

5 files changed

+153
-53
lines changed

5 files changed

+153
-53
lines changed

bindings/ldk_node.udl

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ interface OnchainPayment {
146146
[Throws=NodeError]
147147
Txid send_to_address([ByRef]Address address, u64 amount_sats);
148148
[Throws=NodeError]
149-
Txid send_all_to_address([ByRef]Address address);
149+
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve);
150150
};
151151

152152
interface UnifiedQrPayment {

src/error.rs

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ impl From<bdk::Error> for Error {
181181
fn from(e: bdk::Error) -> Self {
182182
match e {
183183
bdk::Error::Signer(_) => Self::OnchainTxSigningFailed,
184+
bdk::Error::InsufficientFunds { .. } => Self::InsufficientFunds,
184185
_ => Self::WalletOperationFailed,
185186
}
186187
}

src/payment/onchain.rs

+24-17
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
use crate::config::Config;
44
use crate::error::Error;
5-
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
5+
use crate::logger::{log_info, FilesystemLogger, Logger};
66
use crate::types::{ChannelManager, Wallet};
7+
use crate::wallet::OnchainSendType;
78

89
use bitcoin::{Address, Txid};
910

@@ -53,33 +54,39 @@ impl OnchainPayment {
5354

5455
let cur_anchor_reserve_sats =
5556
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
56-
let spendable_amount_sats =
57-
self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
58-
59-
if spendable_amount_sats < amount_sats {
60-
log_error!(self.logger,
61-
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats",
62-
spendable_amount_sats, amount_sats
63-
);
64-
return Err(Error::InsufficientFunds);
65-
}
66-
self.wallet.send_to_address(address, Some(amount_sats))
57+
let send_amount =
58+
OnchainSendType::SendRetainingReserve { amount_sats, cur_anchor_reserve_sats };
59+
self.wallet.send_to_address(address, send_amount)
6760
}
6861

69-
/// Send an on-chain payment to the given address, draining all the available funds.
62+
/// Send an on-chain payment to the given address, draining the available funds.
7063
///
7164
/// This is useful if you have closed all channels and want to migrate funds to another
7265
/// on-chain wallet.
7366
///
74-
/// Please note that this will **not** retain any on-chain reserves, which might be potentially
67+
/// Please note that if `retain_reserves` is set to `false` this will **not** retain any on-chain reserves, which might be potentially
7568
/// dangerous if you have open Anchor channels for which you can't trust the counterparty to
76-
/// spend the Anchor output after channel closure.
77-
pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
69+
/// spend the Anchor output after channel closure. If `retain_reserves` is set to `true`, this
70+
/// will try to send all spendable onchain funds, i.e.,
71+
/// [`BalanceDetails::spendable_onchain_balance_sats`].
72+
///
73+
/// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats
74+
pub fn send_all_to_address(
75+
&self, address: &bitcoin::Address, retain_reserves: bool,
76+
) -> Result<Txid, Error> {
7877
let rt_lock = self.runtime.read().unwrap();
7978
if rt_lock.is_none() {
8079
return Err(Error::NotRunning);
8180
}
8281

83-
self.wallet.send_to_address(address, None)
82+
let send_amount = if retain_reserves {
83+
let cur_anchor_reserve_sats =
84+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
85+
OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats }
86+
} else {
87+
OnchainSendType::SendAllDrainingReserve
88+
};
89+
90+
self.wallet.send_to_address(address, send_amount)
8491
}
8592
}

src/wallet.rs

+126-34
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ enum WalletSyncStatus {
4343
InProgress { subscribers: tokio::sync::broadcast::Sender<Result<(), Error>> },
4444
}
4545

46+
pub(crate) enum OnchainSendType {
47+
SendRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 },
48+
SendAllRetainingReserve { cur_anchor_reserve_sats: u64 },
49+
SendAllDrainingReserve,
50+
}
51+
4652
pub(crate) struct Wallet<D, B: Deref, E: Deref, L: Deref>
4753
where
4854
D: BatchDatabase,
@@ -231,12 +237,8 @@ where
231237
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
232238
}
233239

234-
/// Send funds to the given address.
235-
///
236-
/// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be
237-
/// spent.
238240
pub(crate) fn send_to_address(
239-
&self, address: &bitcoin::Address, amount_msat_or_drain: Option<u64>,
241+
&self, address: &bitcoin::Address, send_amount: OnchainSendType,
240242
) -> Result<Txid, Error> {
241243
let confirmation_target = ConfirmationTarget::OnchainPayment;
242244
let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
@@ -245,30 +247,108 @@ where
245247
let locked_wallet = self.inner.lock().unwrap();
246248
let mut tx_builder = locked_wallet.build_tx();
247249

248-
if let Some(amount_sats) = amount_msat_or_drain {
249-
tx_builder
250-
.add_recipient(address.script_pubkey(), amount_sats)
251-
.fee_rate(fee_rate)
252-
.enable_rbf();
253-
} else {
254-
tx_builder
255-
.drain_wallet()
256-
.drain_to(address.script_pubkey())
257-
.fee_rate(fee_rate)
258-
.enable_rbf();
250+
// Prepare the tx_builder. We properly check the reserve requirements (again) further down.
251+
match send_amount {
252+
OnchainSendType::SendRetainingReserve { amount_sats, .. } => {
253+
tx_builder
254+
.add_recipient(address.script_pubkey(), amount_sats)
255+
.fee_rate(fee_rate)
256+
.enable_rbf();
257+
},
258+
OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => {
259+
let spendable_amount_sats =
260+
self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
261+
// TODO: can we make this closer resemble the actual transaction?
262+
// As draining the wallet always will only add one output, this method likely
263+
// under-estimates the fee rate a bit.
264+
let mut tmp_tx_builder = locked_wallet.build_tx();
265+
tmp_tx_builder
266+
.drain_wallet()
267+
.drain_to(address.script_pubkey())
268+
.fee_rate(fee_rate)
269+
.enable_rbf();
270+
let tmp_tx_details = match tmp_tx_builder.finish() {
271+
Ok((_, tmp_tx_details)) => tmp_tx_details,
272+
Err(err) => {
273+
log_error!(
274+
self.logger,
275+
"Failed to create temporary transaction: {}",
276+
err
277+
);
278+
return Err(err.into());
279+
},
280+
};
281+
282+
let estimated_tx_fee_sats = tmp_tx_details.fee.unwrap_or(0);
283+
let estimated_spendable_amount_sats =
284+
spendable_amount_sats.saturating_sub(estimated_tx_fee_sats);
285+
286+
if estimated_spendable_amount_sats == 0 {
287+
log_error!(self.logger,
288+
"Unable to send payment without infringing on Anchor reserves. Available: {}sats, estimated fee required: {}sats.",
289+
spendable_amount_sats,
290+
estimated_tx_fee_sats,
291+
);
292+
return Err(Error::InsufficientFunds);
293+
}
294+
295+
tx_builder
296+
.add_recipient(address.script_pubkey(), estimated_spendable_amount_sats)
297+
.fee_absolute(estimated_tx_fee_sats)
298+
.enable_rbf();
299+
},
300+
OnchainSendType::SendAllDrainingReserve => {
301+
tx_builder
302+
.drain_wallet()
303+
.drain_to(address.script_pubkey())
304+
.fee_rate(fee_rate)
305+
.enable_rbf();
306+
},
259307
}
260308

261-
let mut psbt = match tx_builder.finish() {
262-
Ok((psbt, _)) => {
309+
let (mut psbt, tx_details) = match tx_builder.finish() {
310+
Ok((psbt, tx_details)) => {
263311
log_trace!(self.logger, "Created PSBT: {:?}", psbt);
264-
psbt
312+
(psbt, tx_details)
265313
},
266314
Err(err) => {
267315
log_error!(self.logger, "Failed to create transaction: {}", err);
268316
return Err(err.into());
269317
},
270318
};
271319

320+
// Check the reserve requirements (again) and return an error if they aren't met.
321+
match send_amount {
322+
OnchainSendType::SendRetainingReserve { amount_sats, cur_anchor_reserve_sats } => {
323+
let spendable_amount_sats =
324+
self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
325+
let tx_fee_sats = tx_details.fee.unwrap_or(0);
326+
if spendable_amount_sats < amount_sats + tx_fee_sats {
327+
log_error!(self.logger,
328+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats + {}sats fee",
329+
spendable_amount_sats,
330+
amount_sats,
331+
tx_fee_sats,
332+
);
333+
return Err(Error::InsufficientFunds);
334+
}
335+
},
336+
OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => {
337+
let spendable_amount_sats =
338+
self.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
339+
let drain_amount_sats = tx_details.sent - tx_details.received;
340+
if spendable_amount_sats < drain_amount_sats {
341+
log_error!(self.logger,
342+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats",
343+
spendable_amount_sats,
344+
drain_amount_sats,
345+
);
346+
return Err(Error::InsufficientFunds);
347+
}
348+
},
349+
_ => {},
350+
}
351+
272352
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
273353
Ok(finalized) => {
274354
if !finalized {
@@ -287,21 +367,33 @@ where
287367

288368
let txid = tx.txid();
289369

290-
if let Some(amount_sats) = amount_msat_or_drain {
291-
log_info!(
292-
self.logger,
293-
"Created new transaction {} sending {}sats on-chain to address {}",
294-
txid,
295-
amount_sats,
296-
address
297-
);
298-
} else {
299-
log_info!(
300-
self.logger,
301-
"Created new transaction {} sending all available on-chain funds to address {}",
302-
txid,
303-
address
304-
);
370+
match send_amount {
371+
OnchainSendType::SendRetainingReserve { amount_sats, .. } => {
372+
log_info!(
373+
self.logger,
374+
"Created new transaction {} sending {}sats on-chain to address {}",
375+
txid,
376+
amount_sats,
377+
address
378+
);
379+
},
380+
OnchainSendType::SendAllRetainingReserve { cur_anchor_reserve_sats } => {
381+
log_info!(
382+
self.logger,
383+
"Created new transaction {} sending available on-chain funds retaining a reserve of {}sats to address {}",
384+
txid,
385+
address,
386+
cur_anchor_reserve_sats,
387+
);
388+
},
389+
OnchainSendType::SendAllDrainingReserve => {
390+
log_info!(
391+
self.logger,
392+
"Created new transaction {} sending all available on-chain funds to address {}",
393+
txid,
394+
address
395+
);
396+
},
305397
}
306398

307399
Ok(txid)

tests/integration_tests_rust.rs

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

288288
let addr_b = node_b.onchain_payment().new_address().unwrap();
289-
let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap();
289+
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false).unwrap();
290290
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
291291
wait_for_tx(&electrsd.client, txid);
292292

0 commit comments

Comments
 (0)