Merge branch 'master' into curly60e-patch-115

This commit is contained in:
curly60e 2020-10-12 20:31:08 -03:00 committed by GitHub
commit 8fe84a0063
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 13808 additions and 0 deletions

1448
grpc_generated/router_pb2.py Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,184 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
from grpc_generated import router_pb2 as router__pb2
class RouterStub(object):
# missing associated documentation comment in .proto file
pass
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SendPayment = channel.unary_stream(
'/routerrpc.Router/SendPayment',
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.TrackPayment = channel.unary_stream(
'/routerrpc.Router/TrackPayment',
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.EstimateRouteFee = channel.unary_unary(
'/routerrpc.Router/EstimateRouteFee',
request_serializer=router__pb2.RouteFeeRequest.SerializeToString,
response_deserializer=router__pb2.RouteFeeResponse.FromString,
)
self.SendToRoute = channel.unary_unary(
'/routerrpc.Router/SendToRoute',
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
response_deserializer=router__pb2.SendToRouteResponse.FromString,
)
self.ResetMissionControl = channel.unary_unary(
'/routerrpc.Router/ResetMissionControl',
request_serializer=router__pb2.ResetMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.ResetMissionControlResponse.FromString,
)
self.QueryMissionControl = channel.unary_unary(
'/routerrpc.Router/QueryMissionControl',
request_serializer=router__pb2.QueryMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.QueryMissionControlResponse.FromString,
)
self.QueryProbability = channel.unary_unary(
'/routerrpc.Router/QueryProbability',
request_serializer=router__pb2.QueryProbabilityRequest.SerializeToString,
response_deserializer=router__pb2.QueryProbabilityResponse.FromString,
)
self.BuildRoute = channel.unary_unary(
'/routerrpc.Router/BuildRoute',
request_serializer=router__pb2.BuildRouteRequest.SerializeToString,
response_deserializer=router__pb2.BuildRouteResponse.FromString,
)
class RouterServicer(object):
# missing associated documentation comment in .proto file
pass
def SendPayment(self, request, context):
"""*
SendPayment attempts to route a payment described by the passed
PaymentRequest to the final destination. The call returns a stream of
payment status updates.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TrackPayment(self, request, context):
"""*
TrackPayment returns an update stream for the payment identified by the
payment hash.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def EstimateRouteFee(self, request, context):
"""*
EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it
may cost to send an HTLC to the target end destination.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SendToRoute(self, request, context):
"""*
SendToRoute attempts to make a payment via the specified route. This method
differs from SendPayment in that it allows users to specify a full route
manually. This can be used for things like rebalancing, and atomic swaps.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ResetMissionControl(self, request, context):
"""*
ResetMissionControl clears all mission control state and starts with a clean
slate.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def QueryMissionControl(self, request, context):
"""*
QueryMissionControl exposes the internal mission control state to callers.
It is a development feature.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def QueryProbability(self, request, context):
"""*
QueryProbability returns the current success probability estimate for a
given node pair and amount.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def BuildRoute(self, request, context):
"""*
BuildRoute builds a fully specified route based on a list of hop public
keys. It retrieves the relevant channel policies from the graph in order to
calculate the correct fees and time locks.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_RouterServicer_to_server(servicer, server):
rpc_method_handlers = {
'SendPayment': grpc.unary_stream_rpc_method_handler(
servicer.SendPayment,
request_deserializer=router__pb2.SendPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
'TrackPayment': grpc.unary_stream_rpc_method_handler(
servicer.TrackPayment,
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
'EstimateRouteFee': grpc.unary_unary_rpc_method_handler(
servicer.EstimateRouteFee,
request_deserializer=router__pb2.RouteFeeRequest.FromString,
response_serializer=router__pb2.RouteFeeResponse.SerializeToString,
),
'SendToRoute': grpc.unary_unary_rpc_method_handler(
servicer.SendToRoute,
request_deserializer=router__pb2.SendToRouteRequest.FromString,
response_serializer=router__pb2.SendToRouteResponse.SerializeToString,
),
'ResetMissionControl': grpc.unary_unary_rpc_method_handler(
servicer.ResetMissionControl,
request_deserializer=router__pb2.ResetMissionControlRequest.FromString,
response_serializer=router__pb2.ResetMissionControlResponse.SerializeToString,
),
'QueryMissionControl': grpc.unary_unary_rpc_method_handler(
servicer.QueryMissionControl,
request_deserializer=router__pb2.QueryMissionControlRequest.FromString,
response_serializer=router__pb2.QueryMissionControlResponse.SerializeToString,
),
'QueryProbability': grpc.unary_unary_rpc_method_handler(
servicer.QueryProbability,
request_deserializer=router__pb2.QueryProbabilityRequest.FromString,
response_serializer=router__pb2.QueryProbabilityResponse.SerializeToString,
),
'BuildRoute': grpc.unary_unary_rpc_method_handler(
servicer.BuildRoute,
request_deserializer=router__pb2.BuildRouteRequest.FromString,
response_serializer=router__pb2.BuildRouteResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'routerrpc.Router', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))

10187
grpc_generated/rpc_pb2.py Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

108
lnd.py Normal file
View file

@ -0,0 +1,108 @@
import base64
import os
from os.path import expanduser
import codecs
import grpc
import sys
from grpc_generated import router_pb2_grpc as lnrouterrpc, router_pb2 as lnrouter, rpc_pb2_grpc as lnrpc, rpc_pb2 as ln
MESSAGE_SIZE_MB = 50 * 1024 * 1024
def debug(message):
sys.stderr.write(message + "\n")
class Lnd:
def __init__(self, lnd_dir, server):
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
lnd_dir = expanduser(lnd_dir)
combined_credentials = self.get_credentials(lnd_dir)
channel_options = [
('grpc.max_message_length', MESSAGE_SIZE_MB),
('grpc.max_receive_message_length', MESSAGE_SIZE_MB)
]
grpc_channel = grpc.secure_channel(server, combined_credentials, channel_options)
self.stub = lnrpc.LightningStub(grpc_channel)
self.router_stub = lnrouterrpc.RouterStub(grpc_channel)
self.graph = None
self.info = None
self.channels = None
@staticmethod
def get_credentials(lnd_dir):
tls_certificate = open(lnd_dir + '/tls.cert', 'rb').read()
ssl_credentials = grpc.ssl_channel_credentials(tls_certificate)
macaroon = codecs.encode(open(lnd_dir + '/data/chain/bitcoin/mainnet/admin.macaroon', 'rb').read(), 'hex')
auth_credentials = grpc.metadata_call_credentials(lambda _, callback: callback([('macaroon', macaroon)], None))
combined_credentials = grpc.composite_channel_credentials(ssl_credentials, auth_credentials)
return combined_credentials
def get_info(self):
if self.info is None:
self.info = self.stub.GetInfo(ln.GetInfoRequest())
return self.info
def get_graph(self):
if self.graph is None:
self.graph = self.stub.DescribeGraph(ln.ChannelGraphRequest(include_unannounced=True))
return self.graph
def get_own_pubkey(self):
return self.get_info().identity_pubkey
def get_edges(self):
return self.get_graph().edges
def generate_invoice(self, memo, amount):
invoice_request = ln.Invoice(
memo=memo,
value=amount,
)
add_invoice_response = self.stub.AddInvoice(invoice_request)
return self.decode_payment_request(add_invoice_response.payment_request)
def decode_payment_request(self, payment_request):
request = ln.PayReqString(
pay_req=payment_request,
)
return self.stub.DecodePayReq(request)
def get_channels(self):
if self.channels is None:
request = ln.ListChannelsRequest(
active_only=True,
)
self.channels = self.stub.ListChannels(request).channels
return self.channels
def get_route(self, pub_key, amount, ignored_edges, ignored_nodes, first_hop_channel_id):
if pub_key:
last_hop_pubkey = base64.b16decode(pub_key, True)
else:
last_hop_pubkey = None
request = ln.QueryRoutesRequest(
pub_key=self.get_own_pubkey(),
last_hop_pubkey=last_hop_pubkey,
amt=amount,
ignored_edges=ignored_edges,
ignored_nodes=ignored_nodes,
use_mission_control=True,
outgoing_chan_id=first_hop_channel_id,
)
try:
response = self.stub.QueryRoutes(request)
return response.routes
except:
return None
def send_payment(self, payment_request, route):
request = lnrouter.SendToRouteRequest(route=route)
request.payment_hash = self.hex_string_to_bytes(payment_request.payment_hash)
return self.router_stub.SendToRoute(request)
@staticmethod
def hex_string_to_bytes(hex_string):
decode_hex = codecs.getdecoder("hex_codec")
return decode_hex(hex_string)[0]

153
logic.py Normal file
View file

@ -0,0 +1,153 @@
import sys
from routes import Routes
DEFAULT_BASE_FEE_SAT_MSAT = 1000
DEFAULT_FEE_RATE_MSAT = 0.001
def debug(message):
sys.stderr.write(message + "\n")
def debugnobreak(message):
sys.stderr.write(message)
class Logic:
def __init__(self, lnd, first_hop_channel, last_hop_channel, amount, channel_ratio, excluded, max_fee_factor):
self.lnd = lnd
self.first_hop_channel = first_hop_channel
self.last_hop_channel = last_hop_channel
self.amount = amount
self.channel_ratio = channel_ratio
self.excluded = []
if excluded:
self.excluded = excluded
self.max_fee_factor = max_fee_factor
def rebalance(self):
if self.last_hop_channel:
debug(("Sending {:,} satoshis to rebalance to channel with ID %d"
% self.last_hop_channel.chan_id).format(self.amount))
else:
debug("Sending {:,} satoshis.".format(self.amount))
if self.channel_ratio != 0.5:
debug("Channel ratio used is %d%%" % int(self.channel_ratio * 100))
if self.first_hop_channel:
debug("Forced first channel has ID %d" % self.first_hop_channel.chan_id)
payment_request = self.generate_invoice()
routes = Routes(self.lnd, payment_request, self.first_hop_channel, self.last_hop_channel)
self.initialize_ignored_channels(routes)
tried_routes = []
while routes.has_next():
route = routes.get_next()
success = self.try_route(payment_request, route, routes, tried_routes)
if success:
return True
debug("Could not find any suitable route")
return False
def try_route(self, payment_request, route, routes, tried_routes):
if self.route_is_invalid(route, routes):
return False
tried_routes.append(route)
debug("")
debug("Trying route #%d" % len(tried_routes))
debug(Routes.print_route(route))
response = self.lnd.send_payment(payment_request, route)
is_successful = response.failure.code == 0
if is_successful:
debug("")
debug("")
debug("")
debug("Success! Paid fees: %s sat (%s msat)" % (route.total_fees, route.total_fees_msat))
debug("Successful route:")
debug(Routes.print_route(route))
debug("")
debug("")
debug("")
return True
else:
self.handle_error(response, route, routes)
return False
@staticmethod
def handle_error(response, route, routes):
code = response.failure.code
failure_source_pubkey = Logic.get_failure_source_pubkey(response, route)
if code == 15:
debugnobreak("Temporary channel failure, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
elif code == 18:
debugnobreak("Unknown next peer, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
elif code == 12:
debugnobreak("Fee insufficient, ")
routes.ignore_edge_on_route(failure_source_pubkey, route)
else:
debug(repr(response))
debug("Unknown error code %s" % repr(code))
@staticmethod
def get_failure_source_pubkey(response, route):
if response.failure.failure_source_index == 0:
failure_source_pubkey = route.hops[-1].pub_key
else:
failure_source_pubkey = route.hops[response.failure.failure_source_index - 1].pub_key
return failure_source_pubkey
def route_is_invalid(self, route, routes):
first_hop = route.hops[0]
if self.low_local_ratio_after_sending(first_hop, route.total_amt):
debugnobreak("Low local ratio after sending, ")
routes.ignore_first_hop(self.get_channel_for_channel_id(first_hop.chan_id))
return True
if self.fees_too_high(route):
routes.ignore_node_with_highest_fee(route)
return True
return False
def low_local_ratio_after_sending(self, first_hop, total_amount):
if self.first_hop_channel:
# Just use the computed/specified amount to drain the first hop, ignoring fees
return False
channel_id = first_hop.chan_id
channel = self.get_channel_for_channel_id(channel_id)
remote = channel.remote_balance + total_amount
local = channel.local_balance - total_amount
ratio = float(local) / (remote + local)
return ratio < self.channel_ratio
def fees_too_high(self, route):
hops_with_fees = len(route.hops) - 1
lnd_fees = hops_with_fees * (DEFAULT_BASE_FEE_SAT_MSAT + (self.amount * DEFAULT_FEE_RATE_MSAT))
limit = self.max_fee_factor * lnd_fees
return route.total_fees_msat > limit
def generate_invoice(self):
if self.last_hop_channel:
memo = "Rebalance of channel with ID %d" % self.last_hop_channel.chan_id
else:
memo = "Rebalance of channel with ID %d" % self.first_hop_channel.chan_id
return self.lnd.generate_invoice(memo, self.amount)
def get_channel_for_channel_id(self, channel_id):
for channel in self.lnd.get_channels():
if channel.chan_id == channel_id:
return channel
def initialize_ignored_channels(self, routes):
for channel in self.lnd.get_channels():
if self.low_local_ratio_after_sending(channel, self.amount):
routes.ignore_first_hop(channel, show_message=False)
if channel.chan_id in self.excluded:
debugnobreak("Channel is excluded, ")
routes.ignore_first_hop(channel)

View file

@ -592,6 +592,42 @@ def localbalanceOC():
print("----------------------------------------------------------------------------------------------------\n")
input("\nContinue... ")
def localrebalancelnd():
lncli = " listchannels"
lsd = os.popen(lndconnectload['ln'] + lncli).read()
lsd0 = str(lsd)
d = json.loads(lsd0)
n = d['channels']
while True:
clear()
print("\033[1;32;40m")
blogo()
print("\033[0;37;40m")
print("<<< Back to the Main Menu Press Control + C.\n\n")
print("\t\nChannels\n")
try:
print("""\n\tLIST CHANNELS TO REBALANCE\n
\t\033[1;34;40mLOCAL\033[0;37;40m BALANCE \t\033[1;31;40mREMOTE\033[0;37;40m BALANCE
""")
for r in range(len(n)):
s = n[r]
if int(s['local_balance']) >= int(s['remote_balance']):
total = int(s['local_balance']) - int(s['remote_balance'])
elif int(s['local_balance']) <= int(s['remote_balance']):
total = int(s['remote_balance']) - int(s['local_balance'])
print("Node ID: " + str(s['chan_id']) + "\t\033[1;34;40m " + str(s['local_balance']) + "\033[0;37;40m sats \033[1;31;40m\t" + str(s['remote_balance']) + "\033[0;37;40m sats \033[3;33;40m" + "\tDIFFERENCE: {}\033[0;37;40m sats".format(str(total)) )
fromnode = input("\nSelect FROM a Node ID : ")
tonode = input("\nSelect TO a Node ID : ")
amt = input("\nAmount in sats: ")
fee = input("\nMax Fee factor in sats: ")
fromtonode = "python3 rebalance.py -f {} -t {} -a {} --max-fee-factor {}".format(fromnode,tonode,amt,fee)
os.system(str(fromtonode))
t.sleep(2)
except:
break
# Remote connection with rest -------------------------------------
def getnewinvoice():

256
rebalance.py Normal file
View file

@ -0,0 +1,256 @@
#!/usr/bin/env python
import argparse
import math
import os
import platform
import sys
from lnd import Lnd
from logic import Logic
MAX_CHANNEL_CAPACITY = 16777215
MAX_SATOSHIS_PER_TRANSACTION = 4294967
def main():
argument_parser = get_argument_parser()
arguments = argument_parser.parse_args()
lnd = Lnd(arguments.lnddir, arguments.grpc)
first_hop_channel_id = vars(arguments)['from']
to_channel = arguments.to
if arguments.ratio < 1 or arguments.ratio > 50:
print("--ratio must be between 1 and 50")
sys.exit(1)
channel_ratio = float(arguments.ratio) / 100
if arguments.incoming is not None and not arguments.list_candidates:
print("--outgoing and --incoming only work in conjunction with --list-candidates")
sys.exit(1)
if arguments.list_candidates:
incoming = arguments.incoming is None or arguments.incoming
if incoming:
list_incoming_candidates(lnd, channel_ratio)
else:
list_outgoing_candidates(lnd, channel_ratio)
sys.exit(0)
if to_channel is None and first_hop_channel_id is None:
argument_parser.print_help()
sys.exit(1)
percentage = arguments.percentage
if percentage:
if percentage < 1 or percentage > 100:
print("--percentage must be between 1 and 100")
argument_parser.print_help()
sys.exit(1)
# the 'to' argument might be an index, or a channel ID
if to_channel and to_channel < 10000:
# here we are in the "channel index" case
index = int(to_channel) - 1
candidates = get_incoming_rebalance_candidates(lnd, channel_ratio)
candidate = candidates[index]
last_hop_channel = candidate
else:
# else the channel argument should be the channel ID
last_hop_channel = get_channel_for_channel_id(lnd, to_channel)
first_hop_channel = get_channel_for_channel_id(lnd, first_hop_channel_id)
amount = get_amount(arguments, first_hop_channel, last_hop_channel)
if amount == 0:
print("Amount is 0, nothing to do")
sys.exit(0)
max_fee_factor = arguments.max_fee_factor
excluded = arguments.exclude
return Logic(lnd, first_hop_channel, last_hop_channel, amount, channel_ratio, excluded,
max_fee_factor).rebalance()
def get_amount(arguments, first_hop_channel, last_hop_channel):
if last_hop_channel:
amount = get_rebalance_amount(last_hop_channel)
else:
amount = get_rebalance_amount(first_hop_channel)
if arguments.percentage:
amount = int(round(amount * arguments.percentage / 100))
if last_hop_channel and first_hop_channel:
rebalance_amount_from_channel = get_rebalance_amount(first_hop_channel)
amount = min(amount, rebalance_amount_from_channel)
if arguments.amount:
amount = min(amount, int(arguments.amount))
amount = min(amount, MAX_SATOSHIS_PER_TRANSACTION)
return amount
def get_channel_for_channel_id(lnd, channel_id):
for channel in lnd.get_channels():
if channel.chan_id == channel_id:
return channel
return None
def get_argument_parser():
parser = argparse.ArgumentParser()
parser.add_argument("--lnddir",
default="~/.lnd",
dest="lnddir",
help="(default ~/.lnd) lnd directory")
parser.add_argument("--grpc",
default="localhost:10009",
dest="grpc",
help="(default localhost:10009) lnd gRPC endpoint")
parser.add_argument("-r", "--ratio",
type=int,
default=50,
help="(default: 50) ratio for channel imbalance between 1 and 50, "
"eg. 45 to only show channels (-l) with less than 45%% of the "
"funds on the local (-i) or remote (-o) side")
list_group = parser.add_argument_group("list candidates", "Show the unbalanced channels.")
list_group.add_argument("-l", "--list-candidates", action="store_true",
help="list candidate channels for rebalance")
direction_group = list_group.add_mutually_exclusive_group()
direction_group.add_argument("-o", "--outgoing",
action="store_const",
const=False,
dest="incoming",
help="lists channels with less than x%% of the funds on the remote side (see --ratio)")
direction_group.add_argument("-i", "--incoming",
action="store_const",
const=True,
dest="incoming",
help="(default) lists channels with less than x%% of the funds on the local side "
"(see --ratio)")
rebalance_group = parser.add_argument_group("rebalance",
"Rebalance a channel. You need to specify at least"
" the 'from' channel (-f) or the 'to' channel (-t).")
rebalance_group.add_argument("-f", "--from",
metavar="CHANNEL",
type=int,
help="channel ID of the outgoing channel "
"(funds will be taken from this channel)")
rebalance_group.add_argument("-t", "--to",
metavar="CHANNEL",
type=int,
help="channel ID of the incoming channel "
"(funds will be sent to this channel). "
"You may also use the index as shown in the incoming candidate list (-l -i).")
amount_group = rebalance_group.add_mutually_exclusive_group()
amount_group.add_argument("-a", "--amount",
type=int,
help="Amount of the rebalance, in satoshis. If not specified, "
"the amount computed for a perfect rebalance will be used"
" (up to the maximum of 4,294,967 satoshis)")
amount_group.add_argument("-p", "--percentage",
type=int,
help="Set the amount to send to a percentage of the amount required to rebalance. "
"As an example, if this is set to 50, the amount will half of the default. "
"See --amount.")
rebalance_group.add_argument("-e", "--exclude",
type=int,
action="append",
help="Exclude the given channel ID as the outgoing channel (no funds will be taken "
"out of excluded channels)")
rebalance_group.add_argument("--max-fee-factor",
type=float,
default=10,
help="(default: 10) Reject routes that cost more than x times the lnd default "
"(base: 1 sat, rate: 1 millionth sat) per hop on average")
return parser
def list_incoming_candidates(lnd, channel_ratio):
candidates = get_incoming_rebalance_candidates(lnd, channel_ratio)
list_candidates(candidates)
def list_outgoing_candidates(lnd, channel_ratio):
candidates = get_outgoing_rebalance_candidates(lnd, channel_ratio)
list_candidates(candidates)
def list_candidates(candidates):
index = 0
for candidate in candidates:
index += 1
rebalance_amount_int = get_rebalance_amount(candidate)
rebalance_amount = "{:,}".format(rebalance_amount_int)
if rebalance_amount_int > MAX_SATOSHIS_PER_TRANSACTION:
rebalance_amount += " (max per transaction: {:,})".format(MAX_SATOSHIS_PER_TRANSACTION)
print("(%2d) Channel ID: " % index + str(candidate.chan_id))
print("Pubkey: " + candidate.remote_pubkey)
print("Local ratio: {:.3f}".format(get_local_ratio(candidate)))
print("Capacity: {:,}".format(candidate.capacity))
print("Remote balance: {:,}".format(candidate.remote_balance))
print("Local balance: {:,}".format(candidate.local_balance))
print("Amount for 50-50: " + rebalance_amount)
print(get_capacity_and_ratio_bar(candidate))
print("")
def get_rebalance_amount(channel):
return abs(int(math.ceil(float(get_remote_surplus(channel)) / 2)))
def get_incoming_rebalance_candidates(lnd, channel_ratio):
low_local = list(filter(lambda c: get_local_ratio(c) < channel_ratio, lnd.get_channels()))
low_local = list(filter(lambda c: get_rebalance_amount(c) > 0, low_local))
return sorted(low_local, key=get_remote_surplus, reverse=False)
def get_outgoing_rebalance_candidates(lnd, channel_ratio):
high_local = list(filter(lambda c: get_local_ratio(c) > 1 - channel_ratio, lnd.get_channels()))
high_local = list(filter(lambda c: get_rebalance_amount(c) > 0, high_local))
return sorted(high_local, key=get_remote_surplus, reverse=True)
def get_local_ratio(channel):
remote = channel.remote_balance
local = channel.local_balance
return float(local) / (remote + local)
def get_remote_surplus(channel):
return channel.remote_balance - channel.local_balance
def get_capacity_and_ratio_bar(candidate):
columns = get_columns()
columns_scaled_to_capacity = int(round(columns * float(candidate.capacity) / MAX_CHANNEL_CAPACITY))
bar_width = columns_scaled_to_capacity - 2
result = "|"
ratio = get_local_ratio(candidate)
length = int(round(ratio * bar_width))
for x in range(0, length):
result += "="
for x in range(length, bar_width):
result += " "
return result + "|"
def get_columns():
if platform.system() == 'Linux' and sys.__stdin__.isatty():
return int(os.popen('stty size', 'r').read().split()[1])
else:
return 80
success = main()
if success:
sys.exit(0)
sys.exit(1)

View file

@ -19,6 +19,10 @@ xmltodict
python-cfonts
termcolor
pycoingecko
googleapis-common-protos==1.52.0
grpcio
protobuf==3.13.0
six==1.15.0
#
###### Requirements with Version Specifiers ######
# See https://www.python.org/dev/peps/pep-0440/#version-specifiers

115
routes.py Normal file
View file

@ -0,0 +1,115 @@
import base64
import sys
MAX_ROUTES_TO_REQUEST = 100
def debug(message):
sys.stderr.write(message + "\n")
def debugnobreak(message):
sys.stderr.write(message)
class Routes:
def __init__(self, lnd, payment_request, first_hop_channel, last_hop_channel):
self.lnd = lnd
self.payment_request = payment_request
self.first_hop_channel = first_hop_channel
self.last_hop_channel = last_hop_channel
self.all_routes = []
self.returned_routes = []
self.ignored_edges = []
self.ignored_nodes = []
self.num_requested_routes = 0
def has_next(self):
self.update_routes()
return self.returned_routes < self.all_routes
def get_next(self):
self.update_routes()
for route in self.all_routes:
if route not in self.returned_routes:
self.returned_routes.append(route)
return route
return None
def update_routes(self):
while True:
if self.returned_routes < self.all_routes:
return
if self.num_requested_routes >= MAX_ROUTES_TO_REQUEST:
return
self.request_route()
def request_route(self):
amount = self.get_amount()
if self.last_hop_channel:
last_hop_pubkey = self.last_hop_channel.remote_pubkey
else:
last_hop_pubkey = None
if self.first_hop_channel:
first_hop_channel_id = self.first_hop_channel.chan_id
else:
first_hop_channel_id = None
routes = self.lnd.get_route(last_hop_pubkey, amount, self.ignored_edges,
self.ignored_nodes, first_hop_channel_id)
if routes is None:
self.num_requested_routes = MAX_ROUTES_TO_REQUEST
else:
self.num_requested_routes += 1
for route in routes:
self.add_route(route)
def add_route(self, route):
if route is None:
return
if route not in self.all_routes:
self.all_routes.append(route)
@staticmethod
def print_route(route):
route_str = " -> ".join(str(h.chan_id) for h in route.hops)
return route_str
def get_amount(self):
return self.payment_request.num_satoshis
def ignore_first_hop(self, channel, show_message=True):
own_key = self.lnd.get_own_pubkey()
other_key = channel.remote_pubkey
self.ignore_edge_from_to(channel.chan_id, own_key, other_key, show_message)
def ignore_edge_on_route(self, failure_source_pubkey, route):
ignore_next = False
for hop in route.hops:
if ignore_next:
self.ignore_edge_from_to(hop.chan_id, failure_source_pubkey, hop.pub_key)
return
if hop.pub_key == failure_source_pubkey:
ignore_next = True
def ignore_node_with_highest_fee(self, route):
max_fee_msat = 0
max_fee_hop = None
for hop in route.hops:
if hop.fee_msat > max_fee_msat:
max_fee_msat = hop.fee_msat
max_fee_hop = hop
pub_key = max_fee_hop.pub_key
debugnobreak("High fees (%s msat), " % max_fee_msat)
self.ignore_node(pub_key)
def ignore_edge_from_to(self, chan_id, from_pubkey, to_pubkey, show_message=True):
if show_message:
debug("ignoring channel %s (from %s to %s)" % (chan_id, from_pubkey, to_pubkey))
direction_reverse = from_pubkey > to_pubkey
edge = {"channel_id": chan_id, "direction_reverse": direction_reverse}
self.ignored_edges.append(edge)
def ignore_node(self, pub_key):
debug("ignoring node %s" % pub_key)
self.ignored_nodes.append(base64.b16decode(pub_key, True))

View file

@ -1 +1,2 @@
{"version":"0.8.0"}