Browse Source

implement user notifications for new_transaction events

As the QML app can have multiple active wallets managed from
a single window (unlike the desktop Qt version), we let each
wallet manage its own user notification queue (as there are
some rules here specific to each wallet, e.g. not emitting
user notifications for each tx while the wallet is still
syncing), including collating and rate limiting. The app then
consumes the userNotify events from all active wallets, and
adds these to its own queue, which get displayed (eventually,
again implementing rate limiting) to the user.

It also uses timers efficiently, only enabling them if there
are actual userNotify events waiting.

If at any point the QML app wants to use multiple windows,
it can forego on the app user notification queue and instead
attach each window to the associated wallet userNotify signal.

        app
         ▲
         │
         │  timer -> userNotify(msg) signal
         │
  ┌──┬───┴───────┐
  │  │           │  app user notification queue
  └──┴───▲───────┘
         │
         │  timer -> userNotify(wallet, msg) signal
         │
  ┌──┬───┴───────┐
  │  │           │  wallet user notification queue
  └──┴───▲───────┘
         │
         │  new_transaction
         │
      wallet
patch-4
Sander van Grieken 3 years ago
parent
commit
6cf4fc9e1e
  1. 84
      electrum/gui/qml/qeapp.py
  2. 127
      electrum/gui/qml/qewallet.py

84
electrum/gui/qml/qeapp.py

@ -1,6 +1,8 @@
import re
import queue
import time
from PyQt5.QtCore import pyqtSlot, QObject, QUrl, QLocale, qInstallMessageHandler
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
from PyQt5.QtGui import QGuiApplication, QFontDatabase
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine #, QQmlComponent
@ -14,11 +16,66 @@ from .qeqr import QEQRParser, QEQRImageProvider
from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin
class QEAppController(QObject):
userNotify = pyqtSignal(str)
def __init__(self, qedaemon):
super().__init__()
self.logger = get_logger(__name__)
self._qedaemon = qedaemon
# set up notification queue and notification_timer
self.user_notification_queue = queue.Queue()
self.user_notification_last_time = 0
self.notification_timer = QTimer(self)
self.notification_timer.setSingleShot(False)
self.notification_timer.setInterval(500) # msec
self.notification_timer.timeout.connect(self.on_notification_timer)
self._qedaemon.walletLoaded.connect(self.on_wallet_loaded)
def on_wallet_loaded(self):
qewallet = self._qedaemon.currentWallet
# attach to the wallet user notification events
# connect only once
try:
qewallet.userNotify.disconnect(self.on_wallet_usernotify)
except:
pass
qewallet.userNotify.connect(self.on_wallet_usernotify)
def on_wallet_usernotify(self, wallet, message):
self.logger.debug(message)
self.user_notification_queue.put(message)
if not self.notification_timer.isActive():
self.logger.debug('starting app notification timer')
self.notification_timer.start()
def on_notification_timer(self):
if self.user_notification_queue.qsize() == 0:
self.logger.debug('queue empty, stopping app notification timer')
self.notification_timer.stop()
return
now = time.time()
rate_limit = 20 # seconds
if self.user_notification_last_time + rate_limit > now:
return
self.user_notification_last_time = now
self.logger.info("Notifying GUI about new user notifications")
try:
self.userNotify.emit(self.user_notification_queue.get_nowait())
except queue.Empty:
pass
@pyqtSlot('QString')
def textToClipboard(self, text):
QGuiApplication.clipboard().setText(text)
class ElectrumQmlApplication(QGuiApplication):
_config = None
_daemon = None
_singletons = {}
_valid = True
def __init__(self, args, config, daemon):
super().__init__(args)
@ -48,12 +105,14 @@ class ElectrumQmlApplication(QGuiApplication):
self.fixedFont = 'Monospace' # hope for the best
self.context = self.engine.rootContext()
self._singletons['config'] = QEConfig(config)
self._singletons['network'] = QENetwork(daemon.network)
self._singletons['daemon'] = QEDaemon(daemon)
self.context.setContextProperty('Config', self._singletons['config'])
self.context.setContextProperty('Network', self._singletons['network'])
self.context.setContextProperty('Daemon', self._singletons['daemon'])
self._qeconfig = QEConfig(config)
self._qenetwork = QENetwork(daemon.network)
self._qedaemon = QEDaemon(daemon)
self._appController = QEAppController(self._qedaemon)
self.context.setContextProperty('AppController', self._appController)
self.context.setContextProperty('Config', self._qeconfig)
self.context.setContextProperty('Network', self._qenetwork)
self.context.setContextProperty('Daemon', self._qedaemon)
self.context.setContextProperty('FixedFont', self.fixedFont)
qInstallMessageHandler(self.message_handler)
@ -61,9 +120,6 @@ class ElectrumQmlApplication(QGuiApplication):
# get notified whether root QML document loads or not
self.engine.objectCreated.connect(self.objectCreated)
_valid = True
# slot is called after loading root QML. If object is None, it has failed.
@pyqtSlot('QObject*', 'QUrl')
def objectCreated(self, object, url):
@ -76,5 +132,3 @@ class ElectrumQmlApplication(QGuiApplication):
if re.search('file:///.*TypeError: Cannot read property.*null$', file):
return
self.logger.warning(file)

