Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,85 @@ impl Node {
)
}

/// Computes the maximum amount that can be spliced into an existing channel
/// using all confirmed on-chain wallet funds.
///
/// Performs a dry-run drain transaction at the current `ChannelFunding`
/// fee rate, accounting for splice tx fees and the anchor-channels reserve.
/// Returns the wallet's net contribution — directly suitable as the
/// `splice_amount_sats` argument to [`Self::splice_in`].
///
/// Returns `Error::InsufficientFunds` if the wallet's confirmed balance
/// (after reserves and fees) would be below the dust limit, and
/// `Error::ChannelSplicingFailed` if the channel is unknown or not yet
/// ready.
///
/// This is a query helper; it does not initiate any splice negotiation
/// or modify wallet state.
pub fn get_max_splice_in_amount(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
) -> Result<u64, Error> {
let open_channels =
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
let channel_details = open_channels
.iter()
.find(|c| c.user_channel_id == user_channel_id.0)
.ok_or_else(|| {
log_error!(
self.logger,
"Channel not found for user_channel_id {} and counterparty {}",
user_channel_id,
counterparty_node_id
);
Error::ChannelSplicingFailed
})?;

const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;

let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap();

let funding_txo = channel_details.funding_txo.ok_or_else(|| {
log_error!(
self.logger,
"Cannot compute max splice amount: channel not yet ready",
);
Error::ChannelSplicingFailed
})?;

let funding_script = make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh();

let shared_input = Input {
outpoint: funding_txo.into_bitcoin_outpoint(),
previous_utxo: bitcoin::TxOut {
value: Amount::from_sat(channel_details.channel_value_satoshis),
script_pubkey: funding_script.clone(),
},
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};

let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let cur_anchor_reserve_sats =
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);

self.wallet
.get_max_splice_in_amount(
vec![shared_input],
funding_script,
cur_anchor_reserve_sats,
fee_rate,
)
.map(|a| a.to_sat())
.map_err(|()| {
log_error!(
self.logger,
"Insufficient confirmed wallet funds for splice into channel {}",
user_channel_id,
);
Error::InsufficientFunds
})
}

/// Add funds from the on-chain wallet into an existing channel.
///
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
Expand Down
79 changes: 79 additions & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,85 @@ impl Wallet {
Ok(txid)
}

/// Computes the maximum amount that can be spliced into a channel using all
/// confirmed wallet UTXOs, given a fixed foreign-contributed input set
/// (typically the channel's existing funding outpoint).
///
/// Builds a dry-run drain transaction: foreign UTXOs in `must_spend` are
/// added as `add_foreign_utxo`, all confirmed wallet UTXOs are drained,
/// the entire selected value goes to `drain_script` (sized to match the
/// real splice funding output), and `cur_anchor_reserve_sats` is reserved
/// as a wallet change output if non-dust. The returned amount equals
/// drain output value − total foreign input value, i.e. the wallet's net
/// contribution after splice fees and anchor reserves.
///
/// Returns `Err(())` if the resulting wallet contribution is below the
/// dust limit, or if BDK's coin selection fails for any other reason.
pub(crate) fn get_max_splice_in_amount(
&self, must_spend: Vec<Input>, drain_script: ScriptBuf,
cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
) -> Result<Amount, ()> {
// Conservative dust threshold matching the legacy P2PKH dust limit at
// the standard 3 sat/vB relay-fee policy. P2WPKH (294) and P2WSH (330)
// would each be tighter;
const DUST_LIMIT_SATS: u64 = 546;

let mut locked_wallet = self.inner.lock().unwrap();
debug_assert!(matches!(
locked_wallet.public_descriptor(KeychainKind::External),
ExtendedDescriptor::Wpkh(_)
));
debug_assert!(matches!(
locked_wallet.public_descriptor(KeychainKind::Internal),
ExtendedDescriptor::Wpkh(_)
));

let reserve_change_script = (cur_anchor_reserve_sats > DUST_LIMIT_SATS)
.then(|| locked_wallet.peek_address(KeychainKind::Internal, 0).address.script_pubkey());

let mut tx_builder = locked_wallet.build_tx();
tx_builder.only_witness_utxo();

for input in &must_spend {
let psbt_input = psbt::Input {
witness_utxo: Some(input.previous_utxo.clone()),
..Default::default()
};
let weight = Weight::from_wu(input.satisfaction_weight);
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?;
}

tx_builder.drain_wallet().drain_to(drain_script.clone()).fee_rate(fee_rate);
tx_builder.exclude_unconfirmed();

if let Some(script) = reserve_change_script {
tx_builder.add_recipient(script, Amount::from_sat(cur_anchor_reserve_sats));
}

let psbt = tx_builder.finish().map_err(|e| {
log_error!(self.logger, "Failed to compute max splice-in amount: {}", e);
})?;

let drain_output_amount = psbt
.unsigned_tx
.output
.iter()
.find(|o| o.script_pubkey == drain_script)
.map(|o| o.value)
.ok_or(())?;

let foreign_input_total: Amount =
must_spend.iter().map(|i| i.previous_utxo.value).sum();

let wallet_contribution = drain_output_amount.checked_sub(foreign_input_total).ok_or(())?;

if wallet_contribution.to_sat() < DUST_LIMIT_SATS {
return Err(());
}

Ok(wallet_contribution)
}

pub(crate) fn select_confirmed_utxos(
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
) -> Result<Vec<FundingTxInput>, ()> {
Expand Down
Loading