qml: initial RbF bump fee feature
This commit is contained in:
264
electrum/gui/qml/components/BumpFeeDialog.qml
Normal file
264
electrum/gui/qml/components/BumpFeeDialog.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
26
electrum/gui/qml/components/controls/FeeMethodComboBox.qml
Normal file
26
electrum/gui/qml/components/controls/FeeMethodComboBox.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,118 +31,10 @@ 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
|
||||
|
||||
@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()
|
||||
@pyqtProperty(int, notify=sliderStepsChanged)
|
||||
def sliderSteps(self):
|
||||
@@ -197,36 +70,200 @@ class QETxFinalizer(QObject):
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
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)
|
||||
self._config.set_key('depth_level', value, True)
|
||||
else:
|
||||
self.config.set_key('fee_level', value, True)
|
||||
self._config.set_key('fee_level', value, True)
|
||||
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()
|
||||
|
||||
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
|
||||
def make_tx(self, 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())))
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user