From 719b468eee8b3e13680f6e7b90194d618181fe0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 27 Feb 2023 10:31:21 +0100 Subject: [PATCH] Refresh bolt11 routing hints when channel liquidity changes: - wallet_db update: separate Invoices and Requests. - do not store bolt11 invoice in Request --- electrum/gui/qml/qeinvoicelistmodel.py | 3 +- electrum/gui/qml/qerequestdetails.py | 2 +- electrum/gui/qt/receive_tab.py | 2 +- electrum/gui/qt/request_list.py | 2 +- electrum/invoices.py | 107 +++++++++++++++---------- electrum/lnworker.py | 62 +++++++------- electrum/submarine_swaps.py | 8 +- electrum/tests/test_invoices.py | 14 ++-- electrum/tests/test_lnpeer.py | 3 + electrum/wallet.py | 41 +++++----- electrum/wallet_db.py | 22 ++++- 11 files changed, 154 insertions(+), 112 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index f5f53a8db..1bd39dd4d 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -124,8 +124,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel): item['address'] = '' item['date'] = format_time(item['timestamp']) item['amount'] = QEAmount(from_invoice=invoice) - item['onchain_fallback'] = invoice.is_lightning() and invoice._lnaddr.get_fallback_address() - item['type'] = 'invoice' + item['onchain_fallback'] = invoice.is_lightning() and invoice.get_address() return item diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 7a1c07d29..1956cdfa4 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -118,7 +118,7 @@ class QERequestDetails(QObject, QtEventListener): def bolt11(self): can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 if self._req and can_receive > 0 and self._req.amount_msat/1000 <= can_receive: - return self._req.lightning_invoice + return self._wallet.wallet.get_bolt11_invoice(self._req) else: return '' diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index dfe3da7fb..fa012c0d4 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -224,7 +224,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): help_texts = self.wallet.get_help_texts_for_receive_request(req) addr = (req.get_address() or '') if not help_texts.address_is_error else '' URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else '' - lnaddr = (req.lightning_invoice or '') if not help_texts.ln_is_error else '' + lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else '' address_help = help_texts.address_help URI_help = help_texts.URI_help ln_help = help_texts.ln_help diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 6db3ed4cd..49cead064 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -197,7 +197,7 @@ class RequestList(MyTreeView): if URI := self.wallet.get_request_URI(req): copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) if req.is_lightning(): - copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) + copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request')) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("Delete"), lambda: self.delete_requests([key])) diff --git a/electrum/invoices.py b/electrum/invoices.py index 0933cba6c..2791258bc 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -7,6 +7,7 @@ import attr from .json_db import StoredObject from .i18n import _ from .util import age, InvoiceError +from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC @@ -83,7 +84,11 @@ LN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60 # 100 years @attr.s -class Invoice(StoredObject): +class BaseInvoice(StoredObject): + """ + Base class for Invoice and Request + In the code, we use 'invoice' for outgoing payments, and 'request' for incoming payments. + """ # mandatory fields amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None @@ -101,10 +106,6 @@ class Invoice(StoredObject): bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str] #bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str] - # lightning only - lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] - - __lnaddr = None def is_lightning(self): return self.lightning_invoice is not None @@ -117,15 +118,6 @@ class Invoice(StoredObject): status_str = _('Expires') + ' ' + age(expiration, include_seconds=True) return status_str - def get_address(self) -> Optional[str]: - """returns the first address, to be displayed in GUI""" - address = None - if self.outputs: - address = self.outputs[0].address if len(self.outputs) > 0 else None - if not address and self.is_lightning(): - address = self._lnaddr.get_fallback_address() or None - return address - def get_outputs(self) -> Sequence[PartialTxOutput]: outputs = self.outputs or [] if not outputs: @@ -135,12 +127,6 @@ class Invoice(StoredObject): outputs = [PartialTxOutput.from_address_and_value(address, int(amount))] return outputs - def can_be_paid_onchain(self) -> bool: - if self.is_lightning(): - return bool(self._lnaddr.get_fallback_address()) - else: - return True - def get_expiration_date(self): # 0 means never return self.exp + self.time if self.exp else 0 @@ -193,12 +179,6 @@ class Invoice(StoredObject): uri = create_bip21_uri(addr, amount, message, extra_query_params=extra) return str(uri) - @lightning_invoice.validator - def _validate_invoice_str(self, attribute, value): - if value is not None: - lnaddr = lndecode(value) # this checks the str can be decoded - self.__lnaddr = lnaddr # save it, just to avoid having to recompute later - @amount_msat.validator def _validate_amount(self, attribute, value): if value is None: @@ -212,16 +192,6 @@ class Invoice(StoredObject): else: raise InvoiceError(f"unexpected amount: {value!r}") - @property - def _lnaddr(self) -> LnAddr: - if self.__lnaddr is None: - self.__lnaddr = lndecode(self.lightning_invoice) - return self.__lnaddr - - @property - def rhash(self) -> str: - return self._lnaddr.paymenthash.hex() - @classmethod def from_bech32(cls, invoice: str) -> 'Invoice': """Constructs Invoice object from BOLT-11 string. @@ -259,6 +229,48 @@ class Invoice(StoredObject): lightning_invoice=None, ) + def get_id(self) -> str: + if self.is_lightning(): + return self.rhash + else: # on-chain + return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + +@attr.s +class Invoice(BaseInvoice): + lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] + __lnaddr = None + + def get_address(self) -> Optional[str]: + """returns the first address, to be displayed in GUI""" + address = None + if self.outputs: + address = self.outputs[0].address if len(self.outputs) > 0 else None + if not address and self.is_lightning(): + address = self._lnaddr.get_fallback_address() or None + return address + + @property + def _lnaddr(self) -> LnAddr: + if self.__lnaddr is None: + self.__lnaddr = lndecode(self.lightning_invoice) + return self.__lnaddr + + @property + def rhash(self) -> str: + return self._lnaddr.paymenthash.hex() + + @lightning_invoice.validator + def _validate_invoice_str(self, attribute, value): + if value is not None: + lnaddr = lndecode(value) # this checks the str can be decoded + self.__lnaddr = lnaddr # save it, just to avoid having to recompute later + + def can_be_paid_onchain(self) -> bool: + if self.is_lightning(): + return bool(self._lnaddr.get_fallback_address()) + else: + return True + def to_debug_json(self) -> Dict[str, Any]: d = self.to_json() d.update({ @@ -274,11 +286,24 @@ class Invoice(StoredObject): d['r_tags'] = [str((a.hex(),b.hex(),c,d,e)) for a,b,c,d,e in ln_routing_info[-1]] return d - def get_id(self) -> str: - if self.is_lightning(): - return self.rhash - else: # on-chain - return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + +@attr.s +class Request(BaseInvoice): + payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes) # type: Optional[bytes] + + def is_lightning(self): + return self.payment_hash is not None + + def get_address(self) -> Optional[str]: + """returns the first address, to be displayed in GUI""" + address = None + if self.outputs: + address = self.outputs[0].address if len(self.outputs) > 0 else None + return address + + @property + def rhash(self) -> str: + return self.payment_hash.hex() def get_id_from_onchain_outputs(outputs: List[PartialTxOutput], *, timestamp: int) -> str: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 6dc9b32c6..5f6a579c2 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -633,6 +633,7 @@ class LNWallet(LNWorker): self.lnrater: LNRater = None self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage + self._bolt11_cache = {} # note: this sweep_address is only used as fallback; as it might result in address-reuse self.logs = defaultdict(list) # type: Dict[str, List[HtlcLog]] # key is RHASH # (not persisted) # used in tests @@ -980,6 +981,7 @@ class LNWallet(LNWorker): def channel_state_changed(self, chan: Channel): if type(chan) is Channel: self.save_channel(chan) + self.clear_invoices_cache() util.trigger_callback('channel', self.wallet, chan) def save_channel(self, chan: Channel): @@ -1781,27 +1783,31 @@ class LNWallet(LNWorker): route[-1].node_features |= invoice_features return route - def create_invoice( + def clear_invoices_cache(self): + self._bolt11_cache.clear() + + def get_bolt11_invoice( self, *, + payment_hash: bytes, amount_msat: Optional[int], message: str, expiry: int, fallback_address: Optional[str], - write_to_disk: bool = True, channels: Optional[Sequence[Channel]] = None, ) -> Tuple[LnAddr, str]: + pair = self._bolt11_cache.get(payment_hash) + if pair: + lnaddr, invoice = pair + assert lnaddr.get_amount_msat() == amount_msat + return pair + assert amount_msat is None or amount_msat > 0 timestamp = int(time.time()) routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels) - if not routing_hints: - self.logger.info( - "Warning. No routing hints added to invoice. " - "Other clients will likely not be able to send to us.") + self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}") invoice_features = self.features.for_invoice() - payment_preimage = os.urandom(32) - payment_hash = sha256(payment_preimage) - info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) + payment_preimage = self.get_preimage(payment_hash) amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None if expiry == 0: expiry = LN_EXPIRY_NEVER @@ -1820,30 +1826,20 @@ class LNWallet(LNWorker): date=timestamp, payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) invoice = lnencode(lnaddr, self.node_keypair.privkey) + pair = lnaddr, invoice + self._bolt11_cache[payment_hash] = pair + return pair + + def create_payment_info(self, amount_sat: Optional[int], write_to_disk=True) -> bytes: + amount_msat = amount_sat * 1000 if amount_sat else None + payment_preimage = os.urandom(32) + payment_hash = sha256(payment_preimage) + info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) self.save_preimage(payment_hash, payment_preimage, write_to_disk=False) self.save_payment_info(info, write_to_disk=False) if write_to_disk: self.wallet.save_db() - return lnaddr, invoice - - def add_request( - self, - *, - amount_sat: Optional[int], - message: str, - expiry: int, - fallback_address: Optional[str], - ) -> str: - # passed expiry is relative, it is absolute in the lightning invoice - amount_msat = amount_sat * 1000 if amount_sat else None - lnaddr, invoice = self.create_invoice( - amount_msat=amount_msat, - message=message, - expiry=expiry, - fallback_address=fallback_address, - write_to_disk=False, - ) - return invoice + return payment_hash def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): assert sha256(preimage) == payment_hash @@ -1853,7 +1849,7 @@ class LNWallet(LNWorker): def get_preimage(self, payment_hash: bytes) -> Optional[bytes]: r = self.preimages.get(payment_hash.hex()) - return bfh(r) if r else None + return bytes.fromhex(r) if r else None def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]: """returns None if payment_hash is a payment we are forwarding""" @@ -1922,6 +1918,8 @@ class LNWallet(LNWorker): self.set_payment_status(bfh(key), status) util.trigger_callback('invoice_status', self.wallet, key, status) self.logger.info(f"invoice status triggered (2) for key {key} and status {status}") + # liquidity changed + self.clear_invoices_cache() def set_request_status(self, payment_hash: bytes, status: int) -> None: if self.get_payment_status(payment_hash) == status: @@ -2138,7 +2136,7 @@ class LNWallet(LNWorker): channels = list(self.channels.values()) # we exclude channels that cannot *right now* receive (e.g. peer offline) channels = [chan for chan in channels - if (chan.is_active() and not chan.is_frozen_for_receiving())] + if (chan.is_open() and not chan.is_frozen_for_receiving())] # Filter out nodes that have low receive capacity compared to invoice amt. # Even with MPP, below a certain threshold, including these channels probably # hurts more than help, as they lead to many failed attempts for the sender. @@ -2283,7 +2281,7 @@ class LNWallet(LNWorker): raise Exception('Rebalance requires two different channels') if self.uses_trampoline() and chan1.node_id == chan2.node_id: raise Exception('Rebalance requires channels from different trampolines') - lnaddr, invoice = self.create_invoice( + lnaddr, invoice = self.add_reqest( amount_msat=amount_msat, message='rebalance', expiry=3600, diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 55defd265..9532bc8a6 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -261,14 +261,16 @@ class SwapManager(Logger): assert self.lnwatcher privkey = os.urandom(32) pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) - lnaddr, invoice = self.lnworker.create_invoice( - amount_msat=lightning_amount_sat * 1000, + amount_msat = lightning_amount_sat * 1000 + payment_hash = self.lnworker.create_payment_info(lightning_amount_sat) + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=amount_msat, message='swap', expiry=3600 * 24, fallback_address=None, channels=channels, ) - payment_hash = lnaddr.paymenthash preimage = self.lnworker.get_preimage(payment_hash) request_data = { "type": "submarine", diff --git a/electrum/tests/test_invoices.py b/electrum/tests/test_invoices.py index 3bdf9d847..06dafd494 100644 --- a/electrum/tests/test_invoices.py +++ b/electrum/tests/test_invoices.py @@ -5,7 +5,7 @@ from . import ElectrumTestCase from electrum.simple_config import SimpleConfig from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet -from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, Invoice +from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED from electrum.transaction import Transaction, PartialTxOutput from electrum.util import TxMinedInfo @@ -20,11 +20,11 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.wallet1_path = os.path.join(self.electrum_path, "somewallet1") self.wallet2_path = os.path.join(self.electrum_path, "somewallet2") - self._orig_get_cur_time = Invoice._get_cur_time + self._orig_get_cur_time = BaseInvoice._get_cur_time def tearDown(self): super().tearDown() - Invoice._get_cur_time = staticmethod(self._orig_get_cur_time) + BaseInvoice._get_cur_time = staticmethod(self._orig_get_cur_time) def create_wallet2(self) -> Standard_Wallet: text = 'cross end slow expose giraffe fuel track awake turtle capital ranch pulp' @@ -156,8 +156,6 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertTrue(pr1.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1)) self.assertEqual(addr1, pr1.get_address()) - self.assertEqual(addr1, pr1._lnaddr.get_fallback_address()) - self.assertTrue(pr1.can_be_paid_onchain()) self.assertFalse(pr1.has_expired()) # create payreq2 @@ -213,7 +211,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertEqual(addr1, pr1.get_address()) self.assertFalse(pr1.has_expired()) - Invoice._get_cur_time = lambda *args: time.time() + 100_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000 self.assertTrue(pr1.has_expired()) # create payreq2 @@ -240,7 +238,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertFalse(pr1.has_expired()) self.assertEqual(pr1, wallet1.get_request_by_addr(addr1)) - Invoice._get_cur_time = lambda *args: time.time() + 100_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000 self.assertTrue(pr1.has_expired()) self.assertEqual(None, wallet1.get_request_by_addr(addr1)) @@ -265,6 +263,6 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr1)) # now make both invoices be past their expiration date. pr2 should be unaffected. - Invoice._get_cur_time = lambda *args: time.time() + 200_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 200_000 self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2)) self.assertEqual(pr2, wallet1.get_request_by_addr(addr1)) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 187a5139c..095165469 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -175,6 +175,9 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}") + def clear_invoices_cache(self): + pass + def pay_scheduled_invoices(self): pass diff --git a/electrum/wallet.py b/electrum/wallet.py index c2812d2b8..630f40beb 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -74,7 +74,7 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) -from .invoices import Invoice +from .invoices import Invoice, Request from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED from .contacts import Contacts from .interface import NetworkException @@ -2444,7 +2444,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): } if is_lightning: d['rhash'] = x.rhash - d['lightning_invoice'] = x.lightning_invoice + d['lightning_invoice'] = self.get_bolt11_invoice(x) d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_receive'] = self.lnworker.can_receive_invoice(x) @@ -2478,7 +2478,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): 'invoice_id': key, } if is_lightning: - d['lightning_invoice'] = x.lightning_invoice + d['lightning_invoice'] = self.get_bolt11_invoice(x) d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_pay'] = self.lnworker.can_pay_invoice(x) @@ -2509,6 +2509,18 @@ class Abstract_Wallet(ABC, Logger, EventListener): relevant_invoice_keys.add(invoice_key) self._update_onchain_invoice_paid_detection(relevant_invoice_keys) + def get_bolt11_invoice(self, req: Request) -> str: + if not self.lnworker: + return '' + amount_msat = req.amount_msat if req.amount_msat > 0 else None + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=req.payment_hash, + amount_msat=amount_msat, + message=req.message, + expiry=req.exp, + fallback_address=req.get_address() if self.config.get('bolt11_fallback', True) else None) + return invoice + def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]): # for receiving amount_sat = amount_sat or 0 @@ -2516,21 +2528,11 @@ class Abstract_Wallet(ABC, Logger, EventListener): message = message or '' address = address or None # converts "" to None exp_delay = exp_delay or 0 - timestamp = int(Invoice._get_cur_time()) - fallback_address = address if self.config.get('bolt11_fallback', True) else None - lightning = self.has_lightning() - if lightning: - lightning_invoice = self.lnworker.add_request( - amount_sat=amount_sat, - message=message, - expiry=exp_delay, - fallback_address=fallback_address, - ) - else: - lightning_invoice = None + timestamp = int(Request._get_cur_time()) + payment_hash = self.lnworker.create_payment_info(amount_sat, write_to_disk=False) if self.has_lightning() else None outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] height = self.adb.get_local_height() - req = Invoice( + req = Request( outputs=outputs, message=message, time=timestamp, @@ -2538,7 +2540,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): exp=exp_delay, height=height, bip70=None, - lightning_invoice=lightning_invoice, + payment_hash=payment_hash, ) key = self.add_payment_request(req) return key @@ -2859,7 +2861,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): ln_is_error = False ln_swap_suggestion = None ln_rebalance_suggestion = None - lnaddr = req.lightning_invoice or '' URI = self.get_request_URI(req) or '' lightning_online = self.lnworker and self.lnworker.num_peers() > 0 can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive() @@ -2879,7 +2880,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): URI_help = _('This request cannot be paid on-chain') if is_amt_too_small_for_onchain: URI_help = _('Amount too small to be received onchain') - if not lnaddr: + if not req.is_lightning(): ln_is_error = True ln_help = _('This request does not have a Lightning invoice.') @@ -2887,7 +2888,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): if self.adb.is_used(addr): address_help = URI_help = (_("This address has already been used. " "For better privacy, do not reuse it for new payments.")) - if lnaddr: + if req.is_lightning(): if not lightning_online: ln_is_error = True ln_help = _('You must be online to receive Lightning payments.') diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index a7a87fa19..157b5a477 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -33,7 +33,7 @@ import binascii from . import util, bitcoin from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh -from .invoices import Invoice +from .invoices import Invoice, Request from .keystore import bip44_derivation from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .logging import Logger @@ -52,7 +52,7 @@ if TYPE_CHECKING: OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 50 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 51 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -199,6 +199,7 @@ class WalletDB(JsonDB): self._convert_version_48() self._convert_version_49() self._convert_version_50() + self._convert_version_51() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -980,6 +981,21 @@ class WalletDB(JsonDB): self._convert_invoices_keys(requests) self.data['seed_version'] = 50 + def _convert_version_51(self): + from .lnaddr import lndecode + if not self._is_upgrade_method_needed(50, 50): + return + requests = self.data.get('payment_requests', {}) + for key, item in list(requests.items()): + lightning_invoice = item.pop('lightning_invoice') + if lightning_invoice is None: + payment_hash = None + else: + lnaddr = lndecode(lightning_invoice) + payment_hash = lnaddr.paymenthash.hex() + item['payment_hash'] = payment_hash + self.data['seed_version'] = 51 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return @@ -1460,7 +1476,7 @@ class WalletDB(JsonDB): if key == 'invoices': v = dict((k, Invoice(**x)) for k, x in v.items()) if key == 'payment_requests': - v = dict((k, Invoice(**x)) for k, x in v.items()) + v = dict((k, Request(**x)) for k, x in v.items()) elif key == 'adds': v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) elif key == 'fee_updates':