lndmanage/lndmanage/lib/listchannels.py
2021-11-14 12:37:38 +01:00

598 lines
17 KiB
Python

"""
Module for printing lightning channels.
"""
import math
import logging
import time
from collections import OrderedDict
from lndmanage.lib.forwardings import get_forwarding_statistics_channels
from lndmanage import settings
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
# define symbols for bool to string conversion
POSITIVE_MARKER = "\u2713"
NEGATIVE_MARKER = "\u2717"
ALIAS_LENGTH = 25
ANNOTATION_LENGTH = 25
# define printing abbreviations
# convert key can specify a function, which lets one do unit conversions
PRINT_CHANNELS_FORMAT = {
"act": {
"dict_key": "active",
"description": "channel is active",
"width": 3,
"format": "^3",
"align": ">",
"convert": lambda x: POSITIVE_MARKER if x else NEGATIVE_MARKER,
},
"age": {
"dict_key": "age",
"description": "channel age [days]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"alias": {
"dict_key": "alias",
"description": "alias",
"width": ALIAS_LENGTH,
"format": ".<" + str(ALIAS_LENGTH),
"align": "^",
"convert": lambda x: alias_cutoff(x),
},
"annotation": {
"dict_key": "annotation",
"description": "channel annotation",
"width": ANNOTATION_LENGTH,
"format": ".<" + str(ANNOTATION_LENGTH),
"align": "^",
"convert": lambda x: alias_cutoff(x),
},
"atb": {
"dict_key": "amount_to_balanced",
"description": "amount to be balanced (local side) [sat]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"bwd": {
"dict_key": "bandwidth_demand",
"description": "bandwidth demand: capacity / max(mean_in, mean_out)",
"width": 5,
"format": "5.2f",
"align": ">",
},
"cap": {
"dict_key": "capacity",
"description": "channel capacity [sat]",
"width": 9,
"format": "9d",
"align": ">",
},
"cid": {
"dict_key": "chan_id",
"description": "channel id",
"width": 18,
"format": "",
"align": "^",
},
"fees": {
"dict_key": "fees_total",
"description": "total fees [sat]",
"width": 7,
"format": "7.2f",
"align": ">",
"convert": lambda x: float(x) / 1000,
},
"f/w": {
"dict_key": "fees_total_per_week",
"description": "total fees per week [sat / week]",
"width": 6,
"format": "6.2f",
"align": ">",
"convert": lambda x: float(x) / 1000,
},
"nfwd/a": {
"dict_key": "forwardings_per_channel_age",
"description": "number of forwardings per channel age in forwarding interval [1 / days]",
"width": 6,
"format": "6.2f",
"align": ">",
},
"flow": {
"dict_key": "flow_direction",
"description": "flow direction (positive is outwards)",
"width": 5,
"format": "5.2f",
"align": ">",
# 'convert': lambda x: '>'*int(nan_to_zero(x)*10/3.0) if x > 0 else
# '<'*(-int(nan_to_zero(x)*10/3.0))
},
"pbf": {
"dict_key": "peer_base_fee",
"description": "peer base fee [msat]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"pfr": {
"dict_key": "peer_fee_rate",
"description": "peer fee rate",
"width": 8,
"format": "1.6f",
"align": ">",
"convert": lambda x: x / 1e6,
},
"lbf": {
"dict_key": "local_base_fee",
"description": "local base fee [msat]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"lfr": {
"dict_key": "local_fee_rate",
"description": "local fee rate",
"width": 8,
"format": "1.6f",
"align": ">",
"convert": lambda x: x / 1e6,
},
"lup": {
"dict_key": "last_update",
"description": "last update time [days ago]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"lupp": {
"dict_key": "last_update_peer",
"description": "last update time by peer [days ago]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"lupl": {
"dict_key": "last_update_local",
"description": "last update time by local [days ago]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"lb": {
"dict_key": "local_balance",
"description": "local balance [sat]",
"width": 9,
"format": "9d",
"align": ">",
},
"nfwd": {
"dict_key": "number_forwardings",
"description": "number of forwardings",
"width": 5,
"format": "5.0f",
"align": ">",
},
"priv": {
"dict_key": "private",
"description": "channel is private",
"width": 5,
"format": "^5",
"align": ">",
"convert": lambda x: POSITIVE_MARKER if x else NEGATIVE_MARKER,
},
"r": {
"dict_key": "action_required",
"description": "action is required",
"width": 1,
"format": "^1",
"align": ">",
"convert": lambda x: NEGATIVE_MARKER if x else "",
},
"rb": {
"dict_key": "remote_balance",
"description": "remote balance [sat]",
"width": 9,
"format": "9d",
"align": ">",
},
"in": {
"dict_key": "total_forwarding_in",
"description": "total forwarding inwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"ini": {
"dict_key": "initiator",
"description": "true if we opened channel",
"width": 3,
"format": "^3",
"align": ">",
"convert": lambda x: POSITIVE_MARKER if x else NEGATIVE_MARKER,
},
"tot": {
"dict_key": "total_forwarding",
"description": "total forwarding [sat]",
"width": 5,
"format": "5.0f",
"align": ">",
},
"ub": {
"dict_key": "unbalancedness",
"description": "unbalancedness [-1 ... 1] (0 is 50:50 balanced)",
"width": 5,
"format": "5.2f",
"align": ">",
},
"ulr": {
"dict_key": "uptime_lifetime_ratio",
"description": "ratio of uptime to lifetime of channel [0 ... 1]",
"width": 5,
"format": "5.2f",
"align": ">",
},
"imed": {
"dict_key": "median_forwarding_in",
"description": "median forwarding inwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"imean": {
"dict_key": "mean_forwarding_in",
"description": "mean forwarding inwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"imax": {
"dict_key": "largest_forwarding_amount_in",
"description": "largest forwarding inwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"omed": {
"dict_key": "median_forwarding_out",
"description": "median forwarding outwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"omean": {
"dict_key": "mean_forwarding_out",
"description": "mean forwarding outwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"omax": {
"dict_key": "largest_forwarding_amount_out",
"description": "largest forwarding outwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"out": {
"dict_key": "total_forwarding_out",
"description": "total forwarding outwards [sat]",
"width": 10,
"format": "10.0f",
"align": ">",
},
"sr/w": {
"dict_key": "sent_received_per_week",
"description": "sent and received per week [sat]",
"width": 9,
"format": "9d",
"align": ">",
},
}
def alias_cutoff(alias):
"""
Cuts off the node alias at a certain length and removes unicode
characters from the node alias.
:param alias: str
:return: str
"""
if len(alias) > ALIAS_LENGTH:
return alias[: ALIAS_LENGTH - 3] + "..."
else:
return alias
class ListChannels(object):
"""
A class to list lightning channels.
"""
def __init__(self, node):
"""
:param node: :class:`lib.node.Node`
"""
self.node = node
def print_all_channels(self, sort_string="rev_alias"):
"""
Prints all active and inactive channels.
:param sort_string: str
"""
channels = self._add_channel_annotations(self.node.get_all_channels())
sort_string, reverse_sorting = self._sorting_order(sort_string)
sort_dict = {
"function": lambda x: (
x[1][PRINT_CHANNELS_FORMAT["priv"]["dict_key"]],
x[1][sort_string],
),
"string": sort_string,
"reverse": reverse_sorting,
}
self._print_channels(
channels,
columns="cid,priv,act,ub,cap,lb,rb,lbf," "lfr,annotation,alias",
sort_dict=sort_dict,
)
def print_channels_unbalanced(self, unbalancedness, sort_string="rev_ub"):
"""
Prints unbalanced channels with
|unbalancedness(channel)| > unbalancedness.
:param unbalancedness: float
:param sort_string: str
"""
channels = self._add_channel_annotations(
self.node.get_unbalanced_channels(unbalancedness)
)
sort_string, reverse_sorting = self._sorting_order(sort_string)
sort_dict = {
"function": lambda x: x[1][sort_string],
"string": sort_string,
"reverse": reverse_sorting,
}
self._print_channels(
channels,
columns="cid,ub,cap,lb,rb,pbf,pfr,annotation,alias",
sort_dict=sort_dict,
)
def print_channels_inactive(self, sort_string="lupp"):
"""
Prints all inactive channels.
:param sort_string: str
"""
channels = self._add_channel_annotations(self.node.get_inactive_channels())
sort_string, reverse_sorting = self._sorting_order(sort_string)
sort_dict = {
"function": lambda x: (-x[1]["private"], x[1][sort_string]),
"string": sort_string,
"reverse": reverse_sorting,
}
self._print_channels(
channels,
columns="cid,lupp,ulr,priv,ini,age,ub,cap,lb,rb," "sr/w,annotation,alias",
sort_dict=sort_dict,
)
def print_channels_forwardings(
self, time_interval_start, time_interval_end, sort_string
):
"""
Prints forwarding statistics for each channel.
:param time_interval_start: int
:param time_interval_end: int
:param sort_string: str
"""
channels = get_forwarding_statistics_channels(
self.node, time_interval_start, time_interval_end
)
channels = self._add_channel_annotations(channels)
sort_string, reverse_sorting = self._sorting_order(sort_string)
sort_dict = {
"function": lambda x: (
float("inf") if math.isnan(x[1][sort_string]) else x[1][sort_string],
x[1][PRINT_CHANNELS_FORMAT["nfwd"]["dict_key"]],
x[1][PRINT_CHANNELS_FORMAT["ub"]["dict_key"]],
),
"string": sort_string,
"reverse": reverse_sorting,
}
self._print_channels(
channels,
columns="cid,nfwd,age,fees,f/w,flow,ub,bwd,r,"
"cap,pbf,pfr,annotation,alias",
sort_dict=sort_dict,
)
def print_channels_hygiene(self, time_interval_start, sort_string):
"""
Prints hygiene statistics for each channel.
:param time_interval_start: int
:param time_interval_end: int
:param sort_string: str
"""
time_interval_end = time.time()
channels = get_forwarding_statistics_channels(
self.node, time_interval_start, time_interval_end
)
channels = self._add_channel_annotations(channels)
sort_string, reverse_sorting = self._sorting_order(sort_string)
sort_dict = {
"function": lambda x: x[1][sort_string],
"string": sort_string,
"reverse": reverse_sorting,
}
self._print_channels(
channels,
columns="cid,age,ini,nfwd/a,nfwd,f/w,ulr,lb,cap,lfr,pfr,annotation,alias",
sort_dict=sort_dict,
)
def _add_channel_annotations(self, channels):
"""
Appends metadata to existing channel dicts from the configuration file.
:param channels: dict
:return: dict
"""
if self.node.config:
logger.debug("Adding annotations from file %s.", self.node.config_file)
# mapping between the channel point and channel id
channel_point_mapping = {
k: v["channel_point"].split(":")[0] for k, v in channels.items()
}
# only read annotations if config file is given
if self.node.config:
annotations = self.node.config["annotations"]
else:
annotations = {}
channel_annotations_funding_id = {}
channel_annotations_channel_id = {}
for id, annotation in annotations.items():
if len(id) == 18 and id.isnumeric():
# valid channel id
channel_annotations_channel_id[int(id)] = annotation
elif len(id) == 64 and id.isalnum():
# valid funding transaction id
channel_annotations_funding_id[id] = annotation
else:
raise ValueError(
"First part needs to be either a channel id or the "
"funding transaction id. \n"
"The funding transaction id can be found with "
"`lncli listchannels` under the channel point (the "
"characters before the colon)."
)
for channel_id, channel_values in channels.items():
# get the annotation by channel id first
annotation = channel_annotations_channel_id.get(channel_id, None)
# if no channel annotation, try with funding id
if annotation is None:
annotation = channel_annotations_funding_id.get(
channel_point_mapping[channel_id], None
)
if annotation is not None:
channels[channel_id]["annotation"] = annotation
else:
channels[channel_id]["annotation"] = ""
return channels
def _print_channels(self, channels, columns, sort_dict):
"""
General purpose channel printing.
:param channels: dict
:param columns: str
:param sort_dict: dict
"""
if not channels:
logger.info(">>> Did not find any channels.")
channels = OrderedDict(
sorted(
channels.items(),
key=sort_dict["function"],
reverse=sort_dict["reverse"],
)
)
logger.info("Sorting channels by %s.", sort_dict["string"])
logger.info("-------- Description --------")
columns = columns.split(",")
for column in columns:
logger.info(
"%-10s %s", column, PRINT_CHANNELS_FORMAT[column]["description"]
)
logger.info("-------- Channels --------")
# prepare the column header
column_header = ""
for column in columns:
column_label = PRINT_CHANNELS_FORMAT[column]["align"]
column_width = PRINT_CHANNELS_FORMAT[column]["width"]
column_header += f"{column:{column_label}{column_width}} "
# print the channel data
for channel_number, (_, channel_data) in enumerate(channels.items()):
if not channel_number % 20:
logger.info(column_header)
row = self._row_string(channel_data, columns)
logger.info(row)
@staticmethod
def _row_string(column_values, columns):
"""
Constructs the formatted row string for table printing.
:param column_values: dict
:param columns: list of str
:return: formatted str
"""
string = ""
for column in columns:
format_string = PRINT_CHANNELS_FORMAT[column]["format"]
conversion_function = PRINT_CHANNELS_FORMAT[column].get(
"convert", lambda x: x
)
value = column_values[PRINT_CHANNELS_FORMAT[column]["dict_key"]]
converted_value = conversion_function(value)
string += f"{converted_value:{format_string}} "
return string
@staticmethod
def _sorting_order(sort_string):
"""
Determines the sorting string and the sorting order.
If sort_string starts with 'rev_', the sorting order is reversed.
:param sort_string: str
:return: bool
"""
reverse_sorting = True
if sort_string[:4] == "rev_":
reverse_sorting = False
sort_string = sort_string[4:]
sort_string = PRINT_CHANNELS_FORMAT[sort_string]["dict_key"]
return sort_string, reverse_sorting