Skip to content

Commit a020d7c

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 3b645b3 commit a020d7c

File tree

4 files changed

+152
-52
lines changed

4 files changed

+152
-52
lines changed

src/error.rs

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ impl From<bdk::Error> for Error {
178178
fn from(e: bdk::Error) -> Self {
179179
match e {
180180
bdk::Error::Signer(_) => Self::OnchainTxSigningFailed,
181+
bdk::Error::InsufficientFunds { .. } => Self::InsufficientFunds,
181182
_ => Self::WalletOperationFailed,
182183
}
183184
}

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 struct Wallet<D, B: Deref, E: Deref, L: Deref>
4753
where
4854
D: BatchDatabase,
@@ -233,12 +239,8 @@ where
233239
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
234240
}
235241

236-
/// Send funds to the given address.
237-
///
238-
/// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be
239-
/// spent.
240242
pub(crate) fn send_to_address(
241-
&self, address: &bitcoin::Address, amount_msat_or_drain: Option<u64>,
243+
&self, address: &bitcoin::Address, send_amount: OnchainSendType,
242244
) -> Result<Txid, Error> {
243245
let confirmation_target = ConfirmationTarget::OutputSpendingFee;
244246
let fee_rate = FeeRate::from_sat_per_kwu(
@@ -249,30 +251,108 @@ where
249251
let locked_wallet = self.inner.lock().unwrap();
250252
let mut tx_builder = locked_wallet.build_tx();
251253

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

265-
let mut psbt = match tx_builder.finish() {
266-
Ok((psbt, _)) => {
313+
let (mut psbt, tx_details) = match tx_builder.finish() {
314+
Ok((psbt, tx_details)) => {
267315
log_trace!(self.logger, "Created PSBT: {:?}", psbt);
268-
psbt
316+
(psbt, tx_details)
269317
},
270318
Err(err) => {
271319
log_error!(self.logger, "Failed to create transaction: {}", err);
272320
return Err(err.into());
273321
},
274322
};
275323

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

292372
let txid = tx.txid();
293373

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

311403
Ok(txid)

tests/integration_tests_rust.rs

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

281281
let addr_b = node_b.onchain_payment().new_address().unwrap();
282-
let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap();
282+
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false).unwrap();
283283
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
284284
wait_for_tx(&electrsd.client, txid);
285285

0 commit comments

Comments
 (0)