What changed, and why it matters
This commit is a routine code cleanup in Electrum's Qt graphical user interface. It creates a reusable 'SwapProvidersButton' widget so that two different parts of the app (the swap dialog and submarine/on-chain payment flow) share the same code for showing and selecting swap providers. There is no security-relevant change visible in the diff.
No security action required. Treat as normal maintenance/refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors swap-provider UI handling by introducing a new SwapProvidersButton class in electrum/gui/qt/swap_dialog.py and using it in both SwapDialog and TxEditor (confirm_tx_dialog.py). It moves button-update logic, NIP-19 pubkey-to-icon conversion, and the choose_swapserver_dialog call into the shared component. It also adds a ‘swap_provider_changed’ callback/event so both dialogs refresh when the provider changes. No cryptographic, network, or permission logic is altered.
Changed components
electrum/gui/qt/swap_dialog.pyelectrum/gui/qt/confirm_tx_dialog.pyInspect captured patch +53 / −45
diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py
index 3935db7..3961c09 100644
--- a/electrum/gui/qt/confirm_tx_dialog.py
+++ b/electrum/gui/qt/confirm_tx_dialog.py
@@ -28,7 +28,6 @@ from decimal import Decimal
from functools import partial
from typing import TYPE_CHECKING, Optional, Union
-from electrum_aionostr.util import from_nip19
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton,
@@ -47,14 +46,13 @@ from electrum.submarine_swaps import NostrTransport, HttpTransport
from .seed_dialog import seed_warning_msg
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,
- read_QIcon, debug_widget_layouts, qt_event_listener, QtEventListener, IconLabel,
- pubkey_to_q_icon)
+ read_QIcon, debug_widget_layouts, qt_event_listener, QtEventListener, IconLabel)
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
from .fee_slider import FeeSlider, FeeComboBox
from .amountedit import FeerateEdit, BTCAmountEdit
from .locktimeedit import LockTimeEdit
from .my_treeview import QMenuWithConfig
-from .swap_dialog import SwapServerDialog
+from .swap_dialog import SwapProvidersButton
if TYPE_CHECKING:
from .main_window import ElectrumWindow
@@ -303,7 +301,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
self.tab_widget.removeTab(0)
# always show onchain payment tab
- self.tab_widget.addTab(self.onchain_tab, _('Onchain'))
+ self.tab_widget.addTab(self.onchain_tab, _('Onchain Transaction'))
allow_swaps = self.allow_preview and self.payee_outputs # allow_preview is false for ln channel opening txs
if self.config.WALLET_ENABLE_SUBMARINE_PAYMENTS and allow_swaps:
@@ -728,10 +726,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
vbox.addWidget(self.submarine_stacked_widget)
vbox.addStretch(1)
- self.server_button = QPushButton()
- self.server_button.clicked.connect(self.choose_swap_server)
- self.server_button.setEnabled(False)
- self.server_button.setVisible(False)
+ self.server_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
self.submarine_ok_button = QPushButton(_('OK'))
self.submarine_ok_button.setDefault(True)
@@ -830,9 +825,8 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
# we couldn't even connect to the relays, this transport is useless. maybe network issues.
return False
- def choose_swap_server(self) -> None:
- assert isinstance(self.swap_transport, NostrTransport), self.swap_transport
- self.main_window.choose_swapserver_dialog(self.swap_transport)
+ @qt_event_listener
+ def on_event_swap_provider_changed(self):
self.update_submarine_tab()
def start_submarine_swap(self):
@@ -881,20 +875,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
return
# Update the swapserver selection button text
- if isinstance(self.swap_transport, NostrTransport):
- self.server_button.setVisible(True)
- self.server_button.setEnabled(True)
- if self.config.SWAPSERVER_NPUB:
- pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex()
- self.server_button.setIcon(pubkey_to_q_icon(pubkey))
- self.server_button.setText(
- f' {len(self.swap_transport.get_recent_offers())} ' +
- (_('providers') if len(self.swap_transport.get_recent_offers()) != 1 else _('provider'))
- )
- else:
- # HTTPTransport or no Network, not showing server selection button
- self.server_button.setEnabled(False)
- self.server_button.setVisible(False)
+ self.server_button.update()
if not self.swap_manager.is_initialized.is_set():
# connected to nostr relays but couldn't find swapserver announcement
@@ -996,6 +977,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger):
@qt_event_listener
def on_event_swap_offers_changed(self, _):
+ self.server_button.update()
if self.ongoing_swap_transport_connection_attempt \
and not self.ongoing_swap_transport_connection_attempt.done():
return
diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py
index cfa85b9..4c501f4 100644
--- a/electrum/gui/qt/swap_dialog.py
+++ b/electrum/gui/qt/swap_dialog.py
@@ -9,7 +9,7 @@ from PyQt6.QtWidgets import QTreeWidget, QTreeWidgetItem, QHeaderView
from electrum_aionostr.util import from_nip19
from electrum.i18n import _
-from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled
+from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled, trigger_callback
from electrum.bitcoin import DummyAddress
from electrum.transaction import PartialTxOutput, PartialTransaction
from electrum.fee_policy import FeePolicy
@@ -43,6 +43,43 @@ ROLE_NPUB = Qt.ItemDataRole.UserRole + 1000
class InvalidSwapParameters(Exception): pass
+class SwapProvidersButton(QPushButton):
+
+ def __init__(self, transport_getter, config, main_window):
+ """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.update()
+
+ def update(self):
+ transport = self.transport_getter()
+ if not isinstance(transport, NostrTransport):
+ # HTTPTransport or no Network, not showing server selection button
+ self.setEnabled(False)
+ self.setVisible(False)
+ return
+ self.setEnabled(True)
+ self.setVisible(True)
+ offer_count = len(transport.get_recent_offers())
+ button_text = f' {offer_count} ' + (_('swap providers') if offer_count != 1 else _('swap provider'))
+ self.setText(button_text)
+ # update icon
+ if self.config.SWAPSERVER_NPUB:
+ pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex()
+ self.setIcon(pubkey_to_q_icon(pubkey))
+
+ def choose_swap_server(self) -> None:
+ transport = self.transport_getter()
+ assert isinstance(transport, NostrTransport), transport
+ self.main_window.choose_swapserver_dialog(transport) # type: ignore
+ self.update()
+ trigger_callback('swap_provider_changed')
+
+
+
class SwapDialog(WindowModalDialog, QtEventListener):
def __init__(
@@ -63,12 +100,8 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.is_reverse = is_reverse if is_reverse is not None else True
vbox = QVBoxLayout(self)
- self.server_button = QPushButton()
- self.set_server_button_text(len(transport.get_recent_offers()) \
- if not self.config.SWAPSERVER_URL and isinstance(transport, NostrTransport) else 0
- )
- self.server_button.clicked.connect(lambda: self.choose_swap_server(transport))
- self.server_button.setEnabled(not self.config.SWAPSERVER_URL)
+ self.transport = transport
+ self.server_button = SwapProvidersButton(lambda: self.transport, self.config, self.window)
self.description_label = WWLabel(self.get_description())
self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)
self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point)
@@ -158,7 +191,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
@qt_event_listener
def on_event_swap_offers_changed(self, recent_offers: Sequence['SwapOffer']):
- self.set_server_button_text(len(recent_offers))
+ self.server_button.update()
if not self.ok_button.isEnabled():
# only update the dialog with the new offer if the user hasn't entered an amount yet.
# if the user has already entered an amount we prefer the swap to fail due to outdated
@@ -166,9 +199,10 @@ class SwapDialog(WindowModalDialog, QtEventListener):
# due to an update happening just before the user initiated the swap
self.update()
- def set_server_button_text(self, offer_count: int):
- button_text = f' {offer_count} ' + (_('providers') if offer_count != 1 else _('provider'))
- self.server_button.setText(button_text)
+ @qt_event_listener
+ def on_event_swap_provider_changed(self):
+ self.update()
+ self.update_send_receive()
def timer_actions(self):
if self.needs_tx_update:
@@ -298,9 +332,6 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.server_fee_label.setText(server_fee_str)
self.server_fee_label.repaint() # macOS hack for #6269
self.needs_tx_update = True
- # update icon
- pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex() if self.config.SWAPSERVER_NPUB else ''
- self.server_button.setIcon(pubkey_to_q_icon(pubkey))
def get_client_swap_limits_sat(self) -> Tuple[int, int]:
"""Returns the (min, max) client swap limits in sat."""
@@ -452,11 +483,6 @@ class SwapDialog(WindowModalDialog, QtEventListener):
capacityType="receiving" if self.is_reverse else "sending",
)
- def choose_swap_server(self, transport: 'SwapServerTransport') -> None:
- self.window.choose_swapserver_dialog(transport) # type: ignore
- self.update()
- self.update_send_receive()
-
class SwapServerDialog(WindowModalDialog, QtEventListener):
Why this scored 15/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.