From 21fa6114eb9d465fc14c7f30335a26e83ab4c260 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 18 Dec 2025 14:07:43 +0000 Subject: [PATCH 1/2] network: dedupe qt/qml get_disconnected_server_addrs code --- electrum/gui/qml/qeserverlistmodel.py | 32 +++++++++------------------ electrum/gui/qt/network_dialog.py | 16 +------------- electrum/network.py | 25 ++++++++++++++++++--- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index e7817582a..b2e22223b 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -111,28 +111,18 @@ class QEServerListModel(QAbstractListModel, QtEventListener): servers.append(server) # disconnected servers - all_servers = self.network.get_servers() - connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces]) - protocol = PREFERRED_NETWORK_PROTOCOL - for _host, d in sorted(all_servers.items()): - if _host in connected_hosts: - continue - if _host.endswith('.onion') and not self.network.is_proxy_tor: - continue - port = d.get(protocol) - if port: - s = ServerAddr(_host, port, protocol=protocol) - server = { - 'chain': '', - 'chain_height': 0, - 'height': 0, - 'is_primary': False, - 'is_connected': False, - 'name': s.net_addr_str() - } - server['address'] = server['name'] + for s in self.network.get_disconnected_server_addrs(): + server = { + 'chain': '', + 'chain_height': 0, + 'height': 0, + 'is_primary': False, + 'is_connected': False, + 'name': s.net_addr_str() + } + server['address'] = server['name'] - servers.append(server) + servers.append(server) self.beginInsertRows(QModelIndex(), 0, len(servers) - 1) self._servers = servers diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 4d7c68008..2b970643b 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -149,9 +149,6 @@ class NodesListWidget(QTreeWidget): 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"), '']) @@ -185,18 +182,7 @@ class NodesListWidget(QTreeWidget): # 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 + for server in network.get_disconnected_server_addrs(): item = QTreeWidgetItem([server.net_addr_str(), ""]) item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER) item.setData(0, self.SERVER_ADDR_ROLE, server) diff --git a/electrum/network.py b/electrum/network.py index 966a31e6f..2bff22e99 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -30,7 +30,7 @@ import threading import json from typing import ( NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar, - Callable + Callable, Mapping, ) import copy import functools @@ -624,7 +624,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): @with_recent_servers_lock - def get_servers(self): + def get_servers(self) -> Mapping[str, Mapping[str, str]]: # note: order of sources when adding servers here is crucial! # don't let "server_peers" overwrite anything, # otherwise main server can eclipse the client @@ -659,6 +659,25 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): out = filter_noonion(out) return out + def get_disconnected_server_addrs(self) -> Sequence[ServerAddr]: + servers = self.get_servers() + disconnected_server_addrs = [] # type: List[ServerAddr] + chains = self.get_blockchains() + 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))] # FIXME this filters out even bookmarked servers from other protocols + server_addrs.sort(key=lambda x: (-self.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 self.is_proxy_tor: + continue + disconnected_server_addrs.append(server) + return disconnected_server_addrs + def _get_next_server_to_try(self) -> Optional[ServerAddr]: now = time.time() with self.interfaces_lock: @@ -1122,7 +1141,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self._blockchain = interface.blockchain return self._blockchain - def get_blockchains(self): + def get_blockchains(self) -> Mapping[str, Sequence[Interface]]: out = {} # blockchain_id -> list(interfaces) with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items()) with self.interfaces_lock: interfaces_values = list(self.interfaces.values()) From cb3c286fe258fecf90e2a1d667f1da7f6f1c0b23 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 18 Dec 2025 14:27:54 +0000 Subject: [PATCH 2/2] network: disconnected servers: do not filter out bookmarked raw ":t" In the GUI, when displaying the list of disconnected servers, we were filtering to only ":s" servers. Instead now we also show ":t" servers if they are bookmarked. Also, if bookmarked, we also show disconnected .onion servers, even when not using a Tor proxy. fixes https://github.com/spesmilo/electrum/issues/10374 --- electrum/gui/qml/qeserverlistmodel.py | 2 +- electrum/gui/qt/network_dialog.py | 2 +- electrum/interface.py | 6 +++--- electrum/network.py | 24 +++++++++++++++--------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index b2e22223b..b93a078c9 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -118,7 +118,7 @@ class QEServerListModel(QAbstractListModel, QtEventListener): 'height': 0, 'is_primary': False, 'is_connected': False, - 'name': s.net_addr_str() + 'name': s.to_friendly_name() } server['address'] = server['name'] diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 2b970643b..ad4ff4823 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -183,7 +183,7 @@ class NodesListWidget(QTreeWidget): 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.net_addr_str(), ""]) + 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): diff --git a/electrum/interface.py b/electrum/interface.py index b9827cf78..2dead63fa 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -75,9 +75,9 @@ ca_path = certifi.where() BUCKET_NAME_OF_ONION_SERVERS = 'onion' -_KNOWN_NETWORK_PROTOCOLS = {'t', 's'} +KNOWN_ELEC_PROTOCOL_TRANSPORTS = {'t', 's'} PREFERRED_NETWORK_PROTOCOL = 's' -assert PREFERRED_NETWORK_PROTOCOL in _KNOWN_NETWORK_PROTOCOLS +assert PREFERRED_NETWORK_PROTOCOL in KNOWN_ELEC_PROTOCOL_TRANSPORTS MAX_NUM_HEADERS_PER_REQUEST = 2016 assert MAX_NUM_HEADERS_PER_REQUEST >= CHUNK_SIZE @@ -462,7 +462,7 @@ class ServerAddr: net_addr = NetAddress(host, port) # this validates host and port except Exception as e: raise ValueError(f"cannot construct ServerAddr: invalid host or port (host={host}, port={port})") from e - if protocol not in _KNOWN_NETWORK_PROTOCOLS: + if protocol not in KNOWN_ELEC_PROTOCOL_TRANSPORTS: raise ValueError(f"invalid network protocol: {protocol}") self.host = str(net_addr.host) # canonical form (if e.g. IPv6 address) self.port = int(net_addr.port) diff --git a/electrum/network.py b/electrum/network.py index 2bff22e99..c74a0af36 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -53,7 +53,7 @@ from .transaction import Transaction from .blockchain import Blockchain from .interface import ( Interface, PREFERRED_NETWORK_PROTOCOL, RequestTimedOut, NetworkTimeout, BUCKET_NAME_OF_ONION_SERVERS, - NetworkException, RequestCorrupted, ServerAddr, TxBroadcastError, + NetworkException, RequestCorrupted, ServerAddr, TxBroadcastError, KNOWN_ELEC_PROTOCOL_TRANSPORTS, ) from .version import PROTOCOL_VERSION_MIN from .i18n import _ @@ -660,21 +660,27 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): return out def get_disconnected_server_addrs(self) -> Sequence[ServerAddr]: - servers = self.get_servers() + hostmap = self.get_servers() disconnected_server_addrs = [] # type: List[ServerAddr] chains = self.get_blockchains() 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))] # FIXME this filters out even bookmarked servers from other protocols + # convert hostmap to list of ServerAddrs (one-to-many mapping) + server_addrs = [] + for host, portmap in hostmap.items(): + for protocol in KNOWN_ELEC_PROTOCOL_TRANSPORTS: + if port := portmap.get(protocol): + server_addrs.append(ServerAddr(host, port, protocol=protocol)) + # sort bookmarked servers to appear first server_addrs.sort(key=lambda x: (-self.is_server_bookmarked(x), str(x))) + # filter out stuff for server in server_addrs: if server.host in connected_hosts: continue - if server.host.endswith('.onion') and not self.is_proxy_tor: - continue + if not self.is_server_bookmarked(server): + if server.protocol != PREFERRED_NETWORK_PROTOCOL: + continue + if server.host.endswith('.onion') and not self.is_proxy_tor: + continue disconnected_server_addrs.append(server) return disconnected_server_addrs