From f6a46f3900944dfa4e5fd0edd589f808c7a191fb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Jun 2022 21:24:43 +0200 Subject: [PATCH] initial create invoice from user input --- electrum/gui/qml/components/OpenChannel.qml | 4 +- electrum/gui/qml/components/Send.qml | 78 +++++++++++++-------- electrum/gui/qml/qeinvoice.py | 65 ++++++++++++----- 3 files changed, 96 insertions(+), 51 deletions(-) diff --git a/electrum/gui/qml/components/OpenChannel.qml b/electrum/gui/qml/components/OpenChannel.qml index 9bc0d9943..571895dfa 100644 --- a/electrum/gui/qml/components/OpenChannel.qml +++ b/electrum/gui/qml/components/OpenChannel.qml @@ -84,7 +84,7 @@ Pane { BtcField { id: amount fiatfield: amountFiat - Layout.preferredWidth: parent.width /2 + Layout.preferredWidth: parent.width /3 onTextChanged: channelopener.amount = Config.unitsToSats(amount.text) enabled: !is_max.checked } @@ -111,7 +111,7 @@ Pane { id: amountFiat btcfield: amount visible: Daemon.fx.enabled - Layout.preferredWidth: parent.width /2 + Layout.preferredWidth: parent.width /3 enabled: !is_max.checked } diff --git a/electrum/gui/qml/components/Send.qml b/electrum/gui/qml/components/Send.qml index 02deca968..9e4ad1c4f 100644 --- a/electrum/gui/qml/components/Send.qml +++ b/electrum/gui/qml/components/Send.qml @@ -14,6 +14,7 @@ Pane { function clear() { recipient.text = '' amount.text = '' + message.text = '' } GridLayout { @@ -21,10 +22,10 @@ Pane { width: parent.width rowSpacing: constants.paddingSmall columnSpacing: constants.paddingSmall - columns: 4 + columns: 3 BalanceSummary { - Layout.columnSpan: 4 + Layout.columnSpan: 3 Layout.alignment: Qt.AlignHCenter } @@ -32,20 +33,22 @@ Pane { text: qsTr('Recipient') } - TextArea { - id: recipient - Layout.columnSpan: 2 + RowLayout { Layout.fillWidth: true - font.family: FixedFont - wrapMode: Text.Wrap - placeholderText: qsTr('Paste address or invoice') - onTextChanged: { - if (activeFocus) - invoice.recipient = text + Layout.columnSpan: 2 + + TextArea { + id: recipient + Layout.fillWidth: true + font.family: FixedFont + wrapMode: Text.Wrap + placeholderText: qsTr('Paste address or invoice') + onTextChanged: { + if (activeFocus) + invoice.recipient = text + } } - } - RowLayout { spacing: 0 ToolButton { icon.source: '../../icons/paste.png' @@ -75,15 +78,26 @@ Pane { id: amount fiatfield: amountFiat Layout.preferredWidth: parent.width /3 + onTextChanged: { + invoice.create_invoice(recipient.text, is_max.checked ? MAX : Config.unitsToSats(amount.text), message.text) + } } - Label { - text: Config.baseUnit - color: Material.accentColor + RowLayout { Layout.fillWidth: true - } - Item { width: 1; height: 1 } + Label { + text: Config.baseUnit + color: Material.accentColor + } + Switch { + id: is_max + text: qsTr('Max') + onCheckedChanged: { + invoice.create_invoice(recipient.text, is_max.checked ? MAX : Config.unitsToSats(amount.text), message.text) + } + } + } Item { width: 1; height: 1; visible: Daemon.fx.enabled } @@ -95,54 +109,56 @@ Pane { } Label { + Layout.fillWidth: true visible: Daemon.fx.enabled text: Daemon.fx.fiatCurrency color: Material.accentColor - Layout.fillWidth: true } - Item { visible: Daemon.fx.enabled ; height: 1; width: 1 } - Label { text: qsTr('Description') } TextField { id: message - font.family: FixedFont placeholderText: qsTr('Message') - Layout.columnSpan: 3 + Layout.columnSpan: 2 Layout.fillWidth: true + onTextChanged: { + invoice.create_invoice(recipient.text, is_max.checked ? MAX : Config.unitsToSats(amount.text), message.text) + } } RowLayout { - Layout.columnSpan: 4 + Layout.columnSpan: 3 Layout.alignment: Qt.AlignHCenter spacing: constants.paddingMedium Button { text: qsTr('Save') - enabled: invoice.invoiceType != Invoice.Invalid + enabled: invoice.canSave icon.source: '../../icons/save.png' onClicked: { - Daemon.currentWallet.create_invoice(recipient.text, amount.text, message.text) + invoice.save_invoice() + invoice.clear() + rootItem.clear() } } Button { text: qsTr('Pay now') - enabled: invoice.invoiceType != Invoice.Invalid // TODO && has funds + enabled: invoice.canPay icon.source: '../../icons/confirmed.png' onClicked: { - var f_amount = parseFloat(amount.text) - if (isNaN(f_amount)) - return + invoice.save_invoice() var dialog = confirmPaymentDialog.createObject(app, { 'address': recipient.text, 'satoshis': Config.unitsToSats(amount.text), 'message': message.text }) dialog.open() + invoice.clear() + rootItem.clear() } } } @@ -291,6 +307,8 @@ Pane { dialog.open() } } + onInvoiceCreateError: console.log(code + ' ' + message) + onInvoiceSaved: { console.log('invoice got saved') Daemon.currentWallet.invoiceModel.init_model() diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index ea25bc1f3..93f3ac8f5 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -5,13 +5,13 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS from electrum.logging import get_logger from electrum.i18n import _ -from electrum.keystore import bip39_is_checksum_valid from electrum.util import (parse_URI, create_bip21_uri, InvalidBitcoinURI, InvoiceError, maybe_extract_bolt11_invoice) from electrum.invoices import Invoice from electrum.invoices import (PR_UNPAID,PR_EXPIRED,PR_UNKNOWN,PR_PAID,PR_INFLIGHT, PR_FAILED,PR_ROUTING,PR_UNCONFIRMED) from electrum.transaction import PartialTxOutput +from electrum import bitcoin from .qewallet import QEWallet from .qetypes import QEAmount @@ -44,6 +44,8 @@ class QEInvoice(QObject): _invoiceType = Type.Invalid _recipient = '' _effectiveInvoice = None + _canSave = False + _canPay = False invoiceChanged = pyqtSignal() invoiceSaved = pyqtSignal() @@ -52,6 +54,8 @@ class QEInvoice(QObject): validationWarning = pyqtSignal([str,str], arguments=['code', 'message']) validationError = pyqtSignal([str,str], arguments=['code', 'message']) + invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) + def __init__(self, config, parent=None): super().__init__(parent) self.config = config @@ -99,10 +103,7 @@ class QEInvoice(QObject): self._amount = QEAmount() if not self._effectiveInvoice: return self._amount - sats = self._effectiveInvoice.get_amount_sat() - if not sats: - return self._amount - self._amount = QEAmount(amount_sat=sats) + self._amount = QEAmount(from_invoice=self._effectiveInvoice) return self._amount @pyqtProperty('quint64', notify=invoiceChanged) @@ -132,12 +133,33 @@ class QEInvoice(QObject): def address(self): return self._effectiveInvoice.get_address() if self._effectiveInvoice else '' + @pyqtProperty(bool, notify=invoiceChanged) + def canSave(self): + return self._canSave + + @canSave.setter + def canSave(self, _canSave): + if self._canSave != _canSave: + self._canSave = _canSave + self.invoiceChanged.emit() + + @pyqtProperty(bool, notify=invoiceChanged) + def canPay(self): + return self._canPay + + @canPay.setter + def canPay(self, _canPay): + if self._canPay != _canPay: + self._canPay = _canPay + self.invoiceChanged.emit() + @pyqtSlot() def clear(self): self.recipient = '' - self.invoiceSetsAmount = False self.setInvoiceType(QEInvoice.Type.Invalid) self._bip21 = None + self._canSave = False + self._canPay = False self.invoiceChanged.emit() # don't parse the recipient string, but init qeinvoice from an invoice key @@ -155,6 +177,9 @@ class QEInvoice(QObject): self.setInvoiceType(QEInvoice.Type.LightningInvoice) else: self.setInvoiceType(QEInvoice.Type.OnchainInvoice) + # TODO check if exists? + self.canSave = True + self.canPay = True # TODO self.invoiceChanged.emit() self.statusChanged.emit() @@ -257,6 +282,7 @@ class QEInvoice(QObject): @pyqtSlot() def save_invoice(self): + self.canSave = False if not self._effectiveInvoice: return # TODO detect duplicate? @@ -265,9 +291,12 @@ class QEInvoice(QObject): @pyqtSlot(str, QEAmount, str) def create_invoice(self, address: str, amount: QEAmount, message: str): - # create onchain invoice from user entered fields + # create invoice from user entered fields # (any other type of invoice is created from parsing recipient) - self._logger.debug('saving invoice to %s' % address) + self._logger.debug('creating invoice to %s, amount=%s, message=%s' % (address, repr(amount), message)) + + self.clear() + if not address: self.invoiceCreateError.emit('fatal', _('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request')) return @@ -276,19 +305,17 @@ class QEInvoice(QObject): self.invoiceCreateError.emit('fatal', _('Invalid Bitcoin address')) return - if not self.amount: + if amount.isEmpty: self.invoiceCreateError.emit('fatal', _('Invalid amount')) return + inv_amt = '!' if amount.isMax else (amount.satsInt * 1000) # FIXME msat precision from UI? + try: + outputs = [PartialTxOutput.from_address_and_value(address, inv_amt)] + invoice = self._wallet.wallet.create_invoice(outputs=outputs, message=message, pr=None, URI=None) + except InvoiceError as e: + self.invoiceCreateError.emit('fatal', _('Error creating payment') + ':\n' + str(e)) + return - # - if self.is_max: - amount = '!' - else: - try: - amount = self.app.get_amount(self.amount) - except: - self.app.show_error(_('Invalid amount') + ':\n' + self.amount) - return - + self.set_effective_invoice(invoice)