1
0

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.
This commit is contained in:
f321x
2025-06-29 19:57:17 +02:00
parent 3f755e19a4
commit 389a0a6e91
3 changed files with 27 additions and 26 deletions

View File

@@ -1365,7 +1365,7 @@ class Commands(Logger):
@command('wnl') @command('wnl')
async def add_hold_invoice( async def add_hold_invoice(
self, self,
preimage: str, payment_hash: str,
amount: Optional[Decimal] = None, amount: Optional[Decimal] = None,
memo: str = "", memo: str = "",
expiry: int = 3600, expiry: int = 3600,
@@ -1373,20 +1373,19 @@ class Commands(Logger):
wallet: Abstract_Wallet = None wallet: Abstract_Wallet = None
) -> dict: ) -> 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. 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:decimal:amount:Optional requested amount (in btc)
arg:str:memo:Optional description of the invoice arg:str:memo:Optional description of the invoice
arg:int:expiry:Optional expiry in seconds (default: 3600s) 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) 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" assert len(payment_hash) == 64, f"Invalid payment hash length: {len(payment_hash)} != 64"
payment_hash: str = crypto.sha256(bfh(preimage)).hex()
assert payment_hash not in wallet.lnworker._preimages, "Preimage already in use!"
assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!" 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 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" 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 amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None
inbound_capacity = wallet.lnworker.num_sats_can_receive() inbound_capacity = wallet.lnworker.num_sats_can_receive()
@@ -1406,7 +1405,6 @@ class Commands(Logger):
satoshis(amount) if amount else None, satoshis(amount) if amount else None,
) )
wallet.lnworker.dont_settle_htlcs[payment_hash] = None wallet.lnworker.dont_settle_htlcs[payment_hash] = None
wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage))
wallet.set_label(payment_hash, memo) wallet.set_label(payment_hash, memo)
result = { result = {
"invoice": invoice "invoice": invoice
@@ -1414,21 +1412,23 @@ class Commands(Logger):
return result return result
@command('wnl') @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. 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 len(preimage) == 64, f"Invalid payment_hash length: {len(preimage)} != 64"
assert payment_hash in wallet.lnworker._preimages, f"Couldn't find preimage for {payment_hash}" payment_hash: str = crypto.sha256(bfh(preimage)).hex()
assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" assert payment_hash not in wallet.lnworker._preimages, f"Invoice {payment_hash=} already settled"
assert payment_hash in wallet.lnworker.payment_info, \ 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)), \ assert wallet.lnworker.is_accepted_mpp(bfh(payment_hash)), \
f"MPP incomplete, cannot settle hold invoice {payment_hash} yet" f"MPP incomplete, cannot settle hold invoice {payment_hash} yet"
del wallet.lnworker.dont_settle_htlcs[payment_hash] del wallet.lnworker.dont_settle_htlcs[payment_hash]
wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage))
util.trigger_callback('wallet_updated', wallet) util.trigger_callback('wallet_updated', wallet)
result = { result = {
"settled": payment_hash "settled": payment_hash
@@ -1444,9 +1444,8 @@ class Commands(Logger):
""" """
assert payment_hash in wallet.lnworker.payment_info, \ 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 {payment_hash}"
assert payment_hash in wallet.lnworker._preimages, "Nothing to cancel, no known preimage." assert payment_hash not in wallet.lnworker._preimages, "Cannot cancel anymore, preimage already given."
assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" assert payment_hash in wallet.lnworker.dont_settle_htlcs, f"{payment_hash=} not a hold invoice?"
del wallet.lnworker._preimages[payment_hash]
# set to PR_UNPAID so it can get deleted # set to PR_UNPAID so it can get deleted
wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID) wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID)
wallet.lnworker.delete_payment_info(payment_hash) wallet.lnworker.delete_payment_info(payment_hash)

View File

@@ -2546,6 +2546,9 @@ class Peer(Logger, EventListener):
callback = lambda: hold_invoice_callback(payment_hash) callback = lambda: hold_invoice_callback(payment_hash)
return None, (payment_key, callback) return None, (payment_key, callback)
if payment_hash.hex() in self.lnworker.dont_settle_htlcs:
return None, None
if not preimage: if not preimage:
if not already_forwarded: if not already_forwarded:
log_fail_reason(f"missing preimage and no hold invoice callback {payment_hash.hex()}") log_fail_reason(f"missing preimage and no hold invoice callback {payment_hash.hex()}")
@@ -2553,9 +2556,6 @@ class Peer(Logger, EventListener):
else: else:
return None, None return None, None
if payment_hash.hex() in self.lnworker.dont_settle_htlcs:
return None, None
chan.opening_fee = None chan.opening_fee = None
self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}")
return preimage, None return preimage, None

View File

@@ -445,7 +445,7 @@ class TestCommandsTestnet(ElectrumTestCase):
payment_hash: str = sha256(bytes.fromhex(preimage)).hex() payment_hash: str = sha256(bytes.fromhex(preimage)).hex()
with (mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000)): with (mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000)):
result = await cmds.add_hold_invoice( result = await cmds.add_hold_invoice(
preimage=preimage, payment_hash=payment_hash,
amount=Decimal(0.0001), amount=Decimal(0.0001),
memo="test", memo="test",
expiry=3500, expiry=3500,
@@ -453,10 +453,12 @@ class TestCommandsTestnet(ElectrumTestCase):
) )
invoice = lndecode(invoice=result['invoice']) invoice = lndecode(invoice=result['invoice'])
assert invoice.paymenthash.hex() == payment_hash 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.payment_info
assert payment_hash in wallet.lnworker.dont_settle_htlcs assert payment_hash in wallet.lnworker.dont_settle_htlcs
assert invoice.get_amount_sat() == 10000 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( cancel_result = await cmds.cancel_hold_invoice(
payment_hash=payment_hash, 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.payment_info
assert payment_hash not in wallet.lnworker.dont_settle_htlcs 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 assert cancel_result['cancelled'] == payment_hash
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
# settling a cancelled invoice should raise # settling a cancelled invoice should raise
await cmds.settle_hold_invoice( await cmds.settle_hold_invoice(
payment_hash=payment_hash, preimage=preimage,
wallet=wallet, wallet=wallet,
) )
# cancelling an unknown invoice should raise # cancelling an unknown invoice should raise
@@ -484,7 +486,7 @@ class TestCommandsTestnet(ElectrumTestCase):
payment_hash: str = sha256(preimage).hex() payment_hash: str = sha256(preimage).hex()
with mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000): with mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000):
await cmds.add_hold_invoice( await cmds.add_hold_invoice(
preimage=preimage.hex(), payment_hash=payment_hash,
amount=Decimal(0.0001), amount=Decimal(0.0001),
wallet=wallet, wallet=wallet,
) )
@@ -496,7 +498,7 @@ class TestCommandsTestnet(ElectrumTestCase):
assert status['amount_sat'] == 10000 assert status['amount_sat'] == 10000
settle_result = await cmds.settle_hold_invoice( settle_result = await cmds.settle_hold_invoice(
payment_hash=payment_hash, preimage=preimage.hex(),
wallet=wallet, wallet=wallet,
) )
assert settle_result['settled'] == payment_hash assert settle_result['settled'] == payment_hash