From 7cb8c347b50ad54018bebb8f4ef41d84f273c1fb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 15 Jun 2022 22:41:33 +0200 Subject: [PATCH] Add @auth_protect decorator. This guards function calls by storing the function, args and kwargs into an added attribute '__auth_fcall' on the object using the decorator, then emits a signal that can be handled by the UI. The UI can signal auth-success or auth-failure back to the object by calling either the authProceed() slot or the authCancel slot. The object utilizing this decorator MUST inherit/mixin the AuthMixin class, which provides the above two slots, and handling of state. The decorator also accepts a 'reject' parameter, containing the name of a parameterless function on the object, which is called when authentication has failed/is cancelled. --- electrum/gui/qml/auth.py | 58 +++++++++++++++++++ .../LightningPaymentProgressDialog.qml | 3 + electrum/gui/qml/components/main.qml | 14 +++++ electrum/gui/qml/qewallet.py | 16 +++-- 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 electrum/gui/qml/auth.py diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py new file mode 100644 index 000000000..e55a0c036 --- /dev/null +++ b/electrum/gui/qml/auth.py @@ -0,0 +1,58 @@ +from functools import wraps, partial + +from PyQt5.QtCore import pyqtSignal, pyqtSlot + +from electrum.logging import get_logger + +def auth_protect(func=None, reject=None): + if func is None: + return partial(auth_protect, reject=reject) + + @wraps(func) + def wrapper(self, *args, **kwargs): + self._logger.debug(str(self)) + if hasattr(self, '__auth_fcall'): + self._logger.debug('object already has a pending authed function call') + raise Exception('object already has a pending authed function call') + setattr(self, '__auth_fcall', (func,args,kwargs,reject)) + getattr(self, 'authRequired').emit() + + return wrapper + +class AuthMixin: + _auth_logger = get_logger(__name__) + + authRequired = pyqtSignal() + + @pyqtSlot() + def authProceed(self): + self._auth_logger.debug('Proceeding with authed fn()') + try: + self._auth_logger.debug(str(getattr(self, '__auth_fcall'))) + (func,args,kwargs,reject) = getattr(self, '__auth_fcall') + r = func(self, *args, **kwargs) + return r + except Exception as e: + self._auth_logger.error('Error executing wrapped fn(): %s' % repr(e)) + raise e + finally: + delattr(self,'__auth_fcall') + + @pyqtSlot() + def authCancel(self): + self._auth_logger.debug('Cancelling authed fn()') + if not hasattr(self, '__auth_fcall'): + return + + try: + (func,args,kwargs,reject) = getattr(self, '__auth_fcall') + if reject is not None: + if hasattr(self, reject): + getattr(self, reject)() + else: + self._auth_logger.error('Reject method \'%s\' not defined' % reject) + except Exception as e: + self._auth_logger.error('Error executing reject function \'%s\': %s' % (reject, repr(e))) + raise e + finally: + delattr(self, '__auth_fcall') diff --git a/electrum/gui/qml/components/LightningPaymentProgressDialog.qml b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml index b81b92201..3dc4191fe 100644 --- a/electrum/gui/qml/components/LightningPaymentProgressDialog.qml +++ b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml @@ -123,5 +123,8 @@ Dialog { s.state = 'failed' errorText.text = reason } + function onPaymentAuthRejected() { + dialog.close() + } } } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 4e6a56723..775b735a4 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -221,4 +221,18 @@ ApplicationWindow notificationPopup.show(message) } } + + Connections { + target: Daemon.currentWallet + function onAuthRequired() { + var dialog = app.messageDialog.createObject(app, {'text': 'Auth placeholder', 'yesno': true}) + dialog.yesClicked.connect(function() { + Daemon.currentWallet.authProceed() + }) + dialog.noClicked.connect(function() { + Daemon.currentWallet.authCancel() + }) + dialog.open() + } + } } diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 5580ae66e..98467a73b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -21,8 +21,9 @@ from .qetransactionlistmodel import QETransactionListModel from .qeaddresslistmodel import QEAddressListModel from .qechannellistmodel import QEChannelListModel from .qetypes import QEAmount +from .auth import AuthMixin, auth_protect -class QEWallet(QObject): +class QEWallet(AuthMixin, QObject): __instances = [] # this factory method should be used to instantiate QEWallet @@ -90,7 +91,7 @@ class QEWallet(QObject): return self.wallet.is_up_to_date() def on_network(self, event, *args): - if event == 'new_transaction': + if event in ['new_transaction', 'payment_succeeded']: # Handle in GUI thread (_network_signal -> on_network_qt) self._network_signal.emit(event, args) else: @@ -356,6 +357,7 @@ class QEWallet(QObject): tx.set_rbf(use_rbf) self.sign_and_broadcast(tx) + @auth_protect def sign_and_broadcast(self, tx): def cb(result): self._logger.info('signing was succesful? %s' % str(result)) @@ -379,13 +381,20 @@ class QEWallet(QObject): return + paymentAuthRejected = pyqtSignal() + def ln_auth_rejected(self): + self.paymentAuthRejected.emit() + @pyqtSlot(str) + @auth_protect(reject='ln_auth_rejected') def pay_lightning_invoice(self, invoice_key): self._logger.debug('about to pay LN') invoice = self.wallet.get_invoice(invoice_key) assert(invoice) assert(invoice.lightning_invoice) + amount_msat = invoice.get_amount_msat() + def pay_thread(): try: coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat) @@ -393,8 +402,7 @@ class QEWallet(QObject): fut.result() except Exception as e: self.userNotify(repr(e)) - #self.app.show_error(repr(e)) - #self.save_invoice(invoice) + threading.Thread(target=pay_thread).start() def create_bitcoin_request(self, amount: int, message: str, expiration: int, ignore_gap: bool) -> Optional[str]: