add confirm payment dialog/feepicker and qobject backing
This commit is contained in:
BIN
electrum/gui/icons/paste.png
Normal file
BIN
electrum/gui/icons/paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
195
electrum/gui/qml/components/ConfirmPaymentDialog.qml
Normal file
195
electrum/gui/qml/components/ConfirmPaymentDialog.qml
Normal file
@@ -0,0 +1,195 @@
|
||||
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"
|
||||
|
||||
Dialog {
|
||||
id: dialog
|
||||
|
||||
property alias address: finalizer.address
|
||||
property alias satoshis: finalizer.amount
|
||||
property string message
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
title: qsTr('Confirm Payment')
|
||||
|
||||
modal: true
|
||||
parent: Overlay.overlay
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#aa000000"
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: layout
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
columns: 2
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Amount to send')
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
font.bold: true
|
||||
text: Config.formatSats(satoshis, false)
|
||||
}
|
||||
|
||||
Label {
|
||||
text: Config.baseUnit
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
Label {
|
||||
id: fiatValue
|
||||
Layout.fillWidth: true
|
||||
text: Daemon.fx.enabled
|
||||
? '(' + Daemon.fx.fiatValue(satoshis, false) + ' ' + Daemon.fx.fiatCurrency + ')'
|
||||
: ''
|
||||
font.pixelSize: constants.fontSizeMedium
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Mining fee')
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
id: fee
|
||||
text: Config.formatSats(finalizer.fee)
|
||||
}
|
||||
|
||||
Label {
|
||||
text: Config.baseUnit
|
||||
color: Material.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Fee rate')
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
id: feeRate
|
||||
text: finalizer.feeRate
|
||||
}
|
||||
|
||||
Label {
|
||||
text: 'sat/vB'
|
||||
color: Material.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Target')
|
||||
}
|
||||
|
||||
Label {
|
||||
id: targetdesc
|
||||
text: finalizer.target
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: feeslider
|
||||
snapMode: Slider.SnapOnRelease
|
||||
stepSize: 1
|
||||
from: 0
|
||||
to: finalizer.sliderSteps
|
||||
onValueChanged: {
|
||||
if (activeFocus)
|
||||
finalizer.sliderPos = value
|
||||
}
|
||||
Component.onCompleted: {
|
||||
value = finalizer.sliderPos
|
||||
}
|
||||
Connections {
|
||||
target: finalizer
|
||||
function onSliderPosChanged() {
|
||||
feeslider.value = finalizer.sliderPos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
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
|
||||
visible: finalizer.warning != ''
|
||||
text: finalizer.warning
|
||||
iconStyle: InfoTextArea.IconStyle.Warn
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: final_cb
|
||||
text: qsTr('Final')
|
||||
Layout.columnSpan: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.columnSpan: 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Button {
|
||||
text: qsTr('Cancel')
|
||||
onClicked: dialog.close()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr('Pay')
|
||||
enabled: finalizer.valid
|
||||
onClicked: {
|
||||
var f_amount = parseFloat(dialog.satoshis)
|
||||
if (isNaN(f_amount))
|
||||
return
|
||||
var result = Daemon.currentWallet.send_onchain(dialog.address, dialog.satoshis, undefined, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
|
||||
}
|
||||
|
||||
TxFinalizer {
|
||||
id: finalizer
|
||||
wallet: Daemon.currentWallet
|
||||
onAmountChanged: console.log(amount)
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@ import QtQuick.Controls 2.0
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls.Material 2.0
|
||||
|
||||
import "controls"
|
||||
|
||||
Pane {
|
||||
id: rootItem
|
||||
|
||||
GridLayout {
|
||||
id: form
|
||||
width: parent.width
|
||||
rowSpacing: constants.paddingSmall
|
||||
columnSpacing: constants.paddingSmall
|
||||
@@ -30,11 +33,29 @@ Pane {
|
||||
placeholderText: qsTr('Paste address or invoice')
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: '../../icons/copy.png'
|
||||
icon.color: 'transparent'
|
||||
icon.height: constants.iconSizeSmall
|
||||
icon.width: constants.iconSizeSmall
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
ToolButton {
|
||||
icon.source: '../../icons/paste.png'
|
||||
icon.height: constants.iconSizeMedium
|
||||
icon.width: constants.iconSizeMedium
|
||||
onClicked: address.text = AppController.clipboardToText()
|
||||
}
|
||||
ToolButton {
|
||||
icon.source: '../../icons/qrcode.png'
|
||||
icon.height: constants.iconSizeMedium
|
||||
icon.width: constants.iconSizeMedium
|
||||
scale: 1.2
|
||||
onClicked: {
|
||||
var page = app.stack.push(Qt.resolvedUrl('Scan.qml'))
|
||||
page.onFound.connect(function() {
|
||||
console.log('got ' + page.invoiceData)
|
||||
address.text = page.invoiceData['address']
|
||||
amount.text = Config.satsToUnits(page.invoiceData['amount'])
|
||||
description.text = page.invoiceData['message']
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -71,7 +92,6 @@ Pane {
|
||||
|
||||
Item { width: 1; height: 1 }
|
||||
|
||||
|
||||
Item { width: 1; height: 1; visible: Daemon.fx.enabled }
|
||||
|
||||
TextField {
|
||||
@@ -97,54 +117,102 @@ Pane {
|
||||
Item { visible: Daemon.fx.enabled ; height: 1; width: 1 }
|
||||
|
||||
Label {
|
||||
text: qsTr('Fee')
|
||||
text: qsTr('Description')
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: fee
|
||||
id: description
|
||||
font.family: FixedFont
|
||||
placeholderText: qsTr('sat/vB')
|
||||
Layout.columnSpan: 2
|
||||
placeholderText: qsTr('Description')
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item { width: 1; height: 1 }
|
||||
|
||||
RowLayout {
|
||||
Layout.columnSpan: 4
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: constants.paddingMedium
|
||||
|
||||
Button {
|
||||
text: qsTr('Pay')
|
||||
text: qsTr('Save')
|
||||
enabled: false
|
||||
onClicked: {
|
||||
console.log('TODO: save')
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr('Pay now')
|
||||
enabled: amount.text != '' && address.text != ''// TODO proper validation
|
||||
onClicked: {
|
||||
var f_amount = parseFloat(amount.text)
|
||||
if (isNaN(f_amount))
|
||||
return
|
||||
var sats = Config.unitsToSats(f_amount)
|
||||
var result = Daemon.currentWallet.send_onchain(address.text, sats, undefined, false)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr('Scan QR Code')
|
||||
onClicked: {
|
||||
var page = app.stack.push(Qt.resolvedUrl('Scan.qml'))
|
||||
page.onFound.connect(function() {
|
||||
console.log('got ' + page.invoiceData)
|
||||
address.text = page.invoiceData['address']
|
||||
amount.text = Config.formatSats(page.invoiceData['amount'])
|
||||
var sats = Config.unitsToSats(amount.text).toString()
|
||||
var dialog = confirmPaymentDialog.createObject(app, {
|
||||
'address': address.text,
|
||||
'satoshis': sats,
|
||||
'message': description.text
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame {
|
||||
verticalPadding: 0
|
||||
horizontalPadding: 0
|
||||
|
||||
anchors {
|
||||
top: form.bottom
|
||||
topMargin: constants.paddingXLarge
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
background: PaneInsetBackground {}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: hitem.height
|
||||
Layout.preferredWidth: parent.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.lighter(Material.background, 1.25)
|
||||
}
|
||||
RowLayout {
|
||||
id: hitem
|
||||
width: parent.width
|
||||
Label {
|
||||
text: qsTr('Send queue')
|
||||
font.pixelSize: constants.fontSizeXLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listview
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: confirmPaymentDialog
|
||||
ConfirmPaymentDialog {}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Daemon.fx
|
||||
function onQuotesUpdated() {
|
||||
var a = Config.unitsToSats(amount.text)
|
||||
amountFiat.text = Daemon.fx.fiatValue(a)
|
||||
amountFiat.text = Daemon.fx.fiatValue(Config.unitsToSats(amount.text))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from .qeqr import QEQRParser, QEQRImageProvider
|
||||
from .qewalletdb import QEWalletDB
|
||||
from .qebitcoin import QEBitcoin
|
||||
from .qefx import QEFX
|
||||
from .qetxfinalizer import QETxFinalizer
|
||||
|
||||
notification = None
|
||||
|
||||
@@ -92,6 +93,10 @@ class QEAppController(QObject):
|
||||
def textToClipboard(self, text):
|
||||
QGuiApplication.clipboard().setText(text)
|
||||
|
||||
@pyqtSlot(result='QString')
|
||||
def clipboardToText(self):
|
||||
return QGuiApplication.clipboard().text()
|
||||
|
||||
class ElectrumQmlApplication(QGuiApplication):
|
||||
|
||||
_valid = True
|
||||
@@ -109,6 +114,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
qmlRegisterType(QEBitcoin, 'org.electrum', 1, 0, 'Bitcoin')
|
||||
qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser')
|
||||
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
|
||||
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
|
||||
|
||||
self.engine = QQmlApplicationEngine(parent=self)
|
||||
self.engine.addImportPath('./qml')
|
||||
|
||||
228
electrum/gui/qml/qetxfinalizer.py
Normal file
228
electrum/gui/qml/qetxfinalizer.py
Normal file
@@ -0,0 +1,228 @@
|
||||
from decimal import Decimal
|
||||
|
||||
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.util import NotEnoughFunds, profiler
|
||||
|
||||
from .qewallet import QEWallet
|
||||
|
||||
class QETxFinalizer(QObject):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
_address = ''
|
||||
_amount = ''
|
||||
_fee = ''
|
||||
_feeRate = ''
|
||||
_wallet = None
|
||||
_valid = False
|
||||
_sliderSteps = 0
|
||||
_sliderPos = 0
|
||||
_method = -1
|
||||
_warning = ''
|
||||
_target = ''
|
||||
config = None
|
||||
|
||||
validChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, notify=validChanged)
|
||||
def valid(self):
|
||||
return self._valid
|
||||
|
||||
walletChanged = pyqtSignal()
|
||||
@pyqtProperty(QEWallet, notify=walletChanged)
|
||||
def wallet(self):
|
||||
return self._wallet
|
||||
|
||||
@wallet.setter
|
||||
def wallet(self, wallet: QEWallet):
|
||||
if self._wallet != wallet:
|
||||
self._wallet = wallet
|
||||
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(str, notify=amountChanged)
|
||||
def amount(self):
|
||||
return self._amount
|
||||
|
||||
@amount.setter
|
||||
def amount(self, amount):
|
||||
if self._amount != amount:
|
||||
self._logger.info('amount = "%s"' % amount)
|
||||
self._amount = amount
|
||||
self.amountChanged.emit()
|
||||
|
||||
feeChanged = pyqtSignal()
|
||||
@pyqtProperty(str, notify=feeChanged)
|
||||
def fee(self):
|
||||
return self._fee
|
||||
|
||||
@fee.setter
|
||||
def fee(self, fee):
|
||||
if self._fee != fee:
|
||||
self._fee = 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()
|
||||
|
||||
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):
|
||||
return self._sliderSteps
|
||||
|
||||
sliderPosChanged = pyqtSignal()
|
||||
@pyqtProperty(int, notify=sliderPosChanged)
|
||||
def sliderPos(self):
|
||||
return self._sliderPos
|
||||
|
||||
@sliderPos.setter
|
||||
def sliderPos(self, sliderPos):
|
||||
if self._sliderPos != sliderPos:
|
||||
self._sliderPos = sliderPos
|
||||
self.save_config()
|
||||
self.sliderPosChanged.emit()
|
||||
|
||||
methodChanged = pyqtSignal()
|
||||
@pyqtProperty(int, notify=methodChanged)
|
||||
def method(self):
|
||||
return self._method
|
||||
|
||||
@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
|
||||
|
||||
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 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(False)
|
||||
|
||||
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(False)
|
||||
|
||||
@profiler
|
||||
def make_tx(self, rbf: bool):
|
||||
coins = self._wallet.wallet.get_spendable_coins(None)
|
||||
outputs = [PartialTxOutput.from_address_and_value(self.address, int(self.amount))]
|
||||
tx = self._wallet.wallet.make_unsigned_transaction(coins=coins,outputs=outputs, fee=None)
|
||||
self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))
|
||||
return tx
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def update(self, rbf):
|
||||
#rbf = not bool(self.ids.final_cb.active) if self.show_final else False
|
||||
try:
|
||||
# make unsigned transaction
|
||||
tx = self.make_tx(rbf)
|
||||
except NotEnoughFunds:
|
||||
self.warning = _("Not enough funds")
|
||||
self._valid = False
|
||||
self.validChanged.emit()
|
||||
return
|
||||
except Exception as e:
|
||||
self._logger.error(str(e))
|
||||
self.warning = repr(e)
|
||||
self._valid = False
|
||||
self.validChanged.emit()
|
||||
return
|
||||
|
||||
amount = int(self.amount) if self.amount != '!' else tx.output_value()
|
||||
tx_size = tx.estimated_size()
|
||||
fee = tx.get_fee()
|
||||
feerate = Decimal(fee) / tx_size # sat/byte
|
||||
|
||||
self.fee = str(fee)
|
||||
self.feeRate = f'{feerate:.1f}'
|
||||
|
||||
#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)
|
||||
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()
|
||||
Reference in New Issue
Block a user