c3d1b2046a
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.
642 lines
26 KiB
Python
642 lines
26 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Electrum - lightweight Bitcoin client
|
|
# Copyright (C) 2012 thomasv@gitorious
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation files
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
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,
|
|
QListWidget, QListWidgetItem,
|
|
)
|
|
from PyQt6.QtGui import QIntValidator
|
|
|
|
from electrum.i18n import _
|
|
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 electrum.gui import messages
|
|
|
|
from .util import (
|
|
Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener,
|
|
qt_event_listener, Spinner, HelpLabel
|
|
)
|
|
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
protocol_names = ['TCP', 'SSL']
|
|
protocol_letters = 'ts'
|
|
|
|
|
|
class NetworkDialog(QDialog, QtEventListener):
|
|
def __init__(self, *, network: Network):
|
|
QDialog.__init__(self)
|
|
self.setWindowTitle(_('Network'))
|
|
self.setMinimumSize(500, 500)
|
|
self.tabs = tabs = QTabWidget()
|
|
self._blockchain_tab = ServerWidget(network)
|
|
self._proxy_tab = ProxyWidget(network)
|
|
self._nostr_tab = NostrWidget(network)
|
|
tabs.addTab(self._blockchain_tab, _('Server'))
|
|
tabs.addTab(self._nostr_tab, _('Nostr'))
|
|
tabs.addTab(self._proxy_tab, _('Proxy'))
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.tabs)
|
|
vbox.addLayout(Buttons(CloseButton(self)))
|
|
|
|
def show(self, *, proxy_tab: bool = False):
|
|
super().show()
|
|
self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)
|
|
|
|
|
|
class NodesListWidget(QTreeWidget):
|
|
"""List of connected servers."""
|
|
|
|
SERVER_ADDR_ROLE = Qt.ItemDataRole.UserRole + 100
|
|
CHAIN_ID_ROLE = Qt.ItemDataRole.UserRole + 101
|
|
ITEMTYPE_ROLE = Qt.ItemDataRole.UserRole + 102
|
|
|
|
class ItemType(IntEnum):
|
|
CHAIN = 0
|
|
CONNECTED_SERVER = 1
|
|
DISCONNECTED_SERVER = 2
|
|
TOPLEVEL = 3
|
|
|
|
followServer = pyqtSignal([ServerAddr], arguments=['server'])
|
|
followChain = pyqtSignal([str], arguments=['chain_id'])
|
|
setServer = pyqtSignal([str], arguments=['server'])
|
|
|
|
def __init__(self, *, network: Network):
|
|
QTreeWidget.__init__(self)
|
|
self.setHeaderLabels([_('Server'), _('Height')])
|
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self.create_menu)
|
|
self.network = network
|
|
|
|
def create_menu(self, position):
|
|
item = self.currentItem()
|
|
if not item:
|
|
return
|
|
item_type = item.data(0, self.ITEMTYPE_ROLE)
|
|
menu = QMenu()
|
|
if item_type in [self.ItemType.CONNECTED_SERVER, self.ItemType.DISCONNECTED_SERVER]:
|
|
server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr
|
|
if item_type == self.ItemType.CONNECTED_SERVER:
|
|
def do_follow_server():
|
|
self.followServer.emit(server)
|
|
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_follow_server)
|
|
elif item_type == self.ItemType.DISCONNECTED_SERVER:
|
|
def do_set_server():
|
|
self.setServer.emit(str(server))
|
|
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_set_server)
|
|
|
|
def set_bookmark(*, add: bool):
|
|
self.network.set_server_bookmark(server, add=add)
|
|
self.update()
|
|
|
|
if self.network.is_server_bookmarked(server):
|
|
menu.addAction(read_QIcon("bookmark_remove.png"), _("Remove from bookmarks"), lambda: set_bookmark(add=False))
|
|
else:
|
|
menu.addAction(read_QIcon("bookmark_add.png"), _("Bookmark this server"), lambda: set_bookmark(add=True))
|
|
elif item_type == self.ItemType.CHAIN:
|
|
chain_id = item.data(0, self.CHAIN_ID_ROLE)
|
|
|
|
def do_follow_chain():
|
|
self.followChain.emit(chain_id)
|
|
|
|
menu.addAction(_("Follow this branch"), do_follow_chain)
|
|
else:
|
|
return
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def keyPressEvent(self, event):
|
|
if event.key() in [Qt.Key.Key_F2, Qt.Key.Key_Return, Qt.Key.Key_Enter]:
|
|
self.on_activated(self.currentItem(), self.currentColumn())
|
|
else:
|
|
QTreeWidget.keyPressEvent(self, event)
|
|
|
|
def on_activated(self, item, column):
|
|
# on 'enter' we show the menu
|
|
pt = self.visualItemRect(item).bottomLeft()
|
|
pt.setX(50)
|
|
self.customContextMenuRequested.emit(pt)
|
|
|
|
def update(self):
|
|
self.clear()
|
|
network = self.network
|
|
|
|
# connected servers
|
|
connected_servers_item = QTreeWidgetItem([_("Connected nodes"), ''])
|
|
connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
|
|
chains = network.get_blockchains()
|
|
n_chains = len(chains)
|
|
for chain_id, interfaces in chains.items():
|
|
b = blockchain.blockchains.get(chain_id)
|
|
if b is None:
|
|
continue
|
|
name = b.get_name()
|
|
if n_chains > 1:
|
|
x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
|
|
x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)
|
|
x.setData(0, self.CHAIN_ID_ROLE, b.get_id())
|
|
else:
|
|
x = connected_servers_item
|
|
for i in interfaces:
|
|
item = QTreeWidgetItem([f"{i.server.to_friendly_name()}", '%d'%i.tip])
|
|
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)
|
|
item.setData(0, self.SERVER_ADDR_ROLE, i.server)
|
|
item.setToolTip(0, str(i.server))
|
|
if i == network.interface:
|
|
item.setIcon(0, read_QIcon("chevron-right.png"))
|
|
elif network.is_server_bookmarked(i.server):
|
|
item.setIcon(0, read_QIcon("bookmark.png"))
|
|
x.addChild(item)
|
|
if n_chains > 1:
|
|
connected_servers_item.addChild(x)
|
|
|
|
# disconnected servers
|
|
disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""])
|
|
disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
|
|
for server in network.get_disconnected_server_addrs():
|
|
item = QTreeWidgetItem([server.to_friendly_name(), ""])
|
|
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)
|
|
item.setData(0, self.SERVER_ADDR_ROLE, server)
|
|
if network.is_server_bookmarked(server):
|
|
item.setIcon(0, read_QIcon("bookmark.png"))
|
|
disconnected_servers_item.addChild(item)
|
|
|
|
self.addTopLevelItem(connected_servers_item)
|
|
self.addTopLevelItem(disconnected_servers_item)
|
|
|
|
connected_servers_item.setExpanded(True)
|
|
for i in range(connected_servers_item.childCount()):
|
|
connected_servers_item.child(i).setExpanded(True)
|
|
disconnected_servers_item.setExpanded(True)
|
|
|
|
# headers
|
|
h = self.header()
|
|
h.setStretchLastSection(False)
|
|
h.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
h.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
|
|
super().update()
|
|
|
|
|
|
class ProxyWidget(QWidget):
|
|
PROXY_MODES = {
|
|
'socks4': 'SOCKS4',
|
|
'socks5': 'SOCKS5/TOR'
|
|
}
|
|
|
|
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
|
|
|
|
def __init__(self, network: Network, parent=None):
|
|
super().__init__(parent)
|
|
self.network = network
|
|
self.config = network.config
|
|
|
|
fixed_width_port = 6 * char_width_in_lineedit()
|
|
|
|
# proxy setting.
|
|
self.proxy_cb = QCheckBox(_('Use proxy'))
|
|
self.proxy_mode = QComboBox()
|
|
for k, v in self.PROXY_MODES.items():
|
|
self.proxy_mode.addItem(v, k)
|
|
self.proxy_mode.setCurrentIndex(1)
|
|
self.proxy_host = QLineEdit()
|
|
self.proxy_port = QLineEdit()
|
|
self.proxy_port.setFixedWidth(fixed_width_port)
|
|
self.proxy_port_validator = QIntValidator(1, 65535)
|
|
self.proxy_port.setValidator(self.proxy_port_validator)
|
|
|
|
self.proxy_user = QLineEdit()
|
|
self.proxy_user.setPlaceholderText(_("Proxy username"))
|
|
self.proxy_password = PasswordLineEdit()
|
|
self.proxy_password.setPlaceholderText(_("Proxy password"))
|
|
|
|
grid = QGridLayout(self)
|
|
grid.setSpacing(8)
|
|
|
|
grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
|
|
proxy_helpbutton = HelpButton(
|
|
_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
|
|
grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
|
|
grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
|
|
grid.addWidget(self.proxy_host, 1, 1, 1, 3)
|
|
grid.addWidget(self.proxy_port, 1, 4, 1, 1)
|
|
grid.addWidget(self.proxy_user, 2, 1, 1, 2)
|
|
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
|
|
|
|
detect_l = QHBoxLayout()
|
|
self.detect_button = QPushButton(_('Detect Tor proxy'))
|
|
self.spinner = Spinner()
|
|
self.spinner.setMargin(5)
|
|
detect_l.addWidget(self.detect_button)
|
|
detect_l.addWidget(self.spinner)
|
|
|
|
grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
|
|
spacer = QVBoxLayout()
|
|
spacer.addStretch(1)
|
|
grid.addLayout(spacer, 4, 0, 1, 5)
|
|
|
|
self.update_from_config()
|
|
self.update()
|
|
|
|
# connect signal handlers after init from config
|
|
self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
|
|
self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
|
|
self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.detect_button.clicked.connect(self.detect_tor)
|
|
|
|
self.torProbeFinished.connect(self.on_tor_probe_finished)
|
|
|
|
def update(self):
|
|
enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
|
|
for item in [
|
|
self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
|
|
self.detect_button
|
|
]:
|
|
item.setEnabled(enabled)
|
|
|
|
if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
|
|
return
|
|
|
|
if not is_valid_host(self.proxy_host.text()):
|
|
return
|
|
|
|
net_params = self.network.get_parameters()
|
|
proxy = self.get_proxy_settings()
|
|
net_params = net_params._replace(proxy=proxy)
|
|
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
|
|
|
def update_from_config(self):
|
|
proxy = ProxySettings.from_config(self.config)
|
|
self.proxy_cb.setChecked(proxy.enabled)
|
|
self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
|
|
self.proxy_host.setText(proxy.host)
|
|
self.proxy_port.setText(proxy.port)
|
|
self.proxy_user.setText(proxy.user)
|
|
self.proxy_password.setText(proxy.password)
|
|
|
|
if not self.config.cv.NETWORK_PROXY.is_modifiable():
|
|
for w in [
|
|
self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
|
|
self.proxy_user, self.proxy_password, self.detect_button
|
|
]:
|
|
w.setEnabled(False)
|
|
|
|
def on_proxy_enable_toggle(self):
|
|
# probe if enabled and no pre-existing settings
|
|
# if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
|
|
# self.detect_tor()
|
|
self.update()
|
|
|
|
def on_proxy_settings_changed(self):
|
|
self.update()
|
|
|
|
def get_proxy_settings(self) -> ProxySettings:
|
|
proxy = ProxySettings()
|
|
proxy.enabled = self.proxy_cb.isChecked()
|
|
proxy.mode = self.proxy_mode.currentData()
|
|
proxy.host = self.proxy_host.text()
|
|
proxy.port = self.proxy_port.text()
|
|
proxy.user = self.proxy_user.text()
|
|
proxy.password = self.proxy_password.text()
|
|
return proxy
|
|
|
|
def detect_tor(self):
|
|
self.detect_button.setEnabled(False)
|
|
self.spinner.setVisible(True)
|
|
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
|
|
|
|
@pyqtSlot(str, int)
|
|
def on_tor_probe_finished(self, host: str, port: int):
|
|
self.detect_button.setEnabled(True)
|
|
self.spinner.setVisible(False)
|
|
if host:
|
|
self.proxy_mode.setCurrentIndex(1)
|
|
self.proxy_host.setText(host)
|
|
self.proxy_port.setText(str(port))
|
|
self.update()
|
|
|
|
|
|
class ConnectMode(IntEnum):
|
|
AUTOCONNECT = 0
|
|
MANUAL = 1
|
|
ONESERVER = 2
|
|
|
|
class ServerWidget(QWidget, QtEventListener):
|
|
CONNECT_MODES = {
|
|
ConnectMode.AUTOCONNECT: messages.MSG_CONNECTMODE_AUTOCONNECT,
|
|
ConnectMode.MANUAL: messages.MSG_CONNECTMODE_MANUAL,
|
|
ConnectMode.ONESERVER: messages.MSG_CONNECTMODE_ONESERVER,
|
|
}
|
|
|
|
server_e_valid = pyqtSignal(bool)
|
|
|
|
def __init__(self, network: Network, parent=None):
|
|
super().__init__(parent)
|
|
self.network = network
|
|
self.config = network.config
|
|
|
|
self.setLayout(QVBoxLayout())
|
|
|
|
grid = QGridLayout()
|
|
|
|
self.connect_combo = QComboBox()
|
|
for i, v in sorted(self.CONNECT_MODES.items()):
|
|
self.connect_combo.addItem(v, i)
|
|
self.connect_combo.currentIndexChanged.connect(self.on_server_settings_changed)
|
|
grid.addWidget(QLabel(_('Connection mode') + ':'), 0, 0)
|
|
msg = (
|
|
f"""
|
|
{messages.MSG_CONNECTMODE_SERVER_HELP}<br/><br/>
|
|
{messages.MSG_CONNECTMODE_NODES_HELP}
|
|
<ul>
|
|
<li><b>{messages.MSG_CONNECTMODE_AUTOCONNECT}</b>: {messages.MSG_CONNECTMODE_AUTOCONNECT_HELP}</li>
|
|
<li><b>{messages.MSG_CONNECTMODE_MANUAL}</b>: {messages.MSG_CONNECTMODE_MANUAL_HELP}</li>
|
|
<li><b>{messages.MSG_CONNECTMODE_ONESERVER}</b>: {messages.MSG_CONNECTMODE_ONESERVER_HELP}</li>
|
|
</ul>
|
|
"""
|
|
)
|
|
grid.addWidget(HelpButton(msg), 0, 4)
|
|
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)
|
|
grid.addWidget(HelpButton(messages.MSG_CONNECTMODE_SERVER_HELP), 1, 4)
|
|
|
|
self.status_label_header = QLabel(_('Status') + ':')
|
|
self.status_label = QLabel('')
|
|
self.status_label_helpbutton = HelpButton(messages.MSG_CONNECTMODE_NODES_HELP)
|
|
grid.addWidget(self.status_label_header, 2, 0)
|
|
grid.addWidget(self.status_label, 2, 1, 1, 3)
|
|
grid.addWidget(self.status_label_helpbutton, 2, 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)
|
|
|
|
self.split_label = QLabel('')
|
|
grid.addWidget(self.split_label, 4, 1, 1, 3)
|
|
|
|
self.layout().addLayout(grid)
|
|
|
|
self.nodes_list_widget = NodesListWidget(network=self.network)
|
|
self.nodes_list_widget.followServer.connect(self.follow_server)
|
|
self.nodes_list_widget.followChain.connect(self.follow_branch)
|
|
|
|
def do_set_server(server):
|
|
self.server_e.setText(server)
|
|
if self.is_auto_connect():
|
|
# switch to manual mode as the user manually selected a server
|
|
self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
|
|
self.on_server_settings_changed()
|
|
self.nodes_list_widget.setServer.connect(do_set_server)
|
|
|
|
self.layout().addWidget(self.nodes_list_widget)
|
|
self.nodes_list_widget.update()
|
|
|
|
self.register_callbacks()
|
|
self.destroyed.connect(lambda: self.unregister_callbacks())
|
|
|
|
def showEvent(self, event):
|
|
# gets called every time the ServerWidget is shown, when opening it and when
|
|
# switching between the tabs.
|
|
super().showEvent(event)
|
|
_logger.debug(f"showing ServerWidget")
|
|
# If the user entered garbage the previous time the ServerWidget was open this will restore
|
|
# it back to the current config
|
|
self.update_from_config()
|
|
self.update()
|
|
|
|
@qt_event_listener
|
|
def on_event_network_updated(self):
|
|
self.nodes_list_widget.update() # NOTE: move event handling to widget itself?
|
|
self.update()
|
|
|
|
def is_auto_connect(self):
|
|
return self.connect_combo.currentIndex() == ConnectMode.AUTOCONNECT
|
|
|
|
def is_one_server(self):
|
|
return self.connect_combo.currentIndex() == ConnectMode.ONESERVER
|
|
|
|
def set_connect_mode(self, connect_mode: ConnectMode, *, block_signals = False):
|
|
# if block_signals = True the on_server_settings_changed won't get called when changing the index
|
|
assert isinstance(connect_mode, ConnectMode), connect_mode
|
|
self.connect_combo.blockSignals(block_signals)
|
|
self.connect_combo.setCurrentIndex(connect_mode)
|
|
self.connect_combo.blockSignals(False)
|
|
|
|
def on_server_settings_changed(self):
|
|
if not self.network._was_started:
|
|
self.update()
|
|
return
|
|
|
|
current_net_params = self.network.get_parameters()
|
|
new_server = ServerAddr.from_str_with_inference(self.server_e.text().strip())
|
|
new_server = new_server or current_net_params.server # keep existing server while input is invalid
|
|
|
|
settings_changed = False
|
|
if new_server != current_net_params.server:
|
|
settings_changed = True
|
|
if self.is_auto_connect() != current_net_params.auto_connect:
|
|
settings_changed = True
|
|
if self.is_one_server() != current_net_params.oneserver:
|
|
settings_changed = True
|
|
|
|
if settings_changed:
|
|
_logger.debug(
|
|
f"ServerWidget.on_server_settings_changed:\n"
|
|
f"[server: {current_net_params.server} -> {new_server}]\n"
|
|
f"[auto_connect: {current_net_params.auto_connect} -> {self.is_auto_connect()}]\n"
|
|
f"[oneserver: {current_net_params.oneserver} -> {self.is_one_server()}]"
|
|
)
|
|
self.set_server(
|
|
new_server,
|
|
auto_connect=self.is_auto_connect(),
|
|
one_server=self.is_one_server(),
|
|
)
|
|
self.update()
|
|
|
|
def update(self):
|
|
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not self.is_auto_connect())
|
|
if self.is_auto_connect():
|
|
self.server_e.clear()
|
|
elif not self.server_e.text():
|
|
self.server_e.setText(self.config.NETWORK_SERVER or "")
|
|
for item in [
|
|
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.
|
|
# behavior in this case is to apply changes immediately.
|
|
# Also, we show block height and potential chain tips
|
|
height_str = _('{} blocks').format(self.network.get_local_height())
|
|
self.height_label.setText(height_str)
|
|
self.status_label.setText(self.network.get_status())
|
|
chains = self.network.get_blockchains()
|
|
if len(chains) > 1:
|
|
chain = self.network.blockchain()
|
|
forkpoint = chain.get_max_forkpoint()
|
|
name = chain.get_name()
|
|
msg = _('Fork detected at block {0}').format(forkpoint) + '\n'
|
|
if self.is_auto_connect():
|
|
msg += _('You are following branch {}').format(name)
|
|
else:
|
|
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
|
|
v = ConnectMode.AUTOCONNECT if auto_connect else ConnectMode.ONESERVER if one_server else ConnectMode.MANUAL
|
|
self.set_connect_mode(v)
|
|
|
|
server = self.config.NETWORK_SERVER
|
|
self.server_e.setText(server)
|
|
|
|
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())
|
|
_logger.debug(f"update from config: done")
|
|
|
|
def follow_branch(self, chain_id):
|
|
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
|
# follow_chain_given_id connects to random interface, so set connect_mode back to AUTOCONNECT
|
|
self.set_connect_mode(ConnectMode.AUTOCONNECT, block_signals=True)
|
|
self.update()
|
|
|
|
def follow_server(self, server: ServerAddr):
|
|
try:
|
|
self.network.follow_chain_given_server(server)
|
|
except KeyError:
|
|
_logger.debug(f"follow_server: cannot follow, not connected to {server.net_addr_str()}.")
|
|
return
|
|
|
|
self.server_e.setText(str(server))
|
|
if self.is_auto_connect():
|
|
# the user manually selected a server, so the ConnectMode gets set to MANUAL
|
|
self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
|
|
|
|
self.set_server(
|
|
server=server,
|
|
auto_connect=False,
|
|
one_server=self.is_one_server(),
|
|
)
|
|
self.update()
|
|
|
|
def set_server(self, server: ServerAddr, *, auto_connect: bool, one_server: bool):
|
|
current_net_params = self.network.get_parameters()
|
|
new_net_params = current_net_params._replace(
|
|
server=server,
|
|
auto_connect=auto_connect,
|
|
oneserver=one_server,
|
|
)
|
|
_logger.debug(f"set_server: {new_net_params=}")
|
|
self.network.run_from_another_thread(self.network.set_parameters(new_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)
|
|
grid = QGridLayout()
|
|
nostr_relays_label = QLabel(self.config.cv.NOSTR_RELAYS.get_short_desc())
|
|
nostr_helpbutton = HelpButton(self.config.cv.NOSTR_RELAYS.get_long_desc())
|
|
grid.addWidget(nostr_relays_label, 0, 0)
|
|
grid.addWidget(nostr_helpbutton, 0, 1)
|
|
vbox.addLayout(grid)
|
|
|
|
self.relays_list = QListWidget()
|
|
self.relay_edit = QLineEdit()
|
|
self.relay_edit.textChanged.connect(self.on_relay_edited)
|
|
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()
|