forked from C-Otto/rebalance-lnd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rebalance.py
executable file
·273 lines (221 loc) · 11.2 KB
/
rebalance.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#!/usr/bin/env python3
import argparse
import math
import os
import platform
import sys
import random
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, or random
if to_channel and 0 < to_channel < 10000:
# here we are in the "channel index" case
index = int(to_channel) - 1
last_hop_channel = get_incoming_rebalance_candidates(lnd, channel_ratio)[index]
elif to_channel == -1:
# here is the random case
last_hop_channel = random.choice(get_incoming_rebalance_candidates(lnd, channel_ratio))
else:
# else the channel argument should be the channel ID
last_hop_channel = get_channel_for_channel_id(lnd, to_channel)
# the 'from' argument might be an index, or a channel ID, or random
if first_hop_channel_id and 0 < first_hop_channel_id < 10000:
# here we are in the "channel" index case
index = int(first_hop_channel_id) - 1
candidates = get_outgoing_rebalance_candidates(lnd, channel_ratio)
first_hop_channel = candidates[index]
elif first_hop_channel_id == -1:
# here is the random case
last_hop_channel = random.choice(get_incoming_rebalance_candidates(lnd, channel_ratio))
else:
# else the channel argument should be the channel ID
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)"
"You may also use the index as shown in the incoming candidate list (-l -o), "
"or -1 to choose a random candidate.")
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), "
"or -1 to choose a random candidate.")
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("Channel Point: " + candidate.channel_point)
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)