diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 8531b2175..abc8bd1d0 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -448,7 +448,7 @@ Pane { onAccepted: { root.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('Transaction fee updated.'), @@ -475,7 +475,7 @@ Pane { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('CPFP fee bump transaction created.'), @@ -501,7 +501,7 @@ Pane { onAccepted: { root.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('Cancel transaction created.'), diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 5897eb3a3..596d06b47 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -310,7 +310,7 @@ class QETxDetails(QObject, QtEventListener): self._short_id = tx_mined_info.short_id() or "" @pyqtSlot() - def sign_and_broadcast(self): + def signAndBroadcast(self): self._sign(broadcast=True) @pyqtSlot() @@ -320,20 +320,24 @@ class QETxDetails(QObject, QtEventListener): def _sign(self, broadcast): # TODO: connecting/disconnecting signal handlers here is hmm try: - self._wallet.transactionSigned.disconnect(self.onSigned) - self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) if broadcast: + self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) except: pass - self._wallet.transactionSigned.connect(self.onSigned) - self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded) + if broadcast: + self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded) self._wallet.broadcastFailed.connect(self.onBroadcastFailed) - self._wallet.sign(self._tx, broadcast=broadcast) + + 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 + def on_signed_tx(self, tx: Transaction): + self._logger.debug('on_signed_tx') + self.update() + @pyqtSlot() def broadcast(self): assert self._tx.is_complete() @@ -349,15 +353,6 @@ class QETxDetails(QObject, QtEventListener): self._wallet.broadcast(self._tx) - @pyqtSlot(str) - def onSigned(self, txid): - if txid != self._txid: - return - - self._logger.debug('onSigned') - self._wallet.transactionSigned.disconnect(self.onSigned) - self.update() - @pyqtSlot(str) def onBroadcastSucceeded(self, txid): if txid != self._txid: diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 593d3bf56..17b952d0e 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -5,7 +5,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger from electrum.i18n import _ -from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.util import NotEnoughFunds, profiler from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP from electrum.network import NetworkException @@ -368,31 +368,18 @@ class QETxFinalizer(TxFeeSlider): self._logger.error('no valid tx') return - # TODO: f_accept handler not used - # if self.f_accept: - # self.f_accept(self._tx) - # return - - try: - self._wallet.transactionSigned.disconnect(self.onSigned) - except: - pass - self._wallet.transactionSigned.connect(self.onSigned) - self._wallet.sign(self._tx) - - @pyqtSlot(str) - def onSigned(self, txid): - if txid != self._tx.txid(): - return - - self._logger.debug('onSigned') - self._wallet.transactionSigned.disconnect(self.onSigned) + self._wallet.sign(self._tx, broadcast=False, on_success=self.on_signed_tx) + def on_signed_tx(self, tx: Transaction): + self._logger.debug('on_signed_tx') if not self._wallet.save_tx(self._tx): self._logger.error('Could not save tx') else: + # FIXME: don't rely on txid. (non-segwit tx don't have a txid + # until tx is complete, and can't save to backend without it). self.finishedSave.emit(self._tx.txid()) + # mixin for watching an existing TX based on its txid for verified event # requires self._wallet to contain a QEWallet instance # exposes txid qt property diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 025e3ad82..bd30127ef 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -2,7 +2,7 @@ import asyncio import queue import threading import time -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple, Callable from functools import partial from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, QMetaObject, Qt @@ -12,7 +12,7 @@ from electrum.i18n import _ from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_BROADCASTING, PR_BROADCAST from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop from electrum.plugin import run_hook from electrum.wallet import Multisig_Wallet @@ -62,7 +62,8 @@ class QEWallet(AuthMixin, QObject, QtEventListener): paymentSucceeded = pyqtSignal([str], arguments=['key']) paymentFailed = pyqtSignal([str,str], arguments=['key','reason']) requestNewPassword = pyqtSignal() - transactionSigned = pyqtSignal([str], arguments=['txid']) + 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']) @@ -486,28 +487,37 @@ class QEWallet(AuthMixin, QObject, QtEventListener): self.dataChanged.emit() @auth_protect() - def sign(self, tx, *, broadcast: bool = False): - sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast), - self.on_sign_failed) + 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, on_success, broadcast), partial(self.on_sign_failed, on_failure)) if sign_hook: - self.do_sign(tx, False) - self._logger.debug('plugin needs to sign tx too') - sign_hook(tx) - return + signSuccess = self.do_sign(tx, False) + if signSuccess: + self._logger.debug('plugin needs to sign tx too') + sign_hook(tx) + return + else: + signSuccess = self.do_sign(tx, broadcast) - self.do_sign(tx, broadcast) + if signSuccess: + if on_success: on_success(tx) + else: + if on_failure: on_failure() def do_sign(self, tx, broadcast): - tx = self.wallet.sign_transaction(tx, self.password) + try: + tx = self.wallet.sign_transaction(tx, self.password) + except BaseException as e: + self._logger.error(f'{e!r}') + self.signFailed.emit(str(e)) if tx is None: self._logger.info('did not sign') - return + return False txid = tx.txid() self._logger.debug(f'do_sign(), txid={txid}') - self.transactionSigned.emit(txid) + self.signSucceeded.emit(txid) if not tx.is_complete(): self._logger.debug('tx not complete') @@ -519,14 +529,19 @@ class QEWallet(AuthMixin, QObject, QtEventListener): # not broadcasted, so refresh history here self.historyModel.init_model(True) + return True + # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok - def on_sign_complete(self, broadcast, tx): + def on_sign_complete(self, broadcast, cb: Callable[[Transaction], None] = None, tx: Transaction = None): self.otpSuccess.emit() + if cb: cb(tx) if broadcast: self.broadcast(tx) - def on_sign_failed(self, error): + # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok + def on_sign_failed(self, cb: Callable[[], None] = None, error: str = None): self.otpFailed.emit('error', error) + if cb: cb() def request_otp(self, on_submit): self._otp_on_submit = on_submit