add initial lnurl-pay
This commit is contained in:
66
electrum/gui/qml/components/LnurlPayRequestDialog.qml
Normal file
66
electrum/gui/qml/components/LnurlPayRequestDialog.qml
Normal file
@@ -0,0 +1,66 @@
|
||||
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"
|
||||
|
||||
ElDialog {
|
||||
id: dialog
|
||||
|
||||
title: qsTr('LNURL Payment request')
|
||||
|
||||
// property var lnurlData
|
||||
property InvoiceParser invoiceParser
|
||||
// property alias lnurlData: dialog.invoiceParser.lnurlData
|
||||
|
||||
standardButtons: Dialog.Cancel
|
||||
|
||||
modal: true
|
||||
parent: Overlay.overlay
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#aa000000"
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
implicitWidth: parent.width
|
||||
|
||||
Label {
|
||||
text: qsTr('Provider')
|
||||
}
|
||||
Label {
|
||||
text: invoiceParser.lnurlData['domain']
|
||||
}
|
||||
Label {
|
||||
text: qsTr('Description')
|
||||
}
|
||||
Label {
|
||||
text: invoiceParser.lnurlData['metadata_plaintext']
|
||||
}
|
||||
Label {
|
||||
text: invoiceParser.lnurlData['min_sendable_sat'] == invoiceParser.lnurlData['max_sendable_sat']
|
||||
? qsTr('Amount')
|
||||
: qsTr('Amount range')
|
||||
}
|
||||
Label {
|
||||
text: invoiceParser.lnurlData['min_sendable_sat'] == invoiceParser.lnurlData['max_sendable_sat']
|
||||
? invoiceParser.lnurlData['min_sendable_sat'] == 0
|
||||
? qsTr('Unspecified')
|
||||
: invoiceParser.lnurlData['min_sendable_sat']
|
||||
: invoiceParser.lnurlData['min_sendable_sat'] + ' < amount < ' + invoiceParser.lnurlData['max_sendable_sat']
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.columnSpan: 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr('Proceed')
|
||||
onClicked: {
|
||||
invoiceParser.lnurlGetInvoice(invoiceParser.lnurlData['min_sendable_sat'])
|
||||
dialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,11 @@ Item {
|
||||
}
|
||||
onInvoiceCreateError: console.log(code + ' ' + message)
|
||||
|
||||
onLnurlRetrieved: {
|
||||
var dialog = lnurlPayDialog.createObject(app, { invoiceParser: invoiceParser })
|
||||
dialog.open()
|
||||
}
|
||||
|
||||
onInvoiceSaved: {
|
||||
Daemon.currentWallet.invoiceModel.init_model()
|
||||
}
|
||||
@@ -249,5 +254,14 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: lnurlPayDialog
|
||||
LnurlPayRequestDialog {
|
||||
width: parent.width * 0.9
|
||||
anchors.centerIn: parent
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import threading
|
||||
import asyncio
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS
|
||||
|
||||
from electrum import bitcoin
|
||||
@@ -11,6 +15,7 @@ from electrum.logging import get_logger
|
||||
from electrum.transaction import PartialTxOutput
|
||||
from electrum.util import (parse_URI, InvalidBitcoinURI, InvoiceError,
|
||||
maybe_extract_lightning_payment_identifier)
|
||||
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl
|
||||
|
||||
from .qetypes import QEAmount
|
||||
from .qewallet import QEWallet
|
||||
@@ -18,10 +23,10 @@ from .qewallet import QEWallet
|
||||
class QEInvoice(QObject):
|
||||
class Type:
|
||||
Invalid = -1
|
||||
OnchainOnlyAddress = 0
|
||||
OnchainInvoice = 1
|
||||
LightningInvoice = 2
|
||||
LightningAndOnchainInvoice = 3
|
||||
OnchainInvoice = 0
|
||||
LightningInvoice = 1
|
||||
LightningAndOnchainInvoice = 2
|
||||
LNURLPayRequest = 3
|
||||
|
||||
class Status:
|
||||
Unpaid = PR_UNPAID
|
||||
@@ -126,6 +131,9 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message'])
|
||||
|
||||
lnurlRetrieved = pyqtSignal()
|
||||
lnurlError = pyqtSignal([str,str], arguments=['code', 'message'])
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.clear()
|
||||
@@ -148,10 +156,15 @@ class QEInvoiceParser(QEInvoice):
|
||||
#if self._recipient != recipient:
|
||||
self.canPay = False
|
||||
self._recipient = recipient
|
||||
self._lnurlData = None
|
||||
if recipient:
|
||||
self.validateRecipient(recipient)
|
||||
self.recipientChanged.emit()
|
||||
|
||||
@pyqtProperty('QVariantMap', notify=lnurlRetrieved)
|
||||
def lnurlData(self):
|
||||
return self._lnurlData
|
||||
|
||||
@pyqtProperty(str, notify=invoiceChanged)
|
||||
def message(self):
|
||||
return self._effectiveInvoice.message if self._effectiveInvoice else ''
|
||||
@@ -167,11 +180,10 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
@amount.setter
|
||||
def amount(self, new_amount):
|
||||
self._logger.debug('set amount')
|
||||
self._logger.debug(f'set new amount {repr(new_amount)}')
|
||||
if self._effectiveInvoice:
|
||||
self._effectiveInvoice.amount_msat = int(new_amount.satsInt * 1000)
|
||||
# TODO: side effects?
|
||||
# TODO: recalc outputs for onchain
|
||||
|
||||
self.determine_can_pay()
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
@@ -220,6 +232,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
self.recipient = ''
|
||||
self.setInvoiceType(QEInvoice.Type.Invalid)
|
||||
self._bip21 = None
|
||||
self._lnurlData = None
|
||||
self.canSave = False
|
||||
self.canPay = False
|
||||
self.userinfo = ''
|
||||
@@ -306,6 +319,12 @@ class QEInvoiceParser(QEInvoice):
|
||||
raise Exception('unexpected Onchain invoice')
|
||||
self.set_effective_invoice(invoice)
|
||||
|
||||
def setValidLNURLPayRequest(self):
|
||||
self._logger.debug('setValidLNURLPayRequest')
|
||||
self.setInvoiceType(QEInvoice.Type.LNURLPayRequest)
|
||||
self._effectiveInvoice = None
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
def create_onchain_invoice(self, outputs, message, payment_request, uri):
|
||||
return self._wallet.wallet.create_invoice(
|
||||
outputs=outputs,
|
||||
@@ -353,6 +372,9 @@ class QEInvoiceParser(QEInvoice):
|
||||
lninvoice = None
|
||||
maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice)
|
||||
if maybe_lightning_invoice is not None:
|
||||
if maybe_lightning_invoice.startswith('lnurl'):
|
||||
self.resolve_lnurl(maybe_lightning_invoice)
|
||||
return
|
||||
try:
|
||||
lninvoice = Invoice.from_bech32(maybe_lightning_invoice)
|
||||
except InvoiceError as e:
|
||||
@@ -378,7 +400,8 @@ class QEInvoiceParser(QEInvoice):
|
||||
# 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()
|
||||
self.validationSuccess.emit()
|
||||
# self.clear()
|
||||
return
|
||||
else:
|
||||
self._logger.debug('flow with LN but not LN enabled AND having bip21 uri')
|
||||
@@ -403,6 +426,59 @@ class QEInvoiceParser(QEInvoice):
|
||||
self.setValidOnchainInvoice(invoice)
|
||||
self.validationSuccess.emit()
|
||||
|
||||
def resolve_lnurl(self, lnurl):
|
||||
self._logger.debug('resolve_lnurl')
|
||||
url = decode_lnurl(lnurl)
|
||||
self._logger.debug(f'{repr(url)}')
|
||||
|
||||
def resolve_task():
|
||||
try:
|
||||
coro = request_lnurl(url)
|
||||
fut = asyncio.run_coroutine_threadsafe(coro, self._wallet.wallet.network.asyncio_loop)
|
||||
self.on_lnurl(fut.result())
|
||||
except Exception as e:
|
||||
self.validationError.emit('lnurl', repr(e))
|
||||
|
||||
threading.Thread(target=resolve_task).start()
|
||||
|
||||
def on_lnurl(self, lnurldata):
|
||||
self._logger.debug('on_lnurl')
|
||||
self._logger.debug(f'{repr(lnurldata)}')
|
||||
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url' : lnurldata.callback_url,
|
||||
'min_sendable_sat': lnurldata.min_sendable_sat,
|
||||
'max_sendable_sat': lnurldata.max_sendable_sat,
|
||||
'metadata_plaintext': lnurldata.metadata_plaintext
|
||||
}
|
||||
self.setValidLNURLPayRequest()
|
||||
self.lnurlRetrieved.emit()
|
||||
|
||||
@pyqtSlot('quint64')
|
||||
def lnurlGetInvoice(self, amount):
|
||||
assert self._lnurlData
|
||||
|
||||
self._logger.debug(f'fetching callback url {self._lnurlData["callback_url"]}')
|
||||
def fetch_invoice_task():
|
||||
try:
|
||||
coro = callback_lnurl(self._lnurlData['callback_url'], {
|
||||
'amount': amount * 1000 # msats
|
||||
})
|
||||
fut = asyncio.run_coroutine_threadsafe(coro, self._wallet.wallet.network.asyncio_loop)
|
||||
self.on_lnurl_invoice(fut.result())
|
||||
except Exception as e:
|
||||
self.lnurlError.emit('lnurl', repr(e))
|
||||
|
||||
threading.Thread(target=fetch_invoice_task).start()
|
||||
|
||||
def on_lnurl_invoice(self, invoice):
|
||||
self._logger.debug('on_lnurl_invoice')
|
||||
self._logger.debug(f'{repr(invoice)}')
|
||||
|
||||
invoice = invoice['pr']
|
||||
self.recipient = invoice
|
||||
|
||||
@pyqtSlot()
|
||||
def save_invoice(self):
|
||||
self.canSave = False
|
||||
|
||||
@@ -52,12 +52,15 @@ async def _request_lnurl(url: str) -> dict:
|
||||
"""Requests payment data from a lnurl."""
|
||||
try:
|
||||
response = await Network.async_send_http_on_proxy("get", url, timeout=10)
|
||||
response = json.loads(response)
|
||||
except asyncio.TimeoutError as e:
|
||||
raise LNURLError("Server did not reply in time.") from e
|
||||
except aiohttp.client_exceptions.ClientError as e:
|
||||
raise LNURLError(f"Client error: {e}") from e
|
||||
except json.JSONDecodeError:
|
||||
raise LNURLError(f"Invalid response from server")
|
||||
# TODO: handling of specific client errors
|
||||
response = json.loads(response)
|
||||
|
||||
if "metadata" in response:
|
||||
response["metadata"] = json.loads(response["metadata"])
|
||||
status = response.get("status")
|
||||
|
||||
Reference in New Issue
Block a user