diff --git a/electrum/gui/qml/components/ServerConfigDialog.qml b/electrum/gui/qml/components/ServerConfigDialog.qml index d5304e064..b9679546e 100644 --- a/electrum/gui/qml/components/ServerConfigDialog.qml +++ b/electrum/gui/qml/components/ServerConfigDialog.qml @@ -42,6 +42,9 @@ ElDialog { text: qsTr('Ok') icon.source: '../../icons/confirmed.png' onClicked: { + Network.oneServer = serverconfig.auto_connect + ? false + : serverconfig.one_server Config.autoConnect = serverconfig.auto_connect Network.server = serverconfig.address rootItem.close() diff --git a/electrum/gui/qml/components/controls/ServerConfig.qml b/electrum/gui/qml/components/controls/ServerConfig.qml index 9c7160489..c2704e89a 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 auto_connect: auto_server_cb.checked property alias address: address_tf.text + property alias one_server: one_server_cb.checked implicitHeight: rootLayout.height @@ -26,7 +27,8 @@ Item { id: auto_server_cb visible: showAutoselectServer text: qsTr('Select server automatically') - checked: true + checked: !showAutoselectServer + enabled: !one_server_cb.checked } Label { @@ -45,6 +47,22 @@ Item { } } + RowLayout { + Layout.fillWidth: true + visible: !auto_server_cb.checked && address_tf.text + + CheckBox { + id: one_server_cb + Layout.fillWidth: true + text: qsTr('One server') + } + + HelpButton { + heading: qsTr('One server') + helptext: Config.longDescFor('NETWORK_ONESERVER') + } + } + ColumnLayout { Heading { text: qsTr('Servers') @@ -96,6 +114,7 @@ Item { Component.onCompleted: { root.auto_connect = Config.autoConnectDefined ? Config.autoConnect : false root.address = Network.server + one_server_cb.checked = Network.oneServer // TODO: initial setup should not connect already, is Network.server defined? } } diff --git a/electrum/gui/qml/components/wizard/WCServerConfig.qml b/electrum/gui/qml/components/wizard/WCServerConfig.qml index df114f84a..a48a1bd70 100644 --- a/electrum/gui/qml/components/wizard/WCServerConfig.qml +++ b/electrum/gui/qml/components/wizard/WCServerConfig.qml @@ -12,6 +12,7 @@ WizardComponent { function apply() { wizard_data['autoconnect'] = sc.address.trim() == "" wizard_data['server'] = sc.address + wizard_data['one_server'] = sc.one_server } ColumnLayout { diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 25dc1c24f..a8074647e 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -254,6 +254,18 @@ class QENetwork(QObject, QtEventListener): def isProxyTor(self): return bool(self.network.is_proxy_tor) + @pyqtProperty(bool, notify=statusChanged) + def oneServer(self): + return self.network.oneserver + + @oneServer.setter + def oneServer(self, one_server: bool): + if one_server != self.network.oneserver: + net_params = self.network.get_parameters() + net_params = net_params._replace(oneserver=one_server) + self.network.run_from_another_thread(self.network.set_parameters(net_params)) + self.statusChanged.emit() + @pyqtProperty('QVariant', notify=feeHistogramUpdated) def feeHistogram(self): return self._fee_histogram diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index e4d31f23f..fe99e3887 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -394,23 +394,29 @@ class ServerWidget(QWidget, QtEventListener): grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3) grid.addWidget(HelpButton(msg), 1, 4) + self.one_server_cb = QCheckBox(_('One server')) + self.one_server_cb.setEnabled(self.config.cv.NETWORK_ONESERVER.is_modifiable()) + self.one_server_cb.stateChanged.connect(self.on_server_settings_changed) + grid.addWidget(self.one_server_cb, 2, 0, 1, 3) + grid.addWidget(HelpButton(self.config.cv.NETWORK_ONESERVER.get_long_desc()), 2, 4) + self.server_e = QLineEdit() self.server_e.editingFinished.connect(self.on_server_settings_changed) msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.") - grid.addWidget(QLabel(_('Server') + ':'), 2, 0) - grid.addWidget(self.server_e, 2, 1, 1, 3) - grid.addWidget(HelpButton(msg), 2, 4) + grid.addWidget(QLabel(_('Server') + ':'), 3, 0) + grid.addWidget(self.server_e, 3, 1, 1, 3) + grid.addWidget(HelpButton(msg), 3, 4) msg = _('This is the height of your local copy of the blockchain.') self.height_label_header = QLabel(_('Blockchain') + ':') self.height_label = QLabel('') self.height_label_helpbutton = HelpButton(msg) - grid.addWidget(self.height_label_header, 3, 0) - grid.addWidget(self.height_label, 3, 1) - grid.addWidget(self.height_label_helpbutton, 3, 4) + grid.addWidget(self.height_label_header, 4, 0) + grid.addWidget(self.height_label, 4, 1) + grid.addWidget(self.height_label_helpbutton, 4, 4) self.split_label = QLabel('') - grid.addWidget(self.split_label, 4, 0, 1, 3) + grid.addWidget(self.split_label, 5, 0, 1, 3) self.layout().addLayout(grid) @@ -442,13 +448,19 @@ class ServerWidget(QWidget, QtEventListener): self.update() return auto_connect = self.autoconnect_cb.isChecked() + one_server = self.one_server_cb.isChecked() + self.autoconnect_cb.setEnabled(not one_server and self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable()) + self.one_server_cb.setEnabled(not auto_connect and self.config.cv.NETWORK_ONESERVER.is_modifiable()) server = self.server_e.text().strip() net_params = self.network.get_parameters() - if server != net_params.server or auto_connect != net_params.auto_connect: + if server != net_params.server or auto_connect != net_params.auto_connect or one_server != net_params.oneserver: self.set_server() def update(self): auto_connect = self.autoconnect_cb.isChecked() + one_server = self.one_server_cb.isChecked() + self.autoconnect_cb.setEnabled(not one_server and self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable()) + self.one_server_cb.setEnabled(not auto_connect and self.config.cv.NETWORK_ONESERVER.is_modifiable()) self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect) for item in [ @@ -479,10 +491,13 @@ class ServerWidget(QWidget, QtEventListener): def update_from_config(self): auto_connect = self.config.NETWORK_AUTO_CONNECT self.autoconnect_cb.setChecked(auto_connect) + one_server = self.config.NETWORK_ONESERVER + self.one_server_cb.setChecked(one_server) server = self.config.NETWORK_SERVER self.server_e.setText(server) - self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable()) + self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable() and not one_server) + self.one_server_cb.setEnabled(self.config.cv.NETWORK_ONESERVER.is_modifiable() and not auto_connect) self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect) self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable()) @@ -504,7 +519,8 @@ class ServerWidget(QWidget, QtEventListener): except Exception: return net_params = net_params._replace(server=server, - auto_connect=self.autoconnect_cb.isChecked()) + auto_connect=self.autoconnect_cb.isChecked(), + oneserver=self.one_server_cb.isChecked()) self.network.run_from_another_thread(self.network.set_parameters(net_params)) diff --git a/electrum/gui/qt/wizard/server_connect.py b/electrum/gui/qt/wizard/server_connect.py index cebe6f146..c7a5f3d2b 100644 --- a/electrum/gui/qt/wizard/server_connect.py +++ b/electrum/gui/qt/wizard/server_connect.py @@ -96,3 +96,4 @@ class WCServerConfig(WizardComponent): def apply(self): self.wizard_data['autoconnect'] = self.sw.server_e.text().strip() == '' self.wizard_data['server'] = self.sw.server_e.text() + self.wizard_data['one_server'] = self.wizard.config.NETWORK_ONESERVER diff --git a/electrum/network.py b/electrum/network.py index 1acc7e7f9..4a5243744 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -601,6 +601,13 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): def _init_parameters_from_config(self) -> None: dns_hacks.configure_dns_resolver() self.auto_connect = self.config.NETWORK_AUTO_CONNECT + if self.auto_connect and self.config.NETWORK_ONESERVER: + # enabling both oneserver and auto_connect doesn't really make sense + # assume oneserver is enabled for privacy reasons, disable auto_connect and assume server is unpredictable + self.logger.warning(f'both "oneserver" and "auto_connect" options enabled, disabling "auto_connect" and resetting "server".') + self.config.NETWORK_SERVER = "" # let _set_default_server set harmless default (localhost) + self.auto_connect = False + self._set_default_server() self._set_proxy(ProxySettings.from_config(self.config)) self._maybe_set_oneserver() @@ -722,7 +729,12 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self.logger.warning(f'failed to parse server-string ({server!r}); falling back to localhost:1:s.') self.default_server = ServerAddr.from_str("localhost:1:s") else: - self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols) + # if oneserver is enabled but no server specified then don't pick a random server + if self.config.NETWORK_ONESERVER: + self.logger.warning(f'"oneserver" option enabled, but no "server" defined; falling back to localhost:1:s.') + self.default_server = ServerAddr.from_str("localhost:1:s") + else: + self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols) assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}" def _set_proxy(self, proxy: ProxySettings): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index c02be88df..19f7f781d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -614,7 +614,12 @@ class SimpleConfig(Logger): # config variables -----> NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) - NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) + NETWORK_ONESERVER = ConfigVar( + 'oneserver', default=False, type_=bool, + short_desc=lambda: _('Connect only to a single Electrum Server'), + long_desc=lambda: _('This is only intended for connecting to your own node. ' + 'Using this option on a public server is a security risk and is discouraged.') + ) NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v) NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str) NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str) diff --git a/electrum/wizard.py b/electrum/wizard.py index c5e748013..9f9dc7676 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -745,6 +745,7 @@ class ServerConnectWizard(AbstractWizard): self._logger.debug(f'configuring server: {wizard_data!r}') net_params = self._daemon.network.get_parameters() server = '' + oneserver = wizard_data.get('one_server', False) if not wizard_data['autoconnect']: try: server = ServerAddr.from_str_with_inference(wizard_data['server']) @@ -752,7 +753,7 @@ class ServerConnectWizard(AbstractWizard): 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']) + net_params = net_params._replace(server=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): diff --git a/tests/test_wizard.py b/tests/test_wizard.py index c3f009faa..350162502 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -112,7 +112,7 @@ class ServerConnectWizardTestCase(WizardTestCase): serverobj = ServerAddr.from_str_with_inference('localhost:1:t') self.assertTrue(w._daemon.network.run_called) - self.assertEqual(NetworkParameters(server=serverobj, proxy=None, auto_connect=False, oneserver=None), w._daemon.network.parameters) + self.assertEqual(NetworkParameters(server=serverobj, proxy=None, auto_connect=False, oneserver=False), w._daemon.network.parameters) class WalletWizardTestCase(WizardTestCase):