923d48f9db
Allows storing two different payment info of the same payment hash by including the direction into the db key. We create and store PaymentInfo for sending attempts and for requests (receiving), if we try to pay ourself (e.g. through a channel rebalance) the checks in `save_payment_info` would prevent this and throw an exception. By storing the PaymentInfos of outgoing and incoming payments separately in the db this collision is avoided and it makes it easier to reason about which PaymentInfo belongs where.
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
from enum import IntEnum
|
|
from typing import Optional
|
|
from urllib.parse import urlparse
|
|
|
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum
|
|
|
|
from electrum.logging import get_logger
|
|
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, RECEIVED
|
|
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
|
|
from .util import QtEventListener, event_listener, status_update_timer_interval
|
|
|
|
|
|
class QERequestDetails(QObject, QtEventListener):
|
|
|
|
@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__)
|
|
|
|
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)
|
|
|
|
self._wallet = None # type: Optional[QEWallet]
|
|
self._key = None
|
|
self._req = None
|
|
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)
|
|
|
|
self.register_callbacks()
|
|
self.destroyed.connect(lambda: self.on_destroy())
|
|
|
|
def on_destroy(self):
|
|
self.unregister_callbacks()
|
|
if self._timer:
|
|
self._timer.stop()
|
|
self._timer = None
|
|
|
|
@event_listener
|
|
def on_event_request_status(self, wallet, key, status):
|
|
if wallet == self._wallet.wallet and key == self._key:
|
|
self._logger.debug('request status %d for key %s' % (status, key))
|
|
self.statusChanged.emit()
|
|
|
|
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()
|
|
self.initRequest()
|
|
|
|
keyChanged = pyqtSignal()
|
|
@pyqtProperty(str, notify=keyChanged)
|
|
def key(self):
|
|
return self._key
|
|
|
|
@key.setter
|
|
def key(self, key):
|
|
if self._key != key:
|
|
self._key = key
|
|
self._logger.debug(f'key={key}')
|
|
self.keyChanged.emit()
|
|
self.initRequest()
|
|
|
|
@pyqtProperty(int, notify=statusChanged)
|
|
def status(self):
|
|
return self._wallet.wallet.get_invoice_status(self._req)
|
|
|
|
@pyqtProperty(str, notify=statusChanged)
|
|
def status_str(self):
|
|
return self._req.get_status_str(self.status) if self._req else ''
|
|
|
|
@pyqtProperty(bool, notify=detailsChanged)
|
|
def isLightning(self):
|
|
return self._req.is_lightning() if self._req else False
|
|
|
|
@pyqtProperty(str, notify=detailsChanged)
|
|
def address(self):
|
|
addr = self._req.get_address() if self._req else ''
|
|
return addr if addr else ''
|
|
|
|
@pyqtProperty(str, notify=detailsChanged)
|
|
def message(self):
|
|
return self._req.get_message() if self._req else ''
|
|
|
|
@pyqtProperty(QEAmount, notify=detailsChanged)
|
|
def amount(self):
|
|
return self._amount
|
|
|
|
@pyqtProperty(int, notify=detailsChanged)
|
|
def timestamp(self):
|
|
return self._req.get_time()
|
|
|
|
@pyqtProperty(int, notify=detailsChanged)
|
|
def expiration(self):
|
|
return self._req.get_expiration_date()
|
|
|
|
@pyqtProperty(str, notify=statusChanged)
|
|
def paidTxid(self):
|
|
"""only used when Request status is PR_PAID"""
|
|
if not self._req:
|
|
return ''
|
|
is_paid, conf_needed, txids = self._wallet.wallet._is_onchain_invoice_paid(self._req)
|
|
if len(txids) > 0:
|
|
return txids[0]
|
|
return ''
|
|
|
|
@pyqtProperty(str, notify=detailsChanged)
|
|
def bolt11(self):
|
|
wallet = self._wallet.wallet
|
|
if not wallet.lnworker:
|
|
return ''
|
|
amount_sat = self._req.get_amount_sat() or 0 if self._req else 0
|
|
can_receive = wallet.lnworker.num_sats_can_receive()
|
|
will_req_zeroconf = wallet.lnworker.receive_requires_jit_channel(amount_msat=amount_sat*1000)
|
|
if self._req and ((can_receive > 0 and amount_sat <= can_receive)
|
|
or (will_req_zeroconf and amount_sat >= MIN_FUNDING_SAT)):
|
|
bolt11 = wallet.get_bolt11_invoice(self._req)
|
|
else:
|
|
return ''
|
|
# encode lightning invoices as uppercase so QR encoding can use
|
|
# alphanumeric mode; resulting in smaller QR codes
|
|
bolt11 = bolt11.upper()
|
|
return bolt11
|
|
|
|
@pyqtProperty(str, notify=detailsChanged)
|
|
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
|
|
|
|
self._req = self._wallet.wallet.get_request(self._key)
|
|
|
|
if self._req is None:
|
|
self._logger.error(f'payment request key {self._key} unknown in wallet {self._wallet.name}')
|
|
return
|
|
|
|
self._amount = QEAmount(from_invoice=self._req)
|
|
|
|
self.detailsChanged.emit()
|
|
self.statusChanged.emit()
|
|
self.set_status_timer()
|
|
|
|
def set_status_timer(self):
|
|
if self.status == PR_UNPAID:
|
|
if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER:
|
|
self._logger.debug(f'set_status_timer, expiration={self.expiration}')
|
|
interval = status_update_timer_interval(self.expiration)
|
|
if interval > 0:
|
|
self._logger.debug(f'setting status update timer to {interval}')
|
|
self._timer.setInterval(interval) # msec
|
|
self._timer.start()
|
|
|
|
@pyqtSlot()
|
|
def updateStatusString(self):
|
|
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)
|
|
info = self._wallet.wallet.lnworker.get_payment_info(req.payment_hash, direction=RECEIVED)
|
|
_lnaddr, b11_invoice = self._wallet.wallet.lnworker.get_bolt11_invoice(
|
|
payment_info=info,
|
|
message=req.get_message(),
|
|
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()
|