Merge pull request #9277 from accumulator/qml_tx_signing_flow
qml: refactor QEWallet.sign() to sign() and sign_and_broadcast(), add user feedback when signing fails.
This commit is contained in:
@@ -644,6 +644,14 @@ Item {
|
||||
}
|
||||
_confirmPaymentDialog.destroy()
|
||||
}
|
||||
onSignError: (message) => {
|
||||
var dialog = app.messageDialog.createObject(mainView, {
|
||||
title: qsTr('Error'),
|
||||
text: [qsTr('Could not sign tx'), message].join('\n\n'),
|
||||
iconSource: '../../../icons/warning.png'
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
// TODO: lingering confirmPaymentDialogs can raise exceptions in
|
||||
// the child finalizer when currentWallet disappears, but we need
|
||||
|
||||
@@ -395,8 +395,10 @@ class QETxDetails(QObject, QtEventListener):
|
||||
if broadcast:
|
||||
self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded)
|
||||
self._wallet.broadcastFailed.connect(self.onBroadcastFailed)
|
||||
self._wallet.sign_and_broadcast(self._tx, on_success=self.on_signed_tx)
|
||||
else:
|
||||
self._wallet.sign(self._tx, on_success=self.on_signed_tx)
|
||||
|
||||
self._wallet.sign(self._tx, broadcast=broadcast, on_success=self.on_signed_tx)
|
||||
# side-effect: signing updates self._tx
|
||||
# we rely on this for broadcast
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ class QETxFinalizer(TxFeeSlider):
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])
|
||||
signError = pyqtSignal([str], arguments=['message'])
|
||||
|
||||
def __init__(self, parent=None, *, make_tx=None, accept=None):
|
||||
super().__init__(parent)
|
||||
@@ -426,7 +427,7 @@ class QETxFinalizer(TxFeeSlider):
|
||||
self.f_accept(self._tx)
|
||||
return
|
||||
|
||||
self._wallet.sign(self._tx, broadcast=True, on_success=partial(self.on_signed_tx, False))
|
||||
self._wallet.sign_and_broadcast(self._tx, on_success=partial(self.on_signed_tx, False), on_failure=self.on_sign_failed)
|
||||
|
||||
@pyqtSlot()
|
||||
def sign(self):
|
||||
@@ -434,7 +435,7 @@ class QETxFinalizer(TxFeeSlider):
|
||||
self._logger.error('no valid tx')
|
||||
return
|
||||
|
||||
self._wallet.sign(self._tx, broadcast=False, on_success=partial(self.on_signed_tx, True))
|
||||
self._wallet.sign(self._tx, on_success=partial(self.on_signed_tx, True), on_failure=self.on_sign_failed)
|
||||
|
||||
def on_signed_tx(self, save: bool, tx: Transaction):
|
||||
self._logger.debug('on_signed_tx')
|
||||
@@ -446,6 +447,10 @@ class QETxFinalizer(TxFeeSlider):
|
||||
self._logger.error('Could not save tx')
|
||||
self.finished.emit(True, saved, tx.is_complete())
|
||||
|
||||
def on_sign_failed(self, msg: str = None):
|
||||
self._logger.debug('on_sign_failed')
|
||||
self.signError.emit(msg)
|
||||
|
||||
@pyqtSlot(result='QVariantList')
|
||||
def getSerializedTx(self):
|
||||
txqr = self._tx.to_qr_data()
|
||||
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Any
|
||||
from functools import partial
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
@@ -65,8 +65,6 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
|
||||
paymentSucceeded = pyqtSignal([str], arguments=['key'])
|
||||
paymentFailed = pyqtSignal([str, str], arguments=['key', 'reason'])
|
||||
requestNewPassword = pyqtSignal()
|
||||
signSucceeded = pyqtSignal([str], arguments=['txid'])
|
||||
signFailed = pyqtSignal([str], arguments=['message'])
|
||||
broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
|
||||
broadcastFailed = pyqtSignal([str, str, str], arguments=['txid', 'code', 'reason'])
|
||||
saveTxSuccess = pyqtSignal([str], arguments=['txid'])
|
||||
@@ -518,42 +516,46 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
|
||||
self.isLightningChanged.emit()
|
||||
self.dataChanged.emit()
|
||||
|
||||
@auth_protect(message=_('Sign on-chain transaction?')) # FIXME auth msg cannot be explicit due to "broadcast" param
|
||||
def sign(self, tx, *, broadcast: bool = False, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[], None] = None):
|
||||
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast, on_success), partial(self.on_sign_failed, on_failure))
|
||||
if sign_hook:
|
||||
success = self.do_sign(tx, False)
|
||||
if success:
|
||||
self._logger.debug('plugin needs to sign tx too')
|
||||
sign_hook(tx)
|
||||
return
|
||||
else:
|
||||
success = self.do_sign(tx, broadcast)
|
||||
@auth_protect(message=_('Sign and send on-chain transaction?'))
|
||||
def sign_and_broadcast(self, tx, *,
|
||||
on_success: Callable[[Transaction], None] = None,
|
||||
on_failure: Callable[[Optional[Any]], None] = None) -> None:
|
||||
self.do_sign(tx, True, on_success, on_failure)
|
||||
|
||||
if success:
|
||||
if on_success:
|
||||
on_success(tx)
|
||||
else:
|
||||
if on_failure:
|
||||
on_failure()
|
||||
@auth_protect(message=_('Sign on-chain transaction?'))
|
||||
def sign(self, tx, *,
|
||||
on_success: Callable[[Transaction], None] = None,
|
||||
on_failure: Callable[[Optional[Any]], None] = None) -> None:
|
||||
self.do_sign(tx, False, on_success, on_failure)
|
||||
|
||||
def do_sign(self, tx, broadcast):
|
||||
def do_sign(self, tx, broadcast, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[Optional[Any]], None] = None):
|
||||
# tc_sign_wrapper is only used by 2fa. don't pass on_failure handler, it is handled via otpFailed signal
|
||||
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx,
|
||||
partial(self.on_sign_complete, broadcast, on_success),
|
||||
partial(self.on_sign_failed, None))
|
||||
try:
|
||||
# ignore_warnings=True, because UI checks and asks user confirmation itself
|
||||
tx = self.wallet.sign_transaction(tx, self.password, ignore_warnings=True)
|
||||
except BaseException as e:
|
||||
self._logger.error(f'{e!r}')
|
||||
self.signFailed.emit(str(e))
|
||||
if on_failure:
|
||||
on_failure(str(e))
|
||||
return
|
||||
|
||||
if tx is None:
|
||||
self._logger.info('did not sign')
|
||||
return False
|
||||
if on_failure:
|
||||
on_failure()
|
||||
return
|
||||
|
||||
if sign_hook:
|
||||
self._logger.debug('plugin needs to sign tx too')
|
||||
sign_hook(tx)
|
||||
return
|
||||
|
||||
txid = tx.txid()
|
||||
self._logger.debug(f'do_sign(), txid={txid}')
|
||||
|
||||
self.signSucceeded.emit(txid)
|
||||
|
||||
if not tx.is_complete():
|
||||
self._logger.debug('tx not complete')
|
||||
broadcast = False
|
||||
@@ -564,7 +566,8 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
|
||||
# not broadcasted, so refresh history here
|
||||
self.historyModel.initModel(True)
|
||||
|
||||
return True
|
||||
if on_success:
|
||||
on_success(tx)
|
||||
|
||||
# this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok
|
||||
def on_sign_complete(self, broadcast, cb: Callable[[Transaction], None] = None, tx: Transaction = None):
|
||||
|
||||
@@ -458,9 +458,11 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
if not wallet.keystores['x3'].can_sign(tx, ignore_watching_only=True):
|
||||
self.logger.info("twofactor: xpub3 not needed")
|
||||
return
|
||||
|
||||
def wrapper(tx):
|
||||
assert tx
|
||||
self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
|
||||
|
||||
return wrapper
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure) -> None:
|
||||
|
||||
Reference in New Issue
Block a user