Sander van Grieken
3 years ago
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