Browse Source

Merge pull request #8231 from spesmilo/fix_8213

Refresh bolt11 routing hints when channel liquidity changes:
master
ThomasV 3 years ago committed by GitHub
parent
commit
4ad9caddab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      electrum/gui/qml/qeinvoicelistmodel.py
  2. 2
      electrum/gui/qml/qerequestdetails.py
  3. 2
      electrum/gui/qt/receive_tab.py
  4. 2
      electrum/gui/qt/request_list.py
  5. 107
      electrum/invoices.py
  6. 62
      electrum/lnworker.py
  7. 8
      electrum/submarine_swaps.py
  8. 14
      electrum/tests/test_invoices.py
  9. 3
      electrum/tests/test_lnpeer.py
  10. 41
      electrum/wallet.py
  11. 22
      electrum/wallet_db.py

3
electrum/gui/qml/qeinvoicelistmodel.py

@ -124,8 +124,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
item['address'] = '' item['address'] = ''
item['date'] = format_time(item['timestamp']) item['date'] = format_time(item['timestamp'])
item['amount'] = QEAmount(from_invoice=invoice) item['amount'] = QEAmount(from_invoice=invoice)
item['onchain_fallback'] = invoice.is_lightning() and invoice._lnaddr.get_fallback_address() item['onchain_fallback'] = invoice.is_lightning() and invoice.get_address()
item['type'] = 'invoice'
return item return item

2
electrum/gui/qml/qerequestdetails.py

@ -118,7 +118,7 @@ class QERequestDetails(QObject, QtEventListener):
def bolt11(self): def bolt11(self):
can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 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: 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: else:
return '' return ''

2
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) help_texts = self.wallet.get_help_texts_for_receive_request(req)
addr = (req.get_address() or '') if not help_texts.address_is_error else '' 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 '' 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 address_help = help_texts.address_help
URI_help = help_texts.URI_help URI_help = help_texts.URI_help
ln_help = help_texts.ln_help ln_help = help_texts.ln_help

2
electrum/gui/qt/request_list.py

@ -197,7 +197,7 @@ class RequestList(MyTreeView):
if URI := self.wallet.get_request_URI(req): if URI := self.wallet.get_request_URI(req):
copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI'))
if req.is_lightning(): 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: #if 'view_url' in req:
# menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
menu.addAction(_("Delete"), lambda: self.delete_requests([key])) menu.addAction(_("Delete"), lambda: self.delete_requests([key]))

107
electrum/invoices.py

@ -7,6 +7,7 @@ import attr
from .json_db import StoredObject from .json_db import StoredObject
from .i18n import _ from .i18n import _
from .util import age, InvoiceError from .util import age, InvoiceError
from .lnutil import hex_to_bytes
from .lnaddr import lndecode, LnAddr from .lnaddr import lndecode, LnAddr
from . import constants from . import constants
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC 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 @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 # mandatory fields
amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None 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 = attr.ib(type=str, kw_only=True) # type: Optional[str]
#bip70_requestor = 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): def is_lightning(self):
return self.lightning_invoice is not None return self.lightning_invoice is not None
@ -117,15 +118,6 @@ class Invoice(StoredObject):
status_str = _('Expires') + ' ' + age(expiration, include_seconds=True) status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
return status_str 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]: def get_outputs(self) -> Sequence[PartialTxOutput]:
outputs = self.outputs or [] outputs = self.outputs or []
if not outputs: if not outputs:
@ -135,12 +127,6 @@ class Invoice(StoredObject):
outputs = [PartialTxOutput.from_address_and_value(address, int(amount))] outputs = [PartialTxOutput.from_address_and_value(address, int(amount))]
return outputs 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): def get_expiration_date(self):
# 0 means never # 0 means never
return self.exp + self.time if self.exp else 0 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) uri = create_bip21_uri(addr, amount, message, extra_query_params=extra)
return str(uri) 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 @amount_msat.validator
def _validate_amount(self, attribute, value): def _validate_amount(self, attribute, value):
if value is None: if value is None:
@ -212,16 +192,6 @@ class Invoice(StoredObject):
else: else:
raise InvoiceError(f"unexpected amount: {value!r}") 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 @classmethod
def from_bech32(cls, invoice: str) -> 'Invoice': def from_bech32(cls, invoice: str) -> 'Invoice':
"""Constructs Invoice object from BOLT-11 string. """Constructs Invoice object from BOLT-11 string.
@ -259,6 +229,48 @@ class Invoice(StoredObject):
lightning_invoice=None, 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]: def to_debug_json(self) -> Dict[str, Any]:
d = self.to_json() d = self.to_json()
d.update({ 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]] d['r_tags'] = [str((a.hex(),b.hex(),c,d,e)) for a,b,c,d,e in ln_routing_info[-1]]
return d return d
def get_id(self) -> str:
if self.is_lightning(): @attr.s
return self.rhash class Request(BaseInvoice):
else: # on-chain payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes) # type: Optional[bytes]
return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time)
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: def get_id_from_onchain_outputs(outputs: List[PartialTxOutput], *, timestamp: int) -> str:

