diff --git a/drain/__init__.py b/drain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drain/drain.py b/drain/drain.py index c726e59..1a28161 100755 --- a/drain/drain.py +++ b/drain/drain.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from pyln.client import Plugin, Millisatoshi, RpcError -from utils import * +from utils import get_ours, wait_ours import re import time import uuid @@ -19,7 +19,7 @@ HTLC_FEE_EST = Millisatoshi('3000sat') HTLC_FEE_PAT = re.compile("^.* HTLC fee: ([0-9]+sat).*$") -def setup_routing_fees(plugin, payload, route, amount, substractfees: bool=False): +def setup_routing_fees(plugin, payload, route, amount, substractfees: bool = False): delay = int(plugin.get_option('cltv-final')) amount_iter = amount @@ -66,9 +66,9 @@ def get_channel(plugin, payload, peer_id, scid=None): try: channel = next(c for c in peer['channels'] if 'short_channel_id' in c and c['short_channel_id'] == scid) except StopIteration: - raise RpcError(payload['command'], payload, {'message': 'Cannot find channel %s for peer %s' % (scid, peer_id) }) + raise RpcError(payload['command'], payload, {'message': 'Cannot find channel %s for peer %s' % (scid, peer_id)}) if channel['state'] != "CHANNELD_NORMAL": - raise RpcError(payload['command'], payload, {'message': 'Channel %s not in state CHANNELD_NORMAL, but: %s' % (scid, channel['state']) }) + raise RpcError(payload['command'], payload, {'message': 'Channel %s not in state CHANNELD_NORMAL, but: %s' % (scid, channel['state'])}) if not peer['connected']: raise RpcError(payload['command'], payload, {'message': 'Channel %s peer is not connected.' % scid}) return channel @@ -152,16 +152,16 @@ def test_or_set_chunks(plugin, payload): # get all spendable/receivables for our channels channels = {} - for channel in plugin.rpc.listchannels(source = payload['my_id']).get('channels'): + for channel in plugin.rpc.listchannels(source=payload['my_id']).get('channels'): if channel['short_channel_id'] == scid: continue try: spend, recv = spendable_from_scid(plugin, payload, channel['short_channel_id'], True) - except RpcError as e: + except RpcError: continue channels[channel['short_channel_id']] = { - 'spendable' : spend, - 'receivable' : recv, + 'spendable': spend, + 'receivable': recv, } if len(channels) == 0: raise RpcError(payload['command'], payload, {'message': 'Not enough usable channels to perform cyclic routing.'}) @@ -232,13 +232,13 @@ def try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable_before): my_id = payload['my_id'] label = payload['command'] + "-" + str(uuid.uuid4()) payload['labels'] += [label] - description = "%s %s %s%s [%d/%d]" % (payload['command'], payload['scid'], payload['percentage'], '%', chunk+1, payload['chunks']) + description = "%s %s %s%s [%d/%d]" % (payload['command'], payload['scid'], payload['percentage'], '%', chunk + 1, payload['chunks']) invoice = plugin.rpc.invoice("any", label, description, payload['retry_for'] + 60) payment_hash = invoice['payment_hash'] plugin.log("Invoice payment_hash: %s" % payment_hash) # exclude selected channel to prevent unwanted shortcuts - excludes = [payload['scid']+'/0', payload['scid']+'/1'] + excludes = [payload['scid'] + '/0', payload['scid'] + '/1'] mychannels = plugin.rpc.listchannels(source=my_id).get('channels') # exclude local channels known to have too little capacity. # getroute currently does not do this. @@ -247,23 +247,23 @@ def try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable_before): continue # already added few lines above spend, recv = spendable_from_scid(plugin, payload, channel['short_channel_id']) if payload['command'] == 'drain' and recv < amount: - excludes += [channel['short_channel_id']+'/0', channel['short_channel_id']+'/1'] + excludes += [channel['short_channel_id'] + '/0', channel['short_channel_id'] + '/1'] if payload['command'] == 'fill' and spend < amount: - excludes += [channel['short_channel_id']+'/0', channel['short_channel_id']+'/1'] + excludes += [channel['short_channel_id'] + '/0', channel['short_channel_id'] + '/1'] while int(time.time()) - start_ts < payload['retry_for']: if payload['command'] == 'drain': r = plugin.rpc.getroute(my_id, amount, riskfactor=0, - cltv=9, fromid=peer_id, fuzzpercent=0, exclude=excludes) + cltv=9, fromid=peer_id, fuzzpercent=0, exclude=excludes) route_out = {'id': peer_id, 'channel': payload['scid'], 'direction': int(my_id >= peer_id)} route = [route_out] + r['route'] setup_routing_fees(plugin, payload, route, amount, True) if payload['command'] == 'fill': r = plugin.rpc.getroute(peer_id, amount, riskfactor=0, - cltv=9, fromid=my_id, fuzzpercent=0, exclude=excludes) + cltv=9, fromid=my_id, fuzzpercent=0, exclude=excludes) route_in = {'id': my_id, 'channel': payload['scid'], 'direction': int(peer_id >= my_id)} route = r['route'] + [route_in] - setup_routing_fees(plugin, payload, route, amount , False) + setup_routing_fees(plugin, payload, route, amount, False) fees = route[0]['amount_msat'] - route[-1]['amount_msat'] @@ -276,7 +276,7 @@ def try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable_before): excludes += [worst_channel_id + '/0', worst_channel_id + '/1'] continue - plugin.log("[%d/%d] Sending over %d hops to %s %s using %s fees" % (chunk+1, payload['chunks'], len(route), payload['command'], amount, fees), 'debug') + plugin.log("[%d/%d] Sending over %d hops to %s %s using %s fees" % (chunk + 1, payload['chunks'], len(route), payload['command'], amount, fees), 'debug') for r in route: plugin.log(" - %s %14s %s" % (r['id'], r['channel'], r['amount_msat']), 'debug') @@ -285,7 +285,7 @@ def try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable_before): plugin.rpc.sendpay(route, payment_hash, label) result = plugin.rpc.waitsendpay(payment_hash, payload['retry_for'] + start_ts - int(time.time())) if result.get('status') == 'complete': - payload['success_msg'] += ["%dmsat sent over %d hops to %s %dmsat [%d/%d]" % (amount + fees, len(route), payload['command'], amount, chunk+1, payload['chunks'])] + payload['success_msg'] += ["%dmsat sent over %d hops to %s %dmsat [%d/%d]" % (amount + fees, len(route), payload['command'], amount, chunk + 1, payload['chunks'])] # we need to wait for HTLC to resolve, so remaining amounts # can be calculated correctly for the next chunk wait_ours(plugin, payload['scid'], ours) @@ -314,7 +314,7 @@ def try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable_before): def read_params(command: str, scid: str, percentage: float, - chunks: int, maxfeepercent: float, retry_for: int, exemptfee: Millisatoshi): + chunks: int, maxfeepercent: float, retry_for: int, exemptfee: Millisatoshi): # check parameters if command != 'drain' and command != 'fill' and command != 'setbalance': @@ -327,15 +327,15 @@ def read_params(command: str, scid: str, percentage: float, # forge operation payload payload = { - "command" : command, + "command": command, "scid": scid, "percentage": percentage, "chunks": chunks, "maxfeepercent": maxfeepercent, "retry_for": retry_for, "exemptfee": exemptfee, - "labels" : [], - "success_msg" : [], + "labels": [], + "success_msg": [], } # cache some often required data @@ -396,7 +396,7 @@ def execute(payload: dict): if amount < htlc_fee: raise RpcError(payload['command'], payload, {'message': 'channel too low to cover fees'}) amount -= htlc_fee - plugin.log("Trying... chunk:%s/%s spendable:%s receivable:%s htlc_fee:%s => amount:%s" % (chunk+1, payload['chunks'], spendable, receivable, htlc_fee, amount)) + plugin.log("Trying... chunk:%s/%s spendable:%s receivable:%s htlc_fee:%s => amount:%s" % (chunk + 1, payload['chunks'], spendable, receivable, htlc_fee, amount)) try: result = try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable) @@ -425,8 +425,8 @@ def execute(payload: dict): @plugin.method("drain") -def drain(plugin, scid: str, percentage: float=100, chunks: int=0, maxfeepercent: float=0.5, - retry_for: int=60, exemptfee: Millisatoshi=Millisatoshi(5000)): +def drain(plugin, scid: str, percentage: float = 100, chunks: int = 0, maxfeepercent: float = 0.5, + retry_for: int = 60, exemptfee: Millisatoshi = Millisatoshi(5000)): """Draining channel liquidity with circular payments. Percentage defaults to 100, resulting in an empty channel. @@ -438,8 +438,8 @@ def drain(plugin, scid: str, percentage: float=100, chunks: int=0, maxfeepercent @plugin.method("fill") -def fill(plugin, scid: str, percentage: float=100, chunks: int=0, maxfeepercent: float=0.5, - retry_for: int=60, exemptfee: Millisatoshi=Millisatoshi(5000)): +def fill(plugin, scid: str, percentage: float = 100, chunks: int = 0, maxfeepercent: float = 0.5, + retry_for: int = 60, exemptfee: Millisatoshi = Millisatoshi(5000)): """Filling channel liquidity with circular payments. Percentage defaults to 100, resulting in a full channel. @@ -451,8 +451,8 @@ def fill(plugin, scid: str, percentage: float=100, chunks: int=0, maxfeepercent: @plugin.method("setbalance") -def setbalance(plugin, scid: str, percentage: float=50, chunks: int=0, maxfeepercent: float=0.5, - retry_for: int=60, exemptfee: Millisatoshi=Millisatoshi(5000)): +def setbalance(plugin, scid: str, percentage: float = 50, chunks: int = 0, maxfeepercent: float = 0.5, + retry_for: int = 60, exemptfee: Millisatoshi = Millisatoshi(5000)): """Brings a channels own liquidity to X percent using circular payments. Percentage defaults to 50, resulting in a balanced channel. diff --git a/drain/test_drain.py b/drain/test_drain.py index a4cee7a..c0c945d 100644 --- a/drain/test_drain.py +++ b/drain/test_drain.py @@ -1,14 +1,16 @@ -from pyln.testing.fixtures import * +from pyln.testing.fixtures import * # noqa: F401,F403 from pyln.testing.utils import DEVELOPER -from pyln.client import RpcError, Millisatoshi -from utils import * +from pyln.client import RpcError +from .utils import get_ours, get_theirs, wait_ours, wait_for_all_htlcs import os import unittest +import pytest pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "drain.py")} EXPERIMENTAL_FEATURES = int(os.environ.get("EXPERIMENTAL_FEATURES", "0")) + @unittest.skipIf(not DEVELOPER, "slow gossip, needs DEVELOPER=1") def test_drain_and_refill(node_factory, bitcoind): # Scenario: first drain then refill @@ -37,7 +39,7 @@ def test_drain_and_refill(node_factory, bitcoind): # wait for each others gossip bitcoind.generate_block(6) for n in nodes: - for scid in [scid12,scid23,scid34,scid41]: + for scid in [scid12, scid23, scid34, scid41]: n.wait_channel_active(scid) # do some draining and filling @@ -87,7 +89,7 @@ def test_fill_and_drain(node_factory, bitcoind): # wait for each others gossip bitcoind.generate_block(6) for n in nodes: - for scid in [scid12,scid23,scid34,scid41]: + for scid in [scid12, scid23, scid34, scid41]: n.wait_channel_active(scid) # for l2 to fill scid12, it needs to send on scid23, where its funder @@ -128,7 +130,7 @@ def test_setbalance(node_factory, bitcoind): # wait for each others gossip bitcoind.generate_block(6) for n in nodes: - for scid in [scid12,scid23,scid34,scid41]: + for scid in [scid12, scid23, scid34, scid41]: n.wait_channel_active(scid) # test auto 50/50 balancing diff --git a/drain/utils.py b/drain/utils.py index a1b079d..aad138a 100644 --- a/drain/utils.py +++ b/drain/utils.py @@ -1,6 +1,6 @@ import time -TIMEOUT=60 +TIMEOUT = 60 def wait_for(success, timeout=TIMEOUT):