1
0

qml: initial RbF bump fee feature

This commit is contained in:
Sander van Grieken
2022-10-25 15:13:57 +02:00
parent 1a7fc2cff7
commit 902f16204c
7 changed files with 656 additions and 189 deletions

View File

@@ -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()
}
}
}
}

View File

@@ -155,31 +155,9 @@ ElDialog {
} }
} }
ComboBox { FeeMethodComboBox {
id: target id: target
textRole: 'text' feeslider: finalizer
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
} }
CheckBox { 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 { Label {
text: qsTr('Outputs') text: qsTr('Outputs')
Layout.columnSpan: 2 Layout.columnSpan: 2

View File

@@ -28,7 +28,18 @@ Pane {
action: Action { action: Action {
text: qsTr('Bump fee') text: qsTr('Bump fee')
enabled: txdetails.canBump 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 { MenuItem {
@@ -36,6 +47,15 @@ Pane {
action: Action { action: Action {
text: qsTr('Cancel double-spend') text: qsTr('Cancel double-spend')
enabled: txdetails.canCancel 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 rawtx: root.rawtx
onLabelChanged: root.detailsChanged() 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()
}
}
} }

View File

@@ -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)
}
}

View File

@@ -19,7 +19,7 @@ from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper
from .qewalletdb import QEWalletDB from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin from .qebitcoin import QEBitcoin
from .qefx import QEFX from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer from .qetxfinalizer import QETxFinalizer, QETxFeeBumper
from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment
from .qerequestdetails import QERequestDetails from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount from .qetypes import QEAmount
@@ -216,6 +216,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails') qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails')
qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper') qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper')
qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails') 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(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') qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property')

View File

@@ -82,6 +82,8 @@ class QETxDetails(QObject):
if self._rawtx != rawtx: if self._rawtx != rawtx:
self._logger.debug('rawtx set -> %s' % rawtx) self._logger.debug('rawtx set -> %s' % rawtx)
self._rawtx = rawtx self._rawtx = rawtx
if not rawtx:
return
try: try:
self._tx = tx_from_any(rawtx, deserialize=True) self._tx = tx_from_any(rawtx, deserialize=True)
self._logger.debug('tx type is %s' % str(type(self._tx))) 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) 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, # can be None if outputs unrelated to wallet seed,
# e.g. to_local local_force_close commitment CSV-locked p2wsh script # e.g. to_local local_force_close commitment CSV-locked p2wsh script

View File

