|
25 | 25 | import io
|
26 | 26 | import sys
|
27 | 27 | import datetime
|
| 28 | +import time |
28 | 29 | import argparse
|
29 | 30 | import json
|
30 | 31 | import ast
|
|
60 | 61 | from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy, Imported_Wallet
|
61 | 62 | from .address_synchronizer import TX_HEIGHT_LOCAL
|
62 | 63 | from .mnemonic import Mnemonic
|
63 |
| -from .lnutil import SENT, RECEIVED |
64 |
| -from .lnutil import LnFeatures |
65 | 64 | from .lntransport import extract_nodeid
|
66 |
| -from .lnutil import channel_id_from_funding_tx |
| 65 | +from .lnutil import channel_id_from_funding_tx, LnFeatures, SENT, RECEIVED, MIN_FINAL_CLTV_DELTA_FOR_INVOICE |
67 | 66 | from .plugin import run_hook, DeviceMgr, Plugins
|
68 | 67 | from .version import ELECTRUM_VERSION
|
69 | 68 | from .simple_config import SimpleConfig
|
|
77 | 76 | if TYPE_CHECKING:
|
78 | 77 | from .network import Network
|
79 | 78 | from .daemon import Daemon
|
| 79 | + from electrum.lnworker import PaymentInfo |
80 | 80 |
|
81 | 81 |
|
82 | 82 | known_commands = {} # type: Dict[str, Command]
|
@@ -995,7 +995,6 @@ async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_
|
995 | 995 | def get_year_timestamps(self, year:int):
|
996 | 996 | kwargs = {}
|
997 | 997 | if year:
|
998 |
| - import time |
999 | 998 | start_date = datetime.datetime(year, 1, 1)
|
1000 | 999 | end_date = datetime.datetime(year+1, 1, 1)
|
1001 | 1000 | kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
|
@@ -1349,6 +1348,134 @@ async def add_request(self, amount, memo='', expiry=3600, lightning=False, force
|
1349 | 1348 | req = wallet.get_request(key)
|
1350 | 1349 | return wallet.export_request(req)
|
1351 | 1350 |
|
| 1351 | + @command('wnl') |
| 1352 | + async def add_hold_invoice( |
| 1353 | + self, |
| 1354 | + preimage: str, |
| 1355 | + amount: Optional[Decimal] = None, |
| 1356 | + memo: str = "", |
| 1357 | + expiry: int = 3600, |
| 1358 | + min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_FOR_INVOICE * 2, |
| 1359 | + wallet: Abstract_Wallet = None |
| 1360 | + ) -> dict: |
| 1361 | + """ |
| 1362 | + Create a lightning hold invoice for the given preimage. Hold invoices have to get settled manually later. |
| 1363 | + HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs. |
| 1364 | +
|
| 1365 | + arg:str:preimage:Hex encoded preimage to be used for the invoice |
| 1366 | + arg:decimal:amount:Optional requested amount (in btc) |
| 1367 | + arg:str:memo:Optional description of the invoice |
| 1368 | + arg:int:expiry:Optional expiry in seconds (default: 3600s) |
| 1369 | + arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks) |
| 1370 | + """ |
| 1371 | + assert len(preimage) == 64, f"Invalid preimage length: {len(preimage)} != 64" |
| 1372 | + payment_hash: str = crypto.sha256(bfh(preimage)).hex() |
| 1373 | + assert preimage not in wallet.lnworker.preimages, "Preimage has been used as payment hash already!" |
| 1374 | + assert payment_hash not in wallet.lnworker.preimages, "Preimage already in use!" |
| 1375 | + assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!" |
| 1376 | + assert payment_hash not in wallet.lnworker.dont_settle_htlcs, "Payment hash already used!" |
| 1377 | + assert MIN_FINAL_CLTV_DELTA_FOR_INVOICE < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value" |
| 1378 | + amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None |
| 1379 | + inbound_capacity = wallet.lnworker.num_sats_can_receive() |
| 1380 | + assert inbound_capacity > satoshis(amount or 0), \ |
| 1381 | + f"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment" |
| 1382 | + |
| 1383 | + lnaddr, invoice = wallet.lnworker.get_bolt11_invoice( |
| 1384 | + payment_hash=bfh(payment_hash), |
| 1385 | + amount_msat=satoshis(amount) * 1000 if amount else None, |
| 1386 | + message=memo, |
| 1387 | + expiry=expiry, |
| 1388 | + min_final_cltv_expiry_delta=min_final_cltv_expiry_delta, |
| 1389 | + fallback_address=None |
| 1390 | + ) |
| 1391 | + wallet.lnworker.add_payment_info_for_hold_invoice( |
| 1392 | + bfh(payment_hash), |
| 1393 | + satoshis(amount) if amount else None, |
| 1394 | + ) |
| 1395 | + wallet.lnworker.dont_settle_htlcs[payment_hash] = None |
| 1396 | + wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage)) |
| 1397 | + wallet.set_label(payment_hash, memo) |
| 1398 | + result = { |
| 1399 | + "invoice": invoice |
| 1400 | + } |
| 1401 | + return result |
| 1402 | + |
| 1403 | + @command('wnl') |
| 1404 | + async def settle_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: |
| 1405 | + """ |
| 1406 | + Settles lightning hold invoice 'payment_hash' using the stored preimage. |
| 1407 | + Doesn't block until actual settlement of the HTLCs. |
| 1408 | +
|
| 1409 | + arg:str:payment_hash:Hex encoded payment hash of the invoice to be settled |
| 1410 | + """ |
| 1411 | + assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64" |
| 1412 | + assert payment_hash in wallet.lnworker.preimages, f"Couldn't find preimage for {payment_hash}" |
| 1413 | + assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" |
| 1414 | + assert payment_hash in wallet.lnworker.payment_info, \ |
| 1415 | + f"Couldn't find lightning invoice for payment hash {payment_hash}" |
| 1416 | + assert wallet.lnworker.is_accepted_mpp(bfh(payment_hash)), \ |
| 1417 | + f"MPP incomplete, cannot settle hold invoice {payment_hash} yet" |
| 1418 | + del wallet.lnworker.dont_settle_htlcs[payment_hash] |
| 1419 | + util.trigger_callback('wallet_updated', wallet) |
| 1420 | + result = { |
| 1421 | + "settled": payment_hash |
| 1422 | + } |
| 1423 | + return result |
| 1424 | + |
| 1425 | + @command('wnl') |
| 1426 | + async def cancel_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: |
| 1427 | + """ |
| 1428 | + Cancels lightning hold invoice 'payment_hash'. |
| 1429 | +
|
| 1430 | + arg:str:payment_hash:Payment hash in hex of the hold invoice |
| 1431 | + """ |
| 1432 | + assert payment_hash in wallet.lnworker.payment_info, \ |
| 1433 | + f"Couldn't find lightning invoice for payment hash {payment_hash}" |
| 1434 | + assert payment_hash in wallet.lnworker.preimages, "Nothing to cancel, no known preimage." |
| 1435 | + assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!" |
| 1436 | + del wallet.lnworker.preimages[payment_hash] |
| 1437 | + # set to PR_UNPAID so it can get deleted |
| 1438 | + wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID) |
| 1439 | + wallet.lnworker.delete_payment_info(payment_hash) |
| 1440 | + wallet.set_label(payment_hash, None) |
| 1441 | + while wallet.lnworker.is_accepted_mpp(bfh(payment_hash)): |
| 1442 | + # wait until the htlcs got failed so the payment won't get settled accidentally in a race |
| 1443 | + await asyncio.sleep(0.1) |
| 1444 | + del wallet.lnworker.dont_settle_htlcs[payment_hash] |
| 1445 | + result = { |
| 1446 | + "cancelled": payment_hash |
| 1447 | + } |
| 1448 | + return result |
| 1449 | + |
| 1450 | + @command('wnl') |
| 1451 | + async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: |
| 1452 | + """ |
| 1453 | + Checks the status of a lightning hold invoice 'payment_hash'. |
| 1454 | + Possible states: unpaid, paid, settled, unknown (cancelled or not found) |
| 1455 | +
|
| 1456 | + arg:str:payment_hash:Payment hash in hex of the hold invoice |
| 1457 | + """ |
| 1458 | + assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64" |
| 1459 | + info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash)) |
| 1460 | + is_accepted_mpp: bool = wallet.lnworker.is_accepted_mpp(bfh(payment_hash)) |
| 1461 | + amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000 |
| 1462 | + status = "unknown" |
| 1463 | + if info is None: |
| 1464 | + pass |
| 1465 | + elif not is_accepted_mpp: |
| 1466 | + status = "unpaid" |
| 1467 | + elif is_accepted_mpp and payment_hash in wallet.lnworker.dont_settle_htlcs: |
| 1468 | + status = "paid" |
| 1469 | + elif (payment_hash in wallet.lnworker.preimages |
| 1470 | + and payment_hash not in wallet.lnworker.dont_settle_htlcs |
| 1471 | + and is_accepted_mpp): |
| 1472 | + status = "settled" |
| 1473 | + result = { |
| 1474 | + "status": status, |
| 1475 | + "amount_sat": amount_sat |
| 1476 | + } |
| 1477 | + return result |
| 1478 | + |
1352 | 1479 | @command('w')
|
1353 | 1480 | async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
|
1354 | 1481 | """
|
|
0 commit comments