Browse Source

qml: initial RbF bump fee feature

master
Sander van Grieken 3 years ago
parent
commit
902f16204c
  1. 264
      electrum/gui/qml/components/BumpFeeDialog.qml
  2. 35
      electrum/gui/qml/components/ConfirmTxDialog.qml
  3. 40
      electrum/gui/qml/components/TxDetails.qml
  4. 26
      electrum/gui/qml/components/controls/FeeMethodComboBox.qml
  5. 3
      electrum/gui/qml/qeapp.py
  6. 4
      electrum/gui/qml/qetxdetails.py
  7. 447
      electrum/gui/qml/qetxfinalizer.py

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

35
electrum/gui/qml/components/ConfirmTxDialog.qml

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

40
electrum/gui/qml/components/TxDetails.qml

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

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

3
electrum/gui/qml/qeapp.py

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

4
electrum/gui/qml/qetxdetails.py

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

447
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.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,37 +31,106 @@ 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() sliderStepsChanged = pyqtSignal()
@pyqtProperty(str, notify=addressChanged) @pyqtProperty(int, notify=sliderStepsChanged)
def address(self): def sliderSteps(self):
return self._address return self._sliderSteps
@address.setter sliderPosChanged = pyqtSignal()
def address(self, address): @pyqtProperty(int, notify=sliderPosChanged)
if self._address != address: def sliderPos(self):
self._address = address return self._sliderPos
self.addressChanged.emit()
amountChanged = pyqtSignal() @sliderPos.setter
@pyqtProperty(QEAmount, notify=amountChanged) def sliderPos(self, sliderPos):
def amount(self): if self._sliderPos != sliderPos:
return self._amount self._sliderPos = sliderPos
self.save_config()
self.sliderPosChanged.emit()
@amount.setter methodChanged = pyqtSignal()
def amount(self, amount): @pyqtProperty(int, notify=methodChanged)
if self._amount != amount: def method(self):
self._logger.debug(str(amount)) return self._method
self._amount.copyFrom(amount)
self.amountChanged.emit()
effectiveAmountChanged = pyqtSignal() @method.setter
@pyqtProperty(QEAmount, notify=effectiveAmountChanged) def method(self, method):
def effectiveAmount(self): if self._method != method:
return self._effectiveAmount 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() feeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=feeChanged) @pyqtProperty(QEAmount, notify=feeChanged)
@ -104,17 +154,6 @@ class QETxFinalizer(QObject):
self._feeRate = feeRate self._feeRate = feeRate
self.feeRateChanged.emit() 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() rbfChanged = pyqtSignal()
@pyqtProperty(bool, notify=rbfChanged) @pyqtProperty(bool, notify=rbfChanged)
def rbf(self): def rbf(self):
@ -127,19 +166,6 @@ class QETxFinalizer(QObject):
self.update() self.update()
self.rbfChanged.emit() 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() outputsChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=outputsChanged) @pyqtProperty('QVariantList', notify=outputsChanged)
def outputs(self): def outputs(self):
@ -162,70 +188,81 @@ class QETxFinalizer(QObject):
self._warning = warning self._warning = warning
self.warningChanged.emit() self.warningChanged.emit()
sliderStepsChanged = pyqtSignal() validChanged = pyqtSignal()
@pyqtProperty(int, notify=sliderStepsChanged) @pyqtProperty(bool, notify=validChanged)
def sliderSteps(self): def valid(self):
return self._sliderSteps return self._valid
sliderPosChanged = pyqtSignal() def update_from_tx(self, tx):
@pyqtProperty(int, notify=sliderPosChanged) tx_size = tx.estimated_size()
def sliderPos(self): fee = tx.get_fee()
return self._sliderPos feerate = Decimal(fee) / tx_size # sat/byte
@sliderPos.setter self.fee = QEAmount(amount_sat=int(fee))
def sliderPos(self, sliderPos): self.feeRate = f'{feerate:.1f}'
if self._sliderPos != sliderPos:
self._sliderPos = sliderPos
self.save_config()
self.sliderPosChanged.emit()
methodChanged = pyqtSignal() outputs = []
@pyqtProperty(int, notify=methodChanged) for o in tx.outputs():
def method(self): outputs.append({
return self._method '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 class QETxFinalizer(TxFeeSlider):
def method(self, method): def __init__(self, parent=None, *, make_tx=None, accept=None):
if self._method != method: super().__init__(parent)
self._method = method self.f_make_tx = make_tx
self.update_slider() self.f_accept = accept
self.methodChanged.emit()
self.save_config()
def get_method(self): _logger = get_logger(__name__)
dynfees = self._method > 0
mempool = self._method == 2
return dynfees, mempool
def update_slider(self): _address = ''
dynfees, mempool = self.get_method() _amount = QEAmount()
maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) _effectiveAmount = QEAmount()
self._sliderSteps = maxp _canRbf = False
self._sliderPos = pos
self.sliderStepsChanged.emit()
self.sliderPosChanged.emit()
def read_config(self): addressChanged = pyqtSignal()
mempool = self.config.use_mempool_fees() @pyqtProperty(str, notify=addressChanged)
dynfees = self.config.is_dynfee() def address(self):
self._method = (2 if mempool else 1) if dynfees else 0 return self._address
self.update_slider()
self.methodChanged.emit()
self.update()
def save_config(self): @address.setter
value = int(self._sliderPos) def address(self, address):
dynfees, mempool = self.get_method() if self._address != address:
self.config.set_key('dynamic_fees', dynfees, False) self._address = address
self.config.set_key('mempool_fees', mempool, False) self.addressChanged.emit()
if dynfees:
if mempool: amountChanged = pyqtSignal()
self.config.set_key('depth_level', value, True) @pyqtProperty(QEAmount, notify=amountChanged)
else: def amount(self):
self.config.set_key('fee_level', value, True) return self._amount
else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True) @amount.setter
self.update() 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):
@ -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)

Loading…
Cancel
Save