From 902f16204cd34eecae15e7961548da57a157b2c8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Oct 2022 15:13:57 +0200 Subject: [PATCH] qml: initial RbF bump fee feature --- electrum/gui/qml/components/BumpFeeDialog.qml | 264 +++++++++++ .../gui/qml/components/ConfirmTxDialog.qml | 35 +- electrum/gui/qml/components/TxDetails.qml | 40 +- .../components/controls/FeeMethodComboBox.qml | 26 + electrum/gui/qml/qeapp.py | 3 +- electrum/gui/qml/qetxdetails.py | 4 +- electrum/gui/qml/qetxfinalizer.py | 447 ++++++++++++------ 7 files changed, 643 insertions(+), 176 deletions(-) create mode 100644 electrum/gui/qml/components/BumpFeeDialog.qml create mode 100644 electrum/gui/qml/components/controls/FeeMethodComboBox.qml diff --git a/electrum/gui/qml/components/BumpFeeDialog.qml b/electrum/gui/qml/components/BumpFeeDialog.qml new file mode 100644 index 000000000..d65fc200a --- /dev/null +++ b/electrum/gui/qml/components/BumpFeeDialog.qml @@ -0,0 +1,264 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.14 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +ElDialog { + id: dialog + + required property string txid + required property QtObject txfeebumper + + signal txaccepted + + title: qsTr('Bump Fee') + + width: parent.width + height: parent.height + padding: 0 + + standardButtons: Dialog.Cancel + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + // function updateAmountText() { + // btcValue.text = Config.formatSats(finalizer.effectiveAmount, false) + // fiatValue.text = Daemon.fx.enabled + // ? '(' + Daemon.fx.fiatValue(finalizer.effectiveAmount, false) + ' ' + Daemon.fx.fiatCurrency + ')' + // : '' + // } + + ColumnLayout { + width: parent.width + height: parent.height + spacing: 0 + + GridLayout { + Layout.preferredWidth: parent.width + Layout.leftMargin: constants.paddingLarge + Layout.rightMargin: constants.paddingLarge + columns: 2 + + Label { + text: qsTr('Old fee') + color: Material.accentColor + } + + RowLayout { + Label { + id: oldfee + text: Config.formatSats(txfeebumper.oldfee) + } + + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + text: qsTr('Old fee rate') + color: Material.accentColor + } + + RowLayout { + Label { + id: oldfeeRate + text: txfeebumper.oldfeeRate + } + + Label { + text: 'sat/vB' + color: Material.accentColor + } + } + + // Label { + // id: amountLabel + // text: qsTr('Amount to send') + // color: Material.accentColor + // } + // + // RowLayout { + // Layout.fillWidth: true + // Label { + // id: btcValue + // font.bold: true + // } + // + // Label { + // text: Config.baseUnit + // color: Material.accentColor + // } + // + // Label { + // id: fiatValue + // Layout.fillWidth: true + // font.pixelSize: constants.fontSizeMedium + // } + // + // Component.onCompleted: updateAmountText() + // Connections { + // target: finalizer + // function onEffectiveAmountChanged() { + // updateAmountText() + // } + // } + // } + + Label { + text: qsTr('Mining fee') + color: Material.accentColor + } + + RowLayout { + Label { + id: fee + text: txfeebumper.valid ? Config.formatSats(txfeebumper.fee) : '' + } + + Label { + visible: txfeebumper.valid + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + text: qsTr('Fee rate') + color: Material.accentColor + } + + RowLayout { + Label { + id: feeRate + text: txfeebumper.valid ? txfeebumper.feeRate : '' + } + + Label { + visible: txfeebumper.valid + text: 'sat/vB' + color: Material.accentColor + } + } + + Label { + text: qsTr('Target') + color: Material.accentColor + } + + Label { + id: targetdesc + text: txfeebumper.target + } + + Slider { + id: feeslider + leftPadding: constants.paddingMedium + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: txfeebumper.sliderSteps + onValueChanged: { + if (activeFocus) + txfeebumper.sliderPos = value + } + Component.onCompleted: { + value = txfeebumper.sliderPos + } + Connections { + target: txfeebumper + function onSliderPosChanged() { + feeslider.value = txfeebumper.sliderPos + } + } + } + + FeeMethodComboBox { + id: target + feeslider: txfeebumper + } + + CheckBox { + id: final_cb + text: qsTr('Replace-by-Fee') + Layout.columnSpan: 2 + checked: txfeebumper.rbf + onCheckedChanged: { + if (activeFocus) + txfeebumper.rbf = checked + } + } + + InfoTextArea { + Layout.columnSpan: 2 + Layout.preferredWidth: parent.width * 3/4 + Layout.alignment: Qt.AlignHCenter + visible: txfeebumper.warning != '' + text: txfeebumper.warning + iconStyle: InfoTextArea.IconStyle.Warn + } + + Label { + visible: txfeebumper.valid + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: txfeebumper.valid ? txfeebumper.outputs : [] + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value_sats) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } + } + } + } + } + + Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } + + FlatButton { + id: sendButton + Layout.fillWidth: true + text: qsTr('Ok') + icon.source: '../../icons/confirmed.png' + enabled: txfeebumper.valid + onClicked: { + txaccepted() + dialog.close() + } + } + } + +} diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 8fe52000a..67ae49499 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -155,31 +155,9 @@ ElDialog { } } - ComboBox { + FeeMethodComboBox { id: target - textRole: 'text' - valueRole: 'value' - model: [ - { text: qsTr('ETA'), value: 1 }, - { text: qsTr('Mempool'), value: 2 }, - { text: qsTr('Static'), value: 0 } - ] - onCurrentValueChanged: { - if (activeFocus) - finalizer.method = currentValue - } - Component.onCompleted: { - currentIndex = indexOfValue(finalizer.method) - } - } - - InfoTextArea { - Layout.columnSpan: 2 - Layout.preferredWidth: parent.width * 3/4 - Layout.alignment: Qt.AlignHCenter - visible: finalizer.warning != '' - text: finalizer.warning - iconStyle: InfoTextArea.IconStyle.Warn + feeslider: finalizer } CheckBox { @@ -194,6 +172,15 @@ ElDialog { } } + InfoTextArea { + Layout.columnSpan: 2 + Layout.preferredWidth: parent.width * 3/4 + Layout.alignment: Qt.AlignHCenter + visible: finalizer.warning != '' + text: finalizer.warning + iconStyle: InfoTextArea.IconStyle.Warn + } + Label { text: qsTr('Outputs') Layout.columnSpan: 2 diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index b0763a14f..70c67b2cc 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -28,7 +28,18 @@ Pane { action: Action { text: qsTr('Bump fee') enabled: txdetails.canBump - //onTriggered: + onTriggered: { + var dialog = bumpFeeDialog.createObject(root, { txid: root.txid }) + dialog.open() + } + } + } + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Child pays for parent') + enabled: txdetails.canCpfp + onTriggered: notificationPopup.show('Not implemented') } } MenuItem { @@ -36,6 +47,15 @@ Pane { action: Action { text: qsTr('Cancel double-spend') enabled: txdetails.canCancel + onTriggered: notificationPopup.show('Not implemented') + } + } + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Remove') + enabled: txdetails.canRemove + onTriggered: notificationPopup.show('Not implemented') } } } @@ -349,4 +369,22 @@ Pane { rawtx: root.rawtx onLabelChanged: root.detailsChanged() } + + Component { + id: bumpFeeDialog + BumpFeeDialog { + id: dialog + txfeebumper: TxFeeBumper { + id: txfeebumper + wallet: Daemon.currentWallet + txid: dialog.txid + } + + onTxaccepted: { + root.rawtx = txfeebumper.getNewTx() + } + onClosed: destroy() + } + } + } diff --git a/electrum/gui/qml/components/controls/FeeMethodComboBox.qml b/electrum/gui/qml/components/controls/FeeMethodComboBox.qml new file mode 100644 index 000000000..b2ca188f7 --- /dev/null +++ b/electrum/gui/qml/components/controls/FeeMethodComboBox.qml @@ -0,0 +1,26 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.0 + +import org.electrum 1.0 + +ComboBox { + id: control + + required property QtObject feeslider + + textRole: 'text' + valueRole: 'value' + + model: [ + { text: qsTr('ETA'), value: 1 }, + { text: qsTr('Mempool'), value: 2 }, + { text: qsTr('Static'), value: 0 } + ] + onCurrentValueChanged: { + if (activeFocus) + feeslider.method = currentValue + } + Component.onCompleted: { + currentIndex = indexOfValue(feeslider.method) + } +} diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index c21aefee3..730a5ded6 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 +from .qetxfinalizer import QETxFinalizer, QETxFeeBumper from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment from .qerequestdetails import QERequestDetails from .qetypes import QEAmount @@ -216,6 +216,7 @@ class ElectrumQmlApplication(QGuiApplication): qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails') qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper') qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails') + qmlRegisterType(QETxFeeBumper, 'org.electrum', 1, 0, 'TxFeeBumper') 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/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index c04b1e143..4abaeb1a1 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -82,6 +82,8 @@ class QETxDetails(QObject): if self._rawtx != rawtx: self._logger.debug('rawtx set -> %s' % rawtx) self._rawtx = rawtx + if not rawtx: + return try: self._tx = tx_from_any(rawtx, deserialize=True) self._logger.debug('tx type is %s' % str(type(self._tx))) @@ -209,7 +211,7 @@ class QETxDetails(QObject): txinfo = self._wallet.wallet.get_tx_info(self._tx) - #self._logger.debug(repr(txinfo)) + self._logger.debug(repr(txinfo)) # can be None if outputs unrelated to wallet seed, # e.g. to_local local_force_close commitment CSV-locked p2wsh script diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index e86b331a3..8c7224c2a 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -4,42 +4,23 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger from electrum.i18n import _ -from electrum.transaction import PartialTxOutput +from electrum.transaction import PartialTxOutput, PartialTransaction from electrum.util import NotEnoughFunds, profiler +from electrum.wallet import CannotBumpFee from .qewallet import QEWallet from .qetypes import QEAmount -class QETxFinalizer(QObject): - def __init__(self, parent=None, *, make_tx=None, accept=None): - super().__init__(parent) - self.f_make_tx = make_tx - self.f_accept = accept - self._tx = None - - _logger = get_logger(__name__) - - _address = '' - _amount = QEAmount() - _effectiveAmount = QEAmount() - _fee = QEAmount() - _feeRate = '' +class FeeSlider(QObject): _wallet = None - _valid = False _sliderSteps = 0 _sliderPos = 0 _method = -1 - _warning = '' _target = '' - _rbf = False - _canRbf = False - _outputs = [] - config = None + _config = None - validChanged = pyqtSignal() - @pyqtProperty(bool, notify=validChanged) - def valid(self): - return self._valid + def __init__(self, parent=None): + super().__init__(parent) walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) @@ -50,37 +31,106 @@ class QETxFinalizer(QObject): def wallet(self, wallet: QEWallet): if self._wallet != wallet: self._wallet = wallet - self.config = self._wallet.wallet.config + self._config = self._wallet.wallet.config self.read_config() self.walletChanged.emit() - addressChanged = pyqtSignal() - @pyqtProperty(str, notify=addressChanged) - def address(self): - return self._address + sliderStepsChanged = pyqtSignal() + @pyqtProperty(int, notify=sliderStepsChanged) + def sliderSteps(self): + return self._sliderSteps - @address.setter - def address(self, address): - if self._address != address: - self._address = address - self.addressChanged.emit() + sliderPosChanged = pyqtSignal() + @pyqtProperty(int, notify=sliderPosChanged) + def sliderPos(self): + return self._sliderPos - amountChanged = pyqtSignal() - @pyqtProperty(QEAmount, notify=amountChanged) - def amount(self): - return self._amount + @sliderPos.setter + def sliderPos(self, sliderPos): + if self._sliderPos != sliderPos: + self._sliderPos = sliderPos + self.save_config() + self.sliderPosChanged.emit() - @amount.setter - def amount(self, amount): - if self._amount != amount: - self._logger.debug(str(amount)) - self._amount.copyFrom(amount) - self.amountChanged.emit() + methodChanged = pyqtSignal() + @pyqtProperty(int, notify=methodChanged) + def method(self): + return self._method - effectiveAmountChanged = pyqtSignal() - @pyqtProperty(QEAmount, notify=effectiveAmountChanged) - def effectiveAmount(self): - return self._effectiveAmount + @method.setter + def method(self, method): + if self._method != method: + self._method = method + self.update_slider() + self.methodChanged.emit() + self.save_config() + + def get_method(self): + dynfees = self._method > 0 + mempool = self._method == 2 + return dynfees, mempool + + targetChanged = pyqtSignal() + @pyqtProperty(str, notify=targetChanged) + def target(self): + return self._target + + @target.setter + def target(self, target): + if self._target != target: + self._target = target + self.targetChanged.emit() + + def update_slider(self): + dynfees, mempool = self.get_method() + maxp, pos, fee_rate = self._config.get_fee_slider(dynfees, mempool) + self._sliderSteps = maxp + self._sliderPos = pos + self.sliderStepsChanged.emit() + self.sliderPosChanged.emit() + + def update_target(self): + target, tooltip, dyn = self._config.get_fee_target() + self.target = target + + def read_config(self): + mempool = self._config.use_mempool_fees() + dynfees = self._config.is_dynfee() + self._method = (2 if mempool else 1) if dynfees else 0 + self.update_slider() + self.methodChanged.emit() + self.update_target() + self.update() + + def save_config(self): + value = int(self._sliderPos) + dynfees, mempool = self.get_method() + self._config.set_key('dynamic_fees', dynfees, False) + self._config.set_key('mempool_fees', mempool, False) + if dynfees: + if mempool: + self._config.set_key('depth_level', value, True) + else: + self._config.set_key('fee_level', value, True) + else: + self._config.set_key('fee_per_kb', self._config.static_fee(value), True) + self.update_target() + self.update() + + def update(self): + raise NotImplementedError() + +class TxFeeSlider(FeeSlider): + _fee = QEAmount() + _feeRate = '' + _rbf = False + _tx = None + _outputs = [] + _valid = False + _warning = '' + + def __init__(self, parent=None): + super().__init__(parent) feeChanged = pyqtSignal() @pyqtProperty(QEAmount, notify=feeChanged) @@ -104,17 +154,6 @@ class QETxFinalizer(QObject): self._feeRate = feeRate self.feeRateChanged.emit() - targetChanged = pyqtSignal() - @pyqtProperty(str, notify=targetChanged) - def target(self): - return self._target - - @target.setter - def target(self, target): - if self._target != target: - self._target = target - self.targetChanged.emit() - rbfChanged = pyqtSignal() @pyqtProperty(bool, notify=rbfChanged) def rbf(self): @@ -127,19 +166,6 @@ class QETxFinalizer(QObject): self.update() self.rbfChanged.emit() - canRbfChanged = pyqtSignal() - @pyqtProperty(bool, notify=canRbfChanged) - def canRbf(self): - return self._canRbf - - @canRbf.setter - def canRbf(self, canRbf): - if self._canRbf != canRbf: - self._canRbf = canRbf - self.canRbfChanged.emit() - if not canRbf and self.rbf: - self.rbf = False - outputsChanged = pyqtSignal() @pyqtProperty('QVariantList', notify=outputsChanged) def outputs(self): @@ -162,70 +188,81 @@ class QETxFinalizer(QObject): self._warning = warning self.warningChanged.emit() - sliderStepsChanged = pyqtSignal() - @pyqtProperty(int, notify=sliderStepsChanged) - def sliderSteps(self): - return self._sliderSteps + validChanged = pyqtSignal() + @pyqtProperty(bool, notify=validChanged) + def valid(self): + return self._valid - sliderPosChanged = pyqtSignal() - @pyqtProperty(int, notify=sliderPosChanged) - def sliderPos(self): - return self._sliderPos + def update_from_tx(self, tx): + tx_size = tx.estimated_size() + fee = tx.get_fee() + feerate = Decimal(fee) / tx_size # sat/byte - @sliderPos.setter - def sliderPos(self, sliderPos): - if self._sliderPos != sliderPos: - self._sliderPos = sliderPos - self.save_config() - self.sliderPosChanged.emit() + self.fee = QEAmount(amount_sat=int(fee)) + self.feeRate = f'{feerate:.1f}' - methodChanged = pyqtSignal() - @pyqtProperty(int, notify=methodChanged) - def method(self): - return self._method + outputs = [] + for o in tx.outputs(): + outputs.append({ + 'address': o.get_ui_address_str(), + 'value_sats': o.value, + 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()) + }) + self.outputs = outputs - @method.setter - def method(self, method): - if self._method != method: - self._method = method - self.update_slider() - self.methodChanged.emit() - self.save_config() +class QETxFinalizer(TxFeeSlider): + def __init__(self, parent=None, *, make_tx=None, accept=None): + super().__init__(parent) + self.f_make_tx = make_tx + self.f_accept = accept - def get_method(self): - dynfees = self._method > 0 - mempool = self._method == 2 - return dynfees, mempool + _logger = get_logger(__name__) - def update_slider(self): - dynfees, mempool = self.get_method() - maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) - self._sliderSteps = maxp - self._sliderPos = pos - self.sliderStepsChanged.emit() - self.sliderPosChanged.emit() + _address = '' + _amount = QEAmount() + _effectiveAmount = QEAmount() + _canRbf = False - def read_config(self): - mempool = self.config.use_mempool_fees() - dynfees = self.config.is_dynfee() - self._method = (2 if mempool else 1) if dynfees else 0 - self.update_slider() - self.methodChanged.emit() - self.update() + addressChanged = pyqtSignal() + @pyqtProperty(str, notify=addressChanged) + def address(self): + return self._address - def save_config(self): - value = int(self._sliderPos) - dynfees, mempool = self.get_method() - self.config.set_key('dynamic_fees', dynfees, False) - self.config.set_key('mempool_fees', mempool, False) - if dynfees: - if mempool: - self.config.set_key('depth_level', value, True) - else: - self.config.set_key('fee_level', value, True) - else: - self.config.set_key('fee_per_kb', self.config.static_fee(value), True) - self.update() + @address.setter + def address(self, address): + if self._address != address: + self._address = address + self.addressChanged.emit() + + amountChanged = pyqtSignal() + @pyqtProperty(QEAmount, notify=amountChanged) + def amount(self): + return self._amount + + @amount.setter + def amount(self, amount): + if self._amount != amount: + self._logger.debug(str(amount)) + self._amount.copyFrom(amount) + self.amountChanged.emit() + + effectiveAmountChanged = pyqtSignal() + @pyqtProperty(QEAmount, notify=effectiveAmountChanged) + def effectiveAmount(self): + return self._effectiveAmount + + canRbfChanged = pyqtSignal() + @pyqtProperty(bool, notify=canRbfChanged) + def canRbf(self): + return self._canRbf + + @canRbf.setter + def canRbf(self, canRbf): + if self._canRbf != canRbf: + self._canRbf = canRbf + self.canRbfChanged.emit() + if not canRbf and self.rbf: + self.rbf = False @profiler def make_tx(self, amount): @@ -241,18 +278,8 @@ class QETxFinalizer(QObject): self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs()))) - outputs = [] - for o in tx.outputs(): - outputs.append({ - 'address': o.get_ui_address_str(), - 'value_sats': o.value, - 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()) - }) - self.outputs = outputs - return tx - @pyqtSlot() def update(self): try: # make unsigned transaction @@ -276,26 +303,18 @@ class QETxFinalizer(QObject): self._effectiveAmount.satsInt = amount self.effectiveAmountChanged.emit() - tx_size = tx.estimated_size() - fee = tx.get_fee() - feerate = Decimal(fee) / tx_size # sat/byte - - self._fee.satsInt = int(fee) - self.feeRate = f'{feerate:.1f}' + self.update_from_tx(tx) #TODO #x_fee = run_hook('get_tx_extra_fee', self._wallet.wallet, tx) fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning( - invoice_amt=amount, tx_size=tx_size, fee=fee) + invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee()) if fee_warning_tuple: allow_send, long_warning, short_warning = fee_warning_tuple self.warning = long_warning else: self.warning = '' - target, tooltip, dyn = self.config.get_fee_target() - self.target = target - self._valid = True self.validChanged.emit() @@ -318,3 +337,133 @@ class QETxFinalizer(QObject): return self._tx.to_qr_data() else: return str(self._tx) + + +class QETxFeeBumper(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.bump_fee( + tx=self._orig_tx, + txid=self._txid, + new_fee_rate=new_fee_rate, + ) + except CannotBumpFee 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) + + # TODO: deduce amount sent? + # TODO: we don't handle send-max txs correctly yet + # fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning( + # invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee()) + # if fee_warning_tuple: + # allow_send, long_warning, short_warning = fee_warning_tuple + # self.warning = long_warning + # else: + # self.warning = '' + + self._valid = True + self.validChanged.emit() + + @pyqtSlot(result=str) + def getNewTx(self): + return str(self._tx)