diff --git a/electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml b/electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml index 63eac2f5e..d923866f2 100644 --- a/electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml @@ -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(); } } } - } diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index f74711112..e17c45ce4 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -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: { diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index fb3387fd4..d8f44ce7a 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -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 = '' } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 45e9feaf2..dadc5d206 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -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') diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 5d18dc4eb..a5f2a77e4 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -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: diff --git a/electrum/gui/qml/qepiresolver.py b/electrum/gui/qml/qepiresolver.py new file mode 100644 index 000000000..9699a4b06 --- /dev/null +++ b/electrum/gui/qml/qepiresolver.py @@ -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() diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 6f5e35dc1..e12f46319 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -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() diff --git a/electrum/lnurl.py b/electrum/lnurl.py index a3676b01e..3f035bae1 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -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")