Merge pull request #10319 from f321x/fix_lightning_num_sats_can_send
lnworker: use config lightning fee for estimate
This commit is contained in:
@@ -2720,6 +2720,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
run_hook('close_settings_dialog')
|
||||
if d.need_restart:
|
||||
self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
|
||||
else:
|
||||
# Some values might need to be updated if settings have changed.
|
||||
# For example 'Can send' in the lightning tab will change if the fees config is changed.
|
||||
self.refresh_tabs()
|
||||
|
||||
def _show_closing_warnings(self) -> bool:
|
||||
"""Show any closing warnings and return True if the user chose to quit anyway."""
|
||||
|
||||
@@ -2030,7 +2030,8 @@ class PaymentFeeBudget(NamedTuple):
|
||||
# cltv-delta the destination wants for itself. (e.g. "min_final_cltv_delta" is excluded)
|
||||
cltv: int # this is cltv-delta-like, no absolute heights here!
|
||||
|
||||
#num_htlc: int
|
||||
PAYMENT_FEE_CUTOFF_CLAMP = 10_000_000 # [0, 10k sat]
|
||||
PAYMENT_FEE_MILLIONTHS_CLAMP = 250_000 # [0, 25%]
|
||||
|
||||
@classmethod
|
||||
def from_invoice_amount(
|
||||
@@ -2067,8 +2068,8 @@ class PaymentFeeBudget(NamedTuple):
|
||||
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]
|
||||
millionths_clamped = min(max(0, fee_millionths), cls.PAYMENT_FEE_MILLIONTHS_CLAMP)
|
||||
cutoff_clamped = min(max(0, fee_cutoff_msat), cls.PAYMENT_FEE_CUTOFF_CLAMP)
|
||||
if fee_millionths != millionths_clamped:
|
||||
_logger.warning(
|
||||
f"PaymentFeeBudget. found insane fee millionths in config. "
|
||||
@@ -2082,3 +2083,29 @@ class PaymentFeeBudget(NamedTuple):
|
||||
fee_msat = invoice_amount_msat * millionths_clamped // 1_000_000
|
||||
fee_msat = max(fee_msat, cutoff_clamped)
|
||||
return fee_msat
|
||||
|
||||
@classmethod
|
||||
def reverse_from_total_amount(cls, *, total_amount_msat: int, config: 'SimpleConfig') -> int:
|
||||
"""
|
||||
Given the total amount (including fees) return the amount of fees
|
||||
included assuming highest allowed from config fees are being used.
|
||||
|
||||
This allows to guess a fee that has to be reserved to reliably allow
|
||||
doing a "Max" amount lightning send (e.g. for submarine swaps).
|
||||
"""
|
||||
assert isinstance(total_amount_msat, int) and total_amount_msat >= 0, repr(total_amount_msat)
|
||||
|
||||
millionths_clamped = min(
|
||||
max(0, config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS),
|
||||
cls.PAYMENT_FEE_MILLIONTHS_CLAMP,
|
||||
)
|
||||
cutoff_clamped = min(
|
||||
max(0, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT),
|
||||
cls.PAYMENT_FEE_CUTOFF_CLAMP,
|
||||
)
|
||||
|
||||
# inverse of _calculate_fee_msat
|
||||
amount_minus_fees = (total_amount_msat * 1_000_000) // (1_000_000 + millionths_clamped)
|
||||
fees_msat = max(total_amount_msat - amount_minus_fees, cutoff_clamped)
|
||||
fees_msat = min(fees_msat, total_amount_msat) # to handle (invalid?) inputs below cutoff_clamped
|
||||
return fees_msat
|
||||
|
||||
@@ -22,6 +22,7 @@ from concurrent import futures
|
||||
import urllib.parse
|
||||
import itertools
|
||||
import dataclasses
|
||||
from math import ceil
|
||||
|
||||
import aiohttp
|
||||
import dns.asyncresolver
|
||||
@@ -2950,15 +2951,17 @@ class LNWallet(LNWorker):
|
||||
if self.channel_db or self.is_trampoline_peer(c.node_id):
|
||||
yield c
|
||||
|
||||
def fee_estimate(self, amount_sat):
|
||||
# Here we have to guess a fee, because some callers (submarine swaps)
|
||||
# use this method to initiate a payment, which would otherwise fail.
|
||||
fee_base_msat = 5000 # FIXME ehh.. there ought to be a better way...
|
||||
fee_proportional_millionths = 500 # FIXME
|
||||
# inverse of fee_for_edge_msat
|
||||
amount_msat = amount_sat * 1000
|
||||
amount_minus_fees = (amount_msat - fee_base_msat) * 1_000_000 // ( 1_000_000 + fee_proportional_millionths)
|
||||
return Decimal(amount_msat - amount_minus_fees) / 1000
|
||||
def estimate_fee_reserve_for_total_amount(self, amount_sat: int | Decimal) -> int:
|
||||
"""
|
||||
Estimate how much of the given amount needs to be reserved for
|
||||
ln payment fees to reliably pay the remaining amount.
|
||||
"""
|
||||
amount_msat = ceil(amount_sat * 1000) # round up to the next sat
|
||||
fee_msat = PaymentFeeBudget.reverse_from_total_amount(
|
||||
total_amount_msat=amount_msat,
|
||||
config=self.config,
|
||||
)
|
||||
return ceil(Decimal(fee_msat) / 1000)
|
||||
|
||||
def num_sats_can_send(self, deltas=None) -> Decimal:
|
||||
"""
|
||||
@@ -2985,7 +2988,7 @@ class LNWallet(LNWorker):
|
||||
can_send_dict[c.node_id] += send_capacity(c)
|
||||
can_send = max(can_send_dict.values()) if can_send_dict else 0
|
||||
can_send_sat = Decimal(can_send)/1000
|
||||
can_send_sat -= self.fee_estimate(can_send_sat)
|
||||
can_send_sat -= self.estimate_fee_reserve_for_total_amount(can_send_sat)
|
||||
return max(can_send_sat, 0)
|
||||
|
||||
def get_channels_for_receiving(
|
||||
@@ -3083,7 +3086,7 @@ class LNWallet(LNWorker):
|
||||
for chan in channels:
|
||||
available_sat = chan.available_to_spend(LOCAL if direction == SENT else REMOTE) // 1000
|
||||
delta = amount_sat - available_sat
|
||||
delta += self.fee_estimate(amount_sat)
|
||||
delta += self.estimate_fee_reserve_for_total_amount(amount_sat)
|
||||
# add safety margin
|
||||
delta += delta // 100 + 1
|
||||
if func(deltas={chan:delta}) >= amount_sat:
|
||||
@@ -3106,7 +3109,7 @@ class LNWallet(LNWorker):
|
||||
return False
|
||||
for chan2, delta in suggestions:
|
||||
# margin for fee caused by rebalancing
|
||||
delta += self.fee_estimate(amount_sat)
|
||||
delta += self.estimate_fee_reserve_for_total_amount(amount_sat)
|
||||
# find other channel or trampoline that can send delta
|
||||
for chan1 in self.channels.values():
|
||||
if chan1.is_frozen_for_sending() or not chan1.is_active():
|
||||
@@ -3129,7 +3132,7 @@ class LNWallet(LNWorker):
|
||||
def num_sats_can_rebalance(self, chan1, chan2):
|
||||
# TODO: we should be able to spend 'max', with variable fee
|
||||
n1 = chan1.available_to_spend(LOCAL)
|
||||
n1 -= self.fee_estimate(n1)
|
||||
n1 -= self.estimate_fee_reserve_for_total_amount(n1)
|
||||
n2 = chan2.available_to_spend(REMOTE)
|
||||
amount_sat = min(n1, n2) // 1000
|
||||
return amount_sat
|
||||
|
||||
@@ -10,7 +10,7 @@ from electrum.lnutil import (
|
||||
derive_privkey, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, get_compressed_pubkey_from_bech32,
|
||||
ScriptHtlc, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features,
|
||||
IncompatibleLightningFeatures, ChannelType, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat,
|
||||
ImportedChannelBackupStorage, list_enabled_ln_feature_bits
|
||||
ImportedChannelBackupStorage, list_enabled_ln_feature_bits, PaymentFeeBudget,
|
||||
)
|
||||
from electrum.util import bfh, MyEncoder
|
||||
from electrum.transaction import Transaction, PartialTransaction, Sighash
|
||||
@@ -1121,3 +1121,33 @@ class TestLNUtil(ElectrumTestCase):
|
||||
),
|
||||
decoded_cb,
|
||||
)
|
||||
|
||||
async def test_payment_fee_budget(self):
|
||||
config = SimpleConfig()
|
||||
# test value above cutoff
|
||||
invoice_amount_msat = 1_000_000 * 1000
|
||||
budget = PaymentFeeBudget.from_invoice_amount(
|
||||
invoice_amount_msat=invoice_amount_msat,
|
||||
config=config,
|
||||
)
|
||||
reversed_fee_msat = PaymentFeeBudget.reverse_from_total_amount(
|
||||
total_amount_msat=invoice_amount_msat + budget.fee_msat,
|
||||
config=config,
|
||||
)
|
||||
self.assertGreater(budget.fee_msat, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT)
|
||||
self.assertEqual(reversed_fee_msat, budget.fee_msat)
|
||||
|
||||
# test value below cutoff
|
||||
invoice_amount_msat = 1000
|
||||
budget = PaymentFeeBudget.from_invoice_amount(
|
||||
invoice_amount_msat=invoice_amount_msat,
|
||||
config=config,
|
||||
)
|
||||
reversed_fee_msat = PaymentFeeBudget.reverse_from_total_amount(
|
||||
total_amount_msat=invoice_amount_msat + budget.fee_msat,
|
||||
config=config,
|
||||
)
|
||||
self.assertEqual(budget.fee_msat, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT)
|
||||
self.assertEqual(reversed_fee_msat, budget.fee_msat)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user