Fixes the issue described in #10406. When scanning a lightning invoice we would pass it to `QEInvoiceParser.fromResolvedPaymentIdentifier()`, however `fromResolvedPaymentIdentifier()` doesn't reset the state of `QEInvoiceParser._lnurlData` which is used in QML to evaluate `payImmediately: invoiceParser.isLnurlPay` in the `onValidationSuccess` connection. This change calls `clear()` in `fromResolvedPaymentIdentifier()` to ensure that `QEInvoiceParser` state gets reset when loading a new invoice. However when retrieving a bolt11 from a lnurl-pay callback we don't wan't to reset `QEInvoiceParser._lnurlData` so that `payImmediately` is true when confirming the lnurl pay dialog, for that I skip calling `fromResolvedPaymentIdentifier()` and instead call `validateRecipient()` directly so the `QEInvoiceParser` state doesn't get reset in this case.
686 lines
25 KiB
Python
686 lines
25 KiB
Python
import copy
|
|
import threading
|
|
from enum import IntEnum
|
|
from typing import Optional, Dict, Any, Tuple
|
|
from urllib.parse import urlparse
|
|
|
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum, QTimer
|
|
|
|
from electrum.i18n import _
|
|
from electrum.logging import get_logger
|
|
from electrum.invoices import (
|
|
Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED,
|
|
PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER
|
|
)
|
|
from electrum.transaction import PartialTxOutput, TxOutput
|
|
from electrum.lnutil import format_short_channel_id
|
|
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
|
|
from electrum.network import Network
|
|
|
|
from .qetypes import QEAmount
|
|
from .qewallet import QEWallet
|
|
from .util import status_update_timer_interval, QtEventListener, event_listener
|
|
from ...util import InvoiceError
|
|
|
|
|
|
class QEInvoice(QObject, QtEventListener):
|
|
@pyqtEnum
|
|
class Type(IntEnum):
|
|
Invalid = -1
|
|
OnchainInvoice = 0
|
|
LightningInvoice = 1
|
|
LNURLPayRequest = 2
|
|
|
|
@pyqtEnum
|
|
class Status(IntEnum):
|
|
Unpaid = PR_UNPAID
|
|
Expired = PR_EXPIRED
|
|
Unknown = PR_UNKNOWN
|
|
Paid = PR_PAID
|
|
Inflight = PR_INFLIGHT
|
|
Failed = PR_FAILED
|
|
Routing = PR_ROUTING
|
|
Unconfirmed = PR_UNCONFIRMED
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
invoiceChanged = pyqtSignal()
|
|
invoiceSaved = pyqtSignal([str], arguments=['key'])
|
|
amountOverrideChanged = pyqtSignal()
|
|
maxAmountMessage = pyqtSignal([str], arguments=['message'])
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self._wallet = None # type: Optional[QEWallet]
|
|
self._isSaved = False
|
|
self._canSave = False
|
|
self._canPay = False
|
|
self._key = None
|
|
self._invoiceType = QEInvoice.Type.Invalid
|
|
self._effectiveInvoice = None # type: Optional[Invoice]
|
|
self._userinfo = ''
|
|
self._lnprops = {}
|
|
self._amount = QEAmount()
|
|
self._amountOverride = QEAmount()
|
|
|
|
self._timer = QTimer(self)
|
|
self._timer.setSingleShot(True)
|
|
self._timer.timeout.connect(self.updateStatusString)
|
|
|
|
self._amountOverride.valueChanged.connect(self._on_amountoverride_value_changed)
|
|
|
|
self._updating_max = False
|
|
|
|
self.register_callbacks()
|
|
self.destroyed.connect(lambda: self.on_destroy())
|
|
|
|
def on_destroy(self):
|
|
self.unregister_callbacks()
|
|
|
|
@event_listener
|
|
def on_event_payment_succeeded(self, wallet, key):
|
|
if wallet == self._wallet.wallet and key == self.key:
|
|
self.statusChanged.emit()
|
|
self.determine_can_pay()
|
|
self.userinfo = _('Paid!')
|
|
|
|
@event_listener
|
|
def on_event_payment_failed(self, wallet, key, reason):
|
|
if wallet == self._wallet.wallet and key == self.key:
|
|
self.statusChanged.emit()
|
|
self.determine_can_pay()
|
|
self.userinfo = _('Payment failed: ') + reason
|
|
|
|
@event_listener
|
|
def on_event_invoice_status(self, wallet, key, status):
|
|
if self._wallet and wallet == self._wallet.wallet and key == self.key:
|
|
self.update_userinfo()
|
|
self.determine_can_pay()
|
|
self.statusChanged.emit()
|
|
|
|
@event_listener
|
|
def on_event_channel(self, wallet, channel):
|
|
if self._wallet and wallet == self._wallet.wallet:
|
|
self.update_userinfo()
|
|
self.determine_can_pay()
|
|
|
|
walletChanged = pyqtSignal()
|
|
@pyqtProperty(QEWallet, notify=walletChanged)
|
|
def wallet(self):
|
|
return self._wallet
|
|
|
|
@wallet.setter
|
|
def wallet(self, wallet: QEWallet):
|
|
if self._wallet != wallet:
|
|
self._wallet = wallet
|
|
self.walletChanged.emit()
|
|
|
|
@pyqtProperty(int, notify=invoiceChanged)
|
|
def invoiceType(self):
|
|
return self._invoiceType
|
|
|
|
# not a qt setter, don't let outside set state
|
|
def setInvoiceType(self, invoiceType: Type):
|
|
self._invoiceType = invoiceType
|
|
|
|
@pyqtProperty(str, notify=invoiceChanged)
|
|
def message(self):
|
|
return self._effectiveInvoice.message if self._effectiveInvoice else ''
|
|
|
|
@pyqtProperty('quint64', notify=invoiceChanged)
|
|
def time(self):
|
|
return self._effectiveInvoice.time if self._effectiveInvoice else 0
|
|
|
|
@pyqtProperty('quint64', notify=invoiceChanged)
|
|
def expiration(self):
|
|
return self._effectiveInvoice.exp if self._effectiveInvoice else 0
|
|
|
|
@pyqtProperty(str, notify=invoiceChanged)
|
|
def address(self):
|
|
return self._effectiveInvoice.get_address() if self._effectiveInvoice else ''
|
|
|
|
@pyqtProperty(QEAmount, notify=invoiceChanged)
|
|
def amount(self):
|
|
if not self._effectiveInvoice:
|
|
self._amount.clear()
|
|
return self._amount
|
|
self._amount.copyFrom(QEAmount(from_invoice=self._effectiveInvoice))
|
|
return self._amount
|
|
|
|
@pyqtProperty(QEAmount, notify=amountOverrideChanged)
|
|
def amountOverride(self):
|
|
return self._amountOverride
|
|
|
|
@amountOverride.setter
|
|
def amountOverride(self, new_amount: QEAmount):
|
|
self._logger.debug(f'set new override amount {repr(new_amount)}')
|
|
self._amountOverride.copyFrom(new_amount)
|
|
self.amountOverrideChanged.emit()
|
|
|
|
@pyqtSlot()
|
|
def _on_amountoverride_value_changed(self):
|
|
self.update_userinfo()
|
|
self.determine_can_pay()
|
|
|
|
statusChanged = pyqtSignal()
|
|
@pyqtProperty(int, notify=statusChanged)
|
|
def status(self):
|
|
if not self._effectiveInvoice:
|
|
return PR_UNKNOWN
|
|
if self.invoiceType == QEInvoice.Type.OnchainInvoice and self._effectiveInvoice.get_amount_sat() == 0:
|
|
# no amount set, not a final invoice, get_invoice_status would be wrong
|
|
return PR_UNPAID
|
|
return self._wallet.wallet.get_invoice_status(self._effectiveInvoice)
|
|
|
|
@pyqtProperty(str, notify=statusChanged)
|
|
def statusString(self):
|
|
if not self._effectiveInvoice:
|
|
return ''
|
|
status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice)
|
|
return self._effectiveInvoice.get_status_str(status)
|
|
|
|
isSavedChanged = pyqtSignal()
|
|
@pyqtProperty(bool, notify=isSavedChanged)
|
|
def isSaved(self):
|
|
return self._isSaved
|
|
|
|
canSaveChanged = pyqtSignal()
|
|
@pyqtProperty(bool, notify=canSaveChanged)
|
|
def canSave(self):
|
|
return self._canSave
|
|
|
|
@canSave.setter
|
|
def canSave(self, canSave):
|
|
if self._canSave != canSave:
|
|
self._canSave = canSave
|
|
self.canSaveChanged.emit()
|
|
|
|
canPayChanged = pyqtSignal()
|
|
@pyqtProperty(bool, notify=canPayChanged)
|
|
def canPay(self):
|
|
return self._canPay
|
|
|
|
@canPay.setter
|
|
def canPay(self, canPay):
|
|
if self._canPay != canPay:
|
|
self._canPay = canPay
|
|
self.canPayChanged.emit()
|
|
|
|
keyChanged = pyqtSignal()
|
|
@pyqtProperty(str, notify=keyChanged)
|
|
def key(self):
|
|
return self._key
|
|
|
|
@key.setter
|
|
def key(self, key):
|
|
self._key = key
|
|
invoice = copy.copy(self._wallet.wallet.get_invoice(key)) # copy, so any mutations stay out of wallet invoice list
|
|
self._logger.debug(f'invoice from key {key}: {repr(invoice)}')
|
|
self.set_effective_invoice(invoice)
|
|
self.keyChanged.emit()
|
|
|
|
userinfoChanged = pyqtSignal()
|
|
@pyqtProperty(str, notify=userinfoChanged)
|
|
def userinfo(self):
|
|
return self._userinfo
|
|
|
|
@userinfo.setter
|
|
def userinfo(self, userinfo):
|
|
if self._userinfo != userinfo:
|
|
self._userinfo = userinfo
|
|
self.userinfoChanged.emit()
|
|
|
|
@pyqtProperty('QVariantMap', notify=invoiceChanged)
|
|
def lnprops(self):
|
|
return self._lnprops
|
|
|
|
def set_lnprops(self):
|
|
self._lnprops = {}
|
|
if not self.invoiceType == QEInvoice.Type.LightningInvoice:
|
|
return
|
|
|
|
lnaddr = self._effectiveInvoice._lnaddr
|
|
ln_routing_info = lnaddr.get_routing_info('r')
|
|
self._logger.debug(str(ln_routing_info))
|
|
|
|
self._lnprops = {
|
|
'pubkey': lnaddr.pubkey.serialize().hex(),
|
|
'payment_hash': lnaddr.paymenthash.hex(),
|
|
'r': [{
|
|
'node': self.name_for_node_id(x[-1][0]),
|
|
'scid': format_short_channel_id(x[-1][1])
|
|
} for x in ln_routing_info] if ln_routing_info else []
|
|
}
|
|
|
|
def name_for_node_id(self, node_id):
|
|
lnworker = self._wallet.wallet.lnworker
|
|
return (lnworker.lnpeermgr.get_node_alias(node_id) if lnworker else None) or node_id.hex()
|
|
|
|
def set_effective_invoice(self, invoice: Invoice):
|
|
self._effectiveInvoice = invoice
|
|
|
|
if invoice is None:
|
|
self.setInvoiceType(QEInvoice.Type.Invalid)
|
|
else:
|
|
if invoice.is_lightning():
|
|
self.setInvoiceType(QEInvoice.Type.LightningInvoice)
|
|
else:
|
|
self.setInvoiceType(QEInvoice.Type.OnchainInvoice)
|
|
self._isSaved = self._wallet.wallet.get_invoice(invoice.get_id()) is not None
|
|
|
|
self.set_lnprops()
|
|
|
|
self.update_userinfo()
|
|
self.determine_can_pay()
|
|
|
|
self.invoiceChanged.emit()
|
|
self.statusChanged.emit()
|
|
self.isSavedChanged.emit()
|
|
|
|
self.set_status_timer()
|
|
|
|
def set_status_timer(self):
|
|
if self.status != PR_EXPIRED:
|
|
if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER:
|
|
interval = status_update_timer_interval(self.time + self.expiration)
|
|
if interval > 0:
|
|
self._timer.setInterval(interval) # msec
|
|
self._timer.start()
|
|
else:
|
|
self.update_userinfo()
|
|
self.determine_can_pay() # status went to PR_EXPIRED
|
|
|
|
@pyqtSlot()
|
|
def updateStatusString(self):
|
|
self.statusChanged.emit()
|
|
self.set_status_timer()
|
|
|
|
def update_userinfo(self):
|
|
self.userinfo = ''
|
|
|
|
if not self.amountOverride.isEmpty:
|
|
amount = self.amountOverride
|
|
else:
|
|
amount = self.amount
|
|
|
|
if self.amount.isEmpty:
|
|
self.userinfo = _('Enter the amount you want to send')
|
|
|
|
status = self.status
|
|
|
|
if amount.isEmpty and status == PR_UNPAID: # unspecified amount
|
|
return
|
|
|
|
def userinfo_for_invoice_status(_status: int) -> str:
|
|
return {
|
|
PR_EXPIRED: _('This invoice has expired'),
|
|
PR_PAID: _('This invoice was already paid'),
|
|
PR_INFLIGHT: _('Payment in progress...'),
|
|
PR_ROUTING: _('Payment in progress...'),
|
|
PR_BROADCASTING: _('Payment in progress...') + ' (' + _('broadcasting') + ')',
|
|
PR_BROADCAST: _('Payment in progress...') + ' (' + _('broadcast successfully') + ')',
|
|
PR_UNCONFIRMED: _('Payment in progress...') + ' (' + _('waiting for confirmation') + ')',
|
|
PR_UNKNOWN: _('Invoice has unknown status'),
|
|
}[_status]
|
|
|
|
if status in [PR_UNPAID, PR_FAILED]:
|
|
x, self.userinfo = self.check_can_pay_amount(amount)
|
|
else:
|
|
self.userinfo = userinfo_for_invoice_status(status)
|
|
|
|
def determine_can_pay(self):
|
|
self.canPay = False
|
|
self.canSave = False
|
|
|
|
if self.invoiceType not in [QEInvoice.Type.LightningInvoice, QEInvoice.Type.OnchainInvoice]:
|
|
return
|
|
|
|
if not self.amountOverride.isEmpty:
|
|
amount = self.amountOverride
|
|
else:
|
|
amount = self.amount
|
|
|
|
self.canSave = not bool(self._wallet.wallet.get_invoice(self._effectiveInvoice.get_id()))
|
|
|
|
status = self.status
|
|
|
|
if amount.isEmpty and status == PR_UNPAID: # unspecified amount
|
|
return
|
|
|
|
if status in [PR_UNPAID, PR_FAILED]:
|
|
self.canPay, x = self.check_can_pay_amount(amount)
|
|
|
|
def check_can_pay_amount(self, amount: QEAmount) -> Tuple[bool, Optional[str]]:
|
|
assert self.status in [PR_UNPAID, PR_FAILED]
|
|
if self.invoiceType == QEInvoice.Type.LightningInvoice:
|
|
if self.get_max_spendable_lightning() * 1000 >= amount.msatsInt:
|
|
lnaddr = self._effectiveInvoice._lnaddr
|
|
if lnaddr.amount and amount.msatsInt < lnaddr.amount * COIN * 1000:
|
|
return False, _('Cannot pay less than the amount specified in the invoice')
|
|
else:
|
|
return True, None
|
|
elif self.address and self.get_max_spendable_onchain() > amount.satsInt:
|
|
return True, None
|
|
elif self.invoiceType == QEInvoice.Type.OnchainInvoice:
|
|
if (amount.isMax and self.get_max_spendable_onchain() > 0) or (self.get_max_spendable_onchain() >= amount.satsInt):
|
|
return True, None
|
|
|
|
return False, _('Insufficient balance')
|
|
|
|
@pyqtSlot()
|
|
def payLightningInvoice(self):
|
|
if not self.canPay:
|
|
raise Exception('can not pay invoice, canPay is false')
|
|
|
|
if self.invoiceType != QEInvoice.Type.LightningInvoice:
|
|
raise Exception('payLightningInvoice can only pay lightning invoices')
|
|
|
|
amount_msat = None
|
|
if self.amount.isEmpty:
|
|
if self.amountOverride.isEmpty:
|
|
raise Exception('can not pay 0 amount')
|
|
amount_msat = self.amountOverride.msatsInt
|
|
|
|
self._wallet.pay_lightning_invoice(self._effectiveInvoice, amount_msat)
|
|
|
|
def get_max_spendable_onchain(self):
|
|
return self._wallet.wallet.get_spendable_balance_sat()
|
|
|
|
def get_max_spendable_lightning(self):
|
|
return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0
|
|
|
|
@pyqtSlot()
|
|
def updateMaxAmount(self):
|
|
if self._updating_max:
|
|
return
|
|
|
|
assert self.invoiceType == QEInvoice.Type.OnchainInvoice
|
|
|
|
# only single address invoice supported
|
|
invoice_address = self._effectiveInvoice.get_address()
|
|
|
|
self._updating_max = True
|
|
|
|
def calc_max(address):
|
|
try:
|
|
outputs = [PartialTxOutput(scriptpubkey=address_to_script(address), value='!')]
|
|
make_tx = lambda fee_policy, *, confirmed_only=False: self._wallet.wallet.make_unsigned_transaction(
|
|
coins=self._wallet.wallet.get_spendable_coins(None),
|
|
outputs=outputs,
|
|
fee_policy=fee_policy,
|
|
is_sweep=False)
|
|
amount, message = self._wallet.determine_max(mktx=make_tx)
|
|
if amount is None:
|
|
self._amountOverride.isMax = False
|
|
else:
|
|
self._amountOverride.satsInt = amount
|
|
if message:
|
|
self.maxAmountMessage.emit(message)
|
|
finally:
|
|
self._updating_max = False
|
|
|
|
threading.Thread(target=calc_max, args=(invoice_address,), daemon=True).start()
|
|
|
|
|
|
class QEInvoiceParser(QEInvoice):
|
|
_logger = get_logger(__name__)
|
|
|
|
validationSuccess = pyqtSignal()
|
|
validationWarning = pyqtSignal([str, str], arguments=['code', 'message'])
|
|
validationError = pyqtSignal([str, str], arguments=['code', 'message'])
|
|
|
|
invoiceCreateError = pyqtSignal([str, str], arguments=['code', 'message'])
|
|
|
|
lnurlRetrieved = pyqtSignal()
|
|
lnurlError = pyqtSignal([str, str], arguments=['code', 'message'])
|
|
|
|
busyChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self._pi = None
|
|
self._lnurlData = None
|
|
self._busy = False
|
|
|
|
self.clear()
|
|
|
|
@pyqtSlot(object)
|
|
def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:
|
|
self.clear()
|
|
self.amountOverride = QEAmount()
|
|
if resolved_pi:
|
|
assert not resolved_pi.need_resolve()
|
|
self.validateRecipient(resolved_pi)
|
|
|
|
@pyqtProperty('QVariantMap', notify=lnurlRetrieved)
|
|
def lnurlData(self):
|
|
return self._lnurlData
|
|
|
|
@pyqtProperty(bool, notify=lnurlRetrieved)
|
|
def isLnurlPay(self):
|
|
return self._lnurlData is not None
|
|
|
|
@pyqtProperty(bool, notify=busyChanged)
|
|
def busy(self):
|
|
return self._busy
|
|
|
|
@pyqtSlot()
|
|
def clear(self):
|
|
self.setInvoiceType(QEInvoice.Type.Invalid)
|
|
self._lnurlData = None
|
|
self.canSave = False
|
|
self.canPay = False
|
|
self.userinfo = ''
|
|
self.invoiceChanged.emit()
|
|
|
|
def setValidOnchainInvoice(self, invoice: Invoice):
|
|
self._logger.debug('setValidOnchainInvoice')
|
|
if invoice.is_lightning():
|
|
raise Exception('unexpected LN invoice')
|
|
self.set_effective_invoice(invoice)
|
|
|
|
def setValidLightningInvoice(self, invoice: Invoice):
|
|
self._logger.debug('setValidLightningInvoice')
|
|
if not invoice.is_lightning():
|
|
raise Exception('unexpected Onchain invoice')
|
|
self._key = invoice.get_id()
|
|
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,
|
|
message=message,
|
|
pr=payment_request,
|
|
URI=uri
|
|
)
|
|
|
|
def _bip70_payment_request_resolved(self, pr: 'PaymentRequest'):
|
|
self._logger.debug('resolved payment request')
|
|
if Network.run_from_another_thread(pr.verify()):
|
|
invoice = Invoice.from_bip70_payreq(pr, height=0)
|
|
if self._wallet.wallet.get_invoice_status(invoice) == PR_PAID:
|
|
self.validationError.emit('unknown', _('Invoice already paid'))
|
|
elif pr.has_expired():
|
|
self.validationError.emit('unknown', _('Payment request has expired'))
|
|
else:
|
|
self.setValidOnchainInvoice(invoice)
|
|
self.validationSuccess.emit()
|
|
else:
|
|
self.validationError.emit('unknown', f'invoice error:\n{pr.error}')
|
|
|
|
def validateRecipient(self, pi: PaymentIdentifier):
|
|
if not pi:
|
|
self.setInvoiceType(QEInvoice.Type.Invalid)
|
|
return
|
|
|
|
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,
|
|
PaymentIdentifierType.OPENALIAS,
|
|
]:
|
|
self.validationError.emit('unknown', _('Unknown invoice'))
|
|
return
|
|
|
|
if self._pi.type == PaymentIdentifierType.SPK:
|
|
txo = TxOutput(scriptpubkey=self._pi.spk, value=0)
|
|
if not txo.address:
|
|
self.validationError.emit('unknown', _('Unknown invoice'))
|
|
return
|
|
|
|
self._update_from_payment_identifier()
|
|
|
|
def _update_from_payment_identifier(self):
|
|
assert not self._pi.need_resolve(), "Should have been resolved by QEPIResolver"
|
|
|
|
if self._pi.type in [
|
|
PaymentIdentifierType.LNURLP,
|
|
PaymentIdentifierType.LNADDR,
|
|
]:
|
|
self.on_lnurl_pay(self._pi.lnurl_data)
|
|
return
|
|
|
|
if self._pi.type == PaymentIdentifierType.BIP70:
|
|
self._bip70_payment_request_resolved(self._pi.bip70_data)
|
|
return
|
|
|
|
if self._pi.is_available():
|
|
if self._pi.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.OPENALIAS]:
|
|
outputs = [PartialTxOutput(scriptpubkey=self._pi.spk, value=0)]
|
|
invoice = self.create_onchain_invoice(outputs, None, None, None)
|
|
self._logger.debug(repr(invoice))
|
|
self.setValidOnchainInvoice(invoice)
|
|
self.validationSuccess.emit()
|
|
return
|
|
elif self._pi.type == PaymentIdentifierType.BOLT11:
|
|
lninvoice = self._pi.bolt11
|
|
if not self._wallet.wallet.has_lightning() and not lninvoice.get_address():
|
|
self.validationError.emit('no_lightning',
|
|
_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.'))
|
|
return
|
|
if self._wallet.wallet.lnworker and not self._wallet.wallet.lnworker.channels and not lninvoice.get_address():
|
|
self.validationWarning.emit('no_channels',
|
|
_('Detected valid Lightning invoice, but there are no open channels'))
|
|
self.setValidLightningInvoice(lninvoice)
|
|
self.validationSuccess.emit()
|
|
elif self._pi.type == PaymentIdentifierType.BIP21:
|
|
if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11:
|
|
lninvoice = self._pi.bolt11
|
|
self.setValidLightningInvoice(lninvoice)
|
|
self.validationSuccess.emit()
|
|
else:
|
|
self._validateRecipient_bip21_onchain(self._pi.bip21)
|
|
|
|
def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None:
|
|
if 'address' not in bip21:
|
|
self._logger.debug('Neither LN invoice nor address in bip21 uri')
|
|
self.validationError.emit('unknown', _('Unknown invoice'))
|
|
return
|
|
|
|
amount = bip21.get('amount', 0)
|
|
outputs = [PartialTxOutput.from_address_and_value(bip21['address'], amount)]
|
|
self._logger.debug(outputs)
|
|
message = bip21.get('message', '')
|
|
invoice = self.create_onchain_invoice(outputs, message, None, bip21)
|
|
self._logger.debug(repr(invoice))
|
|
self.setValidOnchainInvoice(invoice)
|
|
self.validationSuccess.emit()
|
|
|
|
def on_lnurl_pay(self, lnurldata: LNURL6Data):
|
|
assert isinstance(lnurldata, LNURL6Data)
|
|
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,
|
|
'comment_allowed': lnurldata.comment_allowed,
|
|
}
|
|
self.setValidLNURLPayRequest()
|
|
self.lnurlRetrieved.emit()
|
|
|
|
@pyqtSlot()
|
|
@pyqtSlot(str)
|
|
def lnurlGetInvoice(self, comment=None):
|
|
assert self._lnurlData
|
|
assert self._pi.need_finalize()
|
|
assert self.invoiceType == QEInvoice.Type.LNURLPayRequest
|
|
self._logger.debug(f'{repr(self._lnurlData)}')
|
|
|
|
amount = self.amountOverride.satsInt
|
|
|
|
if self._lnurlData['comment_allowed'] == 0:
|
|
comment = None
|
|
|
|
def on_finished(pi):
|
|
self._busy = False
|
|
self.busyChanged.emit()
|
|
|
|
if pi.is_error():
|
|
if pi.state == PaymentIdentifierState.INVALID_AMOUNT:
|
|
self.lnurlError.emit('amount', pi.get_error())
|
|
else:
|
|
self.lnurlError.emit('lnurl', pi.get_error())
|
|
else:
|
|
self.on_lnurl_invoice(self.amountOverride.satsInt, pi.bolt11)
|
|
|
|
self._busy = True
|
|
self.busyChanged.emit()
|
|
|
|
self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)
|
|
|
|
def on_lnurl_invoice(self, orig_amount, invoice):
|
|
self._logger.debug('on_lnurl_invoice')
|
|
self._logger.debug(f'{repr(invoice)}')
|
|
|
|
# assure no shenanigans with the bolt11 invoice we get back
|
|
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.amountOverride = QEAmount()
|
|
self.validateRecipient(
|
|
PaymentIdentifier(self._wallet.wallet, invoice.lightning_invoice)
|
|
)
|
|
|
|
@pyqtSlot(result=bool)
|
|
def saveInvoice(self) -> bool:
|
|
if not self._effectiveInvoice:
|
|
return False
|
|
if self.isSaved:
|
|
return False
|
|
|
|
try:
|
|
if not self._effectiveInvoice.amount_msat and not self.amountOverride.isEmpty:
|
|
if self.invoiceType == QEInvoice.Type.OnchainInvoice and self.amountOverride.isMax:
|
|
self._effectiveInvoice.set_amount_msat('!')
|
|
else:
|
|
self._effectiveInvoice.set_amount_msat(self.amountOverride.satsInt * 1000)
|
|
except InvoiceError as e:
|
|
self.invoiceCreateError.emit('validation', str(e))
|
|
return False
|
|
|
|
self.canSave = False
|
|
|
|
self._wallet.wallet.save_invoice(self._effectiveInvoice)
|
|
self._key = self._effectiveInvoice.get_id()
|
|
self._wallet.invoiceModel.addInvoice(self._key)
|
|
self.invoiceSaved.emit(self._key)
|
|
|
|
return True
|