From fb68931a8d725988aa9115e537868b07e1912b1e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 27 Sep 2022 15:09:06 +0200 Subject: [PATCH] allow zero amount invoices, add edit amount option for invoices --- electrum/gui/qml/components/InvoiceDialog.qml | 216 +++++++++++++----- electrum/gui/qml/components/ReceiveDialog.qml | 6 +- electrum/gui/qml/components/SendDialog.qml | 5 + .../gui/qml/components/WalletMainView.qml | 8 +- .../gui/qml/components/controls/QRScan.qml | 6 + electrum/gui/qml/qeconfig.py | 4 +- electrum/gui/qml/qeinvoice.py | 23 +- electrum/gui/qml/qewallet.py | 12 +- 8 files changed, 205 insertions(+), 75 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index d92f6a0ee..3509b569e 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -15,9 +15,6 @@ ElDialog { signal doPay - width: parent.width - height: parent.height - title: qsTr('Invoice') standardButtons: invoice_key != '' ? Dialog.Close : Dialog.Cancel @@ -40,8 +37,151 @@ ElDialog { color: Material.accentColor } + Label { + text: qsTr('Amount to send') + color: Material.accentColor + Layout.columnSpan: 2 + } + + TextHighlightPane { + id: amountContainer + + Layout.columnSpan: 2 + Layout.preferredWidth: parent.width //* 0.75 + Layout.alignment: Qt.AlignHCenter + + padding: 0 + leftPadding: constants.paddingXXLarge + + property bool editmode: false + + RowLayout { + id: amountLayout + width: parent.width + + GridLayout { + visible: !amountContainer.editmode + columns: 2 + + Label { + font.pixelSize: constants.fontSizeXLarge + font.family: FixedFont + font.bold: true + text: Config.formatSats(invoice.amount, false) + } + + Label { + Layout.fillWidth: true + text: Config.baseUnit + color: Material.accentColor + font.pixelSize: constants.fontSizeXLarge + } + + Label { + id: fiatValue + visible: Daemon.fx.enabled + text: Daemon.fx.fiatValue(invoice.amount, false) + font.pixelSize: constants.fontSizeMedium + color: constants.mutedForeground + } + + Label { + visible: Daemon.fx.enabled + Layout.fillWidth: true + text: Daemon.fx.fiatCurrency + font.pixelSize: constants.fontSizeMedium + color: constants.mutedForeground + } + + } + + ToolButton { + visible: !amountContainer.editmode + icon.source: '../../icons/pen.png' + icon.color: 'transparent' + onClicked: { + amountBtc.text = invoice.amount.satsInt == 0 ? '' : Config.formatSats(invoice.amount) + amountContainer.editmode = true + amountBtc.focus = true + } + } + GridLayout { + visible: amountContainer.editmode + Layout.fillWidth: true + columns: 2 + BtcField { + id: amountBtc + fiatfield: amountFiat + } + + Label { + text: Config.baseUnit + color: Material.accentColor + Layout.fillWidth: true + } + + FiatField { + id: amountFiat + btcfield: amountBtc + visible: Daemon.fx.enabled + } + + Label { + visible: Daemon.fx.enabled + text: Daemon.fx.fiatCurrency + color: Material.accentColor + } + } + ToolButton { + visible: amountContainer.editmode + Layout.fillWidth: false + icon.source: '../../icons/confirmed.png' + icon.color: 'transparent' + onClicked: { + amountContainer.editmode = false + invoice.amount = Config.unitsToSats(amountBtc.text) + } + } + ToolButton { + visible: amountContainer.editmode + Layout.fillWidth: false + icon.source: '../../icons/closebutton.png' + icon.color: 'transparent' + onClicked: amountContainer.editmode = false + } + } + + } + + Label { + text: qsTr('Description') + visible: invoice.message + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + visible: invoice.message + + Layout.columnSpan: 2 + Layout.preferredWidth: parent.width + Layout.alignment: Qt.AlignHCenter + + padding: 0 + leftPadding: constants.paddingMedium + + Label { + text: invoice.message + Layout.fillWidth: true + font.pixelSize: constants.fontSizeXLarge + wrapMode: Text.Wrap + elide: Text.ElideRight + } + } + Label { text: qsTr('Type') + color: Material.accentColor } RowLayout { @@ -64,48 +204,10 @@ ElDialog { } } - Label { - text: qsTr('Amount to send') - } - - RowLayout { - Layout.fillWidth: true - Label { - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - font.bold: true - text: Config.formatSats(invoice.amount, false) - } - - Label { - text: Config.baseUnit - color: Material.accentColor - } - - Label { - id: fiatValue - Layout.fillWidth: true - text: Daemon.fx.enabled - ? '(' + Daemon.fx.fiatValue(invoice.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' - : '' - font.pixelSize: constants.fontSizeMedium - } - } - - Label { - text: qsTr('Description') - } - - Label { - text: invoice.message - Layout.fillWidth: true - wrapMode: Text.Wrap - elide: Text.ElideRight - } - Label { visible: invoice.invoiceType == Invoice.OnchainInvoice text: qsTr('Address') + color: Material.accentColor } Label { @@ -119,6 +221,7 @@ ElDialog { Label { visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Remote Pubkey') + color: Material.accentColor } Label { @@ -132,6 +235,7 @@ ElDialog { Label { visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Route via (t)') + color: Material.accentColor } Label { @@ -145,6 +249,7 @@ ElDialog { Label { visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Route via (r)') + color: Material.accentColor } Label { @@ -157,6 +262,7 @@ ElDialog { Label { text: qsTr('Status') + color: Material.accentColor } Label { @@ -194,30 +300,20 @@ ElDialog { } } - Button { - text: qsTr('Save') - icon.source: '../../icons/save.png' - visible: invoice_key == '' - enabled: invoice.canSave - onClicked: { - invoice.save_invoice() - dialog.close() - } - } - - Button { - text: qsTr('Pay now') + FlatButton { + text: qsTr('Pay') icon.source: '../../icons/confirmed.png' enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay onClicked: { if (invoice_key == '') // save invoice if not retrieved from key invoice.save_invoice() dialog.close() - if (invoice.invoiceType == Invoice.OnchainInvoice) { - doPay() // only signal here - } else if (invoice.invoiceType == Invoice.LightningInvoice) { - doPay() // only signal here - } + doPay() // only signal here + // if (invoice.invoiceType == Invoice.OnchainInvoice) { + // doPay() // only signal here + // } else if (invoice.invoiceType == Invoice.LightningInvoice) { + // doPay() // only signal here + // } } } } diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index eec1568b9..47446c058 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -212,16 +212,16 @@ ElDialog { var qamt = Config.unitsToSats(receiveDetailsDialog.amount) if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { console.log('Creating OnChain request') - Daemon.currentWallet.create_request(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, false, ignoreGaplimit) + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, false, ignoreGaplimit) } else { console.log('Creating Lightning request') - Daemon.currentWallet.create_request(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, true) + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, true) } } function createDefaultRequest(ignoreGaplimit = false) { console.log('Creating default request') - Daemon.currentWallet.create_default_request(ignoreGaplimit) + Daemon.currentWallet.createDefaultRequest(ignoreGaplimit) } Connections { diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index c0a13e6ca..33d295392 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -22,10 +22,15 @@ ElDialog { padding: 0 + function restart() { + qrscan.restart() + } + ColumnLayout { anchors.fill: parent QRScan { + id: qrscan Layout.preferredWidth: parent.width Layout.fillHeight: true diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 6c9d27891..c21fb7838 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -143,6 +143,9 @@ Item { wallet: Daemon.currentWallet onValidationError: { var dialog = app.messageDialog.createObject(app, {'text': message }) + dialog.closed.connect(function() { + _sendDialog.restart() + }) dialog.open() } onValidationWarning: { @@ -176,6 +179,9 @@ Item { Component { id: invoiceDialog InvoiceDialog { + width: parent.width + height: parent.height + onDoPay: { if (invoice.invoiceType == Invoice.OnchainInvoice) { var dialog = confirmPaymentDialog.createObject(mainView, { @@ -206,7 +212,7 @@ Item { } close() } - // onClosed: destroy() + onClosed: destroy() } } diff --git a/electrum/gui/qml/components/controls/QRScan.qml b/electrum/gui/qml/components/controls/QRScan.qml index 97d398396..8b1de1100 100644 --- a/electrum/gui/qml/components/controls/QRScan.qml +++ b/electrum/gui/qml/components/controls/QRScan.qml @@ -15,6 +15,12 @@ Item { signal found + function restart() { + still.source = '' + _pointsVisible = false + active = true + } + VideoOutput { id: vo anchors.fill: parent diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 7ef75dbf1..8446f30f0 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -85,9 +85,7 @@ class QEConfig(AuthMixin, QObject): requestExpiryChanged = pyqtSignal() @pyqtProperty(int, notify=requestExpiryChanged) def requestExpiry(self): - a = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - self._logger.debug(f'request expiry {a}') - return a + return self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) @requestExpiry.setter def requestExpiry(self, expiry): diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index e0d20177c..8abe508cf 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -165,6 +165,16 @@ class QEInvoiceParser(QEInvoice): self._amount = QEAmount(from_invoice=self._effectiveInvoice) return self._amount + @amount.setter + def amount(self, new_amount): + self._logger.debug('set amount') + if self._effectiveInvoice: + self._effectiveInvoice.amount_msat = int(new_amount.satsInt * 1000) + # TODO: side effects? + # TODO: recalc outputs for onchain + self.determine_can_pay() + self.invoiceChanged.emit() + @pyqtProperty('quint64', notify=invoiceChanged) def expiration(self): return self._effectiveInvoice.exp if self._effectiveInvoice else 0 @@ -242,6 +252,10 @@ class QEInvoiceParser(QEInvoice): self.statusChanged.emit() def determine_can_pay(self): + if self.amount.satsInt == 0: + self.canPay = False + return + if self.invoiceType == QEInvoice.Type.LightningInvoice: if self.status in [PR_UNPAID, PR_FAILED]: if self.get_max_spendable_lightning() >= self.amount.satsInt: @@ -374,9 +388,12 @@ class QEInvoiceParser(QEInvoice): else: self._logger.debug('flow without LN but having bip21 uri') if 'amount' not in self._bip21: #TODO can we have amount-less invoices? - self.validationError.emit('no_amount', 'no amount in uri') - return - outputs = [PartialTxOutput.from_address_and_value(self._bip21['address'], self._bip21['amount'])] + # self.validationError.emit('no_amount', 'no amount in uri') + # return + amount = 0 + else: + amount = self._bip21['amount'] + outputs = [PartialTxOutput.from_address_and_value(self._bip21['address'], amount)] self._logger.debug(outputs) message = self._bip21['message'] if 'message' in self._bip21 else '' invoice = self.create_onchain_invoice(outputs, message, None, self._bip21) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index ae12ef72c..c06917a5f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer from electrum import bitcoin from electrum.i18n import _ -from electrum.invoices import (InvoiceError) +from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.transaction import PartialTxOutput @@ -505,7 +505,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener): @pyqtSlot(QEAmount, str, int) @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) - def create_request(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False): + def createRequest(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False): # TODO: unify this method and create_bitcoin_request try: if is_lightning: @@ -531,15 +531,17 @@ class QEWallet(AuthMixin, QObject, QtEventListener): @pyqtSlot() @pyqtSlot(bool) - def create_default_request(self, ignore_gap: bool = False): + def createDefaultRequest(self, ignore_gap: bool = False): try: + default_expiry = self.wallet.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) if self.wallet.lnworker.channels: + addr = None if self.wallet.config.get('bolt11_fallback', True): addr = self.wallet.get_unused_address() # if addr is None, we ran out of addresses. for lightning enabled wallets, ignore for now - key = self.wallet.create_request(None, None, 3600, addr) # TODO : expiration from config + key = self.wallet.create_request(None, None, default_expiry, addr) else: - key, addr = self.create_bitcoin_request(None, None, 3600, ignore_gap) + key, addr = self.create_bitcoin_request(None, None, default_expiry, ignore_gap) if not key: return # self.addressModel.init_model()