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')
|
run_hook('close_settings_dialog')
|
||||||
if d.need_restart:
|
if d.need_restart:
|
||||||
self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
|
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:
|
def _show_closing_warnings(self) -> bool:
|
||||||
"""Show any closing warnings and return True if the user chose to quit anyway."""
|
"""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-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!
|
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
|
@classmethod
|
||||||
def from_invoice_amount(
|
def from_invoice_amount(
|
||||||
@@ -2067,8 +2068,8 @@ class PaymentFeeBudget(NamedTuple):
|
|||||||
fee_millionths = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
|
fee_millionths = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
|
||||||
if fee_cutoff_msat is None:
|
if fee_cutoff_msat is None:
|
||||||
fee_cutoff_msat = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
|
fee_cutoff_msat = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT
|
||||||
millionths_clamped = min(max(0, fee_millionths), 250_000) # clamp into [0, 25%]
|
millionths_clamped = min(max(0, fee_millionths), cls.PAYMENT_FEE_MILLIONTHS_CLAMP)
|
||||||
cutoff_clamped = min(max(0, fee_cutoff_msat), 10_000_000) # clamp into [0, 10k sat]
|
cutoff_clamped = min(max(0, fee_cutoff_msat), cls.PAYMENT_FEE_CUTOFF_CLAMP)
|
||||||
if fee_millionths != millionths_clamped:
|
if fee_millionths != millionths_clamped:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
f"PaymentFeeBudget. found insane fee millionths in config. "
|
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 = invoice_amount_msat * millionths_clamped // 1_000_000
|
||||||
fee_msat = max(fee_msat, cutoff_clamped)
|
fee_msat = max(fee_msat, cutoff_clamped)
|
||||||
return fee_msat
|
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 urllib.parse
|
||||||
import itertools
|
import itertools
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import dns.asyncresolver
|
import dns.asyncresolver
|
||||||
@@ -2950,15 +2951,17 @@ class LNWallet(LNWorker):
|
|||||||
if self.channel_db or self.is_trampoline_peer(c.node_id):
|
if self.channel_db or self.is_trampoline_peer(c.node_id):
|
||||||
yield c
|
yield c
|
||||||
|
|
||||||
def fee_estimate(self, amount_sat):
|
def estimate_fee_reserve_for_total_amount(self, amount_sat: int | Decimal) -> int:
|
||||||
# Here we have to guess a fee, because some callers (submarine swaps)
|
"""
|
||||||
# use this method to initiate a payment, which would otherwise fail.
|
Estimate how much of the given amount needs to be reserved for
|
||||||
fee_base_msat = 5000 # FIXME ehh.. there ought to be a better way...
|
ln payment fees to reliably pay the remaining amount.
|
||||||
fee_proportional_millionths = 500 # FIXME
|
"""
|
||||||
# inverse of fee_for_edge_msat
|
amount_msat = ceil(amount_sat * 1000) # round up to the next sat
|
||||||
amount_msat = amount_sat * 1000
|
fee_msat = PaymentFeeBudget.reverse_from_total_amount(
|
||||||
amount_minus_fees = (amount_msat - fee_base_msat) * 1_000_000 // ( 1_000_000 + fee_proportional_millionths)
|
total_amount_msat=amount_msat,
|
||||||
return Decimal(amount_msat - amount_minus_fees) / 1000
|
config=self.config,
|
||||||
|
)
|
||||||
|
return ceil(Decimal(fee_msat) / 1000)
|
||||||
|
|
||||||
def num_sats_can_send(self, deltas=None) -> Decimal:
|
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_dict[c.node_id] += send_capacity(c)
|
||||||
can_send = max(can_send_dict.values()) if can_send_dict else 0
|
can_send = max(can_send_dict.values()) if can_send_dict else 0
|
||||||
can_send_sat = Decimal(can_send)/1000
|
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)
|
return max(can_send_sat, 0)
|
||||||
|
|
||||||
def get_channels_for_receiving(
|
def get_channels_for_receiving(
|
||||||
@@ -3083,7 +3086,7 @@ class LNWallet(LNWorker):
|
|||||||
for chan in channels:
|
for chan in channels:
|
||||||
available_sat = chan.available_to_spend(LOCAL if direction == SENT else REMOTE) // 1000
|
available_sat = chan.available_to_spend(LOCAL if direction == SENT else REMOTE) // 1000
|
||||||
delta = amount_sat - available_sat
|
delta = amount_sat - available_sat
|
||||||
delta += self.fee_estimate(amount_sat)
|
delta += self.estimate_fee_reserve_for_total_amount(amount_sat)
|
||||||
# add safety margin
|
# add safety margin
|
||||||
delta += delta // 100 + 1
|
delta += delta // 100 + 1
|
||||||
if func(deltas={chan:delta}) >= amount_sat:
|
if func(deltas={chan:delta}) >= amount_sat:
|
||||||
@@ -3106,7 +3109,7 @@ class LNWallet(LNWorker):
|
|||||||
return False
|
return False
|
||||||
for chan2, delta in suggestions:
|
for chan2, delta in suggestions:
|
||||||
# margin for fee caused by rebalancing
|
# 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
|
# find other channel or trampoline that can send delta
|
||||||
for chan1 in self.channels.values():
|
for chan1 in self.channels.values():
|
||||||
if chan1.is_frozen_for_sending() or not chan1.is_active():
|
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):
|
def num_sats_can_rebalance(self, chan1, chan2):
|
||||||
# TODO: we should be able to spend 'max', with variable fee
|
# TODO: we should be able to spend 'max', with variable fee
|
||||||
n1 = chan1.available_to_spend(LOCAL)
|
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)
|
n2 = chan2.available_to_spend(REMOTE)
|
||||||
amount_sat = min(n1, n2) // 1000
|
amount_sat = min(n1, n2) // 1000
|
||||||
return amount_sat
|
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,
|
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,
|
ScriptHtlc, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features,
|
||||||
IncompatibleLightningFeatures, ChannelType, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat,
|
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.util import bfh, MyEncoder
|
||||||
from electrum.transaction import Transaction, PartialTransaction, Sighash
|
from electrum.transaction import Transaction, PartialTransaction, Sighash
|
||||||
@@ -1121,3 +1121,33 @@ class TestLNUtil(ElectrumTestCase):
|
|||||||
),
|
),
|
||||||
decoded_cb,
|
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