The ServerWidget was not working properly, when switching from "Manual Mode" to "Auto Connect" the change wouldn't get saved as it depended on having a correct server string entered (which isn't neccessary for Auto Connect). Also makes the widget behave more sane by cleaning the server input if Auto Connect is enabled and switching to Manual Mode if the user manually selects a server. Update the ServerWidget every time it is shown (on initialization and also when the user opens it again or switches between network dialog tabs). This will clean it up if the user has entered some invalid server and closes it, otherwise this server would stay in the input field until the application is restarted. The list of servers in the ServerWidget allows the user to right click and 'Use as server' on the servers in the list, however internally it was handled differently than what the user would expect when clicking on 'Use as server'. E.g. if the user selects a server in autoconnect mode it would still stay in autoconnect mode so the server could switch again to another server any time? Now it will also change the mode to manual (or stay in single server mode if that was selected before), making it clear that this server will stay selected. If the user clicks on "Follow this branch" the connect mode will get changed to autoconnect as internally we connect to a random interface on this branch.
643 lines
26 KiB
Python
643 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
|
|
servers = self.network.get_servers()
|
|
|
|
use_tor = bool(network.is_proxy_tor)
|
|
|
|
# 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)
|
|
connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])
|
|
protocol = PREFERRED_NETWORK_PROTOCOL
|
|
server_addrs = [
|
|
ServerAddr(_host, port, protocol=protocol)
|
|
for _host, d in servers.items()
|
|
if (port := d.get(protocol))]
|
|
server_addrs.sort(key=lambda x: (-network.is_server_bookmarked(x), str(x)))
|
|
for server in server_addrs:
|
|
if server.host in connected_hosts:
|
|
continue
|
|
if server.host.endswith('.onion') and not use_tor:
|
|
continue
|
|
item = QTreeWidgetItem([server.net_addr_str(), ""])
|
|
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,
|
|
}
|
|
|
|
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.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)
|
|
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 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()
|