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')
|
title: qsTr('LNURL Withdraw request')
|
||||||
iconSource: '../../../icons/link.png'
|
iconSource: '../../../icons/link.png'
|
||||||
|
|
||||||
property InvoiceParser invoiceParser
|
property Wallet wallet: Daemon.currentWallet
|
||||||
|
property RequestDetails requestDetails
|
||||||
|
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
property int walletCanReceive: invoiceParser.wallet.lightningCanReceive.satsInt
|
property int walletCanReceive: 0
|
||||||
property int providerMinWithdrawable: parseInt(invoiceParser.lnurlData['min_withdrawable_sat'])
|
property int providerMinWithdrawable: parseInt(requestDetails.lnurlData['min_withdrawable_sat'])
|
||||||
property int providerMaxWithdrawable: parseInt(invoiceParser.lnurlData['max_withdrawable_sat'])
|
property int providerMaxWithdrawable: parseInt(requestDetails.lnurlData['max_withdrawable_sat'])
|
||||||
property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)
|
property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)
|
||||||
property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)
|
property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)
|
||||||
property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive
|
property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive
|
||||||
@@ -30,6 +31,12 @@ ElDialog {
|
|||||||
amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable
|
amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable
|
||||||
property bool valid: amountValid
|
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 {
|
ColumnLayout {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
@@ -89,17 +96,17 @@ ElDialog {
|
|||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: invoiceParser.lnurlData['domain']
|
text: requestDetails.lnurlData['domain']
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: qsTr('Description')
|
text: qsTr('Description')
|
||||||
color: Material.accentColor
|
color: Material.accentColor
|
||||||
visible: invoiceParser.lnurlData['default_description']
|
visible: requestDetails.lnurlData['default_description']
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: invoiceParser.lnurlData['default_description']
|
text: requestDetails.lnurlData['default_description']
|
||||||
visible: invoiceParser.lnurlData['default_description']
|
visible: requestDetails.lnurlData['default_description']
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +124,6 @@ ElDialog {
|
|||||||
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
|
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
|
||||||
color: Material.foreground // override gray-out on disabled
|
color: Material.foreground // override gray-out on disabled
|
||||||
fiatfield: amountFiat
|
fiatfield: amountFiat
|
||||||
onTextAsSatsChanged: {
|
|
||||||
invoiceParser.amountOverride = textAsSats
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: Config.baseUnit
|
text: Config.baseUnit
|
||||||
@@ -150,12 +154,12 @@ ElDialog {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: qsTr('Withdraw...')
|
text: qsTr('Withdraw...')
|
||||||
icon.source: '../../icons/confirmed.png'
|
icon.source: '../../icons/confirmed.png'
|
||||||
enabled: valid
|
enabled: valid && !requestDetails.busy
|
||||||
onClicked: {
|
onClicked: {
|
||||||
invoiceParser.lnurlRequestWithdrawal()
|
var satsAmount = amountBtc.textAsSats.satsInt;
|
||||||
dialog.close()
|
requestDetails.lnurlRequestWithdrawal(satsAmount);
|
||||||
|
dialog.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ ElDialog {
|
|||||||
id: dialog
|
id: dialog
|
||||||
|
|
||||||
property InvoiceParser invoiceParser
|
property InvoiceParser invoiceParser
|
||||||
|
property PIResolver piResolver
|
||||||
|
|
||||||
signal txFound(data: string)
|
signal txFound(data: string)
|
||||||
signal channelBackupFound(data: string)
|
signal channelBackupFound(data: string)
|
||||||
@@ -36,7 +37,7 @@ ElDialog {
|
|||||||
} else if (Daemon.currentWallet.isValidChannelBackup(data)) {
|
} else if (Daemon.currentWallet.isValidChannelBackup(data)) {
|
||||||
channelBackupFound(data)
|
channelBackupFound(data)
|
||||||
} else {
|
} else {
|
||||||
invoiceParser.recipient = data
|
piResolver.recipient = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ ElDialog {
|
|||||||
FlatButton {
|
FlatButton {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: 1
|
Layout.preferredWidth: 1
|
||||||
enabled: !invoiceParser.busy
|
enabled: !invoiceParser.busy && !piResolver.busy
|
||||||
icon.source: '../../icons/copy_bw.png'
|
icon.source: '../../icons/copy_bw.png'
|
||||||
text: qsTr('Paste')
|
text: qsTr('Paste')
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Item {
|
|||||||
function openSendDialog() {
|
function openSendDialog() {
|
||||||
// Qt based send dialog if not on android
|
// Qt based send dialog if not on android
|
||||||
if (!AppController.isAndroid()) {
|
if (!AppController.isAndroid()) {
|
||||||
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser})
|
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser, piResolver: piResolver})
|
||||||
_sendDialog.open()
|
_sendDialog.open()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ Item {
|
|||||||
})
|
})
|
||||||
dialog.open()
|
dialog.open()
|
||||||
} else {
|
} else {
|
||||||
invoiceParser.recipient = data
|
piResolver.recipient = data
|
||||||
}
|
}
|
||||||
//scanner.destroy() // TODO
|
//scanner.destroy() // TODO
|
||||||
})
|
})
|
||||||
@@ -362,7 +362,7 @@ Item {
|
|||||||
Layout.preferredWidth: 1
|
Layout.preferredWidth: 1
|
||||||
icon.source: '../../icons/tab_send.png'
|
icon.source: '../../icons/tab_send.png'
|
||||||
text: qsTr('Send')
|
text: qsTr('Send')
|
||||||
enabled: !invoiceParser.busy
|
enabled: !invoiceParser.busy && !piResolver.busy && !requestDetails.busy
|
||||||
onClicked: openSendDialog()
|
onClicked: openSendDialog()
|
||||||
onPressAndHold: {
|
onPressAndHold: {
|
||||||
Config.userKnowsPressAndHold = true
|
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 {
|
Invoice {
|
||||||
id: invoice
|
id: invoice
|
||||||
wallet: Daemon.currentWallet
|
wallet: Daemon.currentWallet
|
||||||
@@ -420,17 +459,12 @@ Item {
|
|||||||
})
|
})
|
||||||
dialog.open()
|
dialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
onLnurlRetrieved: {
|
onLnurlRetrieved: {
|
||||||
closeSendDialog()
|
closeSendDialog()
|
||||||
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
|
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
|
||||||
var dialog = lnurlPayDialog.createObject(app, {
|
var dialog = lnurlPayDialog.createObject(app, {
|
||||||
invoiceParser: invoiceParser
|
invoiceParser: invoiceParser
|
||||||
})
|
})
|
||||||
} else if (invoiceParser.invoiceType === Invoice.Type.LNURLWithdrawRequest) {
|
|
||||||
var dialog = lnurlWithdrawDialog.createObject(app, {
|
|
||||||
invoiceParser: invoiceParser
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
|
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
|
||||||
return
|
return
|
||||||
@@ -460,7 +494,7 @@ Item {
|
|||||||
_intentUri = uri
|
_intentUri = uri
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
invoiceParser.recipient = uri
|
piResolver.recipient = uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +503,7 @@ Item {
|
|||||||
function onWalletLoaded() {
|
function onWalletLoaded() {
|
||||||
infobanner.hide() // start hidden when switching wallets
|
infobanner.hide() // start hidden when switching wallets
|
||||||
if (_intentUri) {
|
if (_intentUri) {
|
||||||
invoiceParser.recipient = _intentUri
|
piResolver.recipient = _intentUri
|
||||||
_intentUri = ''
|
_intentUri = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from .qebitcoin import QEBitcoin
|
|||||||
from .qefx import QEFX
|
from .qefx import QEFX
|
||||||
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
|
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
|
||||||
from .qeinvoice import QEInvoice, QEInvoiceParser
|
from .qeinvoice import QEInvoice, QEInvoiceParser
|
||||||
|
from .qepiresolver import QEPIResolver
|
||||||
from .qerequestdetails import QERequestDetails
|
from .qerequestdetails import QERequestDetails
|
||||||
from .qetypes import QEAmount, QEBytes
|
from .qetypes import QEAmount, QEBytes
|
||||||
from .qeaddressdetails import QEAddressDetails
|
from .qeaddressdetails import QEAddressDetails
|
||||||
@@ -439,6 +440,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
|||||||
qmlRegisterType(QEQRScanner, 'org.electrum', 1, 0, 'QRScanner')
|
qmlRegisterType(QEQRScanner, 'org.electrum', 1, 0, 'QRScanner')
|
||||||
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(QEPIResolver, 'org.electrum', 1, 0, 'PIResolver')
|
||||||
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
|
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
|
||||||
qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser')
|
qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser')
|
||||||
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')
|
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from electrum.invoices import (
|
|||||||
)
|
)
|
||||||
from electrum.transaction import PartialTxOutput, TxOutput
|
from electrum.transaction import PartialTxOutput, TxOutput
|
||||||
from electrum.lnutil import format_short_channel_id
|
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.bitcoin import COIN, address_to_script
|
||||||
from electrum.paymentrequest import PaymentRequest
|
from electrum.paymentrequest import PaymentRequest
|
||||||
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType
|
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType
|
||||||
@@ -23,7 +23,6 @@ from electrum.network import Network
|
|||||||
from .qetypes import QEAmount
|
from .qetypes import QEAmount
|
||||||
from .qewallet import QEWallet
|
from .qewallet import QEWallet
|
||||||
from .util import status_update_timer_interval, QtEventListener, event_listener
|
from .util import status_update_timer_interval, QtEventListener, event_listener
|
||||||
from ...fee_policy import FeePolicy
|
|
||||||
from ...util import InvoiceError
|
from ...util import InvoiceError
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ class QEInvoice(QObject, QtEventListener):
|
|||||||
OnchainInvoice = 0
|
OnchainInvoice = 0
|
||||||
LightningInvoice = 1
|
LightningInvoice = 1
|
||||||
LNURLPayRequest = 2
|
LNURLPayRequest = 2
|
||||||
LNURLWithdrawRequest = 3
|
|
||||||
|
|
||||||
@pyqtEnum
|
@pyqtEnum
|
||||||
class Status(IntEnum):
|
class Status(IntEnum):
|
||||||
@@ -453,26 +451,19 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._recipient = ''
|
|
||||||
self._pi = None
|
self._pi = None
|
||||||
self._lnurlData = None
|
self._lnurlData = None
|
||||||
self._busy = False
|
self._busy = False
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
recipientChanged = pyqtSignal()
|
@pyqtSlot(object)
|
||||||
@pyqtProperty(str, notify=recipientChanged)
|
def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:
|
||||||
def recipient(self):
|
|
||||||
return self._recipient
|
|
||||||
|
|
||||||
@recipient.setter
|
|
||||||
def recipient(self, recipient: str):
|
|
||||||
self.canPay = False
|
self.canPay = False
|
||||||
self._recipient = recipient
|
|
||||||
self.amountOverride = QEAmount()
|
self.amountOverride = QEAmount()
|
||||||
if recipient:
|
if resolved_pi:
|
||||||
self.validateRecipient(recipient)
|
assert not resolved_pi.need_resolve()
|
||||||
self.recipientChanged.emit()
|
self.validateRecipient(resolved_pi)
|
||||||
|
|
||||||
@pyqtProperty('QVariantMap', notify=lnurlRetrieved)
|
@pyqtProperty('QVariantMap', notify=lnurlRetrieved)
|
||||||
def lnurlData(self):
|
def lnurlData(self):
|
||||||
@@ -480,7 +471,7 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
|
|
||||||
@pyqtProperty(bool, notify=lnurlRetrieved)
|
@pyqtProperty(bool, notify=lnurlRetrieved)
|
||||||
def isLnurlPay(self):
|
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)
|
@pyqtProperty(bool, notify=busyChanged)
|
||||||
def busy(self):
|
def busy(self):
|
||||||
@@ -488,7 +479,6 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.recipient = ''
|
|
||||||
self.setInvoiceType(QEInvoice.Type.Invalid)
|
self.setInvoiceType(QEInvoice.Type.Invalid)
|
||||||
self._lnurlData = None
|
self._lnurlData = None
|
||||||
self.canSave = False
|
self.canSave = False
|
||||||
@@ -515,12 +505,6 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
self._effectiveInvoice = None
|
self._effectiveInvoice = None
|
||||||
self.invoiceChanged.emit()
|
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):
|
def create_onchain_invoice(self, outputs, message, payment_request, uri):
|
||||||
return self._wallet.wallet.create_invoice(
|
return self._wallet.wallet.create_invoice(
|
||||||
outputs=outputs,
|
outputs=outputs,
|
||||||
@@ -543,17 +527,18 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
else:
|
else:
|
||||||
self.validationError.emit('unknown', f'invoice error:\n{pr.error}')
|
self.validationError.emit('unknown', f'invoice error:\n{pr.error}')
|
||||||
|
|
||||||
def validateRecipient(self, recipient):
|
def validateRecipient(self, pi: PaymentIdentifier):
|
||||||
if not recipient:
|
if not pi:
|
||||||
self.setInvoiceType(QEInvoice.Type.Invalid)
|
self.setInvoiceType(QEInvoice.Type.Invalid)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._pi = PaymentIdentifier(self._wallet.wallet, recipient)
|
self._pi = pi
|
||||||
if not self._pi.is_valid() or self._pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
|
if not self._pi.is_valid() or self._pi.type not in [
|
||||||
PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,
|
PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
|
||||||
PaymentIdentifierType.LNURL,
|
PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,
|
||||||
PaymentIdentifierType.EMAILLIKE,
|
PaymentIdentifierType.LNADDR, PaymentIdentifierType.LNURLP,
|
||||||
PaymentIdentifierType.DOMAINLIKE]:
|
PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE
|
||||||
|
]:
|
||||||
self.validationError.emit('unknown', _('Unknown invoice'))
|
self.validationError.emit('unknown', _('Unknown invoice'))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -566,16 +551,13 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
self._update_from_payment_identifier()
|
self._update_from_payment_identifier()
|
||||||
|
|
||||||
def _update_from_payment_identifier(self):
|
def _update_from_payment_identifier(self):
|
||||||
if self._pi.need_resolve():
|
assert not self._pi.need_resolve(), "Should have been resolved by QEPIResolver"
|
||||||
self.resolve_pi()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._pi.type in [
|
if self._pi.type in [
|
||||||
PaymentIdentifierType.LNURLP,
|
PaymentIdentifierType.LNURLP,
|
||||||
PaymentIdentifierType.LNURLW,
|
|
||||||
PaymentIdentifierType.LNADDR,
|
PaymentIdentifierType.LNADDR,
|
||||||
]:
|
]:
|
||||||
self.on_lnurl(self._pi.lnurl_data)
|
self.on_lnurl_pay(self._pi.lnurl_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._pi.type == PaymentIdentifierType.BIP70:
|
if self._pi.type == PaymentIdentifierType.BIP70:
|
||||||
@@ -624,57 +606,20 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
self.setValidOnchainInvoice(invoice)
|
self.setValidOnchainInvoice(invoice)
|
||||||
self.validationSuccess.emit()
|
self.validationSuccess.emit()
|
||||||
|
|
||||||
def resolve_pi(self):
|
def on_lnurl_pay(self, lnurldata: LNURL6Data):
|
||||||
assert self._pi.need_resolve()
|
assert isinstance(lnurldata, LNURL6Data)
|
||||||
|
|
||||||
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):
|
|
||||||
self._logger.debug('on_lnurl')
|
self._logger.debug('on_lnurl')
|
||||||
self._logger.debug(f'{repr(lnurldata)}')
|
self._logger.debug(f'{repr(lnurldata)}')
|
||||||
|
|
||||||
if isinstance(lnurldata, LNURL6Data):
|
self._lnurlData = {
|
||||||
self._lnurlData = {
|
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
'callback_url': lnurldata.callback_url,
|
||||||
'callback_url': lnurldata.callback_url,
|
'min_sendable_sat': lnurldata.min_sendable_sat,
|
||||||
'min_sendable_sat': lnurldata.min_sendable_sat,
|
'max_sendable_sat': lnurldata.max_sendable_sat,
|
||||||
'max_sendable_sat': lnurldata.max_sendable_sat,
|
'metadata_plaintext': lnurldata.metadata_plaintext,
|
||||||
'metadata_plaintext': lnurldata.metadata_plaintext,
|
'comment_allowed': lnurldata.comment_allowed,
|
||||||
'comment_allowed': lnurldata.comment_allowed,
|
}
|
||||||
}
|
self.setValidLNURLPayRequest()
|
||||||
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.lnurlRetrieved.emit()
|
self.lnurlRetrieved.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@@ -707,54 +652,6 @@ class QEInvoiceParser(QEInvoice):
|
|||||||
|
|
||||||
self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)
|
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):
|
def on_lnurl_invoice(self, orig_amount, invoice):
|
||||||
self._logger.debug('on_lnurl_invoice')
|
self._logger.debug('on_lnurl_invoice')
|
||||||
self._logger.debug(f'{repr(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
|
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')
|
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)
|
@pyqtSlot(result=bool)
|
||||||
def saveInvoice(self) -> 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 enum import IntEnum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum
|
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
|
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.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 .qewallet import QEWallet
|
||||||
from .qetypes import QEAmount
|
from .qetypes import QEAmount
|
||||||
@@ -31,6 +36,9 @@ class QERequestDetails(QObject, QtEventListener):
|
|||||||
|
|
||||||
detailsChanged = pyqtSignal() # generic request properties changed signal
|
detailsChanged = pyqtSignal() # generic request properties changed signal
|
||||||
statusChanged = pyqtSignal()
|
statusChanged = pyqtSignal()
|
||||||
|
needsLNURLUserInput = pyqtSignal()
|
||||||
|
lnurlError = pyqtSignal(str, str) # code, message
|
||||||
|
busyChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -41,6 +49,9 @@ class QERequestDetails(QObject, QtEventListener):
|
|||||||
self._timer = None
|
self._timer = None
|
||||||
self._amount = None
|
self._amount = None
|
||||||
|
|
||||||
|
self._lnurlData = None # type: Optional[dict]
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
self._timer = QTimer(self)
|
self._timer = QTimer(self)
|
||||||
self._timer.setSingleShot(True)
|
self._timer.setSingleShot(True)
|
||||||
self._timer.timeout.connect(self.updateStatusString)
|
self._timer.timeout.connect(self.updateStatusString)
|
||||||
@@ -150,6 +161,14 @@ class QERequestDetails(QObject, QtEventListener):
|
|||||||
def bip21(self):
|
def bip21(self):
|
||||||
return self._req.get_bip21_URI() if self._req else ''
|
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):
|
def initRequest(self):
|
||||||
if self._wallet is None or self._key is None:
|
if self._wallet is None or self._key is None:
|
||||||
return
|
return
|
||||||
@@ -181,3 +200,70 @@ class QERequestDetails(QObject, QtEventListener):
|
|||||||
self.statusChanged.emit()
|
self.statusChanged.emit()
|
||||||
self.set_status_timer()
|
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
|
raise LNURLError(f"Client error: {e}") from e
|
||||||
try:
|
try:
|
||||||
response = json.loads(response_raw)
|
response = json.loads(response_raw)
|
||||||
|
_logger.debug(f"lnurl response: {response}")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise LNURLError(f"Invalid response from LNURL server")
|
raise LNURLError(f"Invalid response from LNURL server")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user