From 6425f9bf7517c6fd9a515c015c7248f0885b46ce Mon Sep 17 00:00:00 2001 From: f321x Date: Fri, 25 Jul 2025 11:44:26 +0200 Subject: [PATCH] cli: return closest htlc expiry from check_hold_invoice Adds a `closest_htlc_expiry_height` value to the `check_hold_invoice` cli command response. This allows to see the next absolute expiry height of the pending htlcs of a payment. Note, htlcs will get failed before the actual expiry height (if block_height + 144 > htlc.cltv_abs). --- electrum/commands.py | 30 ++++++++++++++++++++---------- tests/test_commands.py | 16 ++++++++++++++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index f85d15eee..703687b57 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1465,7 +1465,13 @@ class Commands(Logger): async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: """ Checks the status of a lightning hold invoice 'payment_hash'. - Possible states: unpaid, paid, settled, unknown (cancelled or not found) + Returns: { + "status": unpaid | paid | settled | unknown (cancelled or not found), + "received_amount_sat": currently received amount (pending htlcs or final after settling), + "invoice_amount_sat": Invoice amount, Optional (only if invoice is found), + "closest_htlc_expiry_height": Closest absolute expiry height of all received htlcs + (Note: HTLCs will get failed automatically if block_height + 144 > htlc_expiry_height) + } arg:str:payment_hash:Payment hash in hex of the hold invoice """ @@ -1473,24 +1479,28 @@ class Commands(Logger): info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash)) is_accepted_mpp: bool = wallet.lnworker.is_accepted_mpp(bfh(payment_hash)) amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000 - status = "unknown" + result = { + "status": "unknown", + "received_amount_sat": amount_sat, + } if info is None: pass elif not is_accepted_mpp and not wallet.lnworker.get_preimage_hex(payment_hash): # is_accepted_mpp is False for settled payments - status = "unpaid" + result["status"] = "unpaid" elif is_accepted_mpp and payment_hash in wallet.lnworker.dont_settle_htlcs: - status = "paid" + result["status"] = "paid" + payment_key: str = wallet.lnworker._get_payment_key(bfh(payment_hash)).hex() + htlc_status = wallet.lnworker.received_mpp_htlcs[payment_key] + result["closest_htlc_expiry_height"] = min( + htlc.cltv_abs for _, htlc in htlc_status.htlc_set + ) elif wallet.lnworker.get_preimage_hex(payment_hash) is not None \ and payment_hash not in wallet.lnworker.dont_settle_htlcs: - status = "settled" + result["status"] = "settled" plist = wallet.lnworker.get_payments(status='settled')[bfh(payment_hash)] _dir, amount_msat, _fee, _ts = wallet.lnworker.get_payment_value(info, plist) - amount_sat = amount_msat // 1000 - result = { - "status": status, - "received_amount_sat": amount_sat, - } + result["received_amount_sat"] = amount_msat // 1000 if info is not None: result["invoice_amount_sat"] = (info.amount_msat or 0) // 1000 return result diff --git a/tests/test_commands.py b/tests/test_commands.py index 6ef5d1dad..a4448fb7f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -6,6 +6,7 @@ from os import urandom from electrum.commands import Commands, eval_bool from electrum import storage, wallet +from electrum.lnworker import RecvMPPResolution from electrum.wallet import restore_wallet_from_text, Abstract_Wallet from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED from electrum.simple_config import SimpleConfig @@ -491,11 +492,22 @@ class TestCommandsTestnet(ElectrumTestCase): wallet=wallet, ) - with mock.patch.object(wallet.lnworker, 'is_accepted_mpp', return_value=True), \ - mock.patch.object(wallet.lnworker, 'get_payment_mpp_amount_msat', return_value=10_000 * 1000): + mock_htlc1 = mock.Mock() + mock_htlc1.cltv_abs = 800_000 + mock_htlc1.amount_msat = 4_500_000 + mock_htlc2 = mock.Mock() + mock_htlc2.cltv_abs = 800_144 + mock_htlc2.amount_msat = 5_500_000 + mock_htlc_status = mock.Mock() + mock_htlc_status.htlc_set = [(None, mock_htlc1), (None, mock_htlc2)] + mock_htlc_status.resolution = RecvMPPResolution.ACCEPTED + + payment_key = wallet.lnworker._get_payment_key(bytes.fromhex(payment_hash)).hex() + with mock.patch.dict(wallet.lnworker.received_mpp_htlcs, {payment_key: mock_htlc_status}): status: dict = await cmds.check_hold_invoice(payment_hash=payment_hash, wallet=wallet) assert status['status'] == 'paid' assert status['received_amount_sat'] == 10000 + assert status['closest_htlc_expiry_height'] == 800_000 settle_result = await cmds.settle_hold_invoice( preimage=preimage.hex(),