Browse Source

add confirm payment dialog/feepicker and qobject backing

patch-4
Sander van Grieken 3 years ago
parent
commit
34ef93b2b5
  1. BIN
      electrum/gui/icons/paste.png
  2. 195
      electrum/gui/qml/components/ConfirmPaymentDialog.qml
  3. 120
      electrum/gui/qml/components/Send.qml
  4. 6
      electrum/gui/qml/qeapp.py
  5. 228
      electrum/gui/qml/qetxfinalizer.py

BIN
electrum/gui/icons/paste.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

195
electrum/gui/qml/components/ConfirmPaymentDialog.qml

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

120
electrum/gui/qml/components/Send.qml

@ -3,10 +3,13 @@ import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0 import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.0 import QtQuick.Controls.Material 2.0
import "controls"
Pane { Pane {
id: rootItem id: rootItem
GridLayout { GridLayout {
id: form
width: parent.width width: parent.width
rowSpacing: constants.paddingSmall rowSpacing: constants.paddingSmall
columnSpacing: constants.paddingSmall columnSpacing: constants.paddingSmall
@ -30,11 +33,29 @@ Pane {
placeholderText: qsTr('Paste address or invoice') placeholderText: qsTr('Paste address or invoice')
} }
ToolButton { RowLayout {
icon.source: '../../icons/copy.png' spacing: 0
icon.color: 'transparent' ToolButton {
icon.height: constants.iconSizeSmall icon.source: '../../icons/paste.png'
icon.width: constants.iconSizeSmall 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 { Label {
@ -71,7 +92,6 @@ Pane {
Item { width: 1; height: 1 } Item { width: 1; height: 1 }
Item { width: 1; height: 1; visible: Daemon.fx.enabled } Item { width: 1; height: 1; visible: Daemon.fx.enabled }
TextField { TextField {
@ -97,54 +117,102 @@ Pane {
Item { visible: Daemon.fx.enabled ; height: 1; width: 1 } Item { visible: Daemon.fx.enabled ; height: 1; width: 1 }
Label { Label {
text: qsTr('Fee') text: qsTr('Description')
} }
TextField { TextField {
id: fee id: description
font.family: FixedFont font.family: FixedFont
placeholderText: qsTr('sat/vB') placeholderText: qsTr('Description')
Layout.columnSpan: 2 Layout.columnSpan: 3
Layout.fillWidth: true
} }
Item { width: 1; height: 1 }
RowLayout { RowLayout {
Layout.columnSpan: 4 Layout.columnSpan: 4
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: constants.paddingMedium spacing: constants.paddingMedium
Button { 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 enabled: amount.text != '' && address.text != ''// TODO proper validation
onClicked: { onClicked: {
var f_amount = parseFloat(amount.text) var f_amount = parseFloat(amount.text)
if (isNaN(f_amount)) if (isNaN(f_amount))
return return
var sats = Config.unitsToSats(f_amount) var sats = Config.unitsToSats(amount.text).toString()
var result = Daemon.currentWallet.send_onchain(address.text, sats, undefined, false) var dialog = confirmPaymentDialog.createObject(app, {
'address': address.text,
'satoshis': sats,
'message': description.text
})
dialog.open()
} }
} }
}
}
Button { Frame {
text: qsTr('Scan QR Code') verticalPadding: 0
onClicked: { horizontalPadding: 0
var page = app.stack.push(Qt.resolvedUrl('Scan.qml'))
page.onFound.connect(function() { anchors {
console.log('got ' + page.invoiceData) top: form.bottom
address.text = page.invoiceData['address'] topMargin: constants.paddingXLarge
amount.text = Config.formatSats(page.invoiceData['amount']) 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 { Connections {
target: Daemon.fx target: Daemon.fx
function onQuotesUpdated() { function onQuotesUpdated() {
var a = Config.unitsToSats(amount.text) amountFiat.text = Daemon.fx.fiatValue(Config.unitsToSats(amount.text))
amountFiat.text = Daemon.fx.fiatValue(a)
} }
} }

6
electrum/gui/qml/qeapp.py

@ -18,6 +18,7 @@ from .qeqr import QEQRParser, QEQRImageProvider
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
notification = None notification = None
@ -92,6 +93,10 @@ class QEAppController(QObject):
def textToClipboard(self, text): def textToClipboard(self, text):
QGuiApplication.clipboard().setText(text) QGuiApplication.clipboard().setText(text)
@pyqtSlot(result='QString')
def clipboardToText(self):
return QGuiApplication.clipboard().text()
class ElectrumQmlApplication(QGuiApplication): class ElectrumQmlApplication(QGuiApplication):
_valid = True _valid = True
@ -109,6 +114,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QEBitcoin, 'org.electrum', 1, 0, 'Bitcoin') qmlRegisterType(QEBitcoin, 'org.electrum', 1, 0, 'Bitcoin')
qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser') qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser')
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX') qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
self.engine = QQmlApplicationEngine(parent=self) self.engine = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml') self.engine.addImportPath('./qml')

228
electrum/gui/qml/qetxfinalizer.py

@ -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()
Loading…
Cancel
Save