TxEditor: update dynamically based on swap transport
What changed, and why it matters
This commit is a user-interface improvement for Electrum's transaction editor. It makes the fee display update automatically when a 'send change to Lightning' swap connection is being set up, so the user isn't left waiting with a frozen window. There is no direct evidence in the commit of a security vulnerability being fixed.
No security action required. Treat as a normal UX/refactor commit. If auditing submarine-swap security, review the underlying electrum/submarine_swaps.py transport and swap logic separately, as this commit does not modify it.
Security signals we found
No security-relevant keywords (fix, vulnerability, CVE, exploit, bug, crash, leak, race, bypass) appear in commit title or message.
Diff is limited to Qt GUI code (confirm_tx_dialog.py, swap_dialog.py).
Changes add UI state visibility and async connection triggers rather than altering swap protocol logic.
No bounds checks, sanitization, or privilege changes are introduced.
No vendor or researcher attribution to a security report is present.
Evidence from the diff
The patch refactors Qt transaction-dialog handling of submarine-swap transport state. It replaces a single shared SwapProvidersButton with separate buttons for the on-chain tab and the submarine-payment tab, connects to swap transport earlier when ‘send change to lightning’ is enabled, and updates UI labels dynamically while a connection attempt is in progress. The changes are purely UI/UX and asynchronous wiring; no cryptographic, network-trust, or input-validation fixes are visible.
Changed components
electrum/gui/qt/confirm_tx_dialog.pyelectrum/gui/qt/swap_dialog.pyElectrum Qt transaction editor / submarine swap UIInspect captured patch +53 / −16
diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py
index e793c8a..f59e2ed 100644
--- a/electrum/gui/qt/confirm_tx_dialog.py
+++ b/electrum/gui/qt/confirm_tx_dialog.py
@@ -43,7 +43,7 @@ from electrum.wallet import InternalAddressCorruption
from electrum.bitcoin import DummyAddress
from electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod
from electrum.logging import Logger
-from electrum.submarine_swaps import NostrTransport, HttpTransport
+from electrum.submarine_swaps import NostrTransport, HttpTransport, SwapServerTransport
from electrum.gui.messages import MSG_SUBMARINE_PAYMENT_HELP_TEXT
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,
@@ -100,9 +100,8 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.batching_candidates = batching_candidates
self.swap_manager = self.wallet.lnworker.swap_manager if self.wallet.has_lightning() else None
- self.swap_transport = None # type: Optional[Union[NostrTransport, HttpTransport]]
+ self.swap_transport = None # type: Optional[SwapServerTransport]
self.swap_availability_changed.connect(self.on_swap_availability_changed, Qt.ConnectionType.QueuedConnection)
- self.swapserver_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
self.ongoing_swap_transport_connection_attempt = None # type: Optional[Future]
self.did_swap = False # used to clear the PI on send tab
@@ -137,7 +136,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.tab_widget = QTabWidget()
self.tab_widget.setTabBarAutoHide(True) # hides the tab bar if there is only one tab
self.tab_widget.setContentsMargins(0, 0, 0, 0)
- self.tab_widget.tabBarClicked.connect(self.on_tab_clicked)
+ self.tab_widget.currentChanged.connect(self.on_tab_changed)
self.main_layout = QVBoxLayout()
self.main_layout.addWidget(self.tab_widget)
@@ -179,9 +178,12 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
asyncio.run_coroutine_threadsafe(self.swap_transport.stop(), get_asyncio_loop())
self.swap_transport = None # HTTPTransport doesn't need to be closed
- def on_tab_clicked(self, index):
+ def on_tab_changed(self, index):
if self.tab_widget.widget(index) == self.submarine_payment_tab:
self.prepare_swap_transport()
+ self.update_submarine_payment_tab()
+ else:
+ self.update()
def is_batching(self) -> bool:
return self._base_tx is not None
@@ -472,6 +474,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.update_fee_target()
def create_buttons_bar(self):
+ self.change_to_ln_swap_providers_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
self.preview_button = QPushButton(_('Preview'))
self.preview_button.clicked.connect(self.on_preview)
self.preview_button.setVisible(self.allow_preview)
@@ -479,6 +482,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.ok_button.clicked.connect(self.on_send)
self.ok_button.setDefault(True)
buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button)
+ buttons.insertWidget(0, self.change_to_ln_swap_providers_button)
if self.batching_candidates is not None and len(self.batching_candidates) > 0:
batching_combo = QComboBox()
@@ -509,7 +513,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
can_have_lightning = self.wallet.can_have_lightning()
send_ch_to_ln = self.pref_menu.addConfig(
self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING,
- callback=self.trigger_update,
+ callback=lambda: (self.prepare_swap_transport(), self.trigger_update()), # type: ignore
checked=False if not can_have_lightning else None,
)
sub_payments = self.pref_menu.addConfig(
@@ -595,6 +599,9 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
w.setVisible(b)
def run(self):
+ if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:
+ # if disabled but submarine payments are enabled we only connect once the other tab gets opened
+ self.prepare_swap_transport()
cancelled = not self.exec()
self.stop_editor_updates()
self.deleteLater() # see #3956
@@ -636,6 +643,13 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.fee_label.setText(self.main_window.config.format_amount_and_units(self.tx.get_fee()))
self._update_extra_fees()
+ if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:
+ self.change_to_ln_swap_providers_button.setVisible(True)
+ self.change_to_ln_swap_providers_button.fetching = bool(self.ongoing_swap_transport_connection_attempt)
+ self.change_to_ln_swap_providers_button.update()
+ else:
+ self.change_to_ln_swap_providers_button.setVisible(False)
+
self._update_send_button()
self._update_message()
@@ -665,7 +679,9 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
swap_fees = dummy_output.value - ln_amount_we_recv
swap_fee_msg = " [" + _("Swap fees:") + " " + self.main_window.format_amount_and_units(swap_fees) + "]."
messages.append(swap_msg + swap_fee_msg)
- elif self.config.WALLET_SEND_CHANGE_TO_LIGHTNING and self.tx.has_change():
+ elif self.config.WALLET_SEND_CHANGE_TO_LIGHTNING \
+ and not self.ongoing_swap_transport_connection_attempt \
+ and self.tx.has_change():
swap_msg = _('Will not send change to Lightning')
swap_msg_reason = None
change_amount = sum(c.value for c in self.tx.get_change_outputs() if isinstance(c.value, int))
@@ -684,7 +700,9 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
swap_msg_reason = _('Change amount exceeds the swap providers maximum value of {}.').format(
self.main_window.format_amount_and_units(max_amount)
)
- messages.append(swap_msg + f": {swap_msg_reason}" if swap_msg_reason else '')
+ messages.append(swap_msg + (f": {swap_msg_reason}" if swap_msg_reason else '.'))
+ elif self.ongoing_swap_transport_connection_attempt:
+ messages.append(_("Fetching submarine swap providers..."))
# warn if spending unconf
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.'))
@@ -820,9 +838,9 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
@qt_event_listener
def on_event_swap_offers_changed(self, _):
- self.swapserver_button.update()
- if self.ongoing_swap_transport_connection_attempt \
- and not self.ongoing_swap_transport_connection_attempt.done():
+ self.change_to_ln_swap_providers_button.update()
+ self.submarine_payment_provider_button.update()
+ if self.ongoing_swap_transport_connection_attempt:
return
self.swap_availability_changed.emit()
@@ -832,7 +850,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
if self.tab_widget.currentWidget() == self.submarine_payment_tab:
self.update_submarine_payment_tab()
else:
- pass
+ self.update()
### --- Functionality for reverse submarine swaps to external address ---
def create_submarine_payment_tab(self) -> QWidget:
@@ -885,6 +903,8 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
vbox.addWidget(self.submarine_stacked_widget)
vbox.addStretch(1)
+ self.submarine_payment_provider_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
+
self.submarine_ok_button = QPushButton(_('OK'))
self.submarine_ok_button.setDefault(True)
self.submarine_ok_button.setEnabled(False)
@@ -892,7 +912,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.submarine_ok_button.clicked.connect(self.start_submarine_payment)
buttons = Buttons(CancelButton(self), self.submarine_ok_button)
- buttons.insertWidget(0, self.swapserver_button)
+ buttons.insertWidget(0, self.submarine_payment_provider_button)
vbox.addLayout(buttons)
return tab_widget
@@ -943,13 +963,17 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
if not self.swap_manager:
self.set_submarine_payment_tab_warning(_("Enable Lightning in the 'Channels' tab to use Submarine Swaps."))
return
+ if not self.swap_manager.is_initialized.is_set() \
+ and self.ongoing_swap_transport_connection_attempt:
+ self.show_swap_transport_connection_message()
+ return
if not self.swap_transport:
# couldn't connect to nostr relays or http server didn't respond
self.set_submarine_payment_tab_warning(_("Submarine swap provider unavailable."))
return
# Update the swapserver selection button text
- self.swapserver_button.update()
+ self.submarine_payment_provider_button.update()
if not self.swap_manager.is_initialized.is_set():
# connected to nostr relays but couldn't find swapserver announcement
diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py
index 4c501f4..6676205 100644
--- a/electrum/gui/qt/swap_dialog.py
+++ b/electrum/gui/qt/swap_dialog.py
@@ -1,5 +1,5 @@
import enum
-from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence
+from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence, Callable
from PyQt6.QtCore import pyqtSignal, Qt, QTimer
from PyQt6.QtGui import QIcon, QPixmap, QColor
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
from .main_window import ElectrumWindow
from electrum.submarine_swaps import SwapServerTransport, SwapOffer
from electrum.lnchannel import Channel
+ from electrum.simple_config import SimpleConfig
CANNOT_RECEIVE_WARNING = _(
"""The requested amount is higher than what you can receive in your currently open channels.
@@ -45,16 +46,28 @@ class InvalidSwapParameters(Exception): pass
class SwapProvidersButton(QPushButton):
- def __init__(self, transport_getter, config, main_window):
+ def __init__(
+ self,
+ transport_getter: Callable[[], Optional['SwapServerTransport']],
+ config: 'SimpleConfig',
+ main_window: 'ElectrumWindow',
+ ):
"""parent must have a transport() method"""
QPushButton.__init__(self)
self.config = config
self.transport_getter = transport_getter
self.main_window = main_window
self.clicked.connect(self.choose_swap_server)
+ self.fetching = False
self.update()
def update(self):
+ if self.fetching:
+ self.setEnabled(False)
+ self.setText(_("Fetching..."))
+ self.setVisible(True)
+ return
+
transport = self.transport_getter()
if not isinstance(transport, NostrTransport):
# HTTPTransport or no Network, not showing server selection button
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.