mirror of
https://github.com/bitromortac/lndmanage.git
synced 2026-08-13 12:33:37 +02:00
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:
parent
b8602b1d73
commit
60785f4bcc
32 changed files with 489 additions and 466 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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__':
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
366
lndmanage.py
366
lndmanage.py
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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())}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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']
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
364
lndmanage/lndmanage.py
Executable 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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue