def calc_optimal_amount(out_ours, out_total, in_ours, in_total, payload): out_ours, out_total = int(out_ours), int(out_total) in_ours, in_total = int(in_ours), int(in_total) in_theirs = in_total - in_ours vo = int(out_ours - (out_total/2)) vi = int((in_total/2) - in_ours) # cases where one option can be eliminated because it exceeds other capacity if vo > in_theirs and vi > 0 and vi < out_ours: return Millisatoshi(vi) if vi > out_ours and vo > 0 and vo < in_theirs: return Millisatoshi(vo) # cases where one channel is still capable to bring other to balance if vo < 0 and vi > 0 and vi < out_ours: return Millisatoshi(vi) if vi < 0 and vo > 0 and vo < in_theirs: return Millisatoshi(vo) # when both options are possible take the one with least effort if vo > 0 and vo < in_theirs and vi > 0 and vi < out_ours: return Millisatoshi(min(vi, vo)) raise RpcError("rebalance", payload, {'message': 'rebalancing these channels will make things worse'})
def check_channel_open(openchannel, plugin: Plugin) -> str: push_amount = Millisatoshi(openchannel["push_msat"]).millisatoshis if push_amount != 0: return "push amount not 0" channel_creation = plugin.channel_creation capacity = Millisatoshi(openchannel["funding_satoshis"]).millisatoshis expected_capacity = math.floor( channel_creation.invoice_amount / (1 - (channel_creation.inbound_percentage / 100))) if expected_capacity > capacity: return "minimum capacity requirement of {}msat not met".format( expected_capacity) channel_flags = openchannel["channel_flags"] # A private channel has "channel_flags" 0 if channel_creation.private and channel_flags == 1: return "channel is not private" elif not channel_creation.private and channel_flags == 0: return "channel is not public" return ""
def openchannel_v2(plugin, node_id, amount): change_output_weight = (9 + 22) * 4 funding_output_weight = (9 + 34) * 4 core_weight = 44 feerate_val = 2000 feerate = '{}perkw'.format(feerate_val) funding = plugin.rpc.fundpsbt(amount, feerate, funding_output_weight + core_weight) psbt_obj = psbt_from_base64(funding['psbt']) excess = Millisatoshi(funding['excess_msat']) # FIXME: convert feerate ?! change_cost = Millisatoshi(change_output_weight * feerate_val // 1000 * 1000) dust_limit = Millisatoshi(feerate_val * 1000) if excess > (dust_limit + change_cost): addr = plugin.rpc.newaddr()['bech32'] change = excess - change_cost output = tx_output_init(int(change.to_satoshi()), get_script(addr)) psbt_add_output_at(psbt_obj, 0, 0, output) resp = plugin.rpc.openchannel_init(node_id, amount, psbt_to_base64(psbt_obj, 0), commitment_feerate=feerate, funding_feerate=feerate) cid = resp['channel_id'] # We don't have an updates, so we send update until our peer is also # finished while not resp['commitments_secured']: resp = plugin.rpc.openchannel_update(cid, resp['psbt']) # fixme: pass in array of our input indexes to signonly signed = plugin.rpc.signpsbt(resp['psbt']) return plugin.rpc.openchannel_signed(cid, signed['signed_psbt'])
def on_openchannel(openchannel2, plugin, **kwargs): # We mirror what the peer does, wrt to funding amount amount = openchannel2['their_funding'] feerate = openchannel2['feerate_per_kw_funding'] locktime = openchannel2['locktime'] funding = plugin.rpc.fundpsbt(amount, ''.join([str(feerate), 'perkw']), 0, reserve=True, locktime=locktime) psbt_obj = psbt_from_base64(funding['psbt']) excess = Millisatoshi(funding['excess_msat']) change_cost = Millisatoshi(164 * feerate // 1000 * 1000) dust_limit = Millisatoshi(253 * 1000) if excess > (dust_limit + change_cost): addr = plugin.rpc.newaddr()['bech32'] change = excess - change_cost output = tx_output_init(int(change.to_satoshi()), get_script(addr)) psbt_add_output_at(psbt_obj, 0, 0, output) return { 'result': 'continue', 'psbt': psbt_to_base64(psbt_obj, 0), 'accepter_funding_msat': amount }
def rebalanceall(plugin: Plugin, min_amount: Millisatoshi = Millisatoshi("50000sat"), feeratio: float = 0.5): """Rebalance all unbalanced channels if possible for a very low fee. Default minimum rebalancable amount is 50000sat. Default feeratio = 0.5, half of our node's default fee. To be economical, it tries to fix the liquidity cheaper than it can be ruined by transaction forwards. It may run for a long time (hours) in the background, but can be stopped with the rebalancestop method. """ # some early checks before we start the async thread if plugin.mutex.locked(): return {"message": "Rebalance is already running, this may take a while. To stop it use the cli method 'rebalancestop'."} channels = get_open_channels(plugin) if len(channels) <= 1: return {"message": "Error: Not enough open channels to rebalance anything"} our = sum(ch["to_us_msat"] for ch in channels) total = sum(ch["total_msat"] for ch in channels) min_amount = Millisatoshi(min_amount) if total - our < min_amount or our < min_amount: return {"message": "Error: Not enough liquidity to rebalance anything"} # param parsing ensure correct type plugin.feeratio = float(feeratio) plugin.min_amount = min_amount # run the job t = Thread(target=rebalanceall_thread, args=(plugin, )) t.start() return {"message": f"Rebalance started with min rebalancable amount: {plugin.min_amount}, feeratio: {plugin.feeratio}"}
def test_multiwithdraw_simple(node_factory, bitcoind): """ Test simple multiwithdraw usage. """ l1, l2, l3 = node_factory.get_nodes(3) l1.fundwallet(10**8) addr2 = l2.rpc.newaddr()['bech32'] amount2 = Millisatoshi(2222 * 1000) addr3 = l3.rpc.newaddr()['bech32'] amount3 = Millisatoshi(3333 * 1000) # Multiwithdraw! txid = l1.rpc.multiwithdraw([{addr2: amount2}, {addr3: amount3}])["txid"] bitcoind.generate_block(1) sync_blockheight(bitcoind, [l1, l2, l3]) # l2 shoulda gotten money. funds2 = l2.rpc.listfunds()['outputs'] assert only_one(funds2)["txid"] == txid assert only_one(funds2)["address"] == addr2 assert only_one(funds2)["status"] == "confirmed" assert only_one(funds2)["amount_msat"] == amount2 # l3 shoulda gotten money. funds3 = l3.rpc.listfunds()['outputs'] assert only_one(funds3)["txid"] == txid assert only_one(funds3)["address"] == addr3 assert only_one(funds3)["status"] == "confirmed" assert only_one(funds3)["amount_msat"] == amount3
def setup_routing_fees(plugin, payload, route, amount, substractfees: bool = False): delay = int(plugin.get_option('cltv-final')) amount_iter = amount for r in reversed(route): r['msatoshi'] = amount_iter.millisatoshis r['amount_msat'] = amount_iter r['delay'] = delay channels = plugin.rpc.listchannels(r['channel']) ch = next(c for c in channels.get('channels') if c['destination'] == r['id']) fee = Millisatoshi(ch['base_fee_millisatoshi']) # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 ) fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6 # integer math trick to round up amount_iter += fee delay += ch['delay'] # amounts have to be calculated the other way when being fee substracted # we took the upper loop as well for the delay parameter if substractfees: amount_iter = amount first = True for r in route: channels = plugin.rpc.listchannels(r['channel']) ch = next(c for c in channels.get('channels') if c['destination'] == r['id']) if not first: fee = Millisatoshi(ch['base_fee_millisatoshi']) # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 ) fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6 # integer math trick to round up if fee > amount_iter: raise RpcError(payload['command'], payload, {'message': 'Cannot cover fees to %s %s' % (payload['command'], amount)}) amount_iter -= fee first = False r['msatoshi'] = amount_iter.millisatoshis r['amount_msat'] = amount_iter
def test_queryrates(node_factory, bitcoind): l1, l2 = node_factory.get_nodes(2) amount = 10 ** 6 l1.fundwallet(amount * 10) l2.fundwallet(amount * 10) l1.rpc.connect(l2.info['id'], 'localhost', l2.port) with pytest.raises(RpcError, match=r'not advertising liquidity'): l1.rpc.dev_queryrates(l2.info['id'], amount, amount * 10) l2.rpc.call('funderupdate', {'policy': 'match', 'policy_mod': 100, 'per_channel_max_msat': '1btc', 'fuzz_percent': 0, 'lease_fee_base_msat': '2sat', 'funding_weight': 1000, 'lease_fee_basis': 140, 'channel_fee_max_base_msat': '3sat', 'channel_fee_max_proportional_thousandths': 101}) l1.rpc.connect(l2.info['id'], 'localhost', l2.port) result = l1.rpc.dev_queryrates(l2.info['id'], amount, amount) assert result['our_funding_msat'] == Millisatoshi(amount * 1000) assert result['their_funding_msat'] == Millisatoshi(amount * 1000) assert result['funding_weight'] == 1000 assert result['lease_fee_base_msat'] == Millisatoshi(2000) assert result['lease_fee_basis'] == 140 assert result['channel_fee_max_base_msat'] == Millisatoshi(3000) assert result['channel_fee_max_proportional_thousandths'] == 101
def amounts_from_scid(plugin, scid): channels = plugin.rpc.listfunds().get('channels') channel = next( c for c in channels if 'short_channel_id' in c and c['short_channel_id'] == scid) our_msat = Millisatoshi(channel['our_amount_msat']) total_msat = Millisatoshi(channel['amount_msat']) return our_msat, total_msat
def check_balance_snaps(n, expected_bals): snaps = n.rpc.listsnapshots()['balance_snapshots'] for snap, exp in zip(snaps, expected_bals): assert snap['blockheight'] == exp['blockheight'] for acct, exp_acct in zip(snap['accounts'], exp['accounts']): # FIXME: also check 'account_id's (these change every run) for item in ['balance_msat']: assert Millisatoshi(acct[item]) == Millisatoshi(exp_acct[item])
def rebalancereport(plugin: Plugin): """Show information about rebalance """ res = {} res["rebalanceall_is_running"] = plugin.mutex.locked() res["getroute_method"] = plugin.getroute.__name__ res["maxhops_threshold"] = plugin.maxhops res["msatfactor"] = plugin.msatfactor res["erringnodes_threshold"] = plugin.erringnodes channels = get_open_channels(plugin) health_percent = 0.0 if len(channels) > 1: enough_liquidity = get_enough_liquidity_threshold(channels) ideal_ratio = get_ideal_ratio(channels, enough_liquidity) res["enough_liquidity_threshold"] = enough_liquidity res["ideal_liquidity_ratio"] = f"{ideal_ratio * 100:.2f}%" for ch in channels: liquidity = liquidity_info(ch, enough_liquidity, ideal_ratio) health_percent += health_score(liquidity) * int(ch["total_msat"]) health_percent /= int(sum(ch["total_msat"] for ch in channels)) else: res["enough_liquidity_threshold"] = Millisatoshi(0) res["ideal_liquidity_ratio"] = "0%" res["liquidity_health"] = f"{health_percent:.2f}%" invoices = plugin.rpc.listinvoices()['invoices'] rebalances = [ i for i in invoices if i.get('status') == 'paid' and i.get('label').startswith("Rebalance") ] total_fee = Millisatoshi(0) total_amount = Millisatoshi(0) res["total_successful_rebalances"] = len(rebalances) # pyln-client does not support the 'status' argument as yet # pays = plugin.rpc.listpays(status="complete")["pays"] pays = plugin.rpc.listpays()["pays"] pays = [p for p in pays if p.get('status') == 'complete'] for r in rebalances: try: pay = next(p for p in pays if p["payment_hash"] == r["payment_hash"]) total_amount += pay["amount_msat"] total_fee += pay["amount_sent_msat"] - pay["amount_msat"] except Exception: res["total_successful_rebalances"] -= 1 res["total_rebalanced_amount"] = total_amount res["total_rebalance_fee"] = total_fee if total_amount > Millisatoshi(0): res["average_rebalance_fee_ppm"] = round( total_fee / total_amount * 10**6, 2) else: res["average_rebalance_fee_ppm"] = 0 avg_forward_fees = get_avg_forward_fees(plugin, [1, 7, 30]) res['average_forward_fee_ppm_1d'] = avg_forward_fees[0] res['average_forward_fee_ppm_7d'] = avg_forward_fees[1] res['average_forward_fee_ppm_30d'] = avg_forward_fees[2] return res
def receivedinvoiceless(plugin, min_amount: Millisatoshi = Millisatoshi(10000)): """ List payments received via sendinvoiceless from other nodes. """ mynodeid = plugin.rpc.getinfo()['id'] mychannels = plugin.rpc.listchannels(source=mynodeid)['channels'] forwards = plugin.rpc.listforwards()['forwards'] default_fees = { 'base': int(plugin.get_option('fee-base')), 'ppm': int(plugin.get_option('fee-per-satoshi')) } # build a mapping of mychannel fees # <scid -> {base, ppm}> myfees = {} for channel in mychannels: scid = channel['short_channel_id'] myfees[scid] = { 'base': channel['base_fee_millisatoshi'], 'ppm': channel['fee_per_millionth'] } # loop through settled forwards and check for overpaid routings result = [] for forward in forwards: if forward['status'] != "settled": continue # for old channel, we dont know fees anymore, use defaults scid = forward['out_channel'] fees = myfees.get(scid, default_fees) fee_paid = forward['fee'] fee_required = int(forward['out_msatoshi'] * fees['ppm'] * 10**-6 + fees['base']) if fee_paid > fee_required: amount = Millisatoshi(fee_paid - fee_required) # fess can sometimes not be exact when channel fees changed in the past, filter those if amount < min_amount: continue entry = {'amount_msat': amount, 'amount_btc': amount.to_btc_str()} # old lightningd versions may not support received_time yet if 'resolved_time' in forward: time_secs = int(forward['resolved_time']) time_str = datetime.utcfromtimestamp(time_secs).strftime( '%Y-%m-%d %H:%M:%S (UTC)') entry['resolved_time'] = forward['resolved_time'] entry['timestamp'] = time_str result.append(entry) return result
def account_balance(n, account_id): moves = dedupe_moves(n.rpc.call('listcoinmoves_plugin')['coin_moves']) chan_moves = [m for m in moves if m['account_id'] == account_id] assert len(chan_moves) > 0 m_sum = Millisatoshi(0) for m in chan_moves: m_sum += Millisatoshi(m['credit_msat']) m_sum -= Millisatoshi(m['debit_msat']) return m_sum
def init(options: dict, configuration: dict, plugin: Plugin, **kwargs): plugin.our_node_id = plugin.rpc.getinfo()["id"] plugin.deactivate_fuzz = options.get("feeadjuster-deactivate-fuzz") plugin.forward_event_subscription = not options.get( "feeadjuster-deactivate-fee-update") plugin.update_threshold = float(options.get("feeadjuster-threshold")) plugin.update_threshold_abs = Millisatoshi( options.get("feeadjuster-threshold-abs")) plugin.big_enough_liquidity = Millisatoshi( options.get("feeadjuster-enough-liquidity")) plugin.imbalance = float(options.get("feeadjuster-imbalance")) adjustment_switch = { "soft": get_ratio_soft, "hard": get_ratio_hard, "default": get_ratio } plugin.get_ratio = adjustment_switch.get( options.get("feeadjuster-adjustment-method"), get_ratio) fee_strategy_switch = { "global": get_fees_global, "median": get_fees_median } plugin.fee_strategy = fee_strategy_switch.get( options.get("feeadjuster-feestrategy"), get_fees_global) config = plugin.rpc.listconfigs() plugin.adj_basefee = config["fee-base"] plugin.adj_ppmfee = config["fee-per-satoshi"] # normalize the imbalance percentage value to 0%-50% if plugin.imbalance < 0 or plugin.imbalance > 1: raise ValueError("feeadjuster-imbalance must be between 0 and 1.") if plugin.imbalance > 0.5: plugin.imbalance = 1 - plugin.imbalance # detect if server supports the new listchannels by `destination` (#4614) plugin.listchannels_by_dst = False rpchelp = plugin.rpc.help().get('help') if len([ c for c in rpchelp if c["command"].startswith("listchannels ") and "destination" in c["command"] ]) == 1: plugin.listchannels_by_dst = True plugin.log( f"Plugin feeadjuster initialized " f"({plugin.adj_basefee} base / {plugin.adj_ppmfee} ppm) with an " f"imbalance of {int(100 * plugin.imbalance)}%/{int(100 * ( 1 - plugin.imbalance))}%, " f"update_threshold: {int(100 * plugin.update_threshold)}%, " f"update_threshold_abs: {plugin.update_threshold_abs}, " f"enough_liquidity: {plugin.big_enough_liquidity}, " f"deactivate_fuzz: {plugin.deactivate_fuzz}, " f"forward_event_subscription: {plugin.forward_event_subscription}, " f"adjustment_method: {plugin.get_ratio.__name__}, " f"fee_strategy: {plugin.fee_strategy.__name__}, " f"listchannels_by_dst: {plugin.listchannels_by_dst}") plugin.mutex.release() feeadjust(plugin)
def test_rebalance_all(node_factory, bitcoind): l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt) nodes = [l1, l2, l3] # check we get an error if theres just one channel result = l1.rpc.rebalanceall() assert result[ 'message'] == 'Error: Not enough open channels to rebalance anything' # now we add another 100% outgoing liquidity to l1 which does not help l4 = node_factory.get_node() l1.connect(l4) l1.fundchannel(l4) # test this is still not possible result = l1.rpc.rebalanceall() assert result[ 'message'] == 'Error: Not enough liquidity to rebalance anything' # remove l4 it does not distort further testing l1.rpc.close(l1.get_channel_scid(l4)) # now we form a circle so we can do actually rebalanceall l3.connect(l1) l3.fundchannel(l1) # get scids scid12 = l1.get_channel_scid(l2) scid23 = l2.get_channel_scid(l3) scid31 = l3.get_channel_scid(l1) scids = [scid12, scid23, scid31] # wait for each others gossip bitcoind.generate_block(6) wait_for_all_active(nodes, scids) # check the rebalanceall starts result = l1.rpc.rebalanceall(feeratio=5.0) # we need high fees to work assert result['message'].startswith('Rebalance started') l1.daemon.wait_for_log(f"Try to rebalance: {scid12} -> {scid31}") wait_for(lambda: not l1.rpc.rebalanceall()['message'].startswith( "Rebalance is already running")) result = l1.rpc.rebalancestop() assert result['message'].startswith("Automatic rebalance finished") # wait until listpeers is up2date wait_for_all_htlcs(nodes) # check that channels are now balanced c12 = l1.rpc.listpeers(l2.info['id'])['peers'][0]['channels'][0] c13 = l1.rpc.listpeers(l3.info['id'])['peers'][0]['channels'][0] assert abs(0.5 - (Millisatoshi(c12['to_us_msat']) / Millisatoshi(c12['total_msat']))) < 0.01 assert abs(0.5 - (Millisatoshi(c13['to_us_msat']) / Millisatoshi(c13['total_msat']))) < 0.01
def check_coin_moves(n, account_id, expected_moves, chainparams): moves = n.rpc.call('listcoinmoves_plugin')['coin_moves'] # moves can lag; wait for a few seconds if we don't have correct number. # then move on: we'll get details below. expected_count = 0 for m in enumerate(expected_moves): if isinstance(m, list): expected_count += len(m) else: expected_count += 1 if len(moves) != expected_count: time.sleep(5) moves = n.rpc.call('listcoinmoves_plugin')['coin_moves'] node_id = n.info['id'] acct_moves = [m for m in moves if m['account_id'] == account_id] for mv in acct_moves: print( "{{'type': '{}', 'credit': {}, 'debit': {}, 'tags': '{}' , ['fees'?: '{}']}}," .format(mv['type'], Millisatoshi(mv['credit']).millisatoshis, Millisatoshi(mv['debit']).millisatoshis, mv['tags'], mv['fees'] if 'fees' in mv else '')) assert mv['version'] == 2 assert mv['node_id'] == node_id assert mv['timestamp'] > 0 assert mv['coin_type'] == chainparams['bip173_prefix'] # chain moves should have blockheights if mv['type'] == 'chain_mvt' and mv['account_id'] != 'external': assert mv['blockheight'] is not None for num, m in enumerate(expected_moves): # They can group things which are in any order. if isinstance(m, list): number_moves = len(m) for acct_move in acct_moves[:number_moves]: found = None for i in range(len(m)): if move_matches(m[i], acct_move): found = i break if found is None: raise ValueError("Unexpected move {} amongst {}".format( acct_move, m)) del m[i] acct_moves = acct_moves[number_moves:] else: if not move_matches(m, acct_moves[0]): raise ValueError("Unexpected move {}: {} != {}".format( num, acct_moves[0], m)) acct_moves = acct_moves[1:] assert acct_moves == []
def channels_to_rebalance(rpc): incoming = [] outgoing = [] peers = rpc.listpeers() for p in peers['peers']: if not p['connected']: continue for c in p['channels']: if c['state'] != 'CHANNELD_NORMAL': continue if c['short_channel_id'] in exclude: continue if c['our_reserve_msat'] < c['to_us_msat']: to_us = c['to_us_msat'] - c['our_reserve_msat'] else: to_us = Millisatoshi(0) to_them = c['total_msat'] - c['to_us_msat'] if c['their_reserve_msat'] < to_them: to_them = to_them - c['their_reserve_msat'] else: to_them = Millisatoshi(0) middle = (to_us + to_them) / 2 to_middle = to_us - middle if to_us > to_them else to_them - middle perc = int((to_middle.to_satoshi() / (to_us + to_them).to_satoshi()) * 100) chan = { 'to_us': to_us, 'to_them': to_them, 'chan_id': c['short_channel_id'], 'to_middle': to_middle, 'percent': perc } if perc < SKIPPING_PERC: # consider balanced channel around 10% from middle print("skipping %s" % str(chan)) continue if to_us > to_them: outgoing.append(chan) else: incoming.append(chan) incoming = sorted(incoming, key=lambda k: k['to_middle']) outgoing = sorted(outgoing, key=lambda k: k['to_middle']) print() print("incoming") print_list(incoming) print("outgoing") print_list(outgoing) return incoming, outgoing
def test_rebalance_manual(node_factory, bitcoind): l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt) nodes = [l1, l2, l3] # form a circle so we can do rebalancing l3.connect(l1) l3.fundchannel(l1) # get scids scid12 = l1.get_channel_scid(l2) scid23 = l2.get_channel_scid(l3) scid31 = l3.get_channel_scid(l1) scids = [scid12, scid23, scid31] # wait for each others gossip bitcoind.generate_block(6) for n in nodes: for scid in scids: n.wait_channel_active(scid) # check we can do an auto amount rebalance result = l1.rpc.rebalance(scid12, scid31) print(result) assert result['status'] == 'complete' assert result['outgoing_scid'] == scid12 assert result['incoming_scid'] == scid31 assert result['hops'] == 3 assert result['received'] == '500000000msat' # wait until listpeers is up2date wait_for_all_htlcs(nodes) # check that channels are now balanced c12 = l1.rpc.listpeers(l2.info['id'])['peers'][0]['channels'][0] c13 = l1.rpc.listpeers(l3.info['id'])['peers'][0]['channels'][0] assert abs(0.5 - (Millisatoshi(c12['to_us_msat']) / Millisatoshi(c12['total_msat']))) < 0.01 assert abs(0.5 - (Millisatoshi(c13['to_us_msat']) / Millisatoshi(c13['total_msat']))) < 0.01 # check we can do a manual amount rebalance in the other direction result = l1.rpc.rebalance(scid31, scid12, '250000000msat') assert result['status'] == 'complete' assert result['outgoing_scid'] == scid31 assert result['incoming_scid'] == scid12 assert result['hops'] == 3 assert result['received'] == '250000000msat' # briefly check rebalancereport works report = l1.rpc.rebalancereport() assert report.get('rebalanceall_is_running') == False assert report.get('total_successful_rebalances') == 2
def rebalanceall(plugin: Plugin, min_amount: Millisatoshi = Millisatoshi("50000sat"), feeratio: float = 0.5): """Rebalance all unbalanced channels if possible for a very low fee. Default minimum rebalancable amount is 50000sat. Default feeratio = 0.5, half of our node's default fee. To be economical, it tries to fix the liquidity cheaper than it can be ruined by transaction forwards. It may run for a long time (hours) in the background, but can be stopped with the rebalancestop method. """ if plugin.mutex.locked(): return {"message": "Rebalance is already running, this may take a while. To stop it use the cli method 'rebalancestop'."} plugin.feeratio = float(feeratio) plugin.min_amount = Millisatoshi(min_amount) t = Thread(target=rebalanceall_thread, args=(plugin, )) t.start() return {"message": "Rebalance started"}
def get_enough_liquidity_threshold(channels: list): low = Millisatoshi(0) biggest_channel = max(channels, key=lambda ch: ch["total_msat"]) high = biggest_channel["total_msat"] / 2 while True: mid = (low + high) / 2 if high - low < Millisatoshi("1sat"): break if check_liquidity_threshold(channels, mid): low = mid else: high = mid return mid / 2
def test_init(): # Note: Ongoing Discussion, hence the `with pytest.raises`. # https://github.com/ElementsProject/lightning/pull/4273#discussion_r540369093 # # Initialization with a float should be possible: # Millisatoshi(5) / 2 currently works, and removes the half msat. # So Millisatoshi(5 / 2) should be the same. amount = Millisatoshi(5) / 2 assert amount == Millisatoshi(2) with pytest.raises( TypeError, match="Millisatoshi by float is currently not supported"): assert amount == Millisatoshi(5 / 2) ratio = Millisatoshi(8) / Millisatoshi(5) assert isinstance(ratio, float) with pytest.raises( TypeError, match="Millisatoshi by float is currently not supported"): assert Millisatoshi(ratio) == Millisatoshi(8 / 5) # Check that init by a round float is allowed. # Required by some existing tests: tests/test_wallet.py::test_txprepare amount = Millisatoshi(42.0) assert amount == 42
def on_openchannel(openchannel, plugin, **kwargs): # - a multiple of 11: we send back a valid address (regtest) if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 11 == 0: return {'result': 'continue', 'close_to': 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'} # - a multiple of 7: we send back an empty address if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 7 == 0: return {'result': 'continue', 'close_to': ''} # - a multiple of 5: we send back an address for the wrong chain (mainnet) if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 5 == 0: return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'} # - otherwise: we don't include the close_to return {'result': 'continue'}
def test_millisatoshi_passthrough(node_factory): """ Ensure that Millisatoshi arguments and return work. """ plugin_path = os.path.join(os.getcwd(), 'tests/plugins/millisatoshis.py') n = node_factory.get_node(options={'plugin': plugin_path, 'log-level': 'io'}) # By keyword ret = n.rpc.call('echo', {'msat': Millisatoshi(17), 'not_an_msat': '22msat'})['echo_msat'] assert type(ret) == Millisatoshi assert ret == Millisatoshi(17) # By position ret = n.rpc.call('echo', [Millisatoshi(18), '22msat'])['echo_msat'] assert type(ret) == Millisatoshi assert ret == Millisatoshi(18)
def is_msat_request(checker, instance): """msat fields can be raw integers, sats, btc.""" try: Millisatoshi(instance) return True except TypeError: return False
def init(options, configuration, plugin): config = plugin.rpc.listconfigs() plugin.cltv_final = config.get("cltv-final") plugin.fee_base = Millisatoshi(config.get("fee-base")) plugin.fee_ppm = config.get("fee-per-satoshi") plugin.mutex = Lock() plugin.log("Plugin rebalance.py initialized")
def get_currencyrate(plugin, currency, urlformat, replymembers): # NOTE: Bitstamp has a DNS/Proxy issues that can return 404 # Workaround: retry up to 5 times with a delay currency_lc = currency.lower() url = urlformat.format(currency_lc=currency_lc, currency=currency) r = requests_retry_session(retries=5, status_forcelist=(404)).get( url, proxies=plugin.proxies) if r.status_code != 200: plugin.log(level='info', message='{}: bad response {}'.format(url, r.status_code)) return None json = r.json() for m in replymembers: expanded = m.format(currency_lc=currency_lc, currency=currency) if expanded not in json: plugin.log(level='debug', message='{}: {} not in {}'.format(url, expanded, json)) return None json = json[expanded] try: return Millisatoshi(int(10**11 / float(json))) except Exception: plugin.log(level='info', message='{}: could not convert {} to msat'.format( url, json)) return None
def currencyconvert(plugin, amount, currency): """Converts currency using given APIs.""" rates = get_rates(plugin, currency.upper()) if len(rates) == 0: raise Exception("No values available for currency {}".format(currency.upper())) val = statistics.median([m.millisatoshis for m in rates.values()]) * float(amount) return {"msat": Millisatoshi(round(val))}
def rebalanceall_thread(plugin: Plugin): if not plugin.mutex.acquire(blocking=False): return try: start_ts = time.time() feeadjuster_state = feeadjuster_toggle(plugin, False) channels = get_open_channels(plugin) plugin.enough_liquidity = get_enough_liquidity_threshold(channels) plugin.ideal_ratio = get_ideal_ratio(channels, plugin.enough_liquidity) plugin.log(f"Automatic rebalance is running with enough liquidity threshold: {plugin.enough_liquidity}, " f"ideal liquidity ratio: {plugin.ideal_ratio * 100:.2f}%, " f"min rebalancable amount: {plugin.min_amount}, " f"feeratio: {plugin.feeratio}") failed_pairs = [] success = 0 fee_spent = Millisatoshi(0) while not plugin.rebalance_stop: result = maybe_rebalance_once(plugin, failed_pairs) if not result["success"]: break success += 1 fee_spent += result["fee_spent"] feeadjust_would_be_nice(plugin) feeadjuster_toggle(plugin, feeadjuster_state) elapsed_time = timedelta(seconds=time.time() - start_ts) plugin.log(f"Automatic rebalance finished: {success} successful rebalance, {fee_spent} fee spent, it took {str(elapsed_time)[:-3]}") finally: plugin.mutex.release()
def fundbalancedchannel(self, remote_node, total_capacity, announce=True): ''' Creates a perfectly-balanced channel, as all things should be. ''' if isinstance(total_capacity, Millisatoshi): total_capacity = int(total_capacity.to_satoshi()) else: total_capacity = int(total_capacity) self.fundwallet(total_capacity + 10000) self.rpc.connect(remote_node.info['id'], 'localhost', remote_node.port) # Make sure the fundchannel is confirmed. num_tx = len(self.bitcoin.rpc.getrawmempool()) tx = self.rpc.fundchannel(remote_node.info['id'], total_capacity, feerate='slow', minconf=0, announce=announce, push_msat=Millisatoshi(total_capacity * 500))['tx'] wait_for(lambda: len(self.bitcoin.rpc.getrawmempool()) == num_tx + 1) self.bitcoin.generate_block(1) # Generate the scid. # NOTE This assumes only the coinbase and the fundchannel is # confirmed in the block. return '{}x1x{}'.format( self.bitcoin.rpc.getblockcount(), get_tx_p2wsh_outnum(self.bitcoin, tx, total_capacity))
def rebalancereport(plugin: Plugin): """Show information about rebalance """ res = {} res["rebalanceall_is_running"] = plugin.mutex.locked() res["getroute_method"] = plugin.getroute.__name__ res["maxhops_threshold"] = plugin.maxhops res["msatfactor"] = plugin.msatfactor res["erringnodes_threshold"] = plugin.erringnodes channels = get_open_channels(plugin) health_percent = 0.0 if len(channels) > 1: enough_liquidity = get_enough_liquidity_threshold(channels) ideal_ratio = get_ideal_ratio(channels, enough_liquidity) res["enough_liquidity_threshold"] = enough_liquidity res["ideal_liquidity_ratio"] = f"{ideal_ratio * 100:.2f}%" for ch in channels: liquidity = liquidity_info(ch, enough_liquidity, ideal_ratio) health_percent += health_score(liquidity) * int(ch["total_msat"]) health_percent /= int(sum(ch["total_msat"] for ch in channels)) else: res["enough_liquidity_threshold"] = Millisatoshi(0) res["ideal_liquidity_ratio"] = "0%" res["liquidity_health"] = f"{health_percent:.2f}%" invoices = plugin.rpc.listinvoices()['invoices'] rebalances = [ i for i in invoices if i.get('status') == 'paid' and i.get('label').startswith("Rebalance") ] total_fee = Millisatoshi(0) total_amount = Millisatoshi(0) res["total_successful_rebalances"] = len(rebalances) for r in rebalances: try: pay = plugin.rpc.listpays(r["bolt11"])["pays"][0] total_amount += pay["amount_msat"] total_fee += pay["amount_sent_msat"] - pay["amount_msat"] except Exception: res["total_successful_rebalances"] -= 1 res["total_rebalanced_amount"] = total_amount res["total_rebalance_fee"] = total_fee if total_amount > Millisatoshi(0): res["average_rebalance_fee_ppm"] = round( total_fee / total_amount * 10**6, 2) else: res["average_rebalance_fee_ppm"] = 0 return res