from typing import TYPE_CHECKING from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject from electrum.logging import get_logger from electrum import constants from electrum.interface import ServerAddr from electrum.simple_config import FEERATE_DEFAULT_RELAY from .util import QtEventListener, event_listener from .qeserverlistmodel import QEServerListModel if TYPE_CHECKING: from .qeconfig import QEConfig from electrum.network import Network class QENetwork(QObject, QtEventListener): _logger = get_logger(__name__) networkUpdated = pyqtSignal() blockchainUpdated = pyqtSignal() heightChanged = pyqtSignal([int], arguments=['height']) # local blockchain height serverHeightChanged = pyqtSignal([int], arguments=['height']) proxySet = pyqtSignal() proxyChanged = pyqtSignal() statusChanged = pyqtSignal() feeHistogramUpdated = pyqtSignal() chaintipsChanged = pyqtSignal() isLaggingChanged = pyqtSignal() gossipUpdated = pyqtSignal() # shared signal for static properties dataChanged = pyqtSignal() _height = 0 _server = "" _is_connected = False _server_status = "" _network_status = "" _chaintips = 1 _islagging = False _fee_histogram = [] _gossipPeers = 0 _gossipUnknownChannels = 0 _gossipDbNodes = 0 _gossipDbChannels = 0 _gossipDbPolicies = 0 def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): super().__init__(parent) assert network, "--offline is not yet implemented for this GUI" # TODO self.network = network self._qeconfig = qeconfig self._serverListModel = None self._height = network.get_local_height() # init here, update event can take a while self._server_height = network.get_server_height() # init here, update event can take a while self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) self._qeconfig.useGossipChanged.connect(self.on_gossip_setting_changed) def on_destroy(self): self.unregister_callbacks() @event_listener def on_event_network_updated(self, *args): self.networkUpdated.emit() self._update_status() @event_listener def on_event_blockchain_updated(self): if self._height != self.network.get_local_height(): self._height = self.network.get_local_height() self._logger.debug('new height: %d' % self._height) self.heightChanged.emit(self._height) self.blockchainUpdated.emit() @event_listener def on_event_default_server_changed(self, *args): self._update_status() @event_listener def on_event_proxy_set(self, *args): self._logger.debug('proxy set') self.proxySet.emit() self.proxyTorChanged.emit() @event_listener def on_event_tor_probed(self, *args): self.proxyTorChanged.emit() def _update_status(self): server = str(self.network.get_parameters().server) if self._server != server: self._server = server self.statusChanged.emit() network_status = self.network.get_status() if self._network_status != network_status: self._logger.debug('network_status updated: %s' % network_status) self._network_status = network_status self.statusChanged.emit() is_connected = self.network.is_connected() if self._is_connected != is_connected: self._is_connected = is_connected self.statusChanged.emit() server_status = self.network.get_connection_status_for_GUI() if self._server_status != server_status: self._logger.debug('server_status updated: %s' % server_status) self._server_status = server_status self.statusChanged.emit() server_height = self.network.get_server_height() if self._server_height != server_height: self._logger.debug(f'server_height updated: {server_height}') self._server_height = server_height self.serverHeightChanged.emit(server_height) chains = len(self.network.get_blockchains()) if chains != self._chaintips: self._logger.debug('chain tips # changed: %d', chains) self._chaintips = chains self.chaintipsChanged.emit() server_lag = self.network.get_local_height() - self.network.get_server_height() if self._islagging ^ (server_lag > 1): self._logger.debug('lagging changed: %s', str(server_lag > 1)) self._islagging = server_lag > 1 self.isLaggingChanged.emit() @event_listener def on_event_status(self, *args): self._update_status() @event_listener def on_event_fee_histogram(self, histogram): self._logger.debug(f'fee histogram updated') self.update_histogram(histogram) def update_histogram(self, histogram): if not histogram: histogram = [[FEERATE_DEFAULT_RELAY/1000,1]] # cap the histogram to a limited number of megabytes bytes_limit=10*1000*1000 bytes_current = 0 capped_histogram = [] for item in sorted(histogram, key=lambda x: x[0], reverse=True): if bytes_current >= bytes_limit: break slot = min(item[1], bytes_limit-bytes_current) bytes_current += slot capped_histogram.append([ max(FEERATE_DEFAULT_RELAY/1000, item[0]), # clamped to [FEERATE_DEFAULT_RELAY/1000,inf[ slot, # width of bucket bytes_current, # cumulative depth at far end of bucket ]) # add clamping attributes for the GUI self._fee_histogram = { 'histogram': capped_histogram, 'total': bytes_current, 'min_fee': capped_histogram[-1][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000, 'max_fee': capped_histogram[0][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000 } self.feeHistogramUpdated.emit() @event_listener def on_event_channel_db(self, num_nodes, num_channels, num_policies): self._logger.debug(f'channel_db: {num_nodes} nodes, {num_channels} channels, {num_policies} policies') self._gossipDbNodes = num_nodes self._gossipDbChannels = num_channels self._gossipDbPolicies = num_policies self.gossipUpdated.emit() @event_listener def on_event_gossip_peers(self, num_peers): self._logger.debug(f'gossip peers {num_peers}') self._gossipPeers = num_peers self.gossipUpdated.emit() @event_listener def on_event_unknown_channels(self, unknown): if unknown == 0 and self._gossipUnknownChannels == 0: # TODO: backend sends a lot of unknown=0 events return self._logger.debug(f'unknown channels {unknown}') self._gossipUnknownChannels = unknown self.gossipUpdated.emit() def on_gossip_setting_changed(self): if not self.network: return if self._qeconfig.useGossip: self.network.start_gossip() else: self.network.run_from_another_thread(self.network.stop_gossip()) @pyqtProperty(int, notify=heightChanged) def height(self): # local blockchain height return self._height @pyqtProperty(int, notify=serverHeightChanged) def serverHeight(self): return self._server_height @pyqtProperty(str, notify=statusChanged) def server(self): return self._server @server.setter def server(self, server: str): net_params = self.network.get_parameters() try: server = ServerAddr.from_str_with_inference(server) if not server: raise Exception('failed to parse') except Exception: return net_params = net_params._replace(server=server, auto_connect=self._qeconfig.autoConnect) self.network.run_from_another_thread(self.network.set_parameters(net_params)) @pyqtProperty(str, notify=statusChanged) def serverWithStatus(self): server = self._server if not self.network.is_connected(): # connecting or disconnected return f'{server} (connecting...)' return server @pyqtProperty(str, notify=statusChanged) def status(self): return self._network_status @pyqtProperty(str, notify=statusChanged) def serverStatus(self): return self.network.get_connection_status_for_GUI() @pyqtProperty(bool, notify=statusChanged) def isConnected(self): return self._is_connected @pyqtProperty(int, notify=chaintipsChanged) def chaintips(self): return self._chaintips @pyqtProperty(bool, notify=isLaggingChanged) def isLagging(self): return self._islagging @pyqtProperty(bool, notify=dataChanged) def isTestNet(self): return constants.net.TESTNET @pyqtProperty(str, notify=dataChanged) def networkName(self): return constants.net.__name__.replace('Bitcoin', '') @pyqtProperty('QVariantMap', notify=proxyChanged) def proxy(self): net_params = self.network.get_parameters() return net_params.proxy if net_params.proxy else {} @proxy.setter def proxy(self, proxy_settings): net_params = self.network.get_parameters() if not proxy_settings['enabled']: proxy_settings = None net_params = net_params._replace(proxy=proxy_settings) self.network.run_from_another_thread(self.network.set_parameters(net_params)) self.proxyChanged.emit() proxyTorChanged = pyqtSignal() @pyqtProperty(bool, notify=proxyTorChanged) def isProxyTor(self): return bool(self.network.is_proxy_tor) @pyqtProperty('QVariant', notify=feeHistogramUpdated) def feeHistogram(self): return self._fee_histogram @pyqtProperty('QVariantMap', notify=gossipUpdated) def gossipInfo(self): return { 'peers': self._gossipPeers, 'unknown_channels': self._gossipUnknownChannels, 'db_nodes': self._gossipDbNodes, 'db_channels': self._gossipDbChannels, 'db_policies': self._gossipDbPolicies } serverListModelChanged = pyqtSignal() @pyqtProperty(QEServerListModel, notify=serverListModelChanged) def serverListModel(self): if self._serverListModel is None: self._serverListModel = QEServerListModel(self.network) return self._serverListModel