From 7013f9d26bce81e5b395df6bee7ee37126a32299 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 5 Apr 2022 17:24:20 +0200 Subject: [PATCH] generate and parse bip 21 qr codes --- electrum/gui/qml/components/Receive.qml | 42 +++++++++---------- electrum/gui/qml/components/RequestDialog.qml | 14 +++++-- electrum/gui/qml/components/Scan.qml | 17 ++++++++ electrum/gui/qml/components/Send.qml | 40 +++++++++--------- electrum/gui/qml/qebitcoin.py | 18 ++++++++ electrum/gui/qml/qeconfig.py | 4 ++ electrum/gui/qml/qeqr.py | 7 ++-- electrum/gui/qml/qerequestlistmodel.py | 7 ++-- 8 files changed, 98 insertions(+), 51 deletions(-) diff --git a/electrum/gui/qml/components/Receive.qml b/electrum/gui/qml/components/Receive.qml index cc6eac92e..6598d606c 100644 --- a/electrum/gui/qml/components/Receive.qml +++ b/electrum/gui/qml/components/Receive.qml @@ -40,6 +40,21 @@ Pane { Layout.preferredWidth: parent.width /2 placeholderText: qsTr('Amount') inputMethodHints: Qt.ImhPreferNumbers + + property string textAsSats + onTextChanged: { + textAsSats = Config.unitsToSats(amount.text) + if (amountFiat.activeFocus) + return + amountFiat.text = Daemon.fx.fiatValue(amount.textAsSats) + } + + Connections { + target: Config + function onBaseUnitChanged() { + amount.text = amount.textAsSats != 0 ? Config.satsToUnits(amount.textAsSats) : '' + } + } } Label { @@ -58,6 +73,10 @@ Pane { Layout.preferredWidth: parent.width /2 placeholderText: qsTr('Amount') inputMethodHints: Qt.ImhDigitsOnly + onTextChanged: { + if (amountFiat.activeFocus) + amount.text = Daemon.fx.satoshiValue(amountFiat.text) + } } Label { @@ -216,7 +235,7 @@ Pane { text: qsTr('Timestamp: ') } Label { - text: model.timestamp + text: model.date } Label { @@ -301,32 +320,11 @@ Pane { } dialog.open() } - } - - Connections { - target: Daemon.currentWallet function onRequestStatusChanged(key, status) { Daemon.currentWallet.requestModel.updateRequest(key, status) } } - Connections { - target: amount - function onTextChanged() { - if (amountFiat.activeFocus) - return - var a = Config.unitsToSats(amount.text) - amountFiat.text = Daemon.fx.fiatValue(a) - } - } - Connections { - target: amountFiat - function onTextChanged() { - if (amountFiat.activeFocus) { - amount.text = Daemon.fx.satoshiValue(amountFiat.text) - } - } - } Connections { target: Daemon.fx function onQuotesUpdated() { diff --git a/electrum/gui/qml/components/RequestDialog.qml b/electrum/gui/qml/components/RequestDialog.qml index b3636fccc..33dd68f7c 100644 --- a/electrum/gui/qml/components/RequestDialog.qml +++ b/electrum/gui/qml/components/RequestDialog.qml @@ -56,13 +56,12 @@ Dialog { } Image { + id: qr Layout.columnSpan: 3 Layout.alignment: Qt.AlignHCenter Layout.topMargin: constants.paddingSmall Layout.bottomMargin: constants.paddingSmall - source: 'image://qrgen/' + modelItem.address - Rectangle { property int size: 57 // should be qr pixel multiple color: 'white' @@ -131,7 +130,7 @@ Dialog { } Label { visible: modelItem.amount > 0 - text: Config.formatSats(modelItem.amount, false) + text: Config.formatSats(modelItem.amount) font.family: FixedFont font.pixelSize: constants.fontSizeLarge } @@ -181,4 +180,13 @@ Dialog { modelItem = Daemon.currentWallet.get_request(key) } } + + Component.onCompleted: { + var bip21uri = bitcoin.create_uri(modelItem.address, modelItem.amount, modelItem.message, modelItem.timestamp, modelItem.exp) + qr.source = 'image://qrgen/' + bip21uri + } + + Bitcoin { + id: bitcoin + } } diff --git a/electrum/gui/qml/components/Scan.qml b/electrum/gui/qml/components/Scan.qml index 35f18845a..5ad3d4344 100644 --- a/electrum/gui/qml/components/Scan.qml +++ b/electrum/gui/qml/components/Scan.qml @@ -1,6 +1,8 @@ import QtQuick 2.6 import QtQuick.Controls 2.0 +import org.electrum 1.0 + Item { id: scanPage property string title: qsTr('Scan') @@ -8,6 +10,8 @@ Item { property bool toolbar: false property string scanData + property var invoiceData: undefined + property string error signal found @@ -18,6 +22,16 @@ Item { onFound: { scanPage.scanData = scanData + var invoice = bitcoin.parse_uri(scanData) + if (invoice['error']) { + error = invoice['error'] + console.log(error) + app.stack.pop() + return + } + + invoiceData = invoice + console.log(invoiceData['address']) scanPage.found() app.stack.pop() } @@ -31,4 +45,7 @@ Item { onClicked: app.stack.pop() } + Bitcoin { + id: bitcoin + } } diff --git a/electrum/gui/qml/components/Send.qml b/electrum/gui/qml/components/Send.qml index d2f8e96fc..dfb1df56b 100644 --- a/electrum/gui/qml/components/Send.qml +++ b/electrum/gui/qml/components/Send.qml @@ -47,6 +47,20 @@ Pane { placeholderText: qsTr('Amount') Layout.preferredWidth: parent.width /2 inputMethodHints: Qt.ImhPreferNumbers + property string textAsSats + onTextChanged: { + textAsSats = Config.unitsToSats(amount.text) + if (amountFiat.activeFocus) + return + amountFiat.text = Daemon.fx.fiatValue(amount.textAsSats) + } + + Connections { + target: Config + function onBaseUnitChanged() { + amount.text = amount.textAsSats != 0 ? Config.satsToUnits(amount.textAsSats) : '' + } + } } Label { @@ -67,6 +81,10 @@ Pane { Layout.preferredWidth: parent.width /2 placeholderText: qsTr('Amount') inputMethodHints: Qt.ImhPreferNumbers + onTextChanged: { + if (amountFiat.activeFocus) + amount.text = Daemon.fx.satoshiValue(amountFiat.text) + } } Label { @@ -113,31 +131,15 @@ Pane { onClicked: { var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) page.onFound.connect(function() { - console.log('got ' + page.scanData) - address.text = page.scanData + console.log('got ' + page.invoiceData) + address.text = page.invoiceData['address'] + amount.text = Config.formatSats(page.invoiceData['amount']) }) } } } } - Connections { - target: amount - function onTextChanged() { - if (amountFiat.activeFocus) - return - var a = Config.unitsToSats(amount.text) - amountFiat.text = Daemon.fx.fiatValue(a) - } - } - Connections { - target: amountFiat - function onTextChanged() { - if (amountFiat.activeFocus) { - amount.text = Daemon.fx.satoshiValue(amountFiat.text) - } - } - } Connections { target: Daemon.fx function onQuotesUpdated() { diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 545b8db66..d1f6b681c 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject @@ -7,6 +8,7 @@ from electrum.keystore import bip39_is_checksum_valid from electrum.bip32 import is_bip32_derivation from electrum.slip39 import decode_mnemonic, Slip39Error from electrum import mnemonic +from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI class QEBitcoin(QObject): def __init__(self, config, parent=None): @@ -111,3 +113,19 @@ class QEBitcoin(QObject): @pyqtSlot(str, result=bool) def verify_derivation_path(self, path): return is_bip32_derivation(path) + + @pyqtSlot(str, result='QVariantMap') + def parse_uri(self, uri: str) -> dict: + try: + return parse_URI(uri) + except InvalidBitcoinURI as e: + return { 'error': str(e) } + + @pyqtSlot(str, 'qint64', str, int, int, result=str) + def create_uri(self, address, satoshis, message, timestamp, expiry): + extra_params = {} + if expiry: + extra_params['time'] = str(timestamp) + extra_params['exp'] = str(expiry) + + return create_bip21_uri(address, satoshis, message, extra_query_params=extra_params) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index a8c973a05..f385c3ba4 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -100,3 +100,7 @@ class QEConfig(QObject): #amount = Decimal(max_prec_amount) / Decimal(pow(10, self.max_precision()-self.decimal_point())) #return int(amount) #Decimal(amount) if not self.is_int else int(amount) return 0 + + @pyqtSlot('quint64', result=float) + def satsToUnits(self, satoshis): + return satoshis / pow(10,self.config.decimal_point) diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py index b32ca1c92..3bd856e32 100644 --- a/electrum/gui/qml/qeqr.py +++ b/electrum/gui/qml/qeqr.py @@ -4,8 +4,7 @@ from PyQt5.QtQuick import QQuickImageProvider import asyncio import qrcode -#from qrcode.image.styledpil import StyledPilImage -#from qrcode.image.styles.moduledrawers import * + from PIL import Image, ImageQt from electrum.logging import get_logger @@ -126,10 +125,10 @@ class QEQRImageProvider(QQuickImageProvider): def requestImage(self, qstr, size): self._logger.debug('QR requested for %s' % qstr) - qr = qrcode.QRCode(version=1, box_size=8, border=2) + qr = qrcode.QRCode(version=1, box_size=6, border=2) qr.add_data(qstr) qr.make(fit=True) - pimg = qr.make_image(fill_color='black', back_color='white') #image_factory=StyledPilImage, module_drawer=CircleModuleDrawer()) + pimg = qr.make_image(fill_color='black', back_color='white') self.qimg = ImageQt.ImageQt(pimg) return self.qimg, self.qimg.size() diff --git a/electrum/gui/qml/qerequestlistmodel.py b/electrum/gui/qml/qerequestlistmodel.py index 430f8a132..25ec63ca6 100644 --- a/electrum/gui/qml/qerequestlistmodel.py +++ b/electrum/gui/qml/qerequestlistmodel.py @@ -14,7 +14,7 @@ class QERequestListModel(QAbstractListModel): _logger = get_logger(__name__) # define listmodel rolemap - _ROLE_NAMES=('key','type','timestamp','message','amount','status','address') + _ROLE_NAMES=('key','type','timestamp','date','message','amount','status','address','exp') _ROLE_KEYS = range(Qt.UserRole + 1, Qt.UserRole + 1 + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) @@ -46,10 +46,11 @@ class QERequestListModel(QAbstractListModel): status = self.wallet.get_request_status(key) item['status'] = req.get_status_str(status) item['type'] = req.type # 0=onchain, 2=LN - timestamp = req.time - item['timestamp'] = format_time(timestamp) + item['timestamp'] = req.time + item['date'] = format_time(item['timestamp']) item['amount'] = req.get_amount_sat() item['message'] = req.message + item['exp'] = req.exp if req.type == 0: # OnchainInvoice item['key'] = item['address'] = req.get_address() elif req.type == 2: # LNInvoice