Browse Source

qml: first part of partially signing tx while not having txid yet

master
Sander van Grieken 3 years ago
parent
commit
1aa14e749a
  1. 6
      electrum/gui/qml/components/TxDetails.qml
  2. 25
      electrum/gui/qml/qetxdetails.py
  3. 27
      electrum/gui/qml/qetxfinalizer.py
  4. 47
      electrum/gui/qml/qewallet.py

6
electrum/gui/qml/components/TxDetails.qml

@ -448,7 +448,7 @@ Pane {
onAccepted: { onAccepted: {
root.rawtx = rbffeebumper.getNewTx() root.rawtx = rbffeebumper.getNewTx()
if (txdetails.wallet.canSignWithoutCosigner) { if (txdetails.wallet.canSignWithoutCosigner) {
txdetails.sign_and_broadcast() txdetails.signAndBroadcast()
} else { } else {
var dialog = app.messageDialog.createObject(app, { var dialog = app.messageDialog.createObject(app, {
title: qsTr('Transaction fee updated.'), title: qsTr('Transaction fee updated.'),
@ -475,7 +475,7 @@ Pane {
// replaces parent tx with cpfp tx // replaces parent tx with cpfp tx
root.rawtx = cpfpfeebumper.getNewTx() root.rawtx = cpfpfeebumper.getNewTx()
if (txdetails.wallet.canSignWithoutCosigner) { if (txdetails.wallet.canSignWithoutCosigner) {
txdetails.sign_and_broadcast() txdetails.signAndBroadcast()
} else { } else {
var dialog = app.messageDialog.createObject(app, { var dialog = app.messageDialog.createObject(app, {
title: qsTr('CPFP fee bump transaction created.'), title: qsTr('CPFP fee bump transaction created.'),
@ -501,7 +501,7 @@ Pane {
onAccepted: { onAccepted: {
root.rawtx = txcanceller.getNewTx() root.rawtx = txcanceller.getNewTx()
if (txdetails.wallet.canSignWithoutCosigner) { if (txdetails.wallet.canSignWithoutCosigner) {
txdetails.sign_and_broadcast() txdetails.signAndBroadcast()
} else { } else {
var dialog = app.messageDialog.createObject(app, { var dialog = app.messageDialog.createObject(app, {
title: qsTr('Cancel transaction created.'), title: qsTr('Cancel transaction created.'),

25
electrum/gui/qml/qetxdetails.py

@ -310,7 +310,7 @@ class QETxDetails(QObject, QtEventListener):
self._short_id = tx_mined_info.short_id() or "" self._short_id = tx_mined_info.short_id() or ""
@pyqtSlot() @pyqtSlot()
def sign_and_broadcast(self): def signAndBroadcast(self):
self._sign(broadcast=True) self._sign(broadcast=True)
@pyqtSlot() @pyqtSlot()
@ -320,20 +320,24 @@ class QETxDetails(QObject, QtEventListener):
def _sign(self, broadcast): def _sign(self, broadcast):
# TODO: connecting/disconnecting signal handlers here is hmm # TODO: connecting/disconnecting signal handlers here is hmm
try: try:
self._wallet.transactionSigned.disconnect(self.onSigned)
self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded)
if broadcast: if broadcast:
self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded)
self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed)
except: except:
pass pass
self._wallet.transactionSigned.connect(self.onSigned)
self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded)
if broadcast: if broadcast:
self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded)
self._wallet.broadcastFailed.connect(self.onBroadcastFailed) 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 # side-effect: signing updates self._tx
# we rely on this for broadcast # we rely on this for broadcast
def on_signed_tx(self, tx: Transaction):
self._logger.debug('on_signed_tx')
self.update()
@pyqtSlot() @pyqtSlot()
def broadcast(self): def broadcast(self):
assert self._tx.is_complete() assert self._tx.is_complete()
@ -349,15 +353,6 @@ class QETxDetails(QObject, QtEventListener):
self._wallet.broadcast(self._tx) 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) @pyqtSlot(str)
def onBroadcastSucceeded(self, txid): def onBroadcastSucceeded(self, txid):
if txid != self._txid: if txid != self._txid:

27
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.logging import get_logger
from electrum.i18n import _ 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.util import NotEnoughFunds, profiler
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP
from electrum.network import NetworkException from electrum.network import NetworkException
@ -368,31 +368,18 @@ class QETxFinalizer(TxFeeSlider):
self._logger.error('no valid tx') self._logger.error('no valid tx')
return return
# TODO: f_accept handler not used self._wallet.sign(self._tx, broadcast=False, on_success=self.on_signed_tx)
# 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)
def on_signed_tx(self, tx: Transaction):
self._logger.debug('on_signed_tx')
if not self._wallet.save_tx(self._tx): if not self._wallet.save_tx(self._tx):
self._logger.error('Could not save tx') self._logger.error('Could not save tx')
else: 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()) self.finishedSave.emit(self._tx.txid())
# mixin for watching an existing TX based on its txid for verified event # mixin for watching an existing TX based on its txid for verified event
# requires self._wallet to contain a QEWallet instance # requires self._wallet to contain a QEWallet instance
# exposes txid qt property # exposes txid qt property

47
electrum/gui/qml/qewallet.py

@ -2,7 +2,7 @@ import asyncio
import queue import queue
import threading import threading
import time import time
from typing import TYPE_CHECKING, Optional, Tuple from typing import TYPE_CHECKING, Optional, Tuple, Callable
from functools import partial from functools import partial
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, QMetaObject, Qt 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.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_BROADCASTING, PR_BROADCAST
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.network import TxBroadcastError, BestEffortRequestFailed 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.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.wallet import Multisig_Wallet from electrum.wallet import Multisig_Wallet
@ -62,7 +62,8 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
paymentSucceeded = pyqtSignal([str], arguments=['key']) paymentSucceeded = pyqtSignal([str], arguments=['key'])
paymentFailed = pyqtSignal([str,str], arguments=['key','reason']) paymentFailed = pyqtSignal([str,str], arguments=['key','reason'])
requestNewPassword = pyqtSignal() requestNewPassword = pyqtSignal()
transactionSigned = pyqtSignal([str], arguments=['txid']) signSucceeded = pyqtSignal([str], arguments=['txid'])
signFailed = pyqtSignal([str], arguments=['message'])
broadcastSucceeded = pyqtSignal([str], arguments=['txid']) broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason']) broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason'])
saveTxSuccess = pyqtSignal([str], arguments=['txid']) saveTxSuccess = pyqtSignal([str], arguments=['txid'])
@ -486,28 +487,37 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
self.dataChanged.emit() self.dataChanged.emit()
@auth_protect() @auth_protect()
def sign(self, tx, *, broadcast: bool = False): 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), 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))
self.on_sign_failed)
if sign_hook: if sign_hook:
self.do_sign(tx, False) signSuccess = self.do_sign(tx, False)
self._logger.debug('plugin needs to sign tx too') if signSuccess:
sign_hook(tx) self._logger.debug('plugin needs to sign tx too')
return 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): 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: if tx is None:
self._logger.info('did not sign') self._logger.info('did not sign')
return return False
txid = tx.txid() txid = tx.txid()
self._logger.debug(f'do_sign(), txid={txid}') self._logger.debug(f'do_sign(), txid={txid}')
self.transactionSigned.emit(txid) self.signSucceeded.emit(txid)
if not tx.is_complete(): if not tx.is_complete():
self._logger.debug('tx not complete') self._logger.debug('tx not complete')
@ -519,14 +529,19 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
# not broadcasted, so refresh history here # not broadcasted, so refresh history here
self.historyModel.init_model(True) 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 # 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() self.otpSuccess.emit()
if cb: cb(tx)
if broadcast: if broadcast:
self.broadcast(tx) 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) self.otpFailed.emit('error', error)
if cb: cb()
def request_otp(self, on_submit): def request_otp(self, on_submit):
self._otp_on_submit = on_submit self._otp_on_submit = on_submit

Loading…
Cancel
Save