@@ -4,42 +4,23 @@ 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 from electrum.transaction import PartialTxOutput, PartialTransaction
from electrum.util import NotEnoughFunds, profiler from electrum.util import NotEnoughFunds, profiler
from electrum.wallet import CannotBumpFee
from .qewallet import QEWallet from .qewallet import QEWallet
from .qetypes import QEAmount from .qetypes import QEAmount
class QETxFinalizer(QObject): class FeeSlider(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 = ''
_wallet = None _wallet = None
_valid = False
_sliderSteps = 0 _sliderSteps = 0
_sliderPos = 0 _sliderPos = 0
_method = -1 _method = -1
_warning = ''
_target = '' _target = ''
_rbf = False _config = None
_canRbf = False
_outputs = []
config = None
validChanged = pyqtSignal() def __init__(self, parent=None):
@pyqtProperty(bool, notify=validChanged) super().__init__(parent)
def valid(self):
return self._valid
walletChanged = pyqtSignal() walletChanged = pyqtSignal()
@pyqtProperty(QEWallet, notify=walletChanged) @pyqtProperty(QEWallet, notify=walletChanged)
@@ -50,118 +31,10 @@ class QETxFinalizer(QObject):
def wallet(self, wallet: QEWallet): def wallet(self, wallet: QEWallet):
if self._wallet != wallet: if self._wallet != wallet:
self._wallet = wallet self._wallet = wallet
self.config = self._wallet.wallet.config self._config = self._wallet.wallet.config
self.read_config() self.read_config()
self.walletChanged.emit() self.walletChanged.emit()
addressChanged = pyqtSignal()
@pyqtProperty(str, notify=addressChanged)
def address(self):
return self._address
@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
feeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=feeChanged)
def fee(self):
return self._fee
@fee.setter
def fee(self, fee):
if self._fee != fee:
self._fee.copyFrom(fee)
self.feeChanged.emit()
feeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=feeRateChanged)
def feeRate(self):
return self._feeRate
@feeRate.setter
def feeRate(self, feeRate):
if self._feeRate != feeRate:
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):
return self._rbf
@rbf.setter
def rbf(self, rbf):
if self._rbf != rbf:
self._rbf = rbf
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):
return self._outputs
@outputs.setter
def outputs(self, outputs):
if self._outputs != outputs:
self._outputs = outputs
self.outputsChanged.emit()
warningChanged = pyqtSignal()
@pyqtProperty(str, notify=warningChanged)
def warning(self):
return self._warning
@warning.setter
def warning(self, warning):
if self._warning != warning:
self._warning = warning
self.warningChanged.emit()
sliderStepsChanged = pyqtSignal() sliderStepsChanged = pyqtSignal()
@pyqtProperty(int, notify=sliderStepsChanged) @pyqtProperty(int, notify=sliderStepsChanged)
def sliderSteps(self): def sliderSteps(self):
@@ -197,36 +70,200 @@ class QETxFinalizer(QObject):
mempool = self._method == 2 mempool = self._method == 2
return dynfees, mempool 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): def update_slider(self):
dynfees, mempool = self.get_method() dynfees, mempool = self.get_method()
maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) maxp, pos, fee_rate = self._config.get_fee_slider(dynfees, mempool)
self._sliderSteps = maxp self._sliderSteps = maxp
self._sliderPos = pos self._sliderPos = pos
self.sliderStepsChanged.emit() self.sliderStepsChanged.emit()
self.sliderPosChanged.emit() self.sliderPosChanged.emit()
def update_target(self):
target, tooltip, dyn = self._config.get_fee_target()
self.target = target
def read_config(self): def read_config(self):
mempool = self.config.use_mempool_fees() mempool = self._config.use_mempool_fees()
dynfees = self.config.is_dynfee() dynfees = self._config.is_dynfee()
self._method = (2 if mempool else 1) if dynfees else 0 self._method = (2 if mempool else 1) if dynfees else 0
self.update_slider() self.update_slider()
self.methodChanged.emit() self.methodChanged.emit()
self.update_target()
self.update() self.update()
def save_config(self): def save_config(self):
value = int(self._sliderPos) value = int(self._sliderPos)
dynfees, mempool = self.get_method() dynfees, mempool = self.get_method()
self.config.set_key('dynamic_fees', dynfees, False) self._config.set_key('dynamic_fees', dynfees, False)
self.config.set_key('mempool_fees', mempool, False) self._config.set_key('mempool_fees', mempool, False)
if dynfees: if dynfees:
if mempool: if mempool:
self.config.set_key('depth_level', value, True) self._config.set_key('depth_level', value, True)
else: else:
self.config.set_key('fee_level', value, True) self._config.set_key('fee_level', value, True)
else: else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True) self._config.set_key('fee_per_kb', self._config.static_fee(value), True)
self.update_target()
self.update() 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)
def fee(self):
return self._fee
@fee.setter
def fee(self, fee):
if self._fee != fee:
self._fee.copyFrom(fee)
self.feeChanged.emit()
feeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=feeRateChanged)
def feeRate(self):
return self._feeRate
@feeRate.setter
def feeRate(self, feeRate):
if self._feeRate != feeRate:
self._feeRate = feeRate
self.feeRateChanged.emit()
rbfChanged = pyqtSignal()
@pyqtProperty(bool, notify=rbfChanged)
def rbf(self):
return self._rbf
@rbf.setter
def rbf(self, rbf):
if self._rbf != rbf:
self._rbf = rbf
self.update()
self.rbfChanged.emit()
outputsChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=outputsChanged)
def outputs(self):
return self._outputs
@outputs.setter
def outputs(self, outputs):
if self._outputs != outputs:
self._outputs = outputs
self.outputsChanged.emit()
warningChanged = pyqtSignal()
@pyqtProperty(str, notify=warningChanged)
def warning(self):
return self._warning
@warning.setter
def warning(self, warning):
if self._warning != warning:
self._warning = warning
self.warningChanged.emit()
validChanged = pyqtSignal()
@pyqtProperty(bool, notify=validChanged)
def valid(self):
return self._valid
def update_from_tx(self, tx):
tx_size = tx.estimated_size()
fee = tx.get_fee()
feerate = Decimal(fee) / tx_size # sat/byte
self.fee = QEAmount(amount_sat=int(fee))
self.feeRate = f'{feerate:.1f}'
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
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
_logger = get_logger(__name__)
_address = ''
_amount = QEAmount()
_effectiveAmount = QEAmount()
_canRbf = False
addressChanged = pyqtSignal()
@pyqtProperty(str, notify=addressChanged)
def address(self):
return self._address
@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 @profiler
def make_tx(self, amount): def make_tx(self, amount):
self._logger.debug('make_tx amount = %s' % str(amount)) self._logger.debug('make_tx amount = %s' % str(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()))) 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 return tx
@pyqtSlot()
def update(self): def update(self):
try: try:
# make unsigned transaction # make unsigned transaction
@@ -276,26 +303,18 @@ class QETxFinalizer(QObject):
self._effectiveAmount.satsInt = amount self._effectiveAmount.satsInt = amount
self.effectiveAmountChanged.emit() self.effectiveAmountChanged.emit()
tx_size = tx.estimated_size() self.update_from_tx(tx)
fee = tx.get_fee()
feerate = Decimal(fee) / tx_size # sat/byte
self._fee.satsInt = int(fee)
self.feeRate = f'{feerate:.1f}'
#TODO #TODO
#x_fee = run_hook('get_tx_extra_fee', self._wallet.wallet, tx) #x_fee = run_hook('get_tx_extra_fee', self._wallet.wallet, tx)
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning( 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: if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple allow_send, long_warning, short_warning = fee_warning_tuple
self.warning = long_warning self.warning = long_warning
else: else:
self.warning = '' self.warning = ''
target, tooltip, dyn = self.config.get_fee_target()
self.target = target
self._valid = True self._valid = True
self.validChanged.emit() self.validChanged.emit()
@@ -318,3 +337,133 @@ class QETxFinalizer(QObject):
return self._tx.to_qr_data() return self._tx.to_qr_data()
else: else:
return str(self._tx) 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)