Merge pull request #10067 from f321x/max_cltv_lnpay
cli: add max_cltv, max_fee_msat parameters to lnpay
This commit is contained in:
@@ -67,7 +67,8 @@ from .wallet import (
|
||||
)
|
||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from .mnemonic import Mnemonic
|
||||
from .lnutil import channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
|
||||
PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
|
||||
from .plugin import run_hook, DeviceMgr, Plugins
|
||||
from .version import ELECTRUM_VERSION
|
||||
from .simple_config import SimpleConfig
|
||||
@@ -1735,7 +1736,15 @@ class Commands(Logger):
|
||||
return invoice.to_debug_json()
|
||||
|
||||
@command('wnpl')
|
||||
async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wallet = None):
|
||||
async def lnpay(
|
||||
self,
|
||||
invoice: str,
|
||||
timeout: int = 120,
|
||||
max_cltv: Optional[int] = None,
|
||||
max_fee_msat: Optional[int] = None,
|
||||
password=None,
|
||||
wallet: Abstract_Wallet = None
|
||||
):
|
||||
"""
|
||||
Pay a lightning invoice
|
||||
Note: it is *not* safe to try paying the same invoice multiple times with a timeout.
|
||||
@@ -1744,6 +1753,8 @@ class Commands(Logger):
|
||||
|
||||
arg:str:invoice:Lightning invoice (bolt 11)
|
||||
arg:int:timeout:Timeout in seconds (default=120)
|
||||
arg:int:max_cltv:Maximum total time lock for the route (default=4032+invoice_final_cltv_delta)
|
||||
arg:int:max_fee_msat:Maximum absolute fee budget for the payment (if unset, the default is a percentage fee based on config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)
|
||||
"""
|
||||
# note: The "timeout" param works via black magic.
|
||||
# The CLI-parser stores it in the config, and the argname matches config.cv.CLI_TIMEOUT.key().
|
||||
@@ -1751,11 +1762,27 @@ class Commands(Logger):
|
||||
# - FIXME it does NOT work when calling an offline command (-o)
|
||||
# - FIXME it does NOT work when calling RPC directly (e.g. curl)
|
||||
lnworker = wallet.lnworker
|
||||
lnaddr = lnworker._check_bolt11_invoice(invoice)
|
||||
lnaddr = lnworker._check_bolt11_invoice(invoice) # also checks if amount is given
|
||||
payment_hash = lnaddr.paymenthash
|
||||
invoice_obj = Invoice.from_bech32(invoice)
|
||||
assert not max_fee_msat or max_fee_msat < max(invoice_obj.amount_msat // 2, 1_000_000), \
|
||||
f"{max_fee_msat=} > max(invoice amount msat / 2, 1_000_000)"
|
||||
wallet.save_invoice(invoice_obj)
|
||||
success, log = await lnworker.pay_invoice(invoice_obj)
|
||||
if max_cltv is not None:
|
||||
# The cltv budget excludes the final cltv delta which is why it is deducted here
|
||||
# so the whole used cltv is <= max_cltv
|
||||
assert max_cltv <= NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, \
|
||||
f"{max_cltv=} > {NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE=}"
|
||||
max_cltv_remaining = max_cltv - lnaddr.get_min_final_cltv_delta()
|
||||
assert max_cltv_remaining > 0, f"{max_cltv=} - {lnaddr.get_min_final_cltv_delta()=} < 1"
|
||||
max_cltv = max_cltv_remaining
|
||||
budget = PaymentFeeBudget.from_invoice_amount(
|
||||
config=wallet.config,
|
||||
invoice_amount_msat=invoice_obj.amount_msat,
|
||||
max_cltv_delta=max_cltv,
|
||||
max_fee_msat=max_fee_msat,
|
||||
)
|
||||
success, log = await lnworker.pay_invoice(invoice_obj, budget=budget)
|
||||
return {
|
||||
'payment_hash': payment_hash.hex(),
|
||||
'success': success,
|
||||
|
||||
@@ -1947,24 +1947,52 @@ class PaymentFeeBudget(NamedTuple):
|
||||
#num_htlc: int
|
||||
|
||||
@classmethod
|
||||
def default(cls, *, invoice_amount_msat: int, config: 'SimpleConfig') -> 'PaymentFeeBudget':
|
||||
millionths_orig = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
|
||||
millionths = min(max(0, millionths_orig), 250_000) # clamp into [0, 25%]
|
||||
cutoff_orig = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
|
||||
cutoff = min(max(0, cutoff_orig), 10_000_000) # clamp into [0, 10k sat]
|
||||
if millionths != millionths_orig:
|
||||
def from_invoice_amount(
|
||||
cls,
|
||||
*,
|
||||
invoice_amount_msat: int,
|
||||
config: 'SimpleConfig',
|
||||
max_cltv_delta: Optional[int] = None,
|
||||
max_fee_msat: Optional[int] = None,
|
||||
) -> 'PaymentFeeBudget':
|
||||
if max_fee_msat is None:
|
||||
max_fee_msat = PaymentFeeBudget._calculate_fee_msat(
|
||||
invoice_amount_msat=invoice_amount_msat,
|
||||
config=config,
|
||||
)
|
||||
if max_cltv_delta is None:
|
||||
max_cltv_delta = NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE
|
||||
assert max_cltv_delta > 0, max_cltv_delta
|
||||
return PaymentFeeBudget(
|
||||
fee_msat=max_fee_msat,
|
||||
cltv=max_cltv_delta,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _calculate_fee_msat(
|
||||
cls,
|
||||
*,
|
||||
invoice_amount_msat: int,
|
||||
config: 'SimpleConfig',
|
||||
fee_millionths: Optional[int] = None,
|
||||
fee_cutoff_msat: Optional[int] = None,
|
||||
) -> int:
|
||||
if fee_millionths is None:
|
||||
fee_millionths = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
|
||||
if fee_cutoff_msat is None:
|
||||
fee_cutoff_msat = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
|
||||
millionths_clamped = min(max(0, fee_millionths), 250_000) # clamp into [0, 25%]
|
||||
cutoff_clamped = min(max(0, fee_cutoff_msat), 10_000_000) # clamp into [0, 10k sat]
|
||||
if fee_millionths != millionths_clamped:
|
||||
_logger.warning(
|
||||
f"PaymentFeeBudget. found insane fee millionths in config. "
|
||||
f"clamped: {millionths_orig}->{millionths}")
|
||||
if cutoff != cutoff_orig:
|
||||
f"clamped: {fee_millionths}->{millionths_clamped}")
|
||||
if fee_cutoff_msat != cutoff_clamped:
|
||||
_logger.warning(
|
||||
f"PaymentFeeBudget. found insane fee cutoff in config. "
|
||||
f"clamped: {cutoff_orig}->{cutoff}")
|
||||
f"clamped: {fee_cutoff_msat}->{cutoff_clamped}")
|
||||
# for small payments, fees <= constant cutoff are fine
|
||||
# for large payments, the max fee is percentage-based
|
||||
fee_msat = invoice_amount_msat * millionths // 1_000_000
|
||||
fee_msat = max(fee_msat, cutoff)
|
||||
return PaymentFeeBudget(
|
||||
fee_msat=fee_msat,
|
||||
cltv=NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE,
|
||||
)
|
||||
fee_msat = invoice_amount_msat * millionths_clamped // 1_000_000
|
||||
fee_msat = max(fee_msat, cutoff_clamped)
|
||||
return fee_msat
|
||||
|
||||
@@ -1525,6 +1525,7 @@ class LNWallet(LNWorker):
|
||||
attempts: int = None, # used only in unit tests
|
||||
full_path: LNPaymentPath = None,
|
||||
channels: Optional[Sequence[Channel]] = None,
|
||||
budget: Optional[PaymentFeeBudget] = None,
|
||||
) -> Tuple[bool, List[HtlcLog]]:
|
||||
bolt11 = invoice.lightning_invoice
|
||||
lnaddr = self._check_bolt11_invoice(bolt11, amount_msat=amount_msat)
|
||||
@@ -1547,7 +1548,8 @@ class LNWallet(LNWorker):
|
||||
self.save_payment_info(info)
|
||||
self.wallet.set_label(key, lnaddr.get_description())
|
||||
self.set_invoice_status(key, PR_INFLIGHT)
|
||||
budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay, config=self.config)
|
||||
if budget is None:
|
||||
budget = PaymentFeeBudget.from_invoice_amount(invoice_amount_msat=amount_to_pay, config=self.config)
|
||||
if attempts is None and self.uses_trampoline():
|
||||
# we don't expect lots of failed htlcs with trampoline, so we can fail sooner
|
||||
attempts = 30
|
||||
|
||||
@@ -288,7 +288,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
||||
amount_msat=amount_msat,
|
||||
paysession=paysession,
|
||||
full_path=full_path,
|
||||
budget=PaymentFeeBudget.default(invoice_amount_msat=amount_msat, config=self.config),
|
||||
budget=PaymentFeeBudget.from_invoice_amount(invoice_amount_msat=amount_msat, config=self.config),
|
||||
)]
|
||||
|
||||
get_payments = LNWallet.get_payments
|
||||
|
||||
Reference in New Issue
Block a user