force QEDaemon singleton, and refer to QEDaemon.instance where possible In cases where we would run into circular dependencies, pass the instance also refer to singleton QEConfig instead of passing instance in qeapp.py
281 lines
9.9 KiB
Python
281 lines
9.9 KiB
Python
from typing import TYPE_CHECKING
|
|
|
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
|
|
|
from electrum.logging import get_logger
|
|
from electrum import constants
|
|
from electrum.network import ProxySettings
|
|
from electrum.interface import ServerAddr
|
|
from electrum.fee_policy import FEERATE_DEFAULT_RELAY
|
|
|
|
from .util import QtEventListener, event_listener
|
|
from .qeconfig import QEConfig
|
|
from .qeserverlistmodel import QEServerListModel
|
|
|
|
if TYPE_CHECKING:
|
|
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()
|
|
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
|
|
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', parent=None):
|
|
super().__init__(parent)
|
|
assert network, "--offline is not yet implemented for this GUI" # TODO
|
|
self.network = network
|
|
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())
|
|
|
|
QEConfig.instance.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):
|
|
capped_histogram, bytes_current = histogram.get_capped_data()
|
|
# 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 QEConfig.instance.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=QEConfig.instance.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()
|
|
proxy = net_params.proxy
|
|
return proxy.to_dict()
|
|
|
|
@proxy.setter
|
|
def proxy(self, proxy_dict):
|
|
net_params = self.network.get_parameters()
|
|
proxy = ProxySettings.from_dict(proxy_dict)
|
|
net_params = net_params._replace(proxy=proxy)
|
|
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
|
|
|
|
@pyqtSlot()
|
|
def probeTor(self):
|
|
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
|