Skip to content

Commit 949ba9b

Browse files
committed
add hold invoice cli functionality
1 parent cca29ef commit 949ba9b

File tree

2 files changed

+125
-5
lines changed

2 files changed

+125
-5
lines changed

electrum/commands.py

+104-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from .logging import Logger
4747
from .onion_message import create_blinded_path, send_onion_message_to
4848
from .util import (bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes,
49-
parse_max_spend, to_decimal, UserFacingException, InvalidPassword)
49+
parse_max_spend, to_decimal, UserFacingException, InvalidPassword, make_aiohttp_session)
5050

5151
from . import bitcoin
5252
from .bitcoin import is_address, hash_160, COIN
@@ -55,15 +55,13 @@
5555
from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
5656
tx_from_any, PartialTxInput, TxOutpoint)
5757
from . import transaction
58-
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
58+
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, pr_tooltips
5959
from .synchronizer import Notifier
6060
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy, Imported_Wallet
6161
from .address_synchronizer import TX_HEIGHT_LOCAL
6262
from .mnemonic import Mnemonic
63-
from .lnutil import SENT, RECEIVED
64-
from .lnutil import LnFeatures
6563
from .lntransport import extract_nodeid
66-
from .lnutil import channel_id_from_funding_tx
64+
from .lnutil import channel_id_from_funding_tx, LnFeatures, SENT, RECEIVED, MIN_FINAL_CLTV_DELTA_FOR_INVOICE
6765
from .plugin import run_hook, DeviceMgr, Plugins
6866
from .version import ELECTRUM_VERSION
6967
from .simple_config import SimpleConfig
@@ -1346,6 +1344,107 @@ async def add_request(self, amount, memo='', expiry=3600, lightning=False, force
13461344
req = wallet.get_request(key)
13471345
return wallet.export_request(req)
13481346

1347+
@command('wnl')
1348+
async def add_hold_invoice(self,
1349+
payment_hash,
1350+
amount,
1351+
memo = "",
1352+
expiry = 3600,
1353+
callback_url = None,
1354+
wallet: Abstract_Wallet = None
1355+
) -> str:
1356+
"""
1357+
Create a lightning hold invoice for the given payment hash. Hold invoices have to get settled manually later.
1358+
The invoice has a final cltv delta of 147 blocks.
1359+
HTLCs will get failed if local_height + 144 > htlc.cltv_abs.
1360+
1361+
arg:str:payment_hash:Hex encoded payment hash to be used for the invoice
1362+
arg:decimal:amount:Requested amount (in btc)
1363+
arg:str:memo:Optional description of the invoice
1364+
arg:int:expiry:Optional expiry in seconds (default: 3600s)
1365+
arg:str:callback_url:Optional callback URL for the invoice, if provided a POST request will be triggered once all htlcs arrived (doesn't use proxy)
1366+
"""
1367+
assert len(payment_hash) == 64, f"Invalid payment hash length: {len(payment_hash)} != 64"
1368+
assert payment_hash not in wallet.lnworker.payment_info, "Payment already in use"
1369+
assert amount and satoshis(amount) > 0, "Provide invoice amount > 0"
1370+
if callback_url:
1371+
assert callback_url.startswith("http"), "Callback URL must be http(s)://"
1372+
amount_msat = satoshis(amount) * 1000
1373+
lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(
1374+
payment_hash=bfh(payment_hash),
1375+
amount_msat=amount_msat,
1376+
message=memo,
1377+
expiry=expiry,
1378+
min_final_cltv_expiry_delta=MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
1379+
fallback_address=None
1380+
)
1381+
wallet.lnworker.add_payment_info_for_hold_invoice(bfh(payment_hash), satoshis(amount) or 0)
1382+
wallet.lnworker.register_cli_hold_invoice(payment_hash, callback_url)
1383+
result = json.dumps({
1384+
"invoice": invoice
1385+
}, indent=4, sort_keys=True)
1386+
return result
1387+
1388+
@command('wnl')
1389+
async def settle_hold_invoice(self, preimage, wallet: Abstract_Wallet = None) -> str:
1390+
"""
1391+
Settles lightning hold invoice 'payment_hash' with 'preimage'.
1392+
Doesn't wait for actual settlement of the HTLCs.
1393+
1394+
arg:str:preimage:Hex encoded preimage of the payment hash
1395+
"""
1396+
assert len(preimage) == 64, f"Invalid preimage length: {len(preimage)} != 64"
1397+
payment_hash: str = crypto.sha256(bfh(preimage)).hex()
1398+
assert payment_hash in wallet.lnworker.payment_info, \
1399+
f"Couldn't find lightning invoice for payment hash {payment_hash}"
1400+
wallet.lnworker.preimages[payment_hash] = preimage
1401+
result: str = json.dumps({
1402+
"settled": payment_hash
1403+
}, indent=4)
1404+
return result
1405+
1406+
@command('wnl')
1407+
async def cancel_hold_invoice(self, payment_hash, wallet: Abstract_Wallet = None) -> str:
1408+
"""
1409+
Cancels lightning hold invoice 'payment_hash'.
1410+
1411+
arg:str:payment_hash:Payment hash in hex of the hold invoice
1412+
"""
1413+
assert payment_hash in wallet.lnworker.payment_info, \
1414+
f"Couldn't find lightning invoice for payment hash {payment_hash}"
1415+
assert payment_hash not in wallet.lnworker.preimages, \
1416+
f"Hold invoice already settled with preimage: {crypto.sha256(bfh(payment_hash)).hex()}"
1417+
wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID)
1418+
wallet.lnworker.delete_payment_info(payment_hash)
1419+
wallet.lnworker.unregister_hold_invoice(bfh(payment_hash))
1420+
result: str = json.dumps({
1421+
"cancelled": payment_hash
1422+
}, indent=4)
1423+
return result
1424+
1425+
@command('wnl')
1426+
async def check_hold_invoice(self, payment_hash, wallet: Abstract_Wallet = None) -> str:
1427+
"""
1428+
Checks the status of a lightning hold invoice 'payment_hash'.
1429+
Possible states: unpaid, paid, settled, unknown (cancelled or not found)
1430+
1431+
arg:str:payment_hash:Payment hash in hex of the hold invoice
1432+
"""
1433+
info = wallet.lnworker.get_payment_info(bfh(payment_hash))
1434+
status = "unknown"
1435+
if info is None:
1436+
pass
1437+
elif info.status == PR_UNPAID:
1438+
status = "unpaid"
1439+
elif info.status == PR_PAID and payment_hash not in wallet.lnworker.preimages:
1440+
status = "paid"
1441+
elif info.status == PR_PAID and payment_hash in wallet.lnworker.preimages:
1442+
status = "settled"
1443+
result: str = json.dumps({
1444+
"status": status,
1445+
}, indent=4)
1446+
return result
1447+
13491448
@command('w')
13501449
async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
13511450
"""

electrum/lnworker.py

+21
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from concurrent import futures
2121
import urllib.parse
2222
import itertools
23+
import json
2324

2425
import aiohttp
2526
import dns.resolver
@@ -885,6 +886,9 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv):
885886

886887
# payment_hash -> callback:
887888
self.hold_invoice_callbacks = {} # type: Dict[bytes, Callable[[bytes], Awaitable[None]]]
889+
self.cli_hold_invoice_callbacks = self.db.get_dict('cli_hold_invoice_callbacks') # type: Dict[str, Optional[str]] # payment_hash -> url callback
890+
for payment_hash, callback_url in self.cli_hold_invoice_callbacks.items():
891+
self.register_cli_hold_invoice(payment_hash, callback_url)
888892
self.payment_bundles = [] # lists of hashes. todo:persist
889893

890894
self.nostr_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NOSTR_KEY)
@@ -2310,6 +2314,23 @@ def register_hold_invoice(self, payment_hash: bytes, cb: Callable[[bytes], Await
23102314
def unregister_hold_invoice(self, payment_hash: bytes):
23112315
self.hold_invoice_callbacks.pop(payment_hash)
23122316

2317+
def register_cli_hold_invoice(self, payment_hash: str, callback_url: Optional[str] = None):
2318+
async def cli_hold_invoice_callback(payment_hash_bytes: bytes):
2319+
"""Hold invoice callback for hold invoices registered via CLI."""
2320+
self.logger.info(f"Hold invoice {payment_hash_bytes.hex()} ready for settlement")
2321+
self.set_payment_status(payment_hash_bytes, PR_PAID)
2322+
if not callback_url:
2323+
return
2324+
data = json.dumps({"payment_hash": payment_hash_bytes.hex()})
2325+
try:
2326+
async with make_aiohttp_session(proxy=self.network.proxy) as s:
2327+
await s.post(callback_url, data=data, raise_for_status=False)
2328+
except Exception:
2329+
self.logger.info(f"hold invoice callback request to {callback_url} raised")
2330+
self.register_hold_invoice(bfh(payment_hash), cli_hold_invoice_callback)
2331+
if payment_hash not in self.cli_hold_invoice_callbacks:
2332+
self.cli_hold_invoice_callbacks[payment_hash] = callback_url
2333+
23132334
def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> None:
23142335
key = info.payment_hash.hex()
23152336
assert info.status in SAVED_PR_STATUS

0 commit comments

Comments
 (0)