diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 6b5a3909c..9e6d3e400 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -400,6 +400,16 @@ Pane { } } + FlatButton { + Layout.fillWidth: true + text: qsTr('Cancel Tx') + visible: txdetails.canCancel + onClicked: { + var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + dialog.open() + } + } + } TxDetails { @@ -451,6 +461,23 @@ Pane { } } + Component { + id: rbfCancelDialog + RbfCancelDialog { + id: dialog + txcanceller: TxCanceller { + id: txcanceller + wallet: Daemon.currentWallet + txid: dialog.txid + } + + onTxaccepted: { + root.rawtx = txcanceller.getNewTx() + } + onClosed: destroy() + } + } + Component { id: exportTxDialog ExportTxDialog { diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 4c28b096b..2095b120d 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -19,7 +19,7 @@ from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper from .qewalletdb import QEWalletDB from .qebitcoin import QEBitcoin from .qefx import QEFX -from .qetxfinalizer import QETxFinalizer, QETxFeeBumper +from .qetxfinalizer import QETxFinalizer, QETxFeeBumper, QETxCanceller from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment from .qerequestdetails import QERequestDetails from .qetypes import QEAmount @@ -217,6 +217,7 @@ class ElectrumQmlApplication(QGuiApplication): qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper') qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails') qmlRegisterType(QETxFeeBumper, 'org.electrum', 1, 0, 'TxFeeBumper') + qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property') diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 7e84707a6..6d30efe7c 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -6,7 +6,7 @@ from electrum.logging import get_logger from electrum.i18n import _ from electrum.transaction import PartialTxOutput, PartialTransaction from electrum.util import NotEnoughFunds, profiler -from electrum.wallet import CannotBumpFee +from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx from electrum.network import NetworkException from .qewallet import QEWallet @@ -497,3 +497,121 @@ class QETxFeeBumper(TxFeeSlider): @pyqtSlot(result=str) def getNewTx(self): return str(self._tx) + +class QETxCanceller(TxFeeSlider): + _logger = get_logger(__name__) + + _oldfee = QEAmount() + _oldfee_rate = 0 + _orig_tx = None + _txid = '' + _rbf = True + + def __init__(self, parent=None): + super().__init__(parent) + + txidChanged = pyqtSignal() + @pyqtProperty(str, notify=txidChanged) + def txid(self): + return self._txid + + @txid.setter + def txid(self, txid): + if self._txid != txid: + self._txid = txid + self.get_tx() + self.txidChanged.emit() + + oldfeeChanged = pyqtSignal() + @pyqtProperty(QEAmount, notify=oldfeeChanged) + def oldfee(self): + return self._oldfee + + @oldfee.setter + def oldfee(self, oldfee): + if self._oldfee != oldfee: + self._oldfee.copyFrom(oldfee) + self.oldfeeChanged.emit() + + oldfeeRateChanged = pyqtSignal() + @pyqtProperty(str, notify=oldfeeRateChanged) + def oldfeeRate(self): + return self._oldfee_rate + + @oldfeeRate.setter + def oldfeeRate(self, oldfeerate): + if self._oldfee_rate != oldfeerate: + self._oldfee_rate = oldfeerate + self.oldfeeRateChanged.emit() + + + def get_tx(self): + assert self._txid + self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) + assert self._orig_tx + + if not isinstance(self._orig_tx, PartialTransaction): + self._orig_tx = PartialTransaction.from_tx(self._orig_tx) + + if not self._add_info_to_tx_from_wallet_and_network(self._orig_tx): + return + + self.update_from_tx(self._orig_tx) + + self.oldfee = self.fee + self.oldfeeRate = self.feeRate + self.update() + + # TODO: duplicated from kivy gui, candidate for moving into backend wallet + def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: + """Returns whether successful.""" + # note side-effect: tx is being mutated + assert isinstance(tx, PartialTransaction) + try: + # note: this might download input utxos over network + # FIXME network code in gui thread... + tx.add_info_from_wallet(self._wallet.wallet, ignore_network_issues=False) + except NetworkException as e: + # self.app.show_error(repr(e)) + self._logger.error(repr(e)) + return False + return True + + def update(self): + if not self._txid: + # not initialized yet + return + + fee_per_kb = self._config.fee_per_kb() + if fee_per_kb is None: + # dynamic method and no network + self._logger.debug('no fee_per_kb') + self.warning = _('Cannot determine dynamic fees, not connected') + return + + new_fee_rate = fee_per_kb / 1000 + + try: + self._tx = self._wallet.wallet.dscancel( + tx=self._orig_tx, + new_fee_rate=new_fee_rate, + ) + except CannotDoubleSpendTx as e: + self._valid = False + self.validChanged.emit() + self._logger.error(str(e)) + self.warning = str(e) + return + else: + self.warning = '' + + self._tx.set_rbf(self.rbf) + + self.update_from_tx(self._tx) + + self._valid = True + self.validChanged.emit() + + @pyqtSlot(result=str) + def getNewTx(self): + return str(self._tx)