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')
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)

View File

@@ -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

View File

@@ -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