From 48916f56bc8b0fb2b5a2b0826c73494699ecaefa Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 2 Mar 2026 22:13:09 +0100 Subject: [PATCH] qt: perform 'fully spend' action with coin selection, keep separate from coin control when doing action. Also stop timer when dialog is finished, to avoid re-generating txs with the same input coin set, which results in an exception as these coins have signatures when the swap has started. --- electrum/gui/qt/main_window.py | 16 ++++++++++------ electrum/gui/qt/new_channel_dialog.py | 17 +++++++++++++---- electrum/gui/qt/swap_dialog.py | 15 +++++++++++---- electrum/gui/qt/utxo_list.py | 16 ++++------------ 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 63818cb60..81e2437e1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1264,9 +1264,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def run_swap_dialog( self, + *, is_reverse: Optional[bool] = None, recv_amount_sat_or_max: Optional[Union[int, str]] = None, channels: Optional[Sequence['Channel']] = None, + get_coins: Optional[Callable[..., Sequence[PartialTxInput]]] = None, ) -> bool: if not self.network: self.show_error(_("You are offline.")) @@ -1290,7 +1292,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): transport, is_reverse=is_reverse, recv_amount_sat_or_max=recv_amount_sat_or_max, - channels=channels + channels=channels, + get_coins=get_coins ) try: return d.run(transport) @@ -1484,17 +1487,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): msg = _('Signing transaction...') WaitingDialog(self, msg, task, on_success, on_failure) - def mktx_for_open_channel(self, *, funding_sat, node_id): + def mktx_for_open_channel(self, *, funding_sat, node_id, get_coins=None): def make_tx(fee_policy, *, confirmed_only=False, base_tx=None): assert base_tx is None + coins = get_coins() if get_coins else self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only) return self.wallet.lnworker.mktx_for_open_channel( - coins=self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), + coins=coins, funding_sat=funding_sat, node_id=node_id, fee_policy=fee_policy) return make_tx - def open_channel(self, connect_str, funding_sat, push_amt): + def open_channel(self, connect_str, funding_sat, *, push_amt=0, get_coins=None): try: node_id, rest = extract_nodeid(connect_str) except ConnStringFormatError as e: @@ -1505,7 +1509,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if not self.question(msg): return # we need to know the fee before we broadcast, because the txid is required - make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id) + make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id, get_coins=get_coins) funding_tx, _, _ = self.confirm_tx_dialog(make_tx, funding_sat, context=TxEditorContext.CHANNEL_FUNDING) if not funding_tx: return @@ -2027,7 +2031,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): msg = _('Do you want to create your first channel?') + '\n\n' + messages.MSG_LIGHTNING_WARNING if not self.question(msg): return - d = NewChannelDialog(self, amount_sat, min_amount_sat) + d = NewChannelDialog(self, amount_sat=amount_sat, min_amount_sat=min_amount_sat) return d.run() def new_contact_dialog(self): diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index 85f575d8d..e8af1ea65 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Callable, Sequence from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QComboBox, QLineEdit, QHBoxLayout import electrum_ecc as ecc @@ -16,12 +16,20 @@ from .amountedit import BTCAmountEdit from .my_treeview import create_toolbar_with_menu if TYPE_CHECKING: + from electrum.transaction import PartialTxInput from .main_window import ElectrumWindow class NewChannelDialog(WindowModalDialog): - def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, min_amount_sat: Optional[int] = None): + def __init__( + self, + window: 'ElectrumWindow', + *, + amount_sat: Optional[int] = None, + min_amount_sat: Optional[int] = None, + get_coins: Optional[Callable[..., Sequence['PartialTxInput']]] = None, + ): WindowModalDialog.__init__(self, window, _('Open Channel')) self.window = window self.network = window.network @@ -30,6 +38,7 @@ class NewChannelDialog(WindowModalDialog): self.trampolines = hardcoded_trampoline_nodes() self.trampoline_names = list(self.trampolines.keys()) self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT + self.get_coins = get_coins vbox = QVBoxLayout(self) toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( @@ -141,7 +150,7 @@ class NewChannelDialog(WindowModalDialog): if not self.max_button.isChecked(): return dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True) - make_tx = self.window.mktx_for_open_channel(funding_sat='!', node_id=dummy_nodeid) + make_tx = self.window.mktx_for_open_channel(funding_sat='!', node_id=dummy_nodeid, get_coins=self.get_coins) try: tx = make_tx(FeePolicy(self.config.FEE_POLICY)) except (NotEnoughFunds, NoDynamicFeeEstimates) as e: @@ -176,5 +185,5 @@ class NewChannelDialog(WindowModalDialog): connect_str = str(self.trampolines[name]) if not connect_str: return - self.window.open_channel(connect_str, funding_sat, 0) + self.window.open_channel(connect_str, funding_sat, get_coins=self.get_coins) return True diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 667620540..01a9b790e 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -11,7 +11,7 @@ from electrum_aionostr.util import from_nip19 from electrum.i18n import _ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled, trigger_callback from electrum.bitcoin import DummyAddress -from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.transaction import PartialTxOutput, PartialTransaction, PartialTxInput from electrum.fee_policy import FeePolicy from electrum.submarine_swaps import NostrTransport @@ -92,16 +92,16 @@ class SwapProvidersButton(QPushButton): trigger_callback('swap_provider_changed') - class SwapDialog(WindowModalDialog, QtEventListener): - def __init__( self, window: 'ElectrumWindow', transport: 'SwapServerTransport', + *, is_reverse: Optional[bool] = None, recv_amount_sat_or_max: Optional[Union[int, str]] = None, # sat or '!' channels: Optional[Sequence['Channel']] = None, + get_coins: Optional[Callable[..., Sequence['PartialTxInput']]] = None, ): WindowModalDialog.__init__(self, window, _('Submarine Swap')) self.window = window @@ -111,6 +111,8 @@ class SwapDialog(WindowModalDialog, QtEventListener): self.network = window.network self.channels = channels self.is_reverse = is_reverse if is_reverse is not None else True + self.get_coins = get_coins + vbox = QVBoxLayout(self) self.transport = transport @@ -187,6 +189,8 @@ class SwapDialog(WindowModalDialog, QtEventListener): self.timer.timeout.connect(self.timer_actions) self.timer.start() + self.finished.connect(self.on_finished) + self.fee_slider.update() self.register_callbacks() @@ -194,6 +198,9 @@ class SwapDialog(WindowModalDialog, QtEventListener): self.unregister_callbacks() event.accept() + def on_finished(self, *args): + self.timer.stop() + @qt_event_listener def on_event_fee_histogram(self, *args): self.update_send_receive() @@ -425,7 +432,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): assert not self.is_reverse if onchain_amount is None: raise InvalidSwapParameters("onchain_amount is None") - coins = self.window.get_coins() + coins = self.get_coins() if self.get_coins else self.window.get_coins() if onchain_amount == '!': max_amount = sum(c.value_sats() for c in coins) max_swap_amount = self.swap_manager.client_max_amount_forward_swap() diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 5d9c0e49e..94c94e0ef 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -260,10 +260,7 @@ class UTXOList(MyTreeView): def swap_coins(self, coins: list[PartialTxInput]) -> None: assert coins, "no coins selected?" - #self.clear_coincontrol() - self.add_to_coincontrol(coins) - self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max='!') - self.clear_coincontrol() + self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max='!', get_coins=lambda *args, **kwargs: coins) def can_open_channel(self, coins): if self.wallet.lnworker is None: @@ -274,9 +271,7 @@ class UTXOList(MyTreeView): def open_channel_with_coins(self, coins: list[PartialTxInput]) -> None: assert coins, "no coins selected?" # todo : use a single dialog in new flow - #self.clear_coincontrol() - self.add_to_coincontrol(coins) - d = NewChannelDialog(self.main_window) + d = NewChannelDialog(self.main_window, get_coins=lambda *args, **kwargs: coins) d.max_button.setChecked(True) d.max_button.setEnabled(False) d.min_button.setEnabled(False) @@ -284,7 +279,6 @@ class UTXOList(MyTreeView): d.amount_e.setFrozen(True) d.spend_max() d.run() - self.clear_coincontrol() def clipboard_contains_address(self) -> bool: text = self.main_window.app.clipboard().text() @@ -297,10 +291,8 @@ class UTXOList(MyTreeView): return addr = self.main_window.app.clipboard().text() outputs = [PartialTxOutput.from_address_and_value(addr, '!')] - #self.clear_coincontrol() - self.add_to_coincontrol(coins) - self.main_window.send_tab.pay_onchain_dialog(outputs) - self.clear_coincontrol() + + self.main_window.send_tab.pay_onchain_dialog(outputs, get_coins=lambda *args, **kwargs: coins) def on_double_click(self, idx): outpoint = idx.sibling(idx.row(), self.Columns.OUTPOINT).data(self.ROLE_PREVOUT_STR)