From b57f867c2f1968cd630e5b3ee939eed9d933efbb Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 29 Sep 2025 18:06:12 +0200 Subject: [PATCH] cli: add command to export preimage ..also export preimage in check_hold_invoice return value if available. I intentionally did not return the preimage in the returned dict of wallet.export_requests as this seems risky to do considering some users of the cli might forward the response to a payer and the payserver exposes it too. Closes https://github.com/spesmilo/electrum/issues/10176 --- electrum/commands.py | 12 ++++++++++++ electrum/wallet.py | 7 +++++-- tests/test_commands.py | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 09bc31a14..185243419 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1514,10 +1514,22 @@ class Commands(Logger): plist = wallet.lnworker.get_payments(status='settled')[bfh(payment_hash)] _dir, amount_msat, _fee, _ts = wallet.lnworker.get_payment_value(info, plist) result["received_amount_sat"] = amount_msat // 1000 + result['preimage'] = wallet.lnworker.get_preimage_hex(payment_hash) if info is not None: result["invoice_amount_sat"] = (info.amount_msat or 0) // 1000 return result + @command('wl') + async def export_lightning_preimage(self, payment_hash: str, wallet: 'Abstract_Wallet' = None) -> Optional[str]: + """ + Returns the stored preimage of the given payment_hash if it is known. + + arg:str:payment_hash: Hash of the preimage + """ + preimage = wallet.lnworker.get_preimage_hex(payment_hash) + assert preimage is None or crypto.sha256(bytes.fromhex(preimage)).hex() == payment_hash + return preimage + @command('w') async def addtransaction(self, tx, wallet: Abstract_Wallet = None): """ diff --git a/electrum/wallet.py b/electrum/wallet.py index 7bf807e61..ab1ac2948 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2930,8 +2930,11 @@ class Abstract_Wallet(ABC, Logger, EventListener): if x.is_lightning(): d['rhash'] = x.rhash d['lightning_invoice'] = self.get_bolt11_invoice(x) - if self.lnworker and status == PR_UNPAID: - d['can_receive'] = self.lnworker.can_receive_invoice(x) + if self.lnworker: + if status == PR_UNPAID: + d['can_receive'] = self.lnworker.can_receive_invoice(x) + elif status == PR_PAID and (preimage := self.lnworker.get_preimage(x.payment_hash)): + d['preimage'] = preimage.hex() if address := x.get_address(): d['address'] = address d['URI'] = self.get_request_URI(x) diff --git a/tests/test_commands.py b/tests/test_commands.py index ccd052b38..a97b99f57 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -572,6 +572,7 @@ class TestCommandsTestnet(ElectrumTestCase): assert settled_status['status'] == 'settled' assert settled_status['received_amount_sat'] == 10000 assert settled_status['invoice_amount_sat'] == 10000 + assert settled_status['preimage'] == preimage.hex() with self.assertRaises(AssertionError): # cancelling a settled invoice should raise @@ -723,3 +724,19 @@ class TestCommandsTestnet(ElectrumTestCase): } } self.assertEqual(result, expected_result) + + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + async def test_export_lightning_preimage(self, *mock_args): + w = restore_wallet_from_text__for_unittest( + 'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic', + path='if_this_exists_mocking_failed_648151893', + config=self.config)['wallet'] + cmds = Commands(config=self.config) + + preimage = os.urandom(32) + payment_hash = sha256(preimage) + w.lnworker.save_preimage(payment_hash, preimage) + + assert await cmds.export_lightning_preimage(payment_hash=payment_hash.hex(), wallet=w) == preimage.hex() + assert await cmds.export_lightning_preimage(payment_hash=os.urandom(32).hex(), wallet=w) is None +