|
|
|
|
@ -7,6 +7,7 @@ import os
|
|
|
|
|
from decimal import Decimal |
|
|
|
|
import random |
|
|
|
|
import time |
|
|
|
|
import operator |
|
|
|
|
from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, |
|
|
|
|
NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict) |
|
|
|
|
import threading |
|
|
|
|
@ -91,6 +92,7 @@ if TYPE_CHECKING:
|
|
|
|
|
|
|
|
|
|
SAVED_PR_STATUS = [PR_PAID, PR_UNPAID, PR_SCHEDULED] # status that are persisted |
|
|
|
|
|
|
|
|
|
MPP_RECEIVE_CUTOFF = 0.2 |
|
|
|
|
|
|
|
|
|
NUM_PEERS_TARGET = 4 |
|
|
|
|
|
|
|
|
|
@ -1764,18 +1766,12 @@ class LNWallet(LNWorker):
|
|
|
|
|
|
|
|
|
|
assert amount_msat is None or amount_msat > 0 |
|
|
|
|
timestamp = int(time.time()) |
|
|
|
|
routing_hints = self.calc_routing_hints_for_invoice(amount_msat) |
|
|
|
|
routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat) |
|
|
|
|
if not routing_hints: |
|
|
|
|
self.logger.info( |
|
|
|
|
"Warning. No routing hints added to invoice. " |
|
|
|
|
"Other clients will likely not be able to send to us.") |
|
|
|
|
# if not all hints are trampoline, do not create trampoline invoice |
|
|
|
|
invoice_features = self.features.for_invoice() |
|
|
|
|
trampoline_hints = [] |
|
|
|
|
for r in routing_hints: |
|
|
|
|
node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0] |
|
|
|
|
if len(r[1])== 1 and self.is_trampoline_peer(node_id): |
|
|
|
|
trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta))) |
|
|
|
|
payment_preimage = os.urandom(32) |
|
|
|
|
payment_hash = sha256(payment_preimage) |
|
|
|
|
info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) |
|
|
|
|
@ -2002,16 +1998,12 @@ class LNWallet(LNWorker):
|
|
|
|
|
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]): |
|
|
|
|
"""calculate routing hints (BOLT-11 'r' field)""" |
|
|
|
|
routing_hints = [] |
|
|
|
|
channels = list(self.channels.values()) |
|
|
|
|
# do minimal filtering of channels. |
|
|
|
|
# we include channels that cannot *right now* receive (e.g. peer disconnected or balance insufficient) |
|
|
|
|
channels = [chan for chan in channels |
|
|
|
|
if (chan.is_open() and not chan.is_frozen_for_receiving())] |
|
|
|
|
# Filter out channels that have very low receive capacity compared to invoice amt. |
|
|
|
|
# Even with MPP, below a certain threshold, including these channels probably |
|
|
|
|
# hurts more than help, as they lead to many failed attempts for the sender. |
|
|
|
|
channels = [chan for chan in channels |
|
|
|
|
if chan.available_to_spend(REMOTE) > (amount_msat or 0) * 0.05] |
|
|
|
|
with self.lock: |
|
|
|
|
nodes = self.border_nodes_that_can_receive(amount_msat) |
|
|
|
|
channels = [] |
|
|
|
|
for c in self.channels.values(): |
|
|
|
|
if c.node_id in nodes: |
|
|
|
|
channels.append(c) |
|
|
|
|
# cap max channels to include to keep QR code reasonably scannable |
|
|
|
|
channels = sorted(channels, key=lambda chan: (not chan.is_active(), -chan.available_to_spend(REMOTE))) |
|
|
|
|
channels = channels[:15] |
|
|
|
|
@ -2047,7 +2039,12 @@ class LNWallet(LNWorker):
|
|
|
|
|
fee_base_msat, |
|
|
|
|
fee_proportional_millionths, |
|
|
|
|
cltv_expiry_delta)])) |
|
|
|
|
return routing_hints |
|
|
|
|
trampoline_hints = [] |
|
|
|
|
for r in routing_hints: |
|
|
|
|
node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0] |
|
|
|
|
if len(r[1])== 1 and self.is_trampoline_peer(node_id): |
|
|
|
|
trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta))) |
|
|
|
|
return routing_hints, trampoline_hints |
|
|
|
|
|
|
|
|
|
def delete_payment(self, payment_hash_hex: str): |
|
|
|
|
try: |
|
|
|
|
@ -2085,14 +2082,38 @@ class LNWallet(LNWorker):
|
|
|
|
|
can_send_minus_fees = max(0, can_send_minus_fees) |
|
|
|
|
return Decimal(can_send_minus_fees) / 1000 |
|
|
|
|
|
|
|
|
|
def num_sats_can_receive(self) -> Decimal: |
|
|
|
|
def border_nodes_that_can_receive(self, amount_msat=None): |
|
|
|
|
# if amount_msat is None, use the max amount we can receive |
|
|
|
|
# |
|
|
|
|
# Filter out nodes that have very low receive capacity compared to invoice amt. |
|
|
|
|
# Even with MPP, below a certain threshold, including these channels probably |
|
|
|
|
# hurts more than help, as they lead to many failed attempts for the sender. |
|
|
|
|
# |
|
|
|
|
# We condider nodes instead of channels because both non-strict forwardring |
|
|
|
|
# and trampoline end-to-end payments allow it |
|
|
|
|
nodes_that_can_receive = defaultdict(int) |
|
|
|
|
with self.lock: |
|
|
|
|
channels = [ |
|
|
|
|
c for c in self.channels.values() |
|
|
|
|
if c.is_active() and not c.is_frozen_for_receiving() |
|
|
|
|
] |
|
|
|
|
can_receive = sum([c.available_to_spend(REMOTE) for c in channels]) if channels else 0 |
|
|
|
|
return Decimal(can_receive) / 1000 |
|
|
|
|
for c in self.channels.values(): |
|
|
|
|
if not c.is_active() or c.is_frozen_for_receiving(): |
|
|
|
|
continue |
|
|
|
|
nodes_that_can_receive[c.node_id] += c.available_to_spend(REMOTE) |
|
|
|
|
while True: |
|
|
|
|
max_can_receive = sum(nodes_that_can_receive.values()) |
|
|
|
|
receive_amount = amount_msat or max_can_receive |
|
|
|
|
items = sorted(list(nodes_that_can_receive.items()), key=operator.itemgetter(1)) |
|
|
|
|
for node_id, v in items: |
|
|
|
|
if v < receive_amount * MPP_RECEIVE_CUTOFF: |
|
|
|
|
nodes_that_can_receive.pop(node_id) |
|
|
|
|
# break immediately because max_can_receive needs to be recomputed |
|
|
|
|
break |
|
|
|
|
else: |
|
|
|
|
break |
|
|
|
|
return nodes_that_can_receive |
|
|
|
|
|
|
|
|
|
def num_sats_can_receive(self) -> Decimal: |
|
|
|
|
can_receive_nodes = self.border_nodes_that_can_receive(None) |
|
|
|
|
can_receive_msat = sum(can_receive_nodes.values()) |
|
|
|
|
return Decimal(can_receive_msat) / 1000 |
|
|
|
|
|
|
|
|
|
def num_sats_can_receive_no_mpp(self) -> Decimal: |
|
|
|
|
with self.lock: |
|
|
|
|
|