Skip to content

Commit 2241015

Browse files
committed
cli: add max_cltv parameter to lnpay
Adds a `max_cltv` parameter to the `lnpay` cli command which allows to specify the maximum total locktime of the payment. This is enabled by constructing a custom `PaymentFeeBudget` object in the lnpay command and passing it as argument to `LNWallet.pay_invoice()`. Allowing to specify a `max_cltv` value can be useful for certain usecases, e.g. see #10056. Closes #10056
1 parent 8eb3c43 commit 2241015

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

electrum/commands.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
)
6868
from .address_synchronizer import TX_HEIGHT_LOCAL
6969
from .mnemonic import Mnemonic
70-
from .lnutil import channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE
70+
from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
71+
PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
7172
from .plugin import run_hook, DeviceMgr, Plugins
7273
from .version import ELECTRUM_VERSION
7374
from .simple_config import SimpleConfig
@@ -1725,7 +1726,16 @@ async def decode_invoice(self, invoice: str):
17251726
return invoice.to_debug_json()
17261727

17271728
@command('wnpl')
1728-
async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wallet = None):
1729+
async def lnpay(
1730+
self,
1731+
invoice: str,
1732+
timeout: int = 120,
1733+
max_cltv: Optional[int] = None,
1734+
max_fee_millionths: Optional[int] = None,
1735+
fee_cutoff_msat: Optional[int] = None,
1736+
password=None,
1737+
wallet: Abstract_Wallet = None
1738+
):
17291739
"""
17301740
Pay a lightning invoice
17311741
Note: it is *not* safe to try paying the same invoice multiple times with a timeout.
@@ -1734,6 +1744,9 @@ async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wall
17341744
17351745
arg:str:invoice:Lightning invoice (bolt 11)
17361746
arg:int:timeout:Timeout in seconds (default=120)
1747+
arg:int:max_cltv:Maximum total time lock for the route (default=4032+invoice_final_cltv_delta)
1748+
arg:int:max_fee_millionths:Maximum fees allowed for this payment (default=ConfigVar(lightning_payment_fee_max_millionths))
1749+
arg:int:fee_cutoff_msat:Minimum absolute fee budget for this payment (default=ConfigVar(lightning_payment_fee_cutoff_msat))
17371750
"""
17381751
# note: The "timeout" param works via black magic.
17391752
# The CLI-parser stores it in the config, and the argname matches config.cv.CLI_TIMEOUT.key().
@@ -1745,7 +1758,24 @@ async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wall
17451758
payment_hash = lnaddr.paymenthash
17461759
invoice_obj = Invoice.from_bech32(invoice)
17471760
wallet.save_invoice(invoice_obj)
1748-
success, log = await lnworker.pay_invoice(invoice_obj)
1761+
if max_cltv is not None:
1762+
# The cltv budget excludes the final cltv delta which is why it is deducted here
1763+
# so the whole used cltv is <= max_cltv
1764+
assert max_cltv <= NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, \
1765+
f"{max_cltv=} > {NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE=}"
1766+
max_cltv_remaining = max_cltv - lnaddr.get_min_final_cltv_delta()
1767+
assert max_cltv_remaining > 0, f"{max_cltv=} - {lnaddr.get_min_final_cltv_delta()=} < 1"
1768+
max_cltv = max_cltv_remaining
1769+
assert max_fee_millionths is None or 0 < max_fee_millionths < 250_000, max_fee_millionths
1770+
assert fee_cutoff_msat is None or 0 < fee_cutoff_msat < 10_000_000, fee_cutoff_msat
1771+
budget = PaymentFeeBudget.custom(
1772+
invoice_amount_msat=invoice_obj.amount_msat,
1773+
config=wallet.config,
1774+
max_cltv_delta=max_cltv,
1775+
fee_millionths=max_fee_millionths,
1776+
fee_cutoff_msat=fee_cutoff_msat,
1777+
)
1778+
success, log = await lnworker.pay_invoice(invoice_obj, budget=budget)
17491779
return {
17501780
'payment_hash': payment_hash.hex(),
17511781
'success': success,

electrum/lnutil.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,23 +1948,63 @@ class PaymentFeeBudget(NamedTuple):
19481948

19491949
@classmethod
19501950
def default(cls, *, invoice_amount_msat: int, config: 'SimpleConfig') -> 'PaymentFeeBudget':
1951-
millionths_orig = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
1952-
millionths = min(max(0, millionths_orig), 250_000) # clamp into [0, 25%]
1953-
cutoff_orig = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
1954-
cutoff = min(max(0, cutoff_orig), 10_000_000) # clamp into [0, 10k sat]
1955-
if millionths != millionths_orig:
1951+
fee_msat = PaymentFeeBudget._calculate_fee_msat(
1952+
invoice_amount_msat=invoice_amount_msat,
1953+
config=config,
1954+
)
1955+
return PaymentFeeBudget(
1956+
fee_msat=fee_msat,
1957+
cltv=NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE,
1958+
)
1959+
1960+
@classmethod
1961+
def custom(
1962+
cls,
1963+
*,
1964+
invoice_amount_msat: int,
1965+
config: 'SimpleConfig',
1966+
fee_millionths: int = None,
1967+
fee_cutoff_msat: int = None,
1968+
max_cltv_delta: int = None,
1969+
):
1970+
fee_msat = PaymentFeeBudget._calculate_fee_msat(
1971+
invoice_amount_msat=invoice_amount_msat,
1972+
config=config,
1973+
fee_millionths=fee_millionths,
1974+
fee_cutoff_msat=fee_cutoff_msat,
1975+
)
1976+
if max_cltv_delta is None:
1977+
max_cltv_delta = NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE
1978+
assert max_cltv_delta > 0, max_cltv_delta
1979+
return PaymentFeeBudget(
1980+
fee_msat=fee_msat,
1981+
cltv=max_cltv_delta,
1982+
)
1983+
1984+
@classmethod
1985+
def _calculate_fee_msat(cls,
1986+
*,
1987+
invoice_amount_msat: int,
1988+
config: 'SimpleConfig',
1989+
fee_millionths: int = None,
1990+
fee_cutoff_msat: int = None,
1991+
) -> int:
1992+
if fee_millionths is None:
1993+
fee_millionths = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
1994+
if fee_cutoff_msat is None:
1995+
fee_cutoff_msat = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
1996+
millionths_clamped = min(max(0, fee_millionths), 250_000) # clamp into [0, 25%]
1997+
cutoff_clamped = min(max(0, fee_cutoff_msat), 10_000_000) # clamp into [0, 10k sat]
1998+
if fee_millionths != millionths_clamped:
19561999
_logger.warning(
19572000
f"PaymentFeeBudget. found insane fee millionths in config. "
1958-
f"clamped: {millionths_orig}->{millionths}")
1959-
if cutoff != cutoff_orig:
2001+
f"clamped: {fee_millionths}->{millionths_clamped}")
2002+
if fee_cutoff_msat != cutoff_clamped:
19602003
_logger.warning(
19612004
f"PaymentFeeBudget. found insane fee cutoff in config. "
1962-
f"clamped: {cutoff_orig}->{cutoff}")
2005+
f"clamped: {fee_cutoff_msat}->{cutoff_clamped}")
19632006
# for small payments, fees <= constant cutoff are fine
19642007
# for large payments, the max fee is percentage-based
1965-
fee_msat = invoice_amount_msat * millionths // 1_000_000
1966-
fee_msat = max(fee_msat, cutoff)
1967-
return PaymentFeeBudget(
1968-
fee_msat=fee_msat,
1969-
cltv=NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE,
1970-
)
2008+
fee_msat = invoice_amount_msat * millionths_clamped // 1_000_000
2009+
fee_msat = max(fee_msat, cutoff_clamped)
2010+
return fee_msat

electrum/lnworker.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,7 @@ async def pay_invoice(
15251525
attempts: int = None, # used only in unit tests
15261526
full_path: LNPaymentPath = None,
15271527
channels: Optional[Sequence[Channel]] = None,
1528+
budget: Optional[PaymentFeeBudget] = None,
15281529
) -> Tuple[bool, List[HtlcLog]]:
15291530
bolt11 = invoice.lightning_invoice
15301531
lnaddr = self._check_bolt11_invoice(bolt11, amount_msat=amount_msat)
@@ -1547,7 +1548,8 @@ async def pay_invoice(
15471548
self.save_payment_info(info)
15481549
self.wallet.set_label(key, lnaddr.get_description())
15491550
self.set_invoice_status(key, PR_INFLIGHT)
1550-
budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay, config=self.config)
1551+
if budget is None:
1552+
budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay, config=self.config)
15511553
if attempts is None and self.uses_trampoline():
15521554
# we don't expect lots of failed htlcs with trampoline, so we can fail sooner
15531555
attempts = 30

0 commit comments

Comments
 (0)