From 389a0a6e914d33e4dff66809ddfe93d99740a9ae Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 29 Jun 2025 19:57:17 +0200 Subject: [PATCH] cli: use payment hash for add_hold_invoice Allowing to create hold invoices just by providing a payment hash instead of the preimage right from the beginning allows for additional use cases where the recipient doesn't have access to the preimage when creating the invoice. --- electrum/commands.py | 33 ++++++++++++++++----------------- electrum/lnpeer.py | 6 +++--- tests/test_commands.py | 14 ++++++++------ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 7bdd54125..64327ddf8 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1365,7 +1365,7 @@ class Commands(Logger): @command('wnl') async def add_hold_invoice( self, - preimage: str, + payment_hash: str, amount: Optional[Decimal] = None, memo: str = "", expiry: int = 3600, @@ -1373,20 +1373,19 @@ class Commands(Logger): wallet: Abstract_Wallet = None ) -> dict: """ - Create a lightning hold invoice for the given preimage. Hold invoices have to get settled manually later. + Create a lightning hold invoice for the given payment hash. Hold invoices have to get settled manually later. HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs. - arg:str:preimage:Hex encoded preimage to be used for the invoice + arg:str:payment_hash:Hex encoded payment hash to be used for the invoice arg:decimal:amount:Optional requested amount (in btc) arg:str:memo:Optional description of the invoice arg:int:expiry:Optional expiry in seconds (default: 3600s) arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks) """ - assert len(preimage) == 64, f"Invalid preimage length: {len(preimage)} != 64" - payment_hash: str = crypto.sha256(bfh(preimage)).hex() - assert payment_hash not in wallet.lnworker._preimages, "Preimage already in use!" + assert len(payment_hash) == 64, f"Invalid payment hash length: {len(payment_hash)} != 64" assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!" assert payment_hash not in wallet.lnworker.dont_settle_htlcs, "Payment hash already used!" + assert wallet.lnworker.get_preimage(bfh(payment_hash)) is None, "Already got a preimage for this payment hash!" assert MIN_FINAL_CLTV_DELTA_FOR_INVOICE < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value" amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None inbound_capacity = wallet.lnworker.num_sats_can_receive() @@ -1406,7 +1405,6 @@ class Commands(Logger): satoshis(amount) if amount else None, ) wallet.lnworker.dont_settle_htlcs[payment_hash] = None - wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage)) wallet.set_label(payment_hash, memo) result = { "invoice": invoice @@ -1414,21 +1412,23 @@ class Commands(Logger): return result @command('wnl') - async def settle_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: + async def settle_hold_invoice(self, preimage: str, wallet: Abstract_Wallet = None) -> dict: """ - Settles lightning hold invoice 'payment_hash' using the stored preimage. + Settles lightning hold invoice with the given preimage. Doesn't block until actual settlement of the HTLCs. - arg:str:payment_hash:Hex encoded payment hash of the invoice to be settled + arg:str:preimage:Hex encoded preimage of the invoice to be settled """ - assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64" - assert payment_hash in wallet.lnworker._preimages, f"Couldn't find preimage for {payment_hash}" - assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" + assert len(preimage) == 64, f"Invalid payment_hash length: {len(preimage)} != 64" + payment_hash: str = crypto.sha256(bfh(preimage)).hex() + assert payment_hash not in wallet.lnworker._preimages, f"Invoice {payment_hash=} already settled" assert payment_hash in wallet.lnworker.payment_info, \ - f"Couldn't find lightning invoice for payment hash {payment_hash}" + f"Couldn't find lightning invoice for {payment_hash=}" + assert payment_hash in wallet.lnworker.dont_settle_htlcs, f"Invoice {payment_hash=} not a hold invoice?" assert wallet.lnworker.is_accepted_mpp(bfh(payment_hash)), \ f"MPP incomplete, cannot settle hold invoice {payment_hash} yet" del wallet.lnworker.dont_settle_htlcs[payment_hash] + wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage)) util.trigger_callback('wallet_updated', wallet) result = { "settled": payment_hash @@ -1444,9 +1444,8 @@ class Commands(Logger): """ assert payment_hash in wallet.lnworker.payment_info, \ f"Couldn't find lightning invoice for payment hash {payment_hash}" - assert payment_hash in wallet.lnworker._preimages, "Nothing to cancel, no known preimage." - assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" - del wallet.lnworker._preimages[payment_hash] + assert payment_hash not in wallet.lnworker._preimages, "Cannot cancel anymore, preimage already given." + assert payment_hash in wallet.lnworker.dont_settle_htlcs, f"{payment_hash=} not a hold invoice?" # set to PR_UNPAID so it can get deleted wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID) wallet.lnworker.delete_payment_info(payment_hash) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 5e99e1be6..cdea45f65 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2546,6 +2546,9 @@ class Peer(Logger, EventListener): callback = lambda: hold_invoice_callback(payment_hash) return None, (payment_key, callback) + if payment_hash.hex() in self.lnworker.dont_settle_htlcs: + return None, None + if not preimage: if not already_forwarded: log_fail_reason(f"missing preimage and no hold invoice callback {payment_hash.hex()}") @@ -2553,9 +2556,6 @@ class Peer(Logger, EventListener): else: return None, None - if payment_hash.hex() in self.lnworker.dont_settle_htlcs: - return None, None - chan.opening_fee = None self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") return preimage, None diff --git a/tests/test_commands.py b/tests/test_commands.py index 38d12c7ce..c142bb57d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -445,7 +445,7 @@ class TestCommandsTestnet(ElectrumTestCase): payment_hash: str = sha256(bytes.fromhex(preimage)).hex() with (mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000)): result = await cmds.add_hold_invoice( - preimage=preimage, + payment_hash=payment_hash, amount=Decimal(0.0001), memo="test", expiry=3500, @@ -453,10 +453,12 @@ class TestCommandsTestnet(ElectrumTestCase): ) invoice = lndecode(invoice=result['invoice']) assert invoice.paymenthash.hex() == payment_hash - assert payment_hash in wallet.lnworker._preimages assert payment_hash in wallet.lnworker.payment_info assert payment_hash in wallet.lnworker.dont_settle_htlcs assert invoice.get_amount_sat() == 10000 + assert invoice.get_description() == "test" + assert wallet.get_label_for_rhash(rhash=invoice.paymenthash.hex()) == "test" + assert invoice.get_expiry() == 3500 cancel_result = await cmds.cancel_hold_invoice( payment_hash=payment_hash, @@ -464,13 +466,13 @@ class TestCommandsTestnet(ElectrumTestCase): ) assert payment_hash not in wallet.lnworker.payment_info assert payment_hash not in wallet.lnworker.dont_settle_htlcs - assert payment_hash not in wallet.lnworker._preimages + assert wallet.get_label_for_rhash(rhash=invoice.paymenthash.hex()) == "" assert cancel_result['cancelled'] == payment_hash with self.assertRaises(AssertionError): # settling a cancelled invoice should raise await cmds.settle_hold_invoice( - payment_hash=payment_hash, + preimage=preimage, wallet=wallet, ) # cancelling an unknown invoice should raise @@ -484,7 +486,7 @@ class TestCommandsTestnet(ElectrumTestCase): payment_hash: str = sha256(preimage).hex() with mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000): await cmds.add_hold_invoice( - preimage=preimage.hex(), + payment_hash=payment_hash, amount=Decimal(0.0001), wallet=wallet, ) @@ -496,7 +498,7 @@ class TestCommandsTestnet(ElectrumTestCase): assert status['amount_sat'] == 10000 settle_result = await cmds.settle_hold_invoice( - payment_hash=payment_hash, + preimage=preimage.hex(), wallet=wallet, ) assert settle_result['settled'] == payment_hash