restructure: create lndmanage package structure

* shifted important packages to lndmanage folder
* fixed import statements
* created executable in project directory to be able to run directly
* determine project directory in settings
This commit is contained in:
bitromortac 2019-09-20 07:13:49 +02:00
parent b8602b1d73
commit 60785f4bcc
32 changed files with 489 additions and 466 deletions

View file

@ -1,7 +1,7 @@
import numpy as np
import matplotlib.pyplot as plt
from lib.node import LndNode
from lndmanage.lib.node import LndNode
def extract_fee_settings(node):

View file

@ -1,8 +1,9 @@
import datetime
import matplotlib.pyplot as plt
import matplotlib.colors as colors
from lib.node import LndNode
from lndmanage.lib.node import LndNode
def plot_forwardings(forwarding_events):

View file

@ -1,9 +1,9 @@
import _settings
from lib.network_info import NetworkAnalysis
from lib.node import LndNode
from lndmanage.lib.network_info import NetworkAnalysis
from lndmanage.lib.node import LndNode
from lndmanage import settings
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger(__name__)
if __name__ == '__main__':

View file

@ -1,11 +1,13 @@
import _settings
from lib.listchannels import print_channels_rebalance
from lib.node import LndNode
from lndmanage.lib.listchannels import ListChannels
from lndmanage.lib.node import LndNode
from lndmanage import settings
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
if __name__ == '__main__':
node = LndNode()
listchannels = ListChannels(node)
node.print_status()
print_channels_rebalance(node, unbalancedness_greater_than=_settings.UNBALANCED_CHANNEL)
listchannels.print_channels_unbalanced(
unbalancedness=settings.UNBALANCED_CHANNEL, sort_string='alias')

View file