62
electrum/lnworker.py

@ -633,6 +633,7 @@ class LNWallet(LNWorker):
self.lnrater: LNRater = None self.lnrater: LNRater = None
self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid 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.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 # 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) self.logs = defaultdict(list) # type: Dict[str, List[HtlcLog]] # key is RHASH # (not persisted)
# used in tests # used in tests
@ -980,6 +981,7 @@ class LNWallet(LNWorker):
def channel_state_changed(self, chan: Channel): def channel_state_changed(self, chan: Channel):
if type(chan) is Channel: if type(chan) is Channel:
self.save_channel(chan) self.save_channel(chan)
self.clear_invoices_cache()
util.trigger_callback('channel', self.wallet, chan) util.trigger_callback('channel', self.wallet, chan)
def save_channel(self, chan: Channel): def save_channel(self, chan: Channel):
@ -1781,27 +1783,31 @@ class LNWallet(LNWorker):
route[-1].node_features |= invoice_features route[-1].node_features |= invoice_features
return route return route
def create_invoice( def clear_invoices_cache(self):
self._bolt11_cache.clear()
def get_bolt11_invoice(
self, *, self, *,
payment_hash: bytes,
amount_msat: Optional[int], amount_msat: Optional[int],
message: str, message: str,
expiry: int, expiry: int,
fallback_address: Optional[str], fallback_address: Optional[str],
write_to_disk: bool = True,
channels: Optional[Sequence[Channel]] = None, channels: Optional[Sequence[Channel]] = None,
) -> Tuple[LnAddr, str]: ) -> 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 assert amount_msat is None or amount_msat > 0
timestamp = int(time.time()) timestamp = int(time.time())
routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels) routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels)
if not routing_hints: self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}")
self.logger.info(
"Warning. No routing hints added to invoice. "
"Other clients will likely not be able to send to us.")
invoice_features = self.features.for_invoice() invoice_features = self.features.for_invoice()
payment_preimage = os.urandom(32) payment_preimage = self.get_preimage(payment_hash)
payment_hash = sha256(payment_preimage)
info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID)
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
if expiry == 0: if expiry == 0:
expiry = LN_EXPIRY_NEVER expiry = LN_EXPIRY_NEVER
@ -1820,30 +1826,20 @@ class LNWallet(LNWorker):
date=timestamp, date=timestamp,
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
invoice = lnencode(lnaddr, self.node_keypair.privkey) 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_preimage(payment_hash, payment_preimage, write_to_disk=False)
self.save_payment_info(info, write_to_disk=False) self.save_payment_info(info, write_to_disk=False)
if write_to_disk: if write_to_disk:
self.wallet.save_db() self.wallet.save_db()
return lnaddr, invoice return payment_hash
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
def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True):
assert sha256(preimage) == payment_hash assert sha256(preimage) == payment_hash
@ -1853,7 +1849,7 @@ class LNWallet(LNWorker):
def get_preimage(self, payment_hash: bytes) -> Optional[bytes]: def get_preimage(self, payment_hash: bytes) -> Optional[bytes]:
r = self.preimages.get(payment_hash.hex()) 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]: def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]:
"""returns None if payment_hash is a payment we are forwarding""" """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) self.set_payment_status(bfh(key), status)
util.trigger_callback('invoice_status', self.wallet, 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}") 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: def set_request_status(self, payment_hash: bytes, status: int) -> None:
if self.get_payment_status(payment_hash) == status: if self.get_payment_status(payment_hash) == status:
@ -2138,7 +2136,7 @@ class LNWallet(LNWorker):
channels = list(self.channels.values()) channels = list(self.channels.values())
# we exclude channels that cannot *right now* receive (e.g. peer offline) # we exclude channels that cannot *right now* receive (e.g. peer offline)
channels = [chan for chan in channels 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. # Filter out nodes that have low receive capacity compared to invoice amt.
# Even with MPP, below a certain threshold, including these channels probably # 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. # 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') raise Exception('Rebalance requires two different channels')
if self.uses_trampoline() and chan1.node_id == chan2.node_id: if self.uses_trampoline() and chan1.node_id == chan2.node_id:
raise Exception('Rebalance requires channels from different trampolines') raise Exception('Rebalance requires channels from different trampolines')
lnaddr, invoice = self.create_invoice( lnaddr, invoice = self.add_reqest(
amount_msat=amount_msat, amount_msat=amount_msat,
message='rebalance', message='rebalance',
expiry=3600, expiry=3600,

8
electrum/submarine_swaps.py

@ -261,14 +261,16 @@ class SwapManager(Logger):
assert self.lnwatcher assert self.lnwatcher
privkey = os.urandom(32) privkey = os.urandom(32)
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) 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', message='swap',
expiry=3600 * 24, expiry=3600 * 24,
fallback_address=None, fallback_address=None,
channels=channels, channels=channels,
) )
payment_hash = lnaddr.paymenthash
preimage = self.lnworker.get_preimage(payment_hash) preimage = self.lnworker.get_preimage(payment_hash)
request_data = { request_data = {
"type": "submarine", "type": "submarine",

14
electrum/tests/test_invoices.py

@ -5,7 +5,7 @@ from . import ElectrumTestCase
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet 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.address_synchronizer import TX_HEIGHT_UNCONFIRMED
from electrum.transaction import Transaction, PartialTxOutput from electrum.transaction import Transaction, PartialTxOutput
from electrum.util import TxMinedInfo from electrum.util import TxMinedInfo
@ -20,11 +20,11 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config = SimpleConfig({'electrum_path': self.electrum_path})
self.wallet1_path = os.path.join(self.electrum_path, "somewallet1") self.wallet1_path = os.path.join(self.electrum_path, "somewallet1")
self.wallet2_path = os.path.join(self.electrum_path, "somewallet2") 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): def tearDown(self):
super().tearDown() 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: def create_wallet2(self) -> Standard_Wallet:
text = 'cross end slow expose giraffe fuel track awake turtle capital ranch pulp' 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.assertTrue(pr1.is_lightning())
self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1)) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1))
self.assertEqual(addr1, pr1.get_address()) 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()) self.assertFalse(pr1.has_expired())
# create payreq2 # create payreq2
@ -213,7 +211,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertEqual(addr1, pr1.get_address()) self.assertEqual(addr1, pr1.get_address())
self.assertFalse(pr1.has_expired()) 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()) self.assertTrue(pr1.has_expired())
# create payreq2 # create payreq2
@ -240,7 +238,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertFalse(pr1.has_expired()) self.assertFalse(pr1.has_expired())
self.assertEqual(pr1, wallet1.get_request_by_addr(addr1)) 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.assertTrue(pr1.has_expired())
self.assertEqual(None, wallet1.get_request_by_addr(addr1)) 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)) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr1))
# now make both invoices be past their expiration date. pr2 should be unaffected. # 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(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))
self.assertEqual(pr2, wallet1.get_request_by_addr(addr1)) self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))

3
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()}") self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}")
def clear_invoices_cache(self):
pass
def pay_scheduled_invoices(self): def pay_scheduled_invoices(self):
pass pass

41
electrum/wallet.py

@ -74,7 +74,7 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
from .plugin import run_hook from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) 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 .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED
from .contacts import Contacts from .contacts import Contacts
from .interface import NetworkException from .interface import NetworkException
@ -2443,7 +2443,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
} }
if is_lightning: if is_lightning:
d['rhash'] = x.rhash 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() d['amount_msat'] = x.get_amount_msat()
if self.lnworker and status == PR_UNPAID: if self.lnworker and status == PR_UNPAID:
d['can_receive'] = self.lnworker.can_receive_invoice(x) d['can_receive'] = self.lnworker.can_receive_invoice(x)
@ -2477,7 +2477,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
'invoice_id': key, 'invoice_id': key,
} }
if is_lightning: if is_lightning:
d['lightning_invoice'] = x.lightning_invoice d['lightning_invoice'] = self.get_bolt11_invoice(x)
d['amount_msat'] = x.get_amount_msat() d['amount_msat'] = x.get_amount_msat()
if self.lnworker and status == PR_UNPAID: if self.lnworker and status == PR_UNPAID:
d['can_pay'] = self.lnworker.can_pay_invoice(x) d['can_pay'] = self.lnworker.can_pay_invoice(x)
@ -2508,6 +2508,18 @@ class Abstract_Wallet(ABC, Logger, EventListener):
relevant_invoice_keys.add(invoice_key) relevant_invoice_keys.add(invoice_key)
self._update_onchain_invoice_paid_detection(relevant_invoice_keys) 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]): def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]):
# for receiving # for receiving
amount_sat = amount_sat or 0 amount_sat = amount_sat or 0
@ -2515,21 +2527,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
message = message or '' message = message or ''
address = address or None # converts "" to None address = address or None # converts "" to None
exp_delay = exp_delay or 0 exp_delay = exp_delay or 0
timestamp = int(Invoice._get_cur_time()) timestamp = int(Request._get_cur_time())
fallback_address = address if self.config.get('bolt11_fallback', True) else None payment_hash = self.lnworker.create_payment_info(amount_sat, write_to_disk=False) if self.has_lightning() 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
outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
height = self.adb.get_local_height() height = self.adb.get_local_height()
req = Invoice( req = Request(
outputs=outputs, outputs=outputs,
message=message, message=message,
time=timestamp, time=timestamp,
@ -2537,7 +2539,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
exp=exp_delay, exp=exp_delay,
height=height, height=height,
bip70=None, bip70=None,
lightning_invoice=lightning_invoice, payment_hash=payment_hash,
) )
key = self.add_payment_request(req) key = self.add_payment_request(req)
return key return key
@ -2858,7 +2860,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
ln_is_error = False ln_is_error = False
ln_swap_suggestion = None ln_swap_suggestion = None
ln_rebalance_suggestion = None ln_rebalance_suggestion = None
lnaddr = req.lightning_invoice or ''
URI = self.get_request_URI(req) or '' URI = self.get_request_URI(req) or ''
lightning_online = self.lnworker and self.lnworker.num_peers() > 0 lightning_online = self.lnworker and self.lnworker.num_peers() > 0
can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive() can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
@ -2878,7 +2879,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
URI_help = _('This request cannot be paid on-chain') URI_help = _('This request cannot be paid on-chain')
if is_amt_too_small_for_onchain: if is_amt_too_small_for_onchain:
URI_help = _('Amount too small to be received onchain') URI_help = _('Amount too small to be received onchain')
if not lnaddr: if not req.is_lightning():
ln_is_error = True ln_is_error = True
ln_help = _('This request does not have a Lightning invoice.') ln_help = _('This request does not have a Lightning invoice.')
@ -2886,7 +2887,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if self.adb.is_used(addr): if self.adb.is_used(addr):
address_help = URI_help = (_("This address has already been used. " address_help = URI_help = (_("This address has already been used. "
"For better privacy, do not reuse it for new payments.")) "For better privacy, do not reuse it for new payments."))
if lnaddr: if req.is_lightning():
if not lightning_online: if not lightning_online:
ln_is_error = True ln_is_error = True
ln_help = _('You must be online to receive Lightning payments.') ln_help = _('You must be online to receive Lightning payments.')

22
electrum/wallet_db.py

@ -33,7 +33,7 @@ import binascii
from . import util, bitcoin from . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
from .invoices import Invoice from .invoices import Invoice, Request
from .keystore import bip44_derivation from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
from .logging import Logger from .logging import Logger
@ -52,7 +52,7 @@ if TYPE_CHECKING:
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # 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 # old versions from overwriting new format
@ -199,6 +199,7 @@ class WalletDB(JsonDB):
self._convert_version_48() self._convert_version_48()
self._convert_version_49() self._convert_version_49()
self._convert_version_50() self._convert_version_50()
self._convert_version_51()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -980,6 +981,21 @@ class WalletDB(JsonDB):
self._convert_invoices_keys(requests) self._convert_invoices_keys(requests)
self.data['seed_version'] = 50 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): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return
@ -1460,7 +1476,7 @@ class WalletDB(JsonDB):
if key == 'invoices': if key == 'invoices':
v = dict((k, Invoice(**x)) for k, x in v.items()) v = dict((k, Invoice(**x)) for k, x in v.items())
if key == 'payment_requests': 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': elif key == 'adds':
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
elif key == 'fee_updates': elif key == 'fee_updates':

Loading…
Cancel
Save