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. 9
      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 QtQuick.Controls.Material 2.0
import QtQml.Models 2.1 import QtQml.Models 2.1
import org.electrum 1.0
import "controls" import "controls"
Pane { Pane {
id: rootItem id: rootItem
function clear() {
recipient.text = ''
amount.text = ''
}
GridLayout { GridLayout {
id: form id: form
width: parent.width width: parent.width
@ -26,12 +33,16 @@ Pane {
} }
TextArea { TextArea {
id: address id: recipient
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
font.family: FixedFont font.family: FixedFont
wrapMode: Text.Wrap wrapMode: Text.Wrap
placeholderText: qsTr('Paste address or invoice') placeholderText: qsTr('Paste address or invoice')
onTextChanged: {
if (activeFocus)
invoice.recipient = text
}
} }
RowLayout { RowLayout {
@ -40,7 +51,7 @@ Pane {
icon.source: '../../icons/paste.png' icon.source: '../../icons/paste.png'
icon.height: constants.iconSizeMedium icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium icon.width: constants.iconSizeMedium
onClicked: address.text = AppController.clipboardToText() onClicked: invoice.recipient = AppController.clipboardToText()
} }
ToolButton { ToolButton {
icon.source: '../../icons/qrcode.png' icon.source: '../../icons/qrcode.png'
@ -50,10 +61,7 @@ Pane {
onClicked: { onClicked: {
var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) var page = app.stack.push(Qt.resolvedUrl('Scan.qml'))
page.onFound.connect(function() { page.onFound.connect(function() {
console.log('got ' + page.invoiceData) invoice.recipient = page.scanData
address.text = page.invoiceData['address']
amount.text = Config.satsToUnits(page.invoiceData['amount'])
description.text = page.invoiceData['message']
}) })
} }
} }
@ -122,9 +130,9 @@ Pane {
} }
TextField { TextField {
id: description id: message
font.family: FixedFont font.family: FixedFont
placeholderText: qsTr('Description') placeholderText: qsTr('Message')
Layout.columnSpan: 3 Layout.columnSpan: 3
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -136,24 +144,24 @@ Pane {
Button { Button {
text: qsTr('Save') text: qsTr('Save')
enabled: false enabled: invoice.invoiceType != Invoice.Invalid
onClicked: { onClicked: {
console.log('TODO: save') Daemon.currentWallet.create_invoice(recipient.text, amount.text, message.text)
} }
} }
Button { Button {
text: qsTr('Pay now') text: qsTr('Pay now')
enabled: amount.text != '' && address.text != ''// TODO proper validation enabled: invoice.invoiceType != Invoice.Invalid // TODO && has funds
onClicked: { onClicked: {
var f_amount = parseFloat(amount.text) var f_amount = parseFloat(amount.text)
if (isNaN(f_amount)) if (isNaN(f_amount))
return return
var sats = Config.unitsToSats(amount.text).toString() var sats = Config.unitsToSats(amount.text).toString()
var dialog = confirmPaymentDialog.createObject(app, { var dialog = confirmPaymentDialog.createObject(app, {
'address': address.text, 'address': recipient.text,
'satoshis': sats, 'satoshis': sats,
'message': description.text 'message': message.text
}) })
dialog.open() 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 { Connections {
target: Daemon.fx target: Daemon.fx
function onQuotesUpdated() { function onQuotesUpdated() {
@ -240,4 +266,43 @@ Pane {
FocusScope { id: parkFocus } 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()
}
}
} }

9
electrum/gui/qml/components/WalletMainView.qml

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

2
electrum/gui/qml/qeapp.py

@ -19,6 +19,7 @@ from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin from .qebitcoin import QEBitcoin
from .qefx import QEFX from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer from .qetxfinalizer import QETxFinalizer
from .qeinvoice import QEInvoice
notification = None notification = None
@ -115,6 +116,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser') qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser')
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX') qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer') qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
self.engine = QQmlApplicationEngine(parent=self) self.engine = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml') 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) invoices.append(item)
self.clear() self.clear()
self.beginInsertRows(QModelIndex(), 0, len(self.invoices) - 1) self.beginInsertRows(QModelIndex(), 0, len(invoices) - 1)
self.invoices = invoices self.invoices = invoices
self.endInsertRows() self.endInsertRows()
@ -79,7 +79,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
i = 0 i = 0
for item in self.invoices: for item in self.invoices:
if item['key'] == key: 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'] = status
item['status_str'] = invoice.get_status_str(status) item['status_str'] = invoice.get_status_str(status)
index = self.index(i,0) 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.logging import get_logger
from electrum.qrreader import get_qr_reader from electrum.qrreader import get_qr_reader
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import profiler
class QEQRParser(QObject): class QEQRParser(QObject):
def __init__(self, text=None, parent=None): def __init__(self, text=None, parent=None):
@ -123,6 +123,7 @@ class QEQRImageProvider(QQuickImageProvider):
_logger = get_logger(__name__) _logger = get_logger(__name__)
@profiler
def requestImage(self, qstr, size): def requestImage(self, qstr, size):
self._logger.debug('QR requested for %s' % qstr) self._logger.debug('QR requested for %s' % qstr)
qr = qrcode.QRCode(version=1, box_size=6, border=2) 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']) requestStatusChanged = pyqtSignal([str,int], arguments=['key','status'])
requestCreateSuccess = pyqtSignal() requestCreateSuccess = pyqtSignal()
requestCreateError = pyqtSignal([str,str], arguments=['code','error']) 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) _network_signal = pyqtSignal(str, object)
@ -95,10 +98,15 @@ class QEWallet(QObject):
if event == 'status': if event == 'status':
self.isUptodateChanged.emit() self.isUptodateChanged.emit()
elif event == 'request_status': elif event == 'request_status':
wallet, addr, c = args wallet, key, status = args
if wallet == self.wallet: if wallet == self.wallet:
self._logger.debug('request status %d for address %s' % (c, addr)) self._logger.debug('request status %d for key %s' % (status, key))
self.requestStatusChanged.emit(addr, c) 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': elif event == 'new_transaction':
wallet, tx = args wallet, tx = args
if wallet == self.wallet: if wallet == self.wallet:
@ -351,5 +359,5 @@ class QEWallet(QObject):
@pyqtSlot('QString', result='QVariant') @pyqtSlot('QString', result='QVariant')
def get_invoice(self, key: str): def get_invoice(self, key: str):
req = self.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
return self._invoiceModel.invoice_to_model(req) return self._invoiceModel.invoice_to_model(invoice)

Loading…
Cancel
Save