Browse Source

wip

patch-4
Sander van Grieken 3 years ago
parent
commit
cd4bd39583
  1. 118
      electrum/gui/qml/components/ConfirmInvoiceDialog.qml
  2. 91
      electrum/gui/qml/components/Send.qml
  3. 19
      electrum/gui/qml/components/WalletMainView.qml
  4. 2
      electrum/gui/qml/qeapp.py
  5. 235
      electrum/gui/qml/qeinvoice.py
  6. 4
      electrum/gui/qml/qeinvoicelistmodel.py
  7. 3
      electrum/gui/qml/qeqr.py
  8. 18
      electrum/gui/qml/qewallet.py

118
electrum/gui/qml/components/ConfirmInvoiceDialog.qml

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

91
electrum/gui/qml/components/Send.qml

@ -4,11 +4,18 @@ import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.0
import QtQml.Models 2.1
import org.electrum 1.0
import "controls"
Pane {
id: rootItem
function clear() {
recipient.text = ''
amount.text = ''
}
GridLayout {
id: form
width: parent.width
@ -26,12 +33,16 @@ Pane {
}
TextArea {
id: address
id: recipient
Layout.columnSpan: 2
Layout.fillWidth: true
font.family: FixedFont
wrapMode: Text.Wrap
placeholderText: qsTr('Paste address or invoice')
onTextChanged: {
if (activeFocus)
invoice.recipient = text
}
}
RowLayout {
@ -40,7 +51,7 @@ Pane {
icon.source: '../../icons/paste.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
onClicked: address.text = AppController.clipboardToText()
onClicked: invoice.recipient = AppController.clipboardToText()
}
ToolButton {
icon.source: '../../icons/qrcode.png'
@ -50,10 +61,7 @@ Pane {
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']
invoice.recipient = page.scanData
})
}
}
@ -122,9 +130,9 @@ Pane {
}
TextField {
id: description
id: message
font.family: FixedFont
placeholderText: qsTr('Description')
placeholderText: qsTr('Message')
Layout.columnSpan: 3
Layout.fillWidth: true
}
@ -136,24 +144,24 @@ Pane {
Button {
text: qsTr('Save')
enabled: false
enabled: invoice.invoiceType != Invoice.Invalid
onClicked: {
console.log('TODO: save')
Daemon.currentWallet.create_invoice(recipient.text, amount.text, message.text)
}
}
Button {
text: qsTr('Pay now')
enabled: amount.text != '' && address.text != ''// TODO proper validation
enabled: invoice.invoiceType != Invoice.Invalid // TODO && has funds
onClicked: {
var f_amount = parseFloat(amount.text)
if (isNaN(f_amount))
return
var sats = Config.unitsToSats(amount.text).toString()
var dialog = confirmPaymentDialog.createObject(app, {
'address': address.text,
'address': recipient.text,
'satoshis': sats,
'message': description.text
'message': message.text
})
dialog.open()
}
@ -224,6 +232,24 @@ Pane {
}
}
Component {
id: confirmPaymentDialog
ConfirmPaymentDialog {}
}
Component {
id: confirmInvoiceDialog
ConfirmInvoiceDialog {}
}
Connections {
target: Daemon.currentWallet
function onInvoiceStatusChanged(key, status) {
// TODO: status from?
//Daemon.currentWallet.invoiceModel.updateInvoice(key, status)
}
}
Connections {
target: Daemon.fx
function onQuotesUpdated() {
@ -240,4 +266,43 @@ Pane {
FocusScope { id: parkFocus }
}
Invoice {
id: invoice
wallet: Daemon.currentWallet
onValidationError: {
if (recipient.activeFocus) {
// no popups when editing
return
}
console.log(code + ' ' + message)
var dialog = app.messageDialog.createObject(app, {'text': message })
dialog.open()
rootItem.clear()
}
onValidationWarning: {
if (code == 'no_channels') {
var dialog = app.messageDialog.createObject(app, {'text': message })
dialog.open()
// TODO: ask user to open a channel, if funds allow
// and maybe store invoice if expiry allows
}
}
onInvoiceTypeChanged: {
if (invoiceType == Invoice.Invalid)
return
// address only -> fill form fields
// else -> show invoice confirmation dialog
if (invoiceType == Invoice.OnchainOnlyAddress)
recipient.text = invoice.recipient
else {
var dialog = confirmInvoiceDialog.createObject(rootItem, {'invoice': invoice})
dialog.open()
}
}
onInvoiceSaved: {
console.log('invoice got saved')
Daemon.currentWallet.invoiceModel.init_model()
}
}
}

19
electrum/gui/qml/components/WalletMainView.qml

@ -100,24 +100,33 @@ Item {
currentIndex: tabbar.currentIndex
Item {
Receive {
id: receive
Loader {
anchors.fill: parent
Receive {
id: receive
anchors.fill: parent
}
}
}
Item {
History {
id: history
Loader {
anchors.fill: parent
History {
id: history
anchors.fill: parent
}
}
}
Item {
enabled: !Daemon.currentWallet.isWatchOnly
Send {
Loader {
anchors.fill: parent
Send {
anchors.fill: parent
}
}
}

2
electrum/gui/qml/qeapp.py

@ -19,6 +19,7 @@ from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin
from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer
from .qeinvoice import QEInvoice
notification = None
@ -115,6 +116,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser')
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
self.engine = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml')

235
electrum/gui/qml/qeinvoice.py

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

4
electrum/gui/qml/qeinvoicelistmodel.py

@ -51,7 +51,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
invoices.append(item)
self.clear()
self.beginInsertRows(QModelIndex(), 0, len(self.invoices) - 1)
self.beginInsertRows(QModelIndex(), 0, len(invoices) - 1)
self.invoices = invoices
self.endInsertRows()
@ -79,7 +79,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
i = 0
for item in self.invoices:
if item['key'] == key:
invoice = self.get_invoice_for_key(key) #self.wallet.get_invoice(key)
invoice = self.get_invoice_for_key(key)
item['status'] = status
item['status_str'] = invoice.get_status_str(status)
index = self.index(i,0)

3
electrum/gui/qml/qeqr.py

@ -10,7 +10,7 @@ from PIL import Image, ImageQt
from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader
from electrum.i18n import _
from electrum.util import profiler
class QEQRParser(QObject):
def __init__(self, text=None, parent=None):
@ -123,6 +123,7 @@ class QEQRImageProvider(QQuickImageProvider):
_logger = get_logger(__name__)
@profiler
def requestImage(self, qstr, size):
self._logger.debug('QR requested for %s' % qstr)
qr = qrcode.QRCode(version=1, box_size=6, border=2)

18
electrum/gui/qml/qewallet.py

@ -44,6 +44,9 @@ class QEWallet(QObject):
requestStatusChanged = pyqtSignal([str,int], arguments=['key','status'])
requestCreateSuccess = pyqtSignal()
requestCreateError = pyqtSignal([str,str], arguments=['code','error'])
invoiceStatusChanged = pyqtSignal([str], arguments=['key'])
invoiceCreateSuccess = pyqtSignal()
invoiceCreateError = pyqtSignal([str,str], arguments=['code','error'])
_network_signal = pyqtSignal(str, object)
@ -95,10 +98,15 @@ class QEWallet(QObject):
if event == 'status':
self.isUptodateChanged.emit()
elif event == 'request_status':
wallet, addr, c = args
wallet, key, status = args
if wallet == self.wallet:
self._logger.debug('request status %d for address %s' % (c, addr))
self.requestStatusChanged.emit(addr, c)
self._logger.debug('request status %d for key %s' % (status, key))
self.requestStatusChanged.emit(key, status)
elif event == 'invoice_status':
wallet, key = args
if wallet == self.wallet:
self._logger.debug('invoice status %d for key %s' % (c, key))
self.invoiceStatusChanged.emit(key)
elif event == 'new_transaction':
wallet, tx = args
if wallet == self.wallet:
@ -351,5 +359,5 @@ class QEWallet(QObject):
@pyqtSlot('QString', result='QVariant')
def get_invoice(self, key: str):
req = self.wallet.get_invoice(key)
return self._invoiceModel.invoice_to_model(req)
invoice = self.wallet.get_invoice(key)
return self._invoiceModel.invoice_to_model(invoice)

Loading…
Cancel
Save