8 changed files with 464 additions and 26 deletions
@ -0,0 +1,118 @@
|
||||
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 Invoice invoice |
||||
|
||||
width: parent.width |
||||
height: parent.height |
||||
|
||||
title: qsTr('Invoice') |
||||
|
||||
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('Type') |
||||
} |
||||
|
||||
Label { |
||||
text: invoice.invoiceType == Invoice.OnchainInvoice |
||||
? qsTr('On-chain invoice') |
||||
: invoice.invoiceType == Invoice.LightningInvoice |
||||
? qsTr('Lightning invoice') |
||||
: '' |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Label { |
||||
text: qsTr('Description') |
||||
} |
||||
|
||||
Label { |
||||
text: invoice.message |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Label { |
||||
text: qsTr('Amount to send') |
||||
} |
||||
|
||||
RowLayout { |
||||
Layout.fillWidth: true |
||||
Label { |
||||
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 |
||||
} |
||||
} |
||||
|
||||
RowLayout { |
||||
Layout.columnSpan: 2 |
||||
Layout.alignment: Qt.AlignHCenter |
||||
spacing: constants.paddingMedium |
||||
|
||||
Button { |
||||
text: qsTr('Cancel') |
||||
onClicked: dialog.close() |
||||
} |
||||
|
||||
Button { |
||||
text: qsTr('Save') |
||||
// enabled: invoice.invoiceType != Invoice.Invalid |
||||
enabled: invoice.invoiceType == Invoice.OnchainInvoice |
||||
onClicked: { |
||||
invoice.save_invoice() |
||||
dialog.close() |
||||
} |
||||
} |
||||
|
||||
Button { |
||||
text: qsTr('Pay now') |
||||
enabled: invoice.invoiceType != Invoice.Invalid // TODO && has funds |
||||
onClicked: { |
||||
console.log('pay now') |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,235 @@
|
||||
import asyncio |
||||
from datetime import datetime |
||||
|
||||
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, OnchainInvoice, LNInvoice |
||||
from electrum.transaction import PartialTxOutput |
||||
|
||||
from .qewallet import QEWallet |
||||
|
||||
class QEInvoice(QObject): |
||||
|
||||
_logger = get_logger(__name__) |
||||
|
||||
class Type: |
||||
Invalid = -1 |
||||
OnchainOnlyAddress = 0 |
||||
OnchainInvoice = 1 |
||||
LightningInvoice = 2 |
||||
LightningAndOnchainInvoice = 3 |
||||
|
||||
Q_ENUMS(Type) |
||||
|
||||
_wallet = None |
||||
_invoiceType = Type.Invalid |
||||
_recipient = '' |
||||
_effectiveInvoice = None |
||||
_message = '' |
||||
_amount = 0 |
||||
|
||||
validationError = pyqtSignal([str,str], arguments=['code', 'message']) |
||||
validationWarning = pyqtSignal([str,str], arguments=['code', 'message']) |
||||
invoiceSaved = pyqtSignal() |
||||
|
||||
def __init__(self, config, parent=None): |
||||
super().__init__(parent) |
||||
self.config = config |
||||
self.clear() |
||||
|
||||
invoiceTypeChanged = pyqtSignal() |
||||
@pyqtProperty(int, notify=invoiceTypeChanged) |
||||
def invoiceType(self): |
||||
return self._invoiceType |
||||
|
||||
# not a qt setter, don't let outside set state |
||||
def setInvoiceType(self, invoiceType: Type): |
||||
#if self._invoiceType != invoiceType: |
||||
self._invoiceType = invoiceType |
||||
self.invoiceTypeChanged.emit() |
||||
|
||||
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.walletChanged.emit() |
||||
|
||||
recipientChanged = pyqtSignal() |
||||
@pyqtProperty(str, notify=recipientChanged) |
||||
def recipient(self): |
||||
return self._recipient |
||||
|
||||
@recipient.setter |
||||
def recipient(self, recipient: str): |
||||
#if self._recipient != recipient: |
||||
self._recipient = recipient |
||||
if recipient: |
||||
self.validateRecipient(recipient) |
||||
self.recipientChanged.emit() |
||||
|
||||
messageChanged = pyqtSignal() |
||||
@pyqtProperty(str, notify=messageChanged) |
||||
def message(self): |
||||
return self._message |
||||
|
||||
amountChanged = pyqtSignal() |
||||
@pyqtProperty('quint64', notify=amountChanged) |
||||
def amount(self): |
||||
return self._amount |
||||
|
||||
|
||||
@pyqtSlot() |
||||
def clear(self): |
||||
self.recipient = '' |
||||
self.invoiceSetsAmount = False |
||||
self.setInvoiceType(QEInvoice.Type.Invalid) |
||||
self._bip21 = None |
||||
|
||||
def setValidAddressOnly(self): |
||||
self._logger.debug('setValidAddressOnly') |
||||
self.setInvoiceType(QEInvoice.Type.OnchainOnlyAddress) |
||||
self._effectiveInvoice = None ###TODO |
||||
|
||||
def setValidOnchainInvoice(self, invoice: OnchainInvoice): |
||||
self._logger.debug('setValidOnchainInvoice') |
||||
self.setInvoiceType(QEInvoice.Type.OnchainInvoice) |
||||
self._amount = invoice.get_amount_sat() |
||||
self.amountChanged.emit() |
||||
self._message = invoice.message |
||||
self.messageChanged.emit() |
||||
|
||||
self._effectiveInvoice = invoice |
||||
|
||||
def setValidLightningInvoice(self, invoice: LNInvoice): |
||||
self._logger.debug('setValidLightningInvoice') |
||||
self.setInvoiceType(QEInvoice.Type.LightningInvoice) |
||||
self._effectiveInvoice = invoice |
||||
|
||||
self._amount = int(invoice.get_amount_sat()) # TODO: float/str msat precision |
||||
self.amountChanged.emit() |
||||
self._message = invoice.message |
||||
self.messageChanged.emit() |
||||
|
||||
def create_onchain_invoice(self, outputs, message, payment_request, uri): |
||||
return self._wallet.wallet.create_invoice( |
||||
outputs=outputs, |
||||
message=message, |
||||
pr=payment_request, |
||||
URI=uri |
||||
) |
||||
|
||||
def validateRecipient(self, recipient): |
||||
if not recipient: |
||||
self.setInvoiceType(QEInvoice.Type.Invalid) |
||||
return |
||||
|
||||
maybe_lightning_invoice = recipient |
||||
|
||||
def _payment_request_resolved(request): |
||||
self._logger.debug('resolved payment request') |
||||
outputs = request.get_outputs() |
||||
invoice = self.create_onchain_invoice(outputs, None, request, None) |
||||
self.setValidOnchainInvoice(invoice) |
||||
|
||||
try: |
||||
self._bip21 = parse_URI(recipient, _payment_request_resolved) |
||||
if self._bip21: |
||||
if 'r' in self._bip21 or ('name' in self._bip21 and 'sig' in self._bip21): # TODO set flag in util? |
||||
# let callback handle state |
||||
return |
||||
if ':' not in recipient: |
||||
# address only |
||||
self.setValidAddressOnly() |
||||
return |
||||
else: |
||||
# fallback lightning invoice? |
||||
if 'lightning' in self._bip21: |
||||
maybe_lightning_invoice = self._bip21['lightning'] |
||||
except InvalidBitcoinURI as e: |
||||
self._bip21 = None |
||||
self._logger.debug(repr(e)) |
||||
|
||||
lninvoice = None |
||||
try: |
||||
maybe_lightning_invoice = maybe_extract_bolt11_invoice(maybe_lightning_invoice) |
||||
lninvoice = LNInvoice.from_bech32(maybe_lightning_invoice) |
||||
except InvoiceError as e: |
||||
pass |
||||
|
||||
if not lninvoice and not self._bip21: |
||||
self.validationError.emit('unknown',_('Unknown invoice')) |
||||
self.clear() |
||||
return |
||||
|
||||
if lninvoice: |
||||
if not self._wallet.wallet.has_lightning(): |
||||
if not self._bip21: |
||||
# TODO: lightning onchain fallback in ln invoice |
||||
#self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet')) |
||||
self.setValidLightningInvoice(lninvoice) |
||||
self.clear() |
||||
return |
||||
else: |
||||
self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') |
||||
self.setValidOnchainInvoice(self._bip21['address']) |
||||
elif not self._wallet.wallet.lnworker.channels: |
||||
self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) |
||||
self.setValidLightningInvoice(lninvoice) |
||||
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._logger.debug(outputs) |
||||
message = self._bip21['message'] if 'message' in self._bip21 else '' |
||||
invoice = self.create_onchain_invoice(outputs, message, None, self._bip21) |
||||
self._logger.debug(invoice) |
||||
self.setValidOnchainInvoice(invoice) |
||||
|
||||
@pyqtSlot() |
||||
def save_invoice(self): |
||||
if not self._effectiveInvoice: |
||||
return |
||||
self._wallet.wallet.save_invoice(self._effectiveInvoice) |
||||
self.invoiceSaved.emit() |
||||
|
||||
@pyqtSlot(str, 'quint64', str) |
||||
def create_invoice(self, address: str, amount: int, message: str): |
||||
# create onchain invoice from user entered fields |
||||
# (any other type of invoice is created from parsing recipient) |
||||
self._logger.debug('saving invoice to %s' % address) |
||||
if not address: |
||||
self.invoiceCreateError.emit('fatal', _('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request')) |
||||
return |
||||
|
||||
if not bitcoin.is_address(address): |
||||
self.invoiceCreateError.emit('fatal', _('Invalid Bitcoin address')) |
||||
return |
||||
|
||||
if not self.amount: |
||||
self.invoiceCreateError.emit('fatal', _('Invalid amount')) |
||||
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 |
||||
|
||||
Loading…
Reference in new issue