292 lines
10 KiB
Python
292 lines
10 KiB
Python
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
|