1
0

Merge pull request #10091 from f321x/reserve_input_check_make_unsigned_transaction

wallet: don't spend reserve utxo to create new reserve utxo
This commit is contained in:
ghost43
2025-07-30 12:27:58 +00:00
committed by GitHub
4 changed files with 83 additions and 13 deletions

View File

@@ -434,7 +434,10 @@ class QETxFinalizer(TxFeeSlider):
self.update_fee_warning_from_tx(tx=tx, invoice_amt=amount)
if self._amount.isMax and not self.warning:
if reserve_sats := sum(txo.value for txo in tx.outputs() if txo.is_utxo_reserve):
if reserve_sats := self._wallet.wallet.tx_keeps_ln_utxo_reserve(
tx,
gui_spend_max=self._amount.isMax
):
reserve_str = self._config.format_amount_and_units(reserve_sats)
self.warning = ' '.join([
_('Warning') + ':',

View File

@@ -544,7 +544,7 @@ class TxEditor(WindowModalDialog):
if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()):
messages.append(_('This transaction will spend unconfirmed coins.'))
# warn if a reserve utxo was added
if reserve_sats := sum(txo.value for txo in self.tx.outputs() if txo.is_utxo_reserve):
if reserve_sats := self.wallet.tx_keeps_ln_utxo_reserve(self.tx, gui_spend_max=bool(self.output_value == '!')):
reserve_str = self.main_window.config.format_amount_and_units(reserve_sats)
messages.append(_('Could not spend max: a security reserve of {} was kept for your Lightning channels.').format(reserve_str))
# warn if we merge from mempool

View File

@@ -1882,8 +1882,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def should_keep_reserve_utxo(
self,
tx_inputs: List[PartialTxInput],
tx_outputs: List[PartialTxOutput],
tx_inputs: Sequence[PartialTxInput],
tx_outputs: Sequence[PartialTxOutput],
is_anchor_channel_opening: bool,
) -> bool:
channels_need_reserve = self.lnworker and self.lnworker.has_anchor_channels()
@@ -1911,6 +1911,19 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def is_low_reserve(self) -> bool:
return self.should_keep_reserve_utxo([], [], False)
def tx_keeps_ln_utxo_reserve(self, tx, *, gui_spend_max: bool) -> Optional[int]:
if reserve_output_amount := sum(txo.value for txo in tx.outputs() if txo.is_utxo_reserve):
# tx has a reserve change output
return reserve_output_amount
if gui_spend_max: # user tried to spend max amount
coins_in_wallet = self.get_spendable_coins(nonlocal_only=False, confirmed_only=False)
amount_in_wallet = sum(c.value_sats() for c in coins_in_wallet)
tx_spend_amount = tx.output_value() + tx.get_fee()
if amount_in_wallet - tx_spend_amount == self.config.LN_UTXO_RESERVE:
# tx keeps exactly LN_UTXO_RESERVE amount sats in the wallet
return self.config.LN_UTXO_RESERVE
return None
@profiler(min_threshold=0.1)
def make_unsigned_transaction(
self, *,
@@ -2049,18 +2062,32 @@ class Abstract_Wallet(ABC, Logger, EventListener):
tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))
fee = fee_estimator(tx.estimated_size())
input_amount = sum(c.value_sats() for c in tx_inputs)
input_amount = sum(c.value_sats() for c in tx_inputs) # may change if reserve is needed
allocated_amount = sum(o.value for o in outputs if not parse_max_spend(o.value))
to_distribute = input_amount - allocated_amount
distribute_amount(to_distribute - fee)
if self.should_keep_reserve_utxo(tx_inputs, outputs, is_anchor_channel_opening):
self.logger.info(f'Adding change output to meet utxo reserve requirements')
change_addr = self.get_change_addresses_for_new_transaction(change_addr)[0]
change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE)
change.is_utxo_reserve = True # for GUI
outputs.append(change)
to_distribute -= change.value
# check if any input of the tx is == LN_UTXO_RESERVE, then we can just remove the input
reserve_sized_input = None
for tx_input in tx_inputs:
if tx_input.value_sats() and tx_input.value_sats() == self.config.LN_UTXO_RESERVE:
reserve_sized_input = tx_input
break
if reserve_sized_input:
self.logger.debug(f'Removing LN_UTXO_RESERVE sized input to keep utxo reserve')
tx_inputs.remove(reserve_sized_input)
to_distribute -= reserve_sized_input.value_sats()
else:
self.logger.info(f'Adding change output to meet utxo reserve requirements')
change_addr = self.get_change_addresses_for_new_transaction(change_addr)[0]
change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE)
change.is_utxo_reserve = True # for GUI
outputs.append(change)
to_distribute -= change.value
assert not self.should_keep_reserve_utxo(tx_inputs, outputs, is_anchor_channel_opening)
tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))
fee = fee_estimator(tx.estimated_size())
distribute_amount(to_distribute - fee)