127
electrum/gui/qml/qewallet.py

@ -1,6 +1,8 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
from typing import Optional, TYPE_CHECKING, Sequence, List, Union
import queue
import time
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QTimer
from electrum.i18n import _
from electrum.util import register_callback, Satoshis, format_time
@ -16,6 +18,22 @@ from .qetransactionlistmodel import QETransactionListModel
from .qeaddresslistmodel import QEAddressListModel
class QEWallet(QObject):
_logger = get_logger(__name__)
# emitted when wallet wants to display a user notification
# actual presentation should be handled on app or window level
userNotify = pyqtSignal(object, object)
# shared signal for many static wallet properties
dataChanged = pyqtSignal()
isUptodateChanged = pyqtSignal()
requestStatus = pyqtSignal()
requestCreateSuccess = pyqtSignal()
requestCreateError = pyqtSignal([str,str], arguments=['code','error'])
_network_signal = pyqtSignal(str, object)
def __init__(self, wallet, parent=None):
super().__init__(parent)
self.wallet = wallet
@ -26,20 +44,95 @@ class QEWallet(QObject):
self._historyModel.init_model()
self._requestModel.init_model()
register_callback(self.on_request_status, ['request_status'])
register_callback(self.on_status, ['status'])
self.tx_notification_queue = queue.Queue()
self.tx_notification_last_time = 0
self.notification_timer = QTimer(self)
self.notification_timer.setSingleShot(False)
self.notification_timer.setInterval(500) # msec
self.notification_timer.timeout.connect(self.notify_transactions)
self._network_signal.connect(self.on_network_qt)
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status', 'verified', 'on_history',
'channel', 'channels_updated', 'payment_failed',
'payment_succeeded', 'invoice_status', 'request_status']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be
# partials, lambdas or methods of subobjects. Hence...
register_callback(self.on_network, interests)
_logger = get_logger(__name__)
@pyqtProperty(bool, notify=isUptodateChanged)
def isUptodate(self):
return self.wallet.is_up_to_date()
dataChanged = pyqtSignal() # dummy to silence warnings
def on_network(self, event, *args):
# Handle in GUI thread (_network_signal -> on_network_qt)
self._network_signal.emit(event, args)
def on_network_qt(self, event, args=None):
# note: we get events from all wallets! args are heterogenous so we can't
# shortcut here
if event == 'status':
self.isUptodateChanged.emit()
elif event == 'request_status':
self._logger.info(str(args))
self.requestStatus.emit()
elif event == 'new_transaction':
wallet, tx = args
if wallet == self.wallet:
self.add_tx_notification(tx)
self._historyModel.init_model()
else:
self._logger.debug('unhandled event: %s %s' % (event, str(args)))
requestCreateSuccess = pyqtSignal()
requestCreateError = pyqtSignal([str,str], arguments=['code','error'])
requestStatus = pyqtSignal()
def on_request_status(self, event, *args):
self._logger.debug(str(event))
self.requestStatus.emit()
def add_tx_notification(self, tx):
self._logger.debug('new transaction event')
self.tx_notification_queue.put(tx)
if not self.notification_timer.isActive():
self._logger.debug('starting wallet notification timer')
self.notification_timer.start()
def notify_transactions(self):
if self.tx_notification_queue.qsize() == 0:
self._logger.debug('queue empty, stopping wallet notification timer')
self.notification_timer.stop()
return
if not self.wallet.up_to_date:
return # no notifications while syncing
now = time.time()
rate_limit = 20 # seconds
if self.tx_notification_last_time + rate_limit > now:
return
self.tx_notification_last_time = now
self._logger.info("Notifying app about new transactions")
txns = []
while True:
try:
txns.append(self.tx_notification_queue.get_nowait())
except queue.Empty:
break
from .qeapp import ElectrumQmlApplication
config = ElectrumQmlApplication._config
# Combine the transactions if there are at least three
if len(txns) >= 3:
total_amount = 0
for tx in txns:
tx_wallet_delta = self.wallet.get_wallet_delta(tx)
if not tx_wallet_delta.is_relevant:
continue
total_amount += tx_wallet_delta.delta
self.userNotify.emit(self.wallet, _("{} new transactions: Total amount received in the new transactions {}").format(len(txns), config.format_amount_and_units(total_amount)))
else:
for tx in txns:
tx_wallet_delta = self.wallet.get_wallet_delta(tx)
if not tx_wallet_delta.is_relevant:
continue
self.userNotify.emit(self.wallet,
_("New transaction: {}").format(config.format_amount_and_units(tx_wallet_delta.delta)))
historyModelChanged = pyqtSignal()
@pyqtProperty(QETransactionListModel, notify=historyModelChanged)
@ -105,16 +198,6 @@ class QEWallet(QObject):
return c+x
def on_status(self, status):
self._logger.info('wallet: status update: ' + str(status))
self.isUptodateChanged.emit()
# lightning feature?
isUptodateChanged = pyqtSignal()
@pyqtProperty(bool, notify=isUptodateChanged)
def isUptodate(self):
return self.wallet.is_up_to_date()
@pyqtSlot('QString', int, int, bool)
def send_onchain(self, address, amount, fee=None, rbf=False):
self._logger.info('send_onchain: ' + address + ' ' + str(amount))

Loading…
Cancel
Save