diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index ea747a9ea..b0763a14f 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -277,6 +277,7 @@ Pane { ToolButton { icon.source: '../../icons/share.png' icon.color: 'transparent' + enabled: root.txid onClicked: { var dialog = app.genericShareDialog.createObject(root, { title: qsTr('Transaction ID'), text: root.txid } @@ -324,6 +325,20 @@ Pane { } } + RowLayout { + visible: !txdetails.isMined && !txdetails.isUnrelated + Layout.columnSpan: 2 + Button { + text: qsTr('Sign') + enabled: !txdetails.isComplete + onClicked: txdetails.sign() + } + Button { + text: qsTr('Broadcast') + enabled: txdetails.canBroadcast + onClicked: txdetails.broadcast() + } + } } } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 996cd2ea4..c04b1e143 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -35,6 +35,7 @@ class QETxDetails(QObject): _can_save_as_local = False _can_remove = False _is_unrelated = False + _is_complete = False _is_mined = False @@ -184,6 +185,10 @@ class QETxDetails(QObject): def isUnrelated(self): return self._is_unrelated + @pyqtProperty(bool, notify=detailsChanged) + def isComplete(self): + return self._is_complete + def update(self): if self._wallet is None: self._logger.error('wallet undefined') @@ -230,6 +235,7 @@ class QETxDetails(QObject): else: self._lnamount.satsInt = 0 + self._is_complete = self._tx.is_complete() self._is_unrelated = txinfo.amount is None and self._lnamount.isEmpty self._is_lightning_funding_tx = txinfo.is_lightning_funding_tx self._can_bump = txinfo.can_bump @@ -252,3 +258,28 @@ class QETxDetails(QObject): self._confirmations = tx_mined_info.conf self._txpos = tx_mined_info.txpos self._header_hash = tx_mined_info.header_hash + + @pyqtSlot() + def sign(self): + try: + self._wallet.transactionSigned.disconnect(self.onSigned) + except: + pass + self._wallet.transactionSigned.connect(self.onSigned) + self._wallet.sign(self._tx) + # side-effect: signing updates self._tx + # we rely on this for broadcast + + @pyqtSlot(str) + def onSigned(self, txid): + if txid != self._txid: + return + + self._logger.debug('onSigned') + self._wallet.transactionSigned.disconnect(self.onSigned) + self.update() + + @pyqtSlot() + def broadcast(self): + assert self._tx.is_complete() + self._wallet.broadcast(self._tx) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 39f3f2036..12a8564d0 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -59,6 +59,9 @@ class QEWallet(AuthMixin, QObject, QtEventListener): paymentSucceeded = pyqtSignal([str], arguments=['key']) paymentFailed = pyqtSignal([str,str], arguments=['key','reason']) requestNewPassword = pyqtSignal() + transactionSigned = pyqtSignal([str], arguments=['txid']) + #broadcastSucceeded = pyqtSignal([str], arguments=['txid']) + broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason']) _network_signal = pyqtSignal(str, object) @@ -399,17 +402,31 @@ class QEWallet(AuthMixin, QObject, QtEventListener): use_rbf = bool(self.wallet.config.get('use_rbf', True)) tx.set_rbf(use_rbf) - self.sign_and_broadcast(tx) + self.sign(tx, broadcast=True) @auth_protect - def sign_and_broadcast(self, tx): - def cb(result): - self._logger.info('signing was succesful? %s' % str(result)) + def sign(self, tx, *, broadcast: bool = False): tx = self.wallet.sign_transaction(tx, None) + + if tx is None: + self._logger.info('did not sign') + return + + txid = tx.txid() + self._logger.debug(f'txid={txid}') + + self.transactionSigned.emit(txid) + if not tx.is_complete(): self._logger.info('tx not complete') return + if broadcast: + self.broadcast(tx) + + def broadcast(self, tx): + assert tx.is_complete() + self.network = self.wallet.network # TODO not always defined? try: @@ -417,13 +434,14 @@ class QEWallet(AuthMixin, QObject, QtEventListener): self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) self._logger.info('broadcast submit done') except TxBroadcastError as e: - self._logger.info(e) - return + self.broadcastFailed.emit(tx.txid(),'',repr(e)) + self._logger.error(e) except BestEffortRequestFailed as e: - self._logger.info(e) - return + self.broadcastFailed.emit(tx.txid(),'',repr(e)) + self._logger.error(e) - return + #TODO: properly catch server side errors, e.g. bad-txns-inputs-missingorspent + #might need callback from network.py paymentAuthRejected = pyqtSignal() def ln_auth_rejected(self):