Skip to content

Commit 615814e

Browse files
committed
prefactor: Create helper function for calculating max balance we can send
DRY up the repeated pattern of building a temporary drain transaction to estimate fees. Both send_all_to_address (AllRetainingReserve) and the upcoming open_channel_with_all / splice_in_with_all need this logic, so extract it into shared helpers.
1 parent 3742391 commit 615814e

File tree

1 file changed

+118
-52
lines changed

1 file changed

+118
-52
lines changed

src/wallet/mod.rs

Lines changed: 118 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ pub(crate) enum OnchainSendAmount {
7171
pub(crate) mod persist;
7272
pub(crate) mod ser;
7373

74+
const DUST_LIMIT_SATS: u64 = 546;
75+
7476
pub(crate) struct Wallet {
7577
// A BDK on-chain wallet.
7678
inner: Mutex<PersistedWallet<KVStoreWalletPersister>>,
@@ -533,6 +535,105 @@ impl Wallet {
533535
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
534536
}
535537

538+
fn build_drain_psbt(
539+
&self, locked_wallet: &mut PersistedWallet<KVStoreWalletPersister>,
540+
drain_script: ScriptBuf, cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
541+
shared_input: Option<&Input>,
542+
) -> Result<Psbt, Error> {
543+
let anchor_address = if cur_anchor_reserve_sats > DUST_LIMIT_SATS {
544+
Some(locked_wallet.peek_address(KeychainKind::Internal, 0))
545+
} else {
546+
None
547+
};
548+
549+
let mut tx_builder = locked_wallet.build_tx();
550+
tx_builder.drain_wallet().drain_to(drain_script).fee_rate(fee_rate);
551+
552+
if let Some(address_info) = anchor_address {
553+
tx_builder.add_recipient(
554+
address_info.address.script_pubkey(),
555+
Amount::from_sat(cur_anchor_reserve_sats),
556+
);
557+
}
558+
559+
if let Some(input) = shared_input {
560+
let psbt_input = psbt::Input {
561+
witness_utxo: Some(input.previous_utxo.clone()),
562+
..Default::default()
563+
};
564+
let weight = Weight::from_wu(input.satisfaction_weight);
565+
tx_builder.only_witness_utxo().exclude_unconfirmed();
566+
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|e| {
567+
log_error!(self.logger, "Failed to add shared input for fee estimation: {e}");
568+
Error::ChannelSplicingFailed
569+
})?;
570+
}
571+
572+
let psbt = tx_builder.finish().map_err(|err| {
573+
log_error!(self.logger, "Failed to create temporary drain transaction: {err}");
574+
err
575+
})?;
576+
577+
Ok(psbt)
578+
}
579+
580+
/// Builds a temporary drain transaction and returns the maximum amount that would be sent to
581+
/// the drain output, along with the PSBT for further inspection.
582+
///
583+
/// The caller is responsible for cancelling the PSBT via `locked_wallet.cancel_tx()`.
584+
fn get_max_drain_amount(
585+
&self, locked_wallet: &mut PersistedWallet<KVStoreWalletPersister>,
586+
drain_script: ScriptBuf, cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
587+
shared_input: Option<&Input>,
588+
) -> Result<(u64, Psbt), Error> {
589+
let balance = locked_wallet.balance();
590+
let spendable_amount_sats =
591+
self.get_balances_inner(balance, cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
592+
593+
if spendable_amount_sats == 0 {
594+
log_error!(
595+
self.logger,
596+
"Unable to determine max amount: no spendable funds available."
597+
);
598+
return Err(Error::InsufficientFunds);
599+
}
600+
601+
let tmp_psbt = self.build_drain_psbt(
602+
locked_wallet,
603+
drain_script.clone(),
604+
cur_anchor_reserve_sats,
605+
fee_rate,
606+
shared_input,
607+
)?;
608+
609+
let drain_output_value = tmp_psbt
610+
.unsigned_tx
611+
.output
612+
.iter()
613+
.find(|o| o.script_pubkey == drain_script)
614+
.map(|o| o.value)
615+
.ok_or_else(|| {
616+
log_error!(self.logger, "Failed to find drain output in temporary transaction");
617+
Error::InsufficientFunds
618+
})?;
619+
620+
let shared_input_value = shared_input.map(|i| i.previous_utxo.value.to_sat()).unwrap_or(0);
621+
622+
let max_amount = drain_output_value.to_sat().saturating_sub(shared_input_value);
623+
624+
if max_amount < DUST_LIMIT_SATS {
625+
log_error!(
626+
self.logger,
627+
"Unable to proceed: available funds would be consumed entirely by fees. \
628+
Available: {spendable_amount_sats}sats, drain output: {}sats.",
629+
drain_output_value.to_sat(),
630+
);
631+
return Err(Error::InsufficientFunds);
632+
}
633+
634+
Ok((max_amount, tmp_psbt))
635+
}
636+
536637
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
537638
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
538639
.map_err(|_| Error::InvalidAddress)?
@@ -556,7 +657,6 @@ impl Wallet {
556657
let mut locked_wallet = self.inner.lock().unwrap();
557658

558659
// Prepare the tx_builder. We properly check the reserve requirements (again) further down.
559-
const DUST_LIMIT_SATS: u64 = 546;
560660
let tx_builder = match send_amount {
561661
OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => {
562662
let mut tx_builder = locked_wallet.build_tx();
@@ -567,63 +667,29 @@ impl Wallet {
567667
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats }
568668
if cur_anchor_reserve_sats > DUST_LIMIT_SATS =>
569669
{
570-
let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0);
571-
let balance = locked_wallet.balance();
572-
let spendable_amount_sats = self
573-
.get_balances_inner(balance, cur_anchor_reserve_sats)
574-
.map(|(_, s)| s)
575-
.unwrap_or(0);
576-
let tmp_tx = {
577-
let mut tmp_tx_builder = locked_wallet.build_tx();
578-
tmp_tx_builder
579-
.drain_wallet()
580-
.drain_to(address.script_pubkey())
581-
.add_recipient(
582-
change_address_info.address.script_pubkey(),
583-
Amount::from_sat(cur_anchor_reserve_sats),
584-
)
585-
.fee_rate(fee_rate);
586-
match tmp_tx_builder.finish() {
587-
Ok(psbt) => psbt.unsigned_tx,
588-
Err(err) => {
589-
log_error!(
590-
self.logger,
591-
"Failed to create temporary transaction: {}",
592-
err
593-
);
594-
return Err(err.into());
595-
},
596-
}
597-
};
670+
let (max_amount, tmp_psbt) = self.get_max_drain_amount(
671+
&mut locked_wallet,
672+
address.script_pubkey(),
673+
cur_anchor_reserve_sats,
674+
fee_rate,
675+
None,
676+
)?;
598677

599-
let estimated_tx_fee = locked_wallet.calculate_fee(&tmp_tx).map_err(|e| {
600-
log_error!(
601-
self.logger,
602-
"Failed to calculate fee of temporary transaction: {}",
678+
let estimated_tx_fee =
679+
locked_wallet.calculate_fee(&tmp_psbt.unsigned_tx).map_err(|e| {
680+
log_error!(
681+
self.logger,
682+
"Failed to calculate fee of temporary transaction: {}",
683+
e
684+
);
603685
e
604-
);
605-
e
606-
})?;
607-
608-
// 'cancel' the transaction to free up any used change addresses
609-
locked_wallet.cancel_tx(&tmp_tx);
610-
611-
let estimated_spendable_amount = Amount::from_sat(
612-
spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat()),
613-
);
686+
})?;
614687

615-
if estimated_spendable_amount == Amount::ZERO {
616-
log_error!(self.logger,
617-
"Unable to send payment without infringing on Anchor reserves. Available: {}sats, estimated fee required: {}sats.",
618-
spendable_amount_sats,
619-
estimated_tx_fee,
620-
);
621-
return Err(Error::InsufficientFunds);
622-
}
688+
locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx);
623689

624690
let mut tx_builder = locked_wallet.build_tx();
625691
tx_builder
626-
.add_recipient(address.script_pubkey(), estimated_spendable_amount)
692+
.add_recipient(address.script_pubkey(), Amount::from_sat(max_amount))
627693
.fee_absolute(estimated_tx_fee);
628694
tx_builder
629695
},

0 commit comments

Comments
 (0)