diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 43ce7a716..e4d31f23f 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -28,7 +28,8 @@ from enum import IntEnum from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView, - QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout + QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout, + QListWidget, QListWidgetItem, ) from PyQt6.QtGui import QIntValidator @@ -37,10 +38,11 @@ from electrum import blockchain from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port from electrum.logging import get_logger +from electrum.util import is_valid_websocket_url from .util import ( Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener, - qt_event_listener, Spinner + qt_event_listener, Spinner, HelpLabel ) @@ -56,11 +58,12 @@ class NetworkDialog(QDialog, QtEventListener): self.setWindowTitle(_('Network')) self.setMinimumSize(500, 500) self.tabs = tabs = QTabWidget() - self._blockchain_tab = blockchain_tab = ServerWidget(network) - self._proxy_tab = proxy_tab = ProxyWidget(network) - tabs.addTab(blockchain_tab, _('Overview')) - tabs.addTab(proxy_tab, _('Proxy')) - + self._blockchain_tab = ServerWidget(network) + self._proxy_tab = ProxyWidget(network) + self._nostr_tab = NostrWidget(network) + tabs.addTab(self._blockchain_tab, _('Electrum servers')) + tabs.addTab(self._nostr_tab, _('Nostr')) + tabs.addTab(self._proxy_tab, _('Proxy')) vbox = QVBoxLayout(self) vbox.addWidget(self.tabs) vbox.addLayout(Buttons(CloseButton(self))) @@ -360,6 +363,7 @@ class ProxyWidget(QWidget): class ServerWidget(QWidget, QtEventListener): + def __init__(self, network: Network, parent=None): super().__init__(parent) self.network = network @@ -502,3 +506,55 @@ class ServerWidget(QWidget, QtEventListener): net_params = net_params._replace(server=server, auto_connect=self.autoconnect_cb.isChecked()) self.network.run_from_another_thread(self.network.set_parameters(net_params)) + + +class NostrWidget(QWidget, QtEventListener): + + def __init__(self, network: Network, parent=None): + super().__init__(parent) + self.network = network + self.config = network.config + vbox = QVBoxLayout() + self.setLayout(vbox) + nostr_relays_label = HelpLabel.from_configvar(self.config.cv.NOSTR_RELAYS) + self.relays_list = QListWidget() + self.relay_edit = QLineEdit() + self.relay_edit.textChanged.connect(self.on_relay_edited) + vbox.addWidget(nostr_relays_label) + vbox.addWidget(self.relays_list) + vbox.addStretch() + self.add_button = QPushButton(_('Add')) + self.add_button.clicked.connect(self.add_relay) + self.add_button.setEnabled(False) + remove_button = QPushButton(_('Remove')) + remove_button.clicked.connect(self.remove_relay) + reset_button = QPushButton(_('Reset')) + reset_button.clicked.connect(self.reset_relays) + buttons = Buttons(self.relay_edit, self.add_button, remove_button, reset_button) + vbox.addLayout(buttons) + self.update_list() + + def on_relay_edited(self, text): + self.add_button.setEnabled(is_valid_websocket_url(text)) + + def update_list(self): + self.relays_list.clear() + for relay in self.config.get_nostr_relays(): + item = QListWidgetItem(relay) + self.relays_list.addItem(item) + + def add_relay(self): + relay = self.relay_edit.text() + self.config.add_nostr_relay(relay) + self.update_list() + + def remove_relay(self): + item = self.relays_list.currentItem() + if item is None: + return + self.config.remove_nostr_relay(item.text()) + self.update_list() + + def reset_relays(self): + self.config.NOSTR_RELAYS = None + self.update_list() diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 99a3c6eda..beda9ec57 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (QComboBox, QTabWidget, QDialog, QSpinBox, QCheckB from electrum.i18n import _, languages from electrum import util -from electrum.util import base_units_list, event_listener, is_valid_websocket_url +from electrum.util import base_units_list, event_listener from electrum.gui import messages @@ -176,22 +176,6 @@ class SettingsDialog(QDialog, QtEventListener): self.set_alias_color() self.alias_e.editingFinished.connect(self.on_alias_edit) - nostr_relays_label = HelpLabel.from_configvar(self.config.cv.NOSTR_RELAYS) - nostr_relays = self.config.NOSTR_RELAYS - self.nostr_relays_e = QLineEdit(nostr_relays) - - def on_nostr_edit(): - relays: Dict[str, None] = dict() # dicts keep insertion order - for url in self.nostr_relays_e.text().split(','): - url = url.strip() - if url and is_valid_websocket_url(url): - relays[url] = None - if relays.keys(): - self.config.NOSTR_RELAYS = ",".join(relays.keys()) - else: # if no valid relays are given, assign default relays from config - self.config.NOSTR_RELAYS = None - self.nostr_relays_e.setText(self.config.NOSTR_RELAYS) - self.nostr_relays_e.editingFinished.connect(on_nostr_edit) msat_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_PREC_POST_SAT) msat_cb.setChecked(self.config.BTC_AMOUNTS_PREC_POST_SAT > 0) @@ -402,7 +386,6 @@ class SettingsDialog(QDialog, QtEventListener): misc_widgets = [] misc_widgets.append((updatecheck_cb, None)) misc_widgets.append((filelogging_cb, None)) - misc_widgets.append((nostr_relays_label, self.nostr_relays_e)) misc_widgets.append((alias_label, self.alias_e)) misc_widgets.append((qr_label, qr_combo)) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4ff5a4efd..c02be88df 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1,30 +1,23 @@ import json import threading -import time import os import stat -from decimal import Decimal -from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set, Callable, AbstractSet -from numbers import Real +from typing import Union, Optional, Dict, Sequence, Any, Set, Callable, AbstractSet from functools import cached_property from copy import deepcopy from . import util -from . import constants from . import invoices from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT from .util import format_satoshis, format_fee_satoshis, os_chmod from .util import user_dir, make_dir +from .util import is_valid_websocket_url from .lnutil import LN_MAX_FUNDING_SAT_LEGACY from .i18n import _ from .logging import get_logger, Logger - - - - _logger = get_logger(__name__) @@ -182,7 +175,6 @@ class SimpleConfig(Logger): # a thread-safe way. self.lock = threading.RLock() - # The following two functions are there for dependency injection when # testing. if read_user_config_function is None: @@ -401,14 +393,12 @@ class SimpleConfig(Logger): def convert_version_3(self): if not self._is_upgrade_method_needed(2, 2): return - base_unit = self.user_config.get('base_unit') if isinstance(base_unit, str): self._set_key_in_user_config('base_unit', None) - map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0} + map_ = {'btc': 8, 'mbtc': 5, 'ubtc': 2, 'bits': 2, 'sat': 0} decimal_point = map_.get(base_unit.lower()) self._set_key_in_user_config('decimal_point', decimal_point) - self.set_key('config_version', 3) def _is_upgrade_method_needed(self, min_version, max_version): @@ -557,6 +547,26 @@ class SimpleConfig(Logger): def get_decimal_point(self): return self.decimal_point + def get_nostr_relays(self) -> Sequence[str]: + relays = [] + for url in self.NOSTR_RELAYS.split(','): + url = url.strip() + if url and is_valid_websocket_url(url): + relays.append(url) + return relays + + def add_nostr_relay(self, relay: str): + l = self.get_nostr_relays() + if is_valid_websocket_url(relay) and relay not in l: + l.append(relay) + self.NOSTR_RELAYS = ','.join(l) + + def remove_nostr_relay(self, relay: str): + l = self.get_nostr_relays() + if relay in l: + l.remove(relay) + self.NOSTR_RELAYS = ','.join(l) + def __setattr__(self, name, value): """Disallows setting instance attributes outside __init__. @@ -719,9 +729,9 @@ Warning: setting this to too low will result in lots of payment failures."""), TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) - FEE_POLICY = ConfigVar('fee_policy', default='eta:2', type_=str) # exposed to GUI - FEE_POLICY_LIGHTNING = ConfigVar('fee_policy_lightning', default='eta:2', type_=str) # for txbatcher (sweeping) - FEE_POLICY_SWAPS = ConfigVar('fee_policy_swaps', default='eta:2', type_=str) # for txbatcher (sweeping and sending if we are a swapserver) + FEE_POLICY = ConfigVar('fee_policy', default='eta:2', type_=str) # exposed to GUI + FEE_POLICY_LIGHTNING = ConfigVar('fee_policy_lightning', default='eta:2', type_=str) # for txbatcher (sweeping) + FEE_POLICY_SWAPS = ConfigVar('fee_policy_swaps', default='eta:2', type_=str) # for txbatcher (sweeping and sending if we are a swapserver) RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str) RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)