qml: separate PI resolving from QEInvoiceParser
separates the resolving step from the QEInvoiceParser so the 'recipient' can be resolved first and then either an QEInvoiceParser can be used if it is a sending request that has been resolved (invoice, address, lnurlp, ...), or RequestDetails can be used if the resolved 'recipient' turns out to be a voucher/LNURLW string. # Conflicts: # electrum/gui/qml/qeinvoice.py
This commit is contained in:
@@ -13,13 +13,14 @@ ElDialog {
|
||||
title: qsTr('LNURL Withdraw request')
|
||||
iconSource: '../../../icons/link.png'
|
||||
|
||||
property InvoiceParser invoiceParser
|
||||
property Wallet wallet: Daemon.currentWallet
|
||||
property RequestDetails requestDetails
|
||||
|
||||
padding: 0
|
||||
|
||||
property int walletCanReceive: invoiceParser.wallet.lightningCanReceive.satsInt
|
||||
property int providerMinWithdrawable: parseInt(invoiceParser.lnurlData['min_withdrawable_sat'])
|
||||
property int providerMaxWithdrawable: parseInt(invoiceParser.lnurlData['max_withdrawable_sat'])
|
||||
property int walletCanReceive: 0
|
||||
property int providerMinWithdrawable: parseInt(requestDetails.lnurlData['min_withdrawable_sat'])
|
||||
property int providerMaxWithdrawable: parseInt(requestDetails.lnurlData['max_withdrawable_sat'])
|
||||
property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)
|
||||
property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)
|
||||
property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive
|
||||
@@ -30,6 +31,12 @@ ElDialog {
|
||||
amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable
|
||||
property bool valid: amountValid
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize walletCanReceive (instead of binding wallet.lightningCanReceive.satsInt)
|
||||
// to prevent binding loop if wallet.lightningCanReceive.satsInt changes
|
||||
walletCanReceive = wallet.lightningCanReceive.satsInt
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
@@ -89,17 +96,17 @@ ElDialog {
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: invoiceParser.lnurlData['domain']
|
||||
text: requestDetails.lnurlData['domain']
|
||||
}
|
||||
Label {
|
||||
text: qsTr('Description')
|
||||
color: Material.accentColor
|
||||
visible: invoiceParser.lnurlData['default_description']
|
||||
visible: requestDetails.lnurlData['default_description']
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: invoiceParser.lnurlData['default_description']
|
||||
visible: invoiceParser.lnurlData['default_description']
|
||||
text: requestDetails.lnurlData['default_description']
|
||||
visible: requestDetails.lnurlData['default_description']
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
@@ -117,9 +124,6 @@ ElDialog {
|
||||
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
|
||||
color: Material.foreground // override gray-out on disabled
|
||||
fiatfield: amountFiat
|
||||
onTextAsSatsChanged: {
|
||||
invoiceParser.amountOverride = textAsSats
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: Config.baseUnit
|
||||
@@ -150,12 +154,12 @@ ElDialog {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('Withdraw...')
|
||||
icon.source: '../../icons/confirmed.png'
|
||||
enabled: valid
|
||||
enabled: valid && !requestDetails.busy
|
||||
onClicked: {
|
||||
invoiceParser.lnurlRequestWithdrawal()
|
||||
dialog.close()
|
||||
var satsAmount = amountBtc.textAsSats.satsInt;
|
||||
requestDetails.lnurlRequestWithdrawal(satsAmount);
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ ElDialog {
|
||||
id: dialog
|
||||
|
||||
property InvoiceParser invoiceParser
|
||||
property PIResolver piResolver
|
||||
|
||||
signal txFound(data: string)
|
||||
signal channelBackupFound(data: string)
|
||||
@@ -36,7 +37,7 @@ ElDialog {
|
||||
} else if (Daemon.currentWallet.isValidChannelBackup(data)) {
|
||||
channelBackupFound(data)
|
||||
} else {
|
||||
invoiceParser.recipient = data
|
||||
piResolver.recipient = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ ElDialog {
|
||||
FlatButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
enabled: !invoiceParser.busy
|
||||
enabled: !invoiceParser.busy && !piResolver.busy
|
||||
icon.source: '../../icons/copy_bw.png'
|
||||
text: qsTr('Paste')
|
||||
onClicked: {
|
||||
|
||||
@@ -36,7 +36,7 @@ Item {
|
||||
function openSendDialog() {
|
||||
// Qt based send dialog if not on android
|
||||
if (!AppController.isAndroid()) {
|
||||
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser})
|
||||
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser, piResolver: piResolver})
|
||||
_sendDialog.open()
|
||||
return
|
||||
}
|
||||
@@ -61,7 +61,7 @@ Item {
|
||||
})
|
||||
dialog.open()
|
||||
} else {
|
||||
invoiceParser.recipient = data
|
||||
piResolver.recipient = data
|
||||
}
|
||||
//scanner.destroy() // TODO
|
||||
})
|
||||
@@ -362,7 +362,7 @@ Item {
|
||||
Layout.preferredWidth: 1
|
||||
icon.source: '../../icons/tab_send.png'
|
||||
text: qsTr('Send')
|
||||
enabled: !invoiceParser.busy
|
||||
enabled: !invoiceParser.busy && !piResolver.busy && !requestDetails.busy
|
||||
onClicked: openSendDialog()
|
||||
onPressAndHold: {
|
||||
Config.userKnowsPressAndHold = true
|
||||
@@ -373,6 +373,45 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
PIResolver {
|
||||
id: piResolver
|
||||
wallet: Daemon.currentWallet
|
||||
|
||||
onResolveError: (code, message) => {
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
title: qsTr('Error'),
|
||||
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
|
||||
text: message
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
piResolver.invoiceResolved.connect(invoiceParser.fromResolvedPaymentIdentifier)
|
||||
piResolver.requestResolved.connect(requestDetails.fromResolvedPaymentIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
RequestDetails {
|
||||
id: requestDetails
|
||||
wallet: Daemon.currentWallet
|
||||
onNeedsLNURLUserInput: {
|
||||
closeSendDialog()
|
||||
var dialog = lnurlWithdrawDialog.createObject(app, {
|
||||
requestDetails: requestDetails
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
onLnurlError: (code, message) => {
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
title: qsTr('Error'),
|
||||
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
|
||||
text: message
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Invoice {
|
||||
id: invoice
|
||||
wallet: Daemon.currentWallet
|
||||
@@ -420,17 +459,12 @@ Item {
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
|
||||
onLnurlRetrieved: {
|
||||
closeSendDialog()
|
||||
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
|
||||
var dialog = lnurlPayDialog.createObject(app, {
|
||||
invoiceParser: invoiceParser
|
||||
})
|
||||
} else if (invoiceParser.invoiceType === Invoice.Type.LNURLWithdrawRequest) {
|
||||
var dialog = lnurlWithdrawDialog.createObject(app, {
|
||||
invoiceParser: invoiceParser
|
||||
})
|
||||
} else {
|
||||
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
|
||||
return
|
||||
@@ -460,7 +494,7 @@ Item {
|
||||
_intentUri = uri
|
||||
return
|
||||
}
|
||||
invoiceParser.recipient = uri
|
||||
piResolver.recipient = uri
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +503,7 @@ Item {
|
||||
function onWalletLoaded() {
|
||||
infobanner.hide() // start hidden when switching wallets
|
||||
if (_intentUri) {
|
||||
invoiceParser.recipient = _intentUri
|
||||
piResolver.recipient = _intentUri
|
||||
_intentUri = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ from .qebitcoin import QEBitcoin
|
||||
from .qefx import QEFX
|
||||
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
|
||||
from .qeinvoice import QEInvoice, QEInvoiceParser
|
||||
from .qepiresolver import QEPIResolver
|
||||
from .qerequestdetails import QERequestDetails
|
||||
from .qetypes import QEAmount, QEBytes
|
||||
from .qeaddressdetails import QEAddressDetails
|
||||
@@ -439,6 +440,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
qmlRegisterType(QEQRScanner, 'org.electrum', 1, 0, 'QRScanner')
|
||||
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
|
||||
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
|
||||
qmlRegisterType(QEPIResolver, 'org.electrum', 1, 0, 'PIResolver')
|
||||
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
|
||||
qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser')
|
||||
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')
|
||||
|
||||
@@ -14,7 +14,7 @@ from electrum.invoices import (
|
||||
)
|
||||
from electrum.transaction import PartialTxOutput, TxOutput
|
||||
from electrum.lnutil import format_short_channel_id
|
||||
from electrum.lnurl import LNURLData, LNURL3Data, LNURL6Data, request_lnurl_withdraw_callback, LNURLError
|
||||
from electrum.lnurl import LNURL6Data
|
||||
from electrum.bitcoin import COIN, address_to_script
|
||||
from electrum.paymentrequest import PaymentRequest
|
||||
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType
|
||||
@@ -23,7 +23,6 @@ from electrum.network import Network
|
||||
from .qetypes import QEAmount
|
||||
from .qewallet import QEWallet
|
||||
from .util import status_update_timer_interval, QtEventListener, event_listener
|
||||
from ...fee_policy import FeePolicy
|
||||
from ...util import InvoiceError
|
||||
|
||||
|
||||
@@ -34,7 +33,6 @@ class QEInvoice(QObject, QtEventListener):
|
||||
OnchainInvoice = 0
|
||||
LightningInvoice = 1
|
||||
LNURLPayRequest = 2
|
||||
LNURLWithdrawRequest = 3
|
||||
|
||||
@pyqtEnum
|
||||
class Status(IntEnum):
|
||||
@@ -453,26 +451,19 @@ class QEInvoiceParser(QEInvoice):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._recipient = ''
|
||||
self._pi = None
|
||||
self._lnurlData = None
|
||||
self._busy = False
|
||||
|
||||
self.clear()
|
||||
|
||||
recipientChanged = pyqtSignal()
|
||||
@pyqtProperty(str, notify=recipientChanged)
|
||||
def recipient(self):
|
||||
return self._recipient
|
||||
|
||||
@recipient.setter
|
||||
def recipient(self, recipient: str):
|
||||
@pyqtSlot(object)
|
||||
def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:
|
||||
self.canPay = False
|
||||
self._recipient = recipient
|
||||
self.amountOverride = QEAmount()
|
||||
if recipient:
|
||||
self.validateRecipient(recipient)
|
||||
self.recipientChanged.emit()
|
||||
if resolved_pi:
|
||||
assert not resolved_pi.need_resolve()
|
||||
self.validateRecipient(resolved_pi)
|
||||
|
||||
@pyqtProperty('QVariantMap', notify=lnurlRetrieved)
|
||||
def lnurlData(self):
|
||||
@@ -480,7 +471,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
@pyqtProperty(bool, notify=lnurlRetrieved)
|
||||
def isLnurlPay(self):
|
||||
return self._lnurlData is not None and self.invoiceType == QEInvoice.Type.LNURLPayRequest
|
||||
return self._lnurlData is not None
|
||||
|
||||
@pyqtProperty(bool, notify=busyChanged)
|
||||
def busy(self):
|
||||
@@ -488,7 +479,6 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
@pyqtSlot()
|
||||
def clear(self):
|
||||
self.recipient = ''
|
||||
self.setInvoiceType(QEInvoice.Type.Invalid)
|
||||
self._lnurlData = None
|
||||
self.canSave = False
|
||||
@@ -515,12 +505,6 @@ class QEInvoiceParser(QEInvoice):
|
||||
self._effectiveInvoice = None
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
def setValidLNURLWithdrawRequest(self):
|
||||
self._logger.debug('setValidLNURLWithdrawRequest')
|
||||
self.setInvoiceType(QEInvoice.Type.LNURLWithdrawRequest)
|
||||
self._effectiveInvoice = None
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
def create_onchain_invoice(self, outputs, message, payment_request, uri):
|
||||
return self._wallet.wallet.create_invoice(
|
||||
outputs=outputs,
|
||||
@@ -543,17 +527,18 @@ class QEInvoiceParser(QEInvoice):
|
||||
else:
|
||||
self.validationError.emit('unknown', f'invoice error:\n{pr.error}')
|
||||
|
||||
def validateRecipient(self, recipient):
|
||||
if not recipient:
|
||||
def validateRecipient(self, pi: PaymentIdentifier):
|
||||
if not pi:
|
||||
self.setInvoiceType(QEInvoice.Type.Invalid)
|
||||
return
|
||||
|
||||
self._pi = PaymentIdentifier(self._wallet.wallet, recipient)
|
||||
if not self._pi.is_valid() or self._pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
|
||||
PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,
|
||||
PaymentIdentifierType.LNURL,
|
||||
PaymentIdentifierType.EMAILLIKE,
|
||||
PaymentIdentifierType.DOMAINLIKE]:
|
||||
self._pi = pi
|
||||
if not self._pi.is_valid() or self._pi.type not in [
|
||||
PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
|
||||
PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,
|
||||
PaymentIdentifierType.LNADDR, PaymentIdentifierType.LNURLP,
|
||||
PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE
|
||||
]:
|
||||
self.validationError.emit('unknown', _('Unknown invoice'))
|
||||
return
|
||||
|
||||
@@ -566,16 +551,13 @@ class QEInvoiceParser(QEInvoice):
|
||||
self._update_from_payment_identifier()
|
||||
|
||||
def _update_from_payment_identifier(self):
|
||||
if self._pi.need_resolve():
|
||||
self.resolve_pi()
|
||||
return
|
||||
assert not self._pi.need_resolve(), "Should have been resolved by QEPIResolver"
|
||||
|
||||
if self._pi.type in [
|
||||
PaymentIdentifierType.LNURLP,
|
||||
PaymentIdentifierType.LNURLW,
|
||||
PaymentIdentifierType.LNADDR,
|
||||
]:
|
||||
self.on_lnurl(self._pi.lnurl_data)
|
||||
self.on_lnurl_pay(self._pi.lnurl_data)
|
||||
return
|
||||
|
||||
if self._pi.type == PaymentIdentifierType.BIP70:
|
||||
@@ -624,57 +606,20 @@ class QEInvoiceParser(QEInvoice):
|
||||
self.setValidOnchainInvoice(invoice)
|
||||
self.validationSuccess.emit()
|
||||
|
||||
def resolve_pi(self):
|
||||
assert self._pi.need_resolve()
|
||||
|
||||
def on_finished(pi: PaymentIdentifier):
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
if pi.is_error():
|
||||
if pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE]:
|
||||
msg = _('Could not resolve address')
|
||||
elif pi.type == PaymentIdentifierType.LNURL:
|
||||
msg = _('Could not resolve LNURL') + "\n\n" + pi.get_error()
|
||||
elif pi.type == PaymentIdentifierType.BIP70:
|
||||
msg = _('Could not resolve BIP70 payment request: {}').format(pi.error)
|
||||
else:
|
||||
msg = _('Could not resolve')
|
||||
self.validationError.emit('resolve', msg)
|
||||
else:
|
||||
self._update_from_payment_identifier()
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
self._pi.resolve(on_finished=on_finished)
|
||||
|
||||
def on_lnurl(self, lnurldata: LNURLData):
|
||||
def on_lnurl_pay(self, lnurldata: LNURL6Data):
|
||||
assert isinstance(lnurldata, LNURL6Data)
|
||||
self._logger.debug('on_lnurl')
|
||||
self._logger.debug(f'{repr(lnurldata)}')
|
||||
|
||||
if isinstance(lnurldata, LNURL6Data):
|
||||
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,
|
||||
'comment_allowed': lnurldata.comment_allowed,
|
||||
}
|
||||
self.setValidLNURLPayRequest()
|
||||
elif isinstance(lnurldata, LNURL3Data):
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url': lnurldata.callback_url,
|
||||
'min_withdrawable_sat': lnurldata.min_withdrawable_sat,
|
||||
'max_withdrawable_sat': lnurldata.max_withdrawable_sat,
|
||||
'default_description': lnurldata.default_description,
|
||||
'k1': lnurldata.k1,
|
||||
}
|
||||
self.setValidLNURLWithdrawRequest()
|
||||
else:
|
||||
raise NotImplementedError(f"Invalid lnurl type in on_lnurl {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,
|
||||
'comment_allowed': lnurldata.comment_allowed,
|
||||
}
|
||||
self.setValidLNURLPayRequest()
|
||||
self.lnurlRetrieved.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -707,54 +652,6 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)
|
||||
|
||||
@pyqtSlot()
|
||||
def lnurlRequestWithdrawal(self):
|
||||
assert self._lnurlData
|
||||
assert self.invoiceType == QEInvoice.Type.LNURLWithdrawRequest
|
||||
self._logger.debug(f'{repr(self._lnurlData)}')
|
||||
|
||||
amount_sat = self.amountOverride.satsInt
|
||||
|
||||
try:
|
||||
key = self.wallet.wallet.create_request(
|
||||
amount_sat=amount_sat,
|
||||
message=self._lnurlData.get('default_description', ''),
|
||||
exp_delay=120,
|
||||
address=None,
|
||||
)
|
||||
req = self.wallet.wallet.get_request(key)
|
||||
_lnaddr, b11_invoice = self.wallet.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=req.payment_hash,
|
||||
amount_msat=req.get_amount_msat(),
|
||||
message=req.get_message(),
|
||||
expiry=req.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
except Exception as e:
|
||||
self._logger.exception('')
|
||||
self.lnurlError.emit(
|
||||
'lnurl',
|
||||
_("Failed to create payment request for withdrawal: {}").format(str(e))
|
||||
)
|
||||
return
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
coro = request_lnurl_withdraw_callback(
|
||||
callback_url=self._lnurlData['callback_url'],
|
||||
k1=self._lnurlData['k1'],
|
||||
bolt_11=b11_invoice,
|
||||
)
|
||||
try:
|
||||
Network.run_from_another_thread(coro)
|
||||
except LNURLError as e:
|
||||
self.lnurlError.emit('lnurl', str(e))
|
||||
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
|
||||
def on_lnurl_invoice(self, orig_amount, invoice):
|
||||
self._logger.debug('on_lnurl_invoice')
|
||||
self._logger.debug(f'{repr(invoice)}')
|
||||
@@ -763,7 +660,9 @@ class QEInvoiceParser(QEInvoice):
|
||||
if orig_amount * 1000 != invoice.amount_msat: # TODO msat precision can cause trouble here
|
||||
raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount')
|
||||
|
||||
self.recipient = invoice.lightning_invoice
|
||||
self.fromResolvedPaymentIdentifier(
|
||||
PaymentIdentifier(self._wallet.wallet, invoice.lightning_invoice)
|
||||
)
|
||||
|
||||
@pyqtSlot(result=bool)
|
||||
def saveInvoice(self) -> bool:
|
||||
|
||||
100
electrum/gui/qml/qepiresolver.py
Normal file
100
electrum/gui/qml/qepiresolver.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from electrum.logging import get_logger
|
||||
from electrum.i18n import _
|
||||
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType
|
||||
|
||||
from .qewallet import QEWallet
|
||||
|
||||
|
||||
class QEPIResolver(QObject):
|
||||
"""Intended to handle a user input Payment Identifier (PI), resolve it if necessary, then
|
||||
allow to distinguish between a Request/voucher/lnurlw and an Invoice (e.g. b11 or lnurlp)."""
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
busyChanged = pyqtSignal()
|
||||
resolveError = pyqtSignal([str, str], arguments=['code', 'message'])
|
||||
invoiceResolved = pyqtSignal(object)
|
||||
requestResolved = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._wallet = None # type: Optional[QEWallet]
|
||||
self._recipient = None
|
||||
self._pi = None
|
||||
self._busy = False
|
||||
|
||||
self.clear()
|
||||
|
||||
recipientChanged = pyqtSignal()
|
||||
@pyqtProperty(str, notify=recipientChanged)
|
||||
def recipient(self) -> Optional[str]:
|
||||
return self._recipient
|
||||
|
||||
@recipient.setter
|
||||
def recipient(self, recipient: str) -> None:
|
||||
self.clear()
|
||||
if not recipient:
|
||||
return
|
||||
self._recipient = recipient
|
||||
self.recipientChanged.emit()
|
||||
self._pi = PaymentIdentifier(self._wallet.wallet, recipient)
|
||||
if self._pi.need_resolve():
|
||||
self.resolve_pi()
|
||||
else:
|
||||
# assuming if the PI is an invoice if it doesn't need resolving
|
||||
# as there are no request types that do not need resolving currently
|
||||
self.invoiceResolved.emit(self._pi)
|
||||
|
||||
walletChanged = pyqtSignal()
|
||||
@pyqtProperty(QEWallet, notify=walletChanged)
|
||||
def wallet(self) -> Optional[QEWallet]:
|
||||
return self._wallet
|
||||
|
||||
@wallet.setter
|
||||
def wallet(self, wallet: QEWallet) -> None:
|
||||
self._wallet = wallet
|
||||
|
||||
@pyqtProperty(bool, notify=busyChanged)
|
||||
def busy(self):
|
||||
return self._busy
|
||||
|
||||
def resolve_pi(self) -> None:
|
||||
assert self._pi is not None
|
||||
assert self._pi.need_resolve()
|
||||
|
||||
def on_finished(pi: PaymentIdentifier):
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
if pi.is_error():
|
||||
if pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE]:
|
||||
msg = _('Could not resolve address')
|
||||
elif pi.type == PaymentIdentifierType.LNURL:
|
||||
msg = _('Could not resolve LNURL') + "\n\n" + pi.get_error()
|
||||
elif pi.type == PaymentIdentifierType.BIP70:
|
||||
msg = _('Could not resolve BIP70 payment request: {}').format(pi.error)
|
||||
else:
|
||||
msg = _('Could not resolve')
|
||||
self.resolveError.emit('resolve', msg)
|
||||
else:
|
||||
if pi.type == PaymentIdentifierType.LNURLW:
|
||||
self.requestResolved.emit(pi)
|
||||
else:
|
||||
self.invoiceResolved.emit(pi)
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
self._pi.resolve(on_finished=on_finished)
|
||||
|
||||
def clear(self) -> None:
|
||||
self._recipient = None
|
||||
self._pi = None
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
self.recipientChanged.emit()
|
||||
@@ -1,5 +1,6 @@
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum
|
||||
|
||||
@@ -8,6 +9,10 @@ from electrum.invoices import (
|
||||
PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, LN_EXPIRY_NEVER
|
||||
)
|
||||
from electrum.lnutil import MIN_FUNDING_SAT
|
||||
from electrum.lnurl import LNURL3Data, request_lnurl_withdraw_callback, LNURLError
|
||||
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierType
|
||||
from electrum.i18n import _
|
||||
from electrum.network import Network
|
||||
|
||||
from .qewallet import QEWallet
|
||||
from .qetypes import QEAmount
|
||||
@@ -31,6 +36,9 @@ class QERequestDetails(QObject, QtEventListener):
|
||||
|
||||
detailsChanged = pyqtSignal() # generic request properties changed signal
|
||||
statusChanged = pyqtSignal()
|
||||
needsLNURLUserInput = pyqtSignal()
|
||||
lnurlError = pyqtSignal(str, str) # code, message
|
||||
busyChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -41,6 +49,9 @@ class QERequestDetails(QObject, QtEventListener):
|
||||
self._timer = None
|
||||
self._amount = None
|
||||
|
||||
self._lnurlData = None # type: Optional[dict]
|
||||
self._busy = False
|
||||
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.timeout.connect(self.updateStatusString)
|
||||
@@ -150,6 +161,14 @@ class QERequestDetails(QObject, QtEventListener):
|
||||
def bip21(self):
|
||||
return self._req.get_bip21_URI() if self._req else ''
|
||||
|
||||
@pyqtProperty('QVariantMap', notify=detailsChanged)
|
||||
def lnurlData(self) -> Optional[dict]:
|
||||
return self._lnurlData
|
||||
|
||||
@pyqtProperty(bool, notify=busyChanged)
|
||||
def busy(self):
|
||||
return self._busy
|
||||
|
||||
def initRequest(self):
|
||||
if self._wallet is None or self._key is None:
|
||||
return
|
||||
@@ -181,3 +200,70 @@ class QERequestDetails(QObject, QtEventListener):
|
||||
self.statusChanged.emit()
|
||||
self.set_status_timer()
|
||||
|
||||
@pyqtSlot(object)
|
||||
def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:
|
||||
"""
|
||||
Called when a payment identifier is resolved to a request (currently only LNURLW, but
|
||||
could also be used for other "voucher" type input like redeeming ecash tokens or
|
||||
some bolt12 thing).
|
||||
"""
|
||||
if not self._wallet:
|
||||
return
|
||||
if resolved_pi.type == PaymentIdentifierType.LNURLW:
|
||||
lnurldata = resolved_pi.lnurl_data
|
||||
assert isinstance(lnurldata, LNURL3Data), "Expected LNURL3Data type"
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url': lnurldata.callback_url,
|
||||
'min_withdrawable_sat': lnurldata.min_withdrawable_sat,
|
||||
'max_withdrawable_sat': lnurldata.max_withdrawable_sat,
|
||||
'default_description': lnurldata.default_description,
|
||||
'k1': lnurldata.k1,
|
||||
}
|
||||
self.needsLNURLUserInput.emit()
|
||||
else:
|
||||
raise NotImplementedError("Cannot request withdrawal for this payment identifier type")
|
||||
|
||||
@pyqtSlot(int)
|
||||
def lnurlRequestWithdrawal(self, amount_sat: int) -> None:
|
||||
assert self._lnurlData
|
||||
self._logger.debug(f'requesting lnurlw: {repr(self._lnurlData)}')
|
||||
|
||||
try:
|
||||
key = self.wallet.wallet.create_request(
|
||||
amount_sat=amount_sat,
|
||||
message=self._lnurlData.get('default_description', ''),
|
||||
exp_delay=120,
|
||||
address=None,
|
||||
)
|
||||
req = self.wallet.wallet.get_request(key)
|
||||
_lnaddr, b11_invoice = self.wallet.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=req.payment_hash,
|
||||
amount_msat=req.get_amount_msat(),
|
||||
message=req.get_message(),
|
||||
expiry=req.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
except Exception as e:
|
||||
self._logger.exception('')
|
||||
self.lnurlError.emit(
|
||||
'lnurl',
|
||||
_("Failed to create payment request for withdrawal: {}").format(str(e))
|
||||
)
|
||||
return
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
coro = request_lnurl_withdraw_callback(
|
||||
callback_url=self._lnurlData['callback_url'],
|
||||
k1=self._lnurlData['k1'],
|
||||
bolt_11=b11_invoice,
|
||||
)
|
||||
try:
|
||||
Network.run_from_another_thread(coro)
|
||||
except LNURLError as e:
|
||||
self.lnurlError.emit('lnurl', str(e))
|
||||
finally:
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
@@ -230,6 +230,7 @@ async def callback_lnurl(url: str, params: dict) -> dict:
|
||||
raise LNURLError(f"Client error: {e}") from e
|
||||
try:
|
||||
response = json.loads(response_raw)
|
||||
_logger.debug(f"lnurl response: {response}")
|
||||
except json.JSONDecodeError:
|
||||
raise LNURLError(f"Invalid response from LNURL server")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user