From c3d1b2046a5397b0430c1615e0f3aa82659b9dd6 Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 26 Jan 2026 12:07:04 +0100 Subject: [PATCH 1/4] qt: ServerWidget/wizard: validate server_e The `ServerWidget` didn't validate the input of server_e, which allowed the user to enter an invalid server connection string and exit the dialog without knowing they entered an invalid string. In the wizard this behavior is very misleading as the user explicitly wanted to use a custom server, can click next after entering an invalid server, and we will just silently fall back to automatic server selection. This change will disable the "Next" button in the wizard if an invalid address has been entered and show a red background in the server_e while the input is not valid, so the user clearly sees that this input is not going to be used. --- electrum/gui/qt/network_dialog.py | 13 +++++++++++++ electrum/gui/qt/wizard/server_connect.py | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index ad4ff4823..9545e45b7 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -361,6 +361,8 @@ class ServerWidget(QWidget, QtEventListener): ConnectMode.ONESERVER: messages.MSG_CONNECTMODE_ONESERVER, } + server_e_valid = pyqtSignal(bool) + def __init__(self, network: Network, parent=None): super().__init__(parent) self.network = network @@ -390,6 +392,7 @@ class ServerWidget(QWidget, QtEventListener): grid.addWidget(self.connect_combo, 0, 1, 1, 3) self.server_e = QLineEdit() + self.server_e.textChanged.connect(self.validate_server_e) self.server_e.editingFinished.connect(self.on_server_settings_changed) grid.addWidget(QLabel(_('Server') + ':'), 1, 0) grid.addWidget(self.server_e, 1, 1, 1, 3) @@ -502,6 +505,7 @@ class ServerWidget(QWidget, QtEventListener): self.status_label_header, self.status_label, self.status_label_helpbutton, self.height_label_header, self.height_label, self.height_label_helpbutton]: item.setVisible(self.network._was_started) + self.validate_server_e() msg = _('Fork detection disabled') if self.is_one_server() else '' if self.network._was_started: # Network was started, so we don't run in initial setup wizard. @@ -522,6 +526,15 @@ class ServerWidget(QWidget, QtEventListener): msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size()) self.split_label.setText(msg) + def validate_server_e(self): + if not self.server_e.isEnabled(): + self.server_e.setStyleSheet("") + self.server_e_valid.emit(True) + return + server = ServerAddr.from_str_with_inference(self.server_e.text()) + self.server_e.setStyleSheet("background-color: rgba(255, 0, 0, 0.2);" if not server else "") + self.server_e_valid.emit(server is not None) + def update_from_config(self): auto_connect = self.config.NETWORK_AUTO_CONNECT one_server = self.config.NETWORK_ONESERVER diff --git a/electrum/gui/qt/wizard/server_connect.py b/electrum/gui/qt/wizard/server_connect.py index c7a5f3d2b..19b403d13 100644 --- a/electrum/gui/qt/wizard/server_connect.py +++ b/electrum/gui/qt/wizard/server_connect.py @@ -91,7 +91,10 @@ class WCServerConfig(WizardComponent): WizardComponent.__init__(self, parent, wizard, title=_('Server')) self.sw = ServerWidget(wizard._daemon.network, self) self.layout().addWidget(self.sw) - self._valid = True + self.sw.server_e_valid.connect(self.on_server_e_valid) + + def on_server_e_valid(self, valid): + self.valid = valid def apply(self): self.wizard_data['autoconnect'] = self.sw.server_e.text().strip() == '' From 5952d8c6147d51e5c22234ae2fddbe7f5a75feb3 Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 26 Jan 2026 14:29:58 +0100 Subject: [PATCH 2/4] qml: validate server address in ServerConfig Same as Qt, validate the server address the user enters and prevent the user from continuing in the wizard or clicking 'Ok' in the ServerConfig if the entered address is clearly invalid. --- .../gui/qml/components/ServerConfigDialog.qml | 1 + .../qml/components/controls/ServerConfig.qml | 26 +++++++++++++++++++ .../qml/components/wizard/WCServerConfig.qml | 2 +- electrum/gui/qml/qenetwork.py | 4 +++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ServerConfigDialog.qml b/electrum/gui/qml/components/ServerConfigDialog.qml index 80cc54a31..9fed62953 100644 --- a/electrum/gui/qml/components/ServerConfigDialog.qml +++ b/electrum/gui/qml/components/ServerConfigDialog.qml @@ -39,6 +39,7 @@ ElDialog { FlatButton { Layout.fillWidth: true text: qsTr('Ok') + enabled: serverconfig.addressValid icon.source: '../../icons/confirmed.png' onClicked: { let auto_connect = serverconfig.serverConnectMode == ServerConnectModeComboBox.Mode.Autoconnect diff --git a/electrum/gui/qml/components/controls/ServerConfig.qml b/electrum/gui/qml/components/controls/ServerConfig.qml index 47bd4e680..aae493bae 100644 --- a/electrum/gui/qml/components/controls/ServerConfig.qml +++ b/electrum/gui/qml/components/controls/ServerConfig.qml @@ -12,6 +12,7 @@ Item { property bool showAutoselectServer: true property alias address: address_tf.text property alias serverConnectMode: server_connect_mode_cb.currentValue + property alias addressValid: address_tf.valid implicitHeight: rootLayout.height @@ -28,6 +29,11 @@ Item { ServerConnectModeComboBox { id: server_connect_mode_cb + onCurrentValueChanged: { + if (currentValue == ServerConnectModeComboBox.Mode.Autoconnect) { + address_tf.text = "" + } + } } Item { @@ -63,6 +69,26 @@ Item { enabled: server_connect_mode_cb.currentValue != ServerConnectModeComboBox.Mode.Autoconnect width: parent.width inputMethodHints: Qt.ImhNoPredictiveText + + property bool valid: true + + function validate() { + if (!enabled) { + valid = true + return + } + valid = Network.isValidServerAddress(address_tf.text) + } + + onTextChanged: validate() + onEnabledChanged: validate() + + Rectangle { + anchors.fill: parent + color: "red" + opacity: 0.2 + visible: !parent.valid + } } } diff --git a/electrum/gui/qml/components/wizard/WCServerConfig.qml b/electrum/gui/qml/components/wizard/WCServerConfig.qml index bc784d1b6..b0ab17082 100644 --- a/electrum/gui/qml/components/wizard/WCServerConfig.qml +++ b/electrum/gui/qml/components/wizard/WCServerConfig.qml @@ -5,7 +5,7 @@ import QtQuick.Controls import "../controls" WizardComponent { - valid: true + valid: sc.addressValid last: true title: qsTr('Server') diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index b25712d13..fa088822d 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -206,6 +206,10 @@ class QENetwork(QObject, QtEventListener): def server(self): return self._server + @pyqtSlot(str, result=bool) + def isValidServerAddress(self, server: str) -> bool: + return ServerAddr.from_str_with_inference(server) is not None + @pyqtSlot(str, bool, bool) def setServerParameters(self, server_str: str, auto_connect: bool, one_server: bool): net_params = self.network.get_parameters() From 10aecf66fd6823d8082ef370a9a5715925b17cd8 Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 26 Jan 2026 13:28:09 +0100 Subject: [PATCH 3/4] ServerConnectWizard: use default server instead of '' Passes the default server (Network.default_server) to Network.set_parameters instead of an empty string in ServerConnectWizard.do_configure_server to avoid a traceback like in I am not entirely sure how the user in #10437 managed to trigger the exception as Network.set_parameters should already exit early at `not self._was_started` in the Wizard as the network shouldn't have been started yet during the wizard. However passing the default ServerAddr instead of an empty string to set_parameters should avoid this exception. --- electrum/wizard.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index f15ea6de5..0f5516584 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -855,16 +855,18 @@ class ServerConnectWizard(AbstractWizard): def do_configure_server(self, wizard_data: dict): self._logger.debug(f'configuring server: {wizard_data!r}') net_params = self._daemon.network.get_parameters() - server = '' + server = None oneserver = wizard_data.get('one_server', False) if not wizard_data['autoconnect']: - try: - server = ServerAddr.from_str_with_inference(wizard_data['server']) - if not server: - raise Exception('failed to parse server %s' % wizard_data['server']) - except Exception: - return - net_params = net_params._replace(server=server, auto_connect=wizard_data['autoconnect'], oneserver=oneserver) + server = ServerAddr.from_str_with_inference(wizard_data.get('server', '')) + if not server: + self._logger.warn('failed to parse server %s' % wizard_data.get('server', '')) + return # Network._start() will set autoconnect and default server + net_params = net_params._replace( + server=server or net_params.server, + auto_connect=wizard_data['autoconnect'], + oneserver=oneserver, + ) self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params)) def do_configure_autoconnect(self, wizard_data: dict): From 1c5408cccb9f33fc9d7e2896ee56dde9524c54ac Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 26 Jan 2026 13:33:48 +0100 Subject: [PATCH 4/4] ServerConnectWizard: don't set autoconnect on user cancel Don't set the NETWORK_AUTO_CONNECT config if the user checked the custom server config checkbox and then cancels the wizard, so the next time the user starts the wizard they again will have the choice instead of silently being forced into a random server. Currently if the user cancels the wizard during the network config the welcome component will already have set the NETWORK_AUTO_CONNECT config to False (as the user selected the custom server checkbox), preventing the wizard from starting again on the next startup. --- electrum/wizard.py | 12 +++++++----- tests/test_wizard.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 0f5516584..a71e2dba7 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -826,7 +826,7 @@ class ServerConnectWizard(AbstractWizard): self.navmap = { 'welcome': { 'next': lambda d: 'proxy_config' if d['want_proxy'] else 'server_config', - 'accept': self.do_configure_autoconnect, + 'accept': lambda d: self.do_enable_autoconnect(d) if d['autoconnect'] else None, 'last': lambda d: bool(d['autoconnect'] and not d['want_proxy']) }, 'proxy_config': { @@ -869,11 +869,13 @@ class ServerConnectWizard(AbstractWizard): ) self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params)) - def do_configure_autoconnect(self, wizard_data: dict): - self._logger.debug(f'configuring autoconnect: {wizard_data!r}') + def do_enable_autoconnect(self, wizard_data: dict): + # NETWORK_AUTO_CONNECT will only get explicitly set True, 'autoconnect': False means + # the user requested manual server configuration + self._logger.debug(f'enabling autoconnect: {wizard_data!r}') + assert wizard_data.get('autoconnect'), wizard_data if self._daemon.config.cv.NETWORK_AUTO_CONNECT.is_modifiable(): - if wizard_data.get('autoconnect') is not None: - self._daemon.config.NETWORK_AUTO_CONNECT = wizard_data.get('autoconnect') + self._daemon.config.NETWORK_AUTO_CONNECT = True def start(self, *, start_viewstate: WizardViewState = None) -> WizardViewState: self.reset() diff --git a/tests/test_wizard.py b/tests/test_wizard.py index 6bbab88c9..7c5b740a8 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -81,7 +81,7 @@ class ServerConnectWizardTestCase(WizardTestCase): self.assertFalse(w.is_last_view(v_init.view, d)) v = w.resolve_next(v_init.view, d) self.assertEqual('server_config', v.view) - self.assertEqual(False, self.config.NETWORK_AUTO_CONNECT) + self.assertFalse(self.config.cv.NETWORK_AUTO_CONNECT.is_set()) async def test_proxy(self): w = ServerConnectWizard(DaemonMock(self.config)) @@ -110,7 +110,7 @@ class ServerConnectWizardTestCase(WizardTestCase): self.assertFalse(w.is_last_view(v_init.view, d)) v = w.resolve_next(v_init.view, d) self.assertEqual('proxy_config', v.view) - self.assertEqual(False, self.config.NETWORK_AUTO_CONNECT) + self.assertFalse(self.config.cv.NETWORK_AUTO_CONNECT.is_set()) d_proxy = {'enabled': False} d.update({'proxy': d_proxy}) v = w.resolve_next(v.view, d)