@ -1,365 +1,5 @@
#!/usr/bin/env python
import argparse
import time
import os
#!/usr/bin/env python3
from lib.node import LndNode
from lib.listchannels import ListChannels
from lib.rebalance import Rebalancer
from lib.exceptions import (
DryRunException,
PaymentTimeOut,
TooExpensive,
RebalanceFailure)
from lib.recommend_nodes import RecommendNodes
from lndmanage.lndmanage import main
import _settings
def range_limited_float_type(unchecked_value):
"""
Type function for argparse - a float within some predefined bounds
:param: unchecked_value: float
"""
try:
value = float(unchecked_value)
except ValueError:
raise argparse.ArgumentTypeError("Must be a floating point number")
if value < 1E-6 or value > 1:
raise argparse.ArgumentTypeError(
"Argument must be < " + str(1E-6) + " and > " + str(1))
return value
def unbalanced_float(x):
"""
Checks if the value is a valid unbalancedness between [-1 ... 1]
"""
x = float(x)
if x < -1.0 or x > 1.0:
raise argparse.ArgumentTypeError(f"{x} not in range [-1.0, 1.0]")
return x
class Parser(object):
def __init__(self):
# setup the command line parser
self.parser = argparse.ArgumentParser(
prog='lndmanage.py',
description='Lightning network daemon channel management tool.')
self.parser.add_argument(
'--loglevel', default='INFO', choices=['INFO', 'DEBUG'])
subparsers = self.parser.add_subparsers(dest='cmd')
# cmd: status
self.parser_status = subparsers.add_parser(
'status', help='display node status',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# cmd: listchannels
self.parser_listchannels = subparsers.add_parser(
'listchannels',
help='lists channels with extended information '
'[see also subcommands with -h]',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
listchannels_subparsers = self.parser_listchannels.add_subparsers(
dest='subcmd')
# subcmd: listchannels rebalance
parser_listchannels_rebalance = listchannels_subparsers.add_parser(
'rebalance', help='displays unbalanced channels')
parser_listchannels_rebalance.add_argument(
'--unbalancedness', type=float,
default=_settings.UNBALANCED_CHANNEL,
help='Unbalancedness is a way to express how balanced a '
'channel is, a value between [-1, 1] (a perfectly balanced '
'channel has a value of 0). The flag excludes channels with '
'an absolute unbalancedness smaller than UNBALANCEDNESS.')
parser_listchannels_rebalance.add_argument(
'--sort-by', default='rev_ub', type=str,
help='sort by column (look at description)')
# subcmd: listchannels inactive
parser_listchannels_inactive = listchannels_subparsers.add_parser(
'inactive', help="displays inactive channels")
parser_listchannels_inactive.add_argument(
'--sort-by', default='lup', type=str,
help='sort by column (look at description)')
# subcmd: listchannels forwardings
parser_listchannels_forwardings = listchannels_subparsers.add_parser(
'forwardings',
help="displays channels with forwarding information")
parser_listchannels_forwardings.add_argument(
'--from-days-ago', default=365, type=int,
help='time interval start (days ago)')
parser_listchannels_forwardings.add_argument(
'--to-days-ago', default=0, type=int,
help='time interval end (days ago)')
parser_listchannels_forwardings.add_argument(
'--sort-by', default='f/w', type=str,
help='sort by column (look at description)')
# cmd: rebalance
self.parser_rebalance = subparsers.add_parser(
'rebalance', help='rebalance a channel',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_rebalance.add_argument('channel', type=int,
help='channel_id')
self.parser_rebalance.add_argument(
'--max-fee-sat', type=int, default=20,
help='Sets the maximal fees in satoshis to be paid.')
self.parser_rebalance.add_argument(
'--chunksize', type=float, default=1.0,
help='Specifies if the individual rebalance attempts should be '
'split into smaller relative amounts. This increases success'
' rates, but also increases costs!')
self.parser_rebalance.add_argument(
'--max-fee-rate', type=range_limited_float_type, default=5E-5,
help='Sets the maximal effective fee rate to be paid.'
' The effective fee rate is defined by '
'(base_fee + amt * fee_rate) / amt.')
self.parser_rebalance.add_argument(
'--reckless', help='Execute action in the network.',
action='store_true')
self.parser_rebalance.add_argument(
'--allow-unbalancing',
help=f'Allow channels to get an unbalancedness'
f' up to +-{_settings.UNBALANCED_CHANNEL}.',
action='store_true')
self.parser_rebalance.add_argument(
'--target', help=f'This feature is still experimental! '
f'The unbalancedness target is between [-1, 1]. '
f'A target of -1 leads to a maximal local balance, a target of 0 '
f'to a 50:50 balanced channel and a target of 1 to a maximal '
f'remote balance. Default is a target of 0.',
type=unbalanced_float, default=None)
rebalancing_strategies = ['most-affordable-first',
'lowest-feerate-first', 'match-unbalanced']
self.parser_rebalance.add_argument(
'--strategy',
help=f'Rebalancing strategy.',
choices=rebalancing_strategies, type=str, default=None)
# cmd: circle
self.parser_circle = subparsers.add_parser(
'circle', help='circular self-payment',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_circle.add_argument('channel_from', type=int,
help='channel_from')
self.parser_circle.add_argument('channel_to', type=int,
help='channel_from')
self.parser_circle.add_argument('amt_sat', type=int,
help='amount in satoshis')
self.parser_circle.add_argument(
'--max-fee-sat', type=int, default=20,
help='Sets the maximal fees in satoshis to be paid.')
self.parser_circle.add_argument(
'--max-fee-rate', type=range_limited_float_type, default=5E-5,
help='Sets the maximal effective fee rate to be paid. '
'The effective fee rate is defined by '
'(base_fee + amt * fee_rate) / amt.')
self.parser_circle.add_argument(
'--reckless', help='Execute action in the network.',
action='store_true')
# cmd: recommend-nodes
self.parser_recommend_nodes = subparsers.add_parser(
'recommend-nodes',
help='recommends nodes [see also subcommands with -h]',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_recommend_nodes.add_argument(
'--show-connected', action='store_true', default=False,
help='specifies if already connected nodes should be '
'removed from list')
self.parser_recommend_nodes.add_argument(
'--show-addresses', action='store_true', default=False,
help='specifies if node addresses should be shown')
parser_recommend_nodes_subparsers = \
self.parser_recommend_nodes.add_subparsers(
dest='subcmd')
# TODO: put global options to the
# parent parser (e.g. number of nodes, sort-by flag)
# subcmd: recommend-nodes good-old
parser_recommend_nodes_good_old = \
parser_recommend_nodes_subparsers.add_parser(
'good-old',
help='shows nodes already interacted with but no '
'active channels',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_good_old.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_good_old.add_argument(
'--sort-by', default='tot', type=str,
help="sort by column [abbreviation, e.g. 'tot']")
# subcmd: recommend-nodes flow-analysis
parser_recommend_nodes_flow_analysis = \
parser_recommend_nodes_subparsers.add_parser(
'flow-analysis', help='recommends nodes from a flow analysis',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_flow_analysis.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_flow_analysis.add_argument(
'--forwarding-events', default=200, type=int,
help='sets the number of forwarding events in the flow analysis')
parser_recommend_nodes_flow_analysis.add_argument(
'--inwards', action='store_true',
help='if True, inward-flowing nodes are displayed '
'instead of outward-flowing nodes')
parser_recommend_nodes_flow_analysis.add_argument(
'--sort-by', default='weight', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
# subcmd: recommend-nodes external_source
parser_recommend_nodes_external_source = \
parser_recommend_nodes_subparsers.add_parser(
'external-source', help='recommends nodes from a given file/url',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_external_source.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_external_source.add_argument(
'--source', type=str,
default='https://github.com/lightningnetworkstores/'
'lightningnetworkstores.github.io/raw/master/sites.json',
help='url/file to be analyzed')
parser_recommend_nodes_external_source.add_argument(
'--distributing-nodes', action='store_true',
help='if True, distributing nodes are '
'displayed instead of the bare nodes')
parser_recommend_nodes_external_source.add_argument(
'--sort-by', default='cpc', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
# subcmd: recommend-nodes channel-openings
parser_recommend_nodes_channel_openings = \
parser_recommend_nodes_subparsers.add_parser(
'channel-openings',
help='recommends nodes from recent channel openings',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_channel_openings.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_channel_openings.add_argument(
'--from-days-ago', type=int,
default=30,
help='channel openings starting from a time frame days ago')
parser_recommend_nodes_channel_openings.add_argument(
'--sort-by', default='msteady', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
def parse_arguments(self):
return self.parser.parse_args()
def main():
parser = Parser()
args = parser.parse_arguments()
# print(args)
if args.cmd is None:
parser.parser.print_help()
return 0
# program execution
if args.loglevel:
# update the loglevel of the stdout handler to the user choice
logger.handlers[0].setLevel(args.loglevel)
# config.ini is expected to be in root directory
root_dir = os.path.dirname(os.path.realpath(__file__))
config_file = os.path.join(root_dir, 'config.ini')
node = LndNode(config_file=config_file)
if args.cmd == 'status':
node.print_status()
elif args.cmd == 'listchannels':
listchannels = ListChannels(node)
if not args.subcmd:
listchannels.print_all_channels('rev_alias')
if args.subcmd == 'rebalance':
listchannels.print_channels_unbalanced(
args.unbalancedness, sort_string=args.sort_by)
elif args.subcmd == 'inactive':
listchannels.print_channels_inactive(
sort_string=args.sort_by)
elif args.subcmd == 'forwardings':
# convert time interval into unix timestamp
time_from = time.time() - args.from_days_ago * 24 * 60 * 60
time_to = time.time() - args.to_days_ago * 24 * 60 * 60
listchannels.print_channels_forwardings(
time_interval_start=time_from, time_interval_end=time_to,
sort_string=args.sort_by)
elif args.cmd == 'rebalance':
if args.target:
logger.warning("Warning: Target is set, this is still an "
"experimental feature.")
rebalancer = Rebalancer(node, args.max_fee_rate, args.max_fee_sat)
try:
rebalancer.rebalance(
args.channel, dry=not args.reckless, chunksize=args.chunksize,
target=args.target, allow_unbalancing=args.allow_unbalancing,
strategy=args.strategy)
except RebalanceFailure as e:
logger.error(f"Error: {e}")
elif args.cmd == 'circle':
rebalancer = Rebalancer(node, args.max_fee_rate, args.max_fee_sat)
invoice_r_hash = node.get_rebalance_invoice(memo='circular payment')
try:
rebalancer.rebalance_two_channels(
args.channel_from, args.channel_to,
args.amt_sat, invoice_r_hash, args.max_fee_sat,
dry=not args.reckless)
except DryRunException:
logger.info("This was just a dry run.")
except TooExpensive:
logger.error("Payment failed. This is likely due to a too low "
"default --max-fee-rate.")
except PaymentTimeOut:
logger.error("Payment failed because the payment timed out. "
"This is an unresolved issue.")
elif args.cmd == 'recommend-nodes':
if not args.subcmd:
parser.parser_recommend_nodes.print_help()
return 0
recommend_nodes = RecommendNodes(
node, show_connected=args.show_connected,
show_addresses=args.show_addresses)
if args.subcmd == 'good-old':
recommend_nodes.print_good_old(number_of_nodes=args.nnodes,
sort_by=args.sort_by)
elif args.subcmd == 'flow-analysis':
recommend_nodes.print_flow_analysis(
out_direction=(not args.inwards),
number_of_nodes=args.nnodes,
forwarding_events=args.forwarding_events,
sort_by=args.sort_by)
elif args.subcmd == 'external-source':
recommend_nodes.print_external_source(
args.source, distributing_nodes=args.distributing_nodes,
number_of_nodes=args.nnodes, sort_by=args.sort_by)
elif args.subcmd == 'channel-openings':
recommend_nodes.print_channel_openings(
from_days_ago=args.from_days_ago,
number_of_nodes=args.nnodes, sort_by=args.sort_by)
if __name__ == '__main__':
import logging.config
logging.config.dictConfig(_settings.logger_config)
logger = logging.getLogger()
main()
main()

View file

@ -1,7 +1,7 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
import grpc_compiled.rpc_pb2 as rpc__pb2
import lndmanage.grpc_compiled.rpc_pb2 as rpc__pb2
class WalletUnlockerStub(object):

View file

@ -1,4 +1,4 @@
import grpc_compiled.rpc_pb2 as ln
import lndmanage.grpc_compiled.rpc_pb2 as ln
import logging
logger = logging.getLogger(__name__)
@ -56,10 +56,11 @@ def set_fees_by_balancedness(
if __name__ == '__main__':
from lib.node import LndNode
from lndmanage.lib.node import LndNode
import logging.config
import _settings
logging.config.dictConfig(_settings.logger_config)
from lndmanage import settings
logging.config.dictConfig(settings.logger_config)
nd = LndNode()

View file

@ -3,9 +3,8 @@ from collections import OrderedDict, defaultdict
import numpy as np
from lib.node import LndNode
import _settings
from lndmanage.lib.node import LndNode
from lndmanage import settings
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
@ -575,7 +574,7 @@ def get_forwarding_statistics_channels(node, time_interval_start,
# TODO: refine 'action_required' by better metric
if c['unbalancedness'] * c['flow_direction'] > 0 and abs(
c['unbalancedness']) > _settings.UNBALANCED_CHANNEL:
c['unbalancedness']) > settings.UNBALANCED_CHANNEL:
c['action_required'] = True
else:
c['action_required'] = False
@ -596,7 +595,7 @@ def get_forwarding_statistics_channels(node, time_interval_start,
c['largest_forwarding_amount_out'] = float('nan')
c['total_forwarding_in'] = float('nan')
c['total_forwarding_out'] = float('nan')
if abs(c['unbalancedness']) > _settings.UNBALANCED_CHANNEL:
if abs(c['unbalancedness']) > settings.UNBALANCED_CHANNEL:
c['action_required'] = True
else:
c['action_required'] = False
@ -608,7 +607,7 @@ def get_forwarding_statistics_channels(node, time_interval_start,
if __name__ == '__main__':
import time
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger()
nd = LndNode()

View file

@ -6,8 +6,7 @@ import math
import logging
from collections import OrderedDict
import _settings
from lib.forwardings import get_forwarding_statistics_channels
from lndmanage.lib.forwardings import get_forwarding_statistics_channels
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
@ -392,7 +391,7 @@ class ListChannels(object):
for k, v in channels.items()}
# only read annotations if config file is given
if self.node.config_file:
config = _settings.read_config(self.node.config_file)
config = settings.read_config(self.node.config_file)
annotations = config['annotations']
else:
annotations = {}
@ -517,9 +516,10 @@ class ListChannels(object):
if __name__ == '__main__':
import logging.config
from lib.node import LndNode
import _settings
logging.config.dictConfig(_settings.logger_config)
from lndmanage.lib.node import LndNode
from lndmanage import settings
logging.config.dictConfig(settings.logger_config)
node_instance = LndNode()
listchannels = ListChannels(node_instance)

View file

@ -1,11 +1,11 @@
import _settings
import os
import time
import pickle
import networkx as nx
from lib.ln_utilities import convert_channel_id_to_short_channel_id
from lndmanage.lib.ln_utilities import convert_channel_id_to_short_channel_id
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -34,16 +34,18 @@ class Network(object):
Checks if networkx and edges dictionary pickles are present. If they are older than
CACHING_RETENTION_MINUTES, make fresh pickles, else read them from the files.
"""
directory = os.path.dirname(__file__)
cache_edges_filename = os.path.join(directory, '..', 'cache', 'graph.gpickle')
cache_graph_filename = os.path.join(directory, '..', 'cache', 'edges.gpickle')
cache_edges_filename = os.path.join(
settings.home_dir, 'cache', 'graph.gpickle')
cache_graph_filename = os.path.join(
settings.home_dir, 'cache', 'edges.gpickle')
try:
timestamp_graph = os.path.getmtime(cache_graph_filename)
except FileNotFoundError:
timestamp_graph = 0 # set very old timestamp
if timestamp_graph < time.time() - _settings.CACHING_RETENTION_MINUTES * 60: # old graph in file
if timestamp_graph < time.time() - settings.CACHING_RETENTION_MINUTES * 60: # old graph in file
logger.info(f"Saved graph is too old. Fetching new one.")
self.set_graph_and_edges()
nx.write_gpickle(self.graph, cache_graph_filename)
@ -247,9 +249,9 @@ class Network(object):
if __name__ == '__main__':
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
from lib.node import LndNode
from lndmanage.lib.node import LndNode
nd = LndNode()
print(f"Graph size: {nd.network.graph.size()}")
print(f"Number of channels: {len(nd.network.edges.keys())}")

View file

@ -3,9 +3,8 @@ from collections import defaultdict
import numpy as np
import networkx as nx
import _settings
from lib.ln_utilities import convert_channel_id_to_short_channel_id
from lndmanage.lib.ln_utilities import convert_channel_id_to_short_channel_id
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -140,7 +139,7 @@ class NetworkAnalysis(object):
edges = self.node.network.graph.edges(node_pub_key)
for e in edges:
degree_neighbor = self.node.network.graph.degree(e[1]) // 2
if degree_neighbor <= _settings.NUMBER_CHANNELS_DEFINING_USER_NODE:
if degree_neighbor <= settings.NUMBER_CHANNELS_DEFINING_USER_NODE:
connected_end_nodes += 1
return connected_end_nodes
@ -305,10 +304,10 @@ class NetworkAnalysis(object):
if __name__ == '__main__':
from lib.node import LndNode
from lndmanage.lib.node import LndNode
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
nd = LndNode()
network_analysis = NetworkAnalysis(nd)

View file

@ -6,20 +6,20 @@ from collections import OrderedDict
import grpc
from grpc._channel import _Rendezvous
import grpc_compiled.rpc_pb2 as ln
import grpc_compiled.rpc_pb2_grpc as lnrpc
from google.protobuf.json_format import MessageToDict
import _settings
from lib.network import Network
from lib.utilities import convert_dictionary_number_strings_to_ints
from lib.ln_utilities import (extract_short_channel_id_from_string,
convert_short_channel_id_to_channel_id,
convert_channel_id_to_short_channel_id,
channel_unbalancedness_and_commit_fee)
from lib.exceptions import PaymentTimeOut, NoRouteError
import lndmanage.grpc_compiled.rpc_pb2 as ln
import lndmanage.grpc_compiled.rpc_pb2_grpc as lnrpc
from lndmanage.lib.network import Network
from lndmanage.lib.exceptions import PaymentTimeOut, NoRouteError
from lndmanage.lib.utilities import convert_dictionary_number_strings_to_ints
from lndmanage.lib.ln_utilities import (
extract_short_channel_id_from_string,
convert_short_channel_id_to_channel_id,
convert_channel_id_to_short_channel_id,
channel_unbalancedness_and_commit_fee
)
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -92,7 +92,7 @@ class LndNode(Node):
raise ValueError('if lnd_home is given, lnd_host must be given also')
lnd_host = self.lnd_host
else:
config = _settings.read_config(self.config_file)
config = settings.read_config(self.config_file)
cert_file = os.path.expanduser(config['network']['tls_cert_file'])
macaroon_file = os.path.expanduser(config['network']['admin_macaroon_file'])
lnd_host = config['network']['lnd_grpc_host']

View file

@ -1,6 +1,8 @@
import networkx as nx
import queue
import _settings
import networkx as nx
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -22,7 +24,7 @@ def ksp_discard_high_cost_paths(graph, source, target, num_k, weight):
routes, route_costs = ksp(graph, source, target, num_k, weight)
logger.debug("Approximate costs [msat] of routes:")
for r, rc in zip(routes, route_costs):
if rc < _settings.PENALTY:
if rc < settings.PENALTY:
logger.debug(f" {rc} msat: {r}")
final_routes.append(r)
return final_routes

View file

@ -1,4 +1,4 @@
import _settings
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -29,15 +29,15 @@ class ChannelRater(object):
"""
node_penalty = 0
if u in self.bad_nodes or v in self.bad_nodes:
node_penalty = _settings.PENALTY
node_penalty = settings.PENALTY
costs = [node_penalty + self.channel_weight(eattr, amt_msat) for eattr in e.values()]
return min(costs)
def channel_weight(self, e, amt_msat):
long_path_penalty = 0
if _settings.PREFER_SHORT_PATHS:
long_path_penalty = _settings.LONG_PATH_PENALTY_MSAT
if settings.PREFER_SHORT_PATHS:
long_path_penalty = settings.LONG_PATH_PENALTY_MSAT
cost = (long_path_penalty
+ e.get('fees')['fee_base_msat']
@ -81,7 +81,7 @@ class ChannelRater(object):
"""
# TODO: consider also direction
if channel_id in self.get_bad_channels():
return _settings.PENALTY
return settings.PENALTY
else:
return 0
@ -95,7 +95,7 @@ class ChannelRater(object):
:return: penalty
"""
if capacity_sat < 0.50 * amt_msat // 1000 :
return _settings.PENALTY
return settings.PENALTY
else:
return 0
@ -108,6 +108,6 @@ class ChannelRater(object):
:return: high penalty
"""
if policy['disabled']:
return _settings.PENALTY
return settings.PENALTY
else:
return 0

View file

@ -1,15 +1,15 @@
import logging
import math
import _settings
from lib.exceptions import (
from lndmanage.lib.routing import Router
from lndmanage.lib.exceptions import (
NoRouteError,
RebalanceFailure,
DryRunException,
PaymentTimeOut,
TooExpensive
)
from lib.routing import Router
from lndmanage import settings
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
@ -79,7 +79,7 @@ class Rebalancer(object):
while True:
# only attempt a fixed number of times
count += 1
if count > _settings.REBALANCING_TRIALS:
if count > settings.REBALANCING_TRIALS:
raise RebalanceFailure
routes = self.router.get_routes_for_rebalancing(
@ -229,7 +229,7 @@ class Rebalancer(object):
# the rebalance candidate channel (allows for unbalancing a channel)
if allow_unbalancing:
# target is a bit into the non-ideal direction
lower_bound = -_settings.UNBALANCED_CHANNEL
lower_bound = -settings.UNBALANCED_CHANNEL
else:
# target is perfect balance
lower_bound = 0
@ -246,7 +246,7 @@ class Rebalancer(object):
if allow_unbalancing:
c['amt_affordable'] = int(
c['amt_to_balanced'] +
direction * _settings.UNBALANCED_CHANNEL
direction * settings.UNBALANCED_CHANNEL
* c['capacity'] / 2)
else:
c['amt_affordable'] = c['amt_to_balanced']

View file

@ -5,13 +5,12 @@ from collections import OrderedDict
import urllib.request
from urllib.error import HTTPError
import _settings
from lib.forwardings import ForwardingAnalyzer
from lib.network_info import NetworkAnalysis
from lndmanage.lib.forwardings import ForwardingAnalyzer
from lndmanage.lib.network_info import NetworkAnalysis
from lndmanage import settings
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger(__name__)
# define printing shortcuts, alignments, and cutoffs
@ -309,7 +308,7 @@ class RecommendNodes(object):
if exclude_hubs: # we exclude hubs in the neighbor analysis
nodes_list = [
n for n in nodes.keys()
if self.node.network.number_channels(n) < _settings.NUMBER_CHANNELS_DEFINING_HUB
if self.node.network.number_channels(n) < settings.NUMBER_CHANNELS_DEFINING_HUB
]
else:
nodes_list = nodes.keys()
@ -405,7 +404,7 @@ class RecommendNodes(object):
node_new['distance'] = \
self.network_analysis.distance(self.node.pub_key, k)
if exclude_hubs:
if node_new['number_channels'] < _settings.NUMBER_CHANNELS_DEFINING_HUB:
if node_new['number_channels'] < settings.NUMBER_CHANNELS_DEFINING_HUB:
nodes_new[k] = node_new
else:
nodes_new[k] = node_new
@ -416,7 +415,7 @@ class RecommendNodes(object):
if exclude_hubs:
logger.info(f"Excluding hubs (defined by number of channels > "
f"{_settings.NUMBER_CHANNELS_DEFINING_HUB}).")
f"{settings.NUMBER_CHANNELS_DEFINING_HUB}).")
if not self.show_connected:
nodes_new = self.exclude_connected_nodes(nodes_new)
@ -519,6 +518,6 @@ class RecommendNodes(object):
if __name__ == '__main__':
from lib.node import LndNode
from lndmanage.lib.node import LndNode
nd = LndNode()
rn = RecommendNodes(nd)

View file

@ -1,8 +1,7 @@
import _settings
from lib.pathfinding import ksp_discard_high_cost_paths
from lib.exceptions import RouteWithTooSmallCapacity, NoRouteError
from lib.rating import ChannelRater
from lndmanage.lib.rating import ChannelRater
from lndmanage.lib.exceptions import RouteWithTooSmallCapacity, NoRouteError
from lndmanage.lib.pathfinding import ksp_discard_high_cost_paths
from lndmanage import settings
import logging
logger = logging.getLogger(__name__)
@ -287,6 +286,6 @@ class Router(object):
if __name__ == '__main__':
import logging.config
logging.config.dictConfig(_settings.logger_config)
from lib.node import LndNode
logging.config.dictConfig(settings.logger_config)
from lndmanage.lib.node import LndNode
nd = LndNode()

364
lndmanage/lndmanage.py Executable file
View file

@ -0,0 +1,364 @@
#!/usr/bin/env python
import argparse
import time
import os
from lndmanage.lib.node import LndNode
from lndmanage.lib.listchannels import ListChannels
from lndmanage.lib.rebalance import Rebalancer
from lndmanage.lib.recommend_nodes import RecommendNodes
from lndmanage.lib.exceptions import (
DryRunException,
PaymentTimeOut,
TooExpensive,
RebalanceFailure
)
from lndmanage import settings
import logging.config
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger()
def range_limited_float_type(unchecked_value):
"""
Type function for argparse - a float within some predefined bounds
:param: unchecked_value: float
"""
try:
value = float(unchecked_value)
except ValueError:
raise argparse.ArgumentTypeError("Must be a floating point number")
if value < 1E-6 or value > 1:
raise argparse.ArgumentTypeError(
"Argument must be < " + str(1E-6) + " and > " + str(1))
return value
def unbalanced_float(x):
"""
Checks if the value is a valid unbalancedness between [-1 ... 1]
"""
x = float(x)
if x < -1.0 or x > 1.0:
raise argparse.ArgumentTypeError(f"{x} not in range [-1.0, 1.0]")
return x
class Parser(object):
def __init__(self):
# setup the command line parser
self.parser = argparse.ArgumentParser(
prog='lndmanage.py',
description='Lightning network daemon channel management tool.')
self.parser.add_argument(
'--loglevel', default='INFO', choices=['INFO', 'DEBUG'])
subparsers = self.parser.add_subparsers(dest='cmd')
# cmd: status
self.parser_status = subparsers.add_parser(
'status', help='display node status',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# cmd: listchannels
self.parser_listchannels = subparsers.add_parser(
'listchannels',
help='lists channels with extended information '
'[see also subcommands with -h]',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
listchannels_subparsers = self.parser_listchannels.add_subparsers(
dest='subcmd')
# subcmd: listchannels rebalance
parser_listchannels_rebalance = listchannels_subparsers.add_parser(
'rebalance', help='displays unbalanced channels')
parser_listchannels_rebalance.add_argument(
'--unbalancedness', type=float,
default=settings.UNBALANCED_CHANNEL,
help='Unbalancedness is a way to express how balanced a '
'channel is, a value between [-1, 1] (a perfectly balanced '
'channel has a value of 0). The flag excludes channels with '
'an absolute unbalancedness smaller than UNBALANCEDNESS.')
parser_listchannels_rebalance.add_argument(
'--sort-by', default='rev_ub', type=str,
help='sort by column (look at description)')
# subcmd: listchannels inactive
parser_listchannels_inactive = listchannels_subparsers.add_parser(
'inactive', help="displays inactive channels")
parser_listchannels_inactive.add_argument(
'--sort-by', default='lup', type=str,
help='sort by column (look at description)')
# subcmd: listchannels forwardings
parser_listchannels_forwardings = listchannels_subparsers.add_parser(
'forwardings',
help="displays channels with forwarding information")
parser_listchannels_forwardings.add_argument(
'--from-days-ago', default=365, type=int,
help='time interval start (days ago)')
parser_listchannels_forwardings.add_argument(
'--to-days-ago', default=0, type=int,
help='time interval end (days ago)')
parser_listchannels_forwardings.add_argument(
'--sort-by', default='f/w', type=str,
help='sort by column (look at description)')
# cmd: rebalance
self.parser_rebalance = subparsers.add_parser(
'rebalance', help='rebalance a channel',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_rebalance.add_argument('channel', type=int,
help='channel_id')
self.parser_rebalance.add_argument(
'--max-fee-sat', type=int, default=20,
help='Sets the maximal fees in satoshis to be paid.')
self.parser_rebalance.add_argument(
'--chunksize', type=float, default=1.0,
help='Specifies if the individual rebalance attempts should be '
'split into smaller relative amounts. This increases success'
' rates, but also increases costs!')
self.parser_rebalance.add_argument(
'--max-fee-rate', type=range_limited_float_type, default=5E-5,
help='Sets the maximal effective fee rate to be paid.'
' The effective fee rate is defined by '
'(base_fee + amt * fee_rate) / amt.')
self.parser_rebalance.add_argument(
'--reckless', help='Execute action in the network.',
action='store_true')
self.parser_rebalance.add_argument(
'--allow-unbalancing',
help=f'Allow channels to get an unbalancedness'
f' up to +-{settings.UNBALANCED_CHANNEL}.',
action='store_true')
self.parser_rebalance.add_argument(
'--target', help=f'This feature is still experimental! '
f'The unbalancedness target is between [-1, 1]. '
f'A target of -1 leads to a maximal local balance, a target of 0 '
f'to a 50:50 balanced channel and a target of 1 to a maximal '
f'remote balance. Default is a target of 0.',
type=unbalanced_float, default=None)
rebalancing_strategies = ['most-affordable-first',
'lowest-feerate-first', 'match-unbalanced']
self.parser_rebalance.add_argument(
'--strategy',
help=f'Rebalancing strategy.',
choices=rebalancing_strategies, type=str, default=None)
# cmd: circle
self.parser_circle = subparsers.add_parser(
'circle', help='circular self-payment',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_circle.add_argument('channel_from', type=int,
help='channel_from')
self.parser_circle.add_argument('channel_to', type=int,
help='channel_from')
self.parser_circle.add_argument('amt_sat', type=int,
help='amount in satoshis')
self.parser_circle.add_argument(
'--max-fee-sat', type=int, default=20,
help='Sets the maximal fees in satoshis to be paid.')
self.parser_circle.add_argument(
'--max-fee-rate', type=range_limited_float_type, default=5E-5,
help='Sets the maximal effective fee rate to be paid. '
'The effective fee rate is defined by '
'(base_fee + amt * fee_rate) / amt.')
self.parser_circle.add_argument(
'--reckless', help='Execute action in the network.',
action='store_true')
# cmd: recommend-nodes
self.parser_recommend_nodes = subparsers.add_parser(
'recommend-nodes',
help='recommends nodes [see also subcommands with -h]',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
self.parser_recommend_nodes.add_argument(
'--show-connected', action='store_true', default=False,
help='specifies if already connected nodes should be '
'removed from list')
self.parser_recommend_nodes.add_argument(
'--show-addresses', action='store_true', default=False,
help='specifies if node addresses should be shown')
parser_recommend_nodes_subparsers = \
self.parser_recommend_nodes.add_subparsers(
dest='subcmd')
# TODO: put global options to the
# parent parser (e.g. number of nodes, sort-by flag)
# subcmd: recommend-nodes good-old
parser_recommend_nodes_good_old = \
parser_recommend_nodes_subparsers.add_parser(
'good-old',
help='shows nodes already interacted with but no '
'active channels',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_good_old.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_good_old.add_argument(
'--sort-by', default='tot', type=str,
help="sort by column [abbreviation, e.g. 'tot']")
# subcmd: recommend-nodes flow-analysis
parser_recommend_nodes_flow_analysis = \
parser_recommend_nodes_subparsers.add_parser(
'flow-analysis', help='recommends nodes from a flow analysis',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_flow_analysis.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_flow_analysis.add_argument(
'--forwarding-events', default=200, type=int,
help='sets the number of forwarding events in the flow analysis')
parser_recommend_nodes_flow_analysis.add_argument(
'--inwards', action='store_true',
help='if True, inward-flowing nodes are displayed '
'instead of outward-flowing nodes')
parser_recommend_nodes_flow_analysis.add_argument(
'--sort-by', default='weight', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
# subcmd: recommend-nodes external_source
parser_recommend_nodes_external_source = \
parser_recommend_nodes_subparsers.add_parser(
'external-source', help='recommends nodes from a given file/url',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_external_source.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_external_source.add_argument(
'--source', type=str,
default='https://github.com/lightningnetworkstores/'
'lightningnetworkstores.github.io/raw/master/sites.json',
help='url/file to be analyzed')
parser_recommend_nodes_external_source.add_argument(
'--distributing-nodes', action='store_true',
help='if True, distributing nodes are '
'displayed instead of the bare nodes')
parser_recommend_nodes_external_source.add_argument(
'--sort-by', default='cpc', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
# subcmd: recommend-nodes channel-openings
parser_recommend_nodes_channel_openings = \
parser_recommend_nodes_subparsers.add_parser(
'channel-openings',
help='recommends nodes from recent channel openings',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_recommend_nodes_channel_openings.add_argument(
'--nnodes', default=20, type=int,
help='sets the number of nodes displayed')
parser_recommend_nodes_channel_openings.add_argument(
'--from-days-ago', type=int,
default=30,
help='channel openings starting from a time frame days ago')
parser_recommend_nodes_channel_openings.add_argument(
'--sort-by', default='msteady', type=str,
help="sort by column [abbreviation, e.g. 'nchan']")
def parse_arguments(self):
return self.parser.parse_args()
def main():
parser = Parser()
args = parser.parse_arguments()
# print(args)
if args.cmd is None:
parser.parser.print_help()
return 0
# program execution
if args.loglevel:
# update the loglevel of the stdout handler to the user choice
logger.handlers[0].setLevel(args.loglevel)
# config.ini is expected to be in home/.lndmanage directory
config_file = os.path.join(settings.home_dir, 'config.ini')
print(config_file)
node = LndNode(config_file=config_file)
if args.cmd == 'status':
node.print_status()
elif args.cmd == 'listchannels':
listchannels = ListChannels(node)
if not args.subcmd:
listchannels.print_all_channels('rev_alias')
if args.subcmd == 'rebalance':
listchannels.print_channels_unbalanced(
args.unbalancedness, sort_string=args.sort_by)
elif args.subcmd == 'inactive':
listchannels.print_channels_inactive(
sort_string=args.sort_by)
elif args.subcmd == 'forwardings':
# convert time interval into unix timestamp
time_from = time.time() - args.from_days_ago * 24 * 60 * 60
time_to = time.time() - args.to_days_ago * 24 * 60 * 60
listchannels.print_channels_forwardings(
time_interval_start=time_from, time_interval_end=time_to,
sort_string=args.sort_by)
elif args.cmd == 'rebalance':
if args.target:
logger.warning("Warning: Target is set, this is still an "
"experimental feature.")
rebalancer = Rebalancer(node, args.max_fee_rate, args.max_fee_sat)
try:
rebalancer.rebalance(
args.channel, dry=not args.reckless, chunksize=args.chunksize,
target=args.target, allow_unbalancing=args.allow_unbalancing,
strategy=args.strategy)
except RebalanceFailure as e:
logger.error(f"Error: {e}")
elif args.cmd == 'circle':
rebalancer = Rebalancer(node, args.max_fee_rate, args.max_fee_sat)
invoice_r_hash = node.get_rebalance_invoice(memo='circular payment')
try:
rebalancer.rebalance_two_channels(
args.channel_from, args.channel_to,
args.amt_sat, invoice_r_hash, args.max_fee_sat,
dry=not args.reckless)
except DryRunException:
logger.info("This was just a dry run.")
except TooExpensive:
logger.error("Payment failed. This is likely due to a too low "
"default --max-fee-rate.")
except PaymentTimeOut:
logger.error("Payment failed because the payment timed out. "
"This is an unresolved issue.")
elif args.cmd == 'recommend-nodes':
if not args.subcmd:
parser.parser_recommend_nodes.print_help()
return 0
recommend_nodes = RecommendNodes(
node, show_connected=args.show_connected,
show_addresses=args.show_addresses)
if args.subcmd == 'good-old':
recommend_nodes.print_good_old(number_of_nodes=args.nnodes,
sort_by=args.sort_by)
elif args.subcmd == 'flow-analysis':
recommend_nodes.print_flow_analysis(
out_direction=(not args.inwards),
number_of_nodes=args.nnodes,
forwarding_events=args.forwarding_events,
sort_by=args.sort_by)
elif args.subcmd == 'external-source':
recommend_nodes.print_external_source(
args.source, distributing_nodes=args.distributing_nodes,
number_of_nodes=args.nnodes, sort_by=args.sort_by)
elif args.subcmd == 'channel-openings':
recommend_nodes.print_channel_openings(
from_days_ago=args.from_days_ago,
number_of_nodes=args.nnodes, sort_by=args.sort_by)
if __name__ == '__main__':
main()

View file

@ -1,5 +1,20 @@
import os
import configparser
# determine directories
settings_dir = os.path.dirname(os.path.realpath(__file__))
# determine home folder, prioritized by environment variable LNDMANAGE_HOME
environ_home = os.environ.get('LNDMANAGE_HOME', None)
if environ_home:
if not os.path.exists(environ_home):
raise EnvironmentError(
'LNDMANAGE_HOME is not set to a path that exists')
home_dir = environ_home
else:
home_dir = os.path.join(settings_dir, '../')
# -------- graph settings --------
# accepted age of the network graph
CACHING_RETENTION_MINUTES = 30

View file

@ -6,11 +6,11 @@ from unittest import TestCase
from lnregtest.lib.network import RegtestNetwork
import _settings
from lib.node import LndNode
from lib.listchannels import ListChannels
from lib.rebalance import Rebalancer
from lib.exceptions import RebalanceFailure, TooExpensive
from lndmanage import settings
from lndmanage.lib.node import LndNode
from lndmanage.lib.listchannels import ListChannels
from lndmanage.lib.rebalance import Rebalancer
from lndmanage.lib.exceptions import RebalanceFailure, TooExpensive
from test.testing_common import (
bin_dir,
@ -20,7 +20,7 @@ from test.testing_common import (
)
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.handlers[0].setLevel(logging.DEBUG)

View file

@ -1,5 +1,5 @@
from unittest import TestCase
from lib.ln_utilities import channel_unbalancedness_and_commit_fee
from lndmanage.lib.ln_utilities import channel_unbalancedness_and_commit_fee
class LnUtilityTest(TestCase):

View file

@ -6,11 +6,11 @@ from unittest import TestCase
from lnregtest.lib.network import RegtestNetwork
import _settings
from lib.node import LndNode
from lib.listchannels import ListChannels
from lib.rebalance import Rebalancer
from lib.ln_utilities import channel_unbalancedness_and_commit_fee
from lndmanage import settings
from lndmanage.lib.node import LndNode
from lndmanage.lib.listchannels import ListChannels
from lndmanage.lib.rebalance import Rebalancer
from lndmanage.lib.ln_utilities import channel_unbalancedness_and_commit_fee
from test.testing_common import (
bin_dir,
@ -19,7 +19,7 @@ from test.testing_common import (
SLEEP_SEC_AFTER_REBALANCING)
import logging.config
logging.config.dictConfig(_settings.logger_config)
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.handlers[0].setLevel(logging.DEBUG)

View file

@ -1,7 +1,7 @@
import os
import _settings
from lndmanage import settings
_settings.CACHING_RETENTION_MINUTES = 0
settings.CACHING_RETENTION_MINUTES = 0
# constants for testing
SLEEP_SEC_AFTER_REBALANCING = 2
@ -15,6 +15,6 @@ test_data_dir = os.path.join(test_dir, 'test_data')
test_graphs_paths = {
'small_star_ring': os.path.join(
graph_definitions_dir, 'star_ring_3_liquid.py'),
graph_definitions_dir, 'small_star_ring.py'),
}