From 93af2a273163b47ead36bae5cf6de55194d03868 Mon Sep 17 00:00:00 2001 From: Djuri Baars Date: Sat, 18 Dec 2021 18:30:02 +0100 Subject: [PATCH] Implemented c-lightning support with short channel IDs --- checkring.py | 15 +++++++------- clightning.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ lnclient.py | 18 +++++++++++++++++ lnd.py | 3 ++- requirements.txt | 3 ++- ringtools.py | 28 ++++++++++++++++++++++---- status.py | 20 ++++++++++--------- 7 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 clightning.py create mode 100644 lnclient.py diff --git a/checkring.py b/checkring.py index 0667528..9f601a2 100644 --- a/checkring.py +++ b/checkring.py @@ -7,8 +7,8 @@ from yachalk import chalk class CheckRing: - def __init__(self, lnd, output, pubkeys_file, write_channels, show_fees, channels_file): - self.lnd = lnd + def __init__(self, client, output, pubkeys_file, write_channels, show_fees, channels_file): + self.client = client self.output = output self.pubkeys_file = pubkeys_file self.show_fees = show_fees @@ -34,10 +34,11 @@ class CheckRing: # pubkeys format is , to be able to mimic the manual pubkey overview with usernames pubkey = pubkeyInfo.split(',') try: - response = self.lnd.get_node_channels(pubkey[0]) + alias = self.client.get_node_alias(pubkey[0]) + response = self.client.get_node_channels(pubkey[0]) print("%s" % - (chalk.yellow(response.node.alias))) + (chalk.yellow(alias))) channelTo = pubkeys[(idx+1) % (len(pubkeys))].split(',')[0] hasChannel = False @@ -62,9 +63,9 @@ class CheckRing: print(chalk.green("Channel is open with ID: %s") % channelId) if self.show_fees: - response = self.lnd.get_edge(int(channelId)) - node1 = self.lnd.get_node(response.node1_pub) - node2 = self.lnd.get_node(response.node2_pub) + response = self.client.get_edge(int(channelId)) + node1 = self.client.get_node(response.node1_pub) + node2 = self.client.get_node(response.node2_pub) disabled = response.node1_policy.disabled or response.node2_policy.disabled self.print_channel( channelInfo, node1.alias, node2.alias, disabled) diff --git a/clightning.py b/clightning.py new file mode 100644 index 0000000..f564faf --- /dev/null +++ b/clightning.py @@ -0,0 +1,52 @@ +from os.path import expanduser + +from pyln.client import LightningRpc +import argparse +from lnclient import LNClient + +class CLightning(LNClient): + def __init__(self, clrpc): + self.rpc = LightningRpc(clrpc) + + # TODO: handle invalid channel ids + def get_edge(self, channel_id): + response = self.rpc.listchannels(short_channel_id=channel_id)['channels'] + return self.convert_cl_to_lnd(response) + + def get_node_channels(self, pub_key): + response =self.rpc.listchannels(source=pub_key)['channels'] + + output = argparse.Namespace() + output.channels = [] + for r in response: + output.channels.append(self.get_edge(r['short_channel_id'])) + return output + + def get_node_alias(self, pub_key): + return self.get_node(pub_key).alias + + def get_node(self, pub_key): + return argparse.Namespace(**self.rpc.listnodes(pub_key)['nodes'][0]) + + def convert_cl_to_lnd(self, d): + output = argparse.Namespace() + output.channel_id = d[0]['short_channel_id'] + output.node1_pub = d[0]['source'] + output.node2_pub = d[0]['destination'] + + output.node1_policy = argparse.Namespace() + output.node2_policy = argparse.Namespace() + + output.node1_policy.disabled = not d[0]['active'] + + if (len(d) > 1): + output.node2_policy.disabled = not d[1]['active'] + + output.node1_policy.fee_base_msat = d[0]['base_fee_millisatoshi'] + output.node1_policy.fee_rate_milli_msat = d[0]['fee_per_millionth'] + + if (len(d) > 1): + output.node2_policy.fee_base_msat = d[1]['base_fee_millisatoshi'] + output.node2_policy.fee_rate_milli_msat = d[1]['fee_per_millionth'] + + return output \ No newline at end of file diff --git a/lnclient.py b/lnclient.py new file mode 100644 index 0000000..fe30712 --- /dev/null +++ b/lnclient.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +class LNClient(ABC): + + @abstractmethod + def get_edge(self, channel_id): + pass + + @abstractmethod + def get_node_channels(self, pub_key): + pass + + @abstractmethod + def get_node_alias(self, pub_key): + pass + + @abstractmethod + def get_node(self, pub_key): + pass diff --git a/lnd.py b/lnd.py index 041e6f4..d1bff9e 100644 --- a/lnd.py +++ b/lnd.py @@ -8,11 +8,12 @@ from grpc_gen import router_pb2 as lnrouter from grpc_gen import router_pb2_grpc as lnrouterrpc from grpc_gen import lightning_pb2 as ln from grpc_gen import lightning_bp2_grpc as lnrpc +from lnclient import LNClient MESSAGE_SIZE_MB = 50 * 1024 * 1024 -class Lnd: +class Lnd(LNClient): def __init__(self, lnd_dir, server): os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA" lnd_dir = expanduser(lnd_dir) diff --git a/requirements.txt b/requirements.txt index eaef035..3bcd02a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ googleapis-common-protos==1.53.0 grpcio==1.39.0 protobuf==3.17.3 -yachalk==0.1.4 \ No newline at end of file +yachalk==0.1.4 +pyln-client==0.10.2.post0 \ No newline at end of file diff --git a/ringtools.py b/ringtools.py index 7f62bb2..d31b5ee 100644 --- a/ringtools.py +++ b/ringtools.py @@ -1,5 +1,6 @@ import argparse import sys +from clightning import CLightning from lnd import Lnd from output import Output @@ -9,19 +10,22 @@ from checkring import CheckRing class RingTools: def __init__(self, arguments): - self.lnd = Lnd(arguments.lnddir, arguments.grpc) - self.output = Output(self.lnd) + if (arguments.client == 'cl'): + self.client = CLightning(arguments.clrpc) + else: + self.client = Lnd(arguments.lnddir, arguments.grpc) + self.output = Output(self.client) self.arguments = arguments def start(self): if self.arguments.function == "status": - Status(self.lnd, + Status(self.client, self.output, self.arguments.channels_file, self.arguments.loop, self.arguments.show_fees).run() elif self.arguments.function == "check": - CheckRing(self.lnd, + CheckRing(self.client, self.output, self.arguments.pubkeys_file, self.arguments.write_channels, @@ -47,6 +51,15 @@ def get_argument_parser(): "like to use", default="help", ) + + parser.add_argument( + "--client", + choices=['lnd', 'cl'], + default="lnd", + dest="client", + help="(default: lnd) Which client to use", + ) + #If nodeos is Umbrel use the default umbrel lnd location lnd_dir = "~/.lnd" if is_umbrel(): @@ -64,6 +77,13 @@ def get_argument_parser(): dest="grpc", help="(default localhost:10009) lnd gRPC endpoint", ) + + parser.add_argument( + "--clrpc", + default="~/.lightning/bitcoin/lightning-rpc", + dest="clrpc", + help="(default ~/.lightning/bitcoin/lightning-rpc) C-Lightning unix-socket", + ) status_group = parser.add_argument_group( "status", diff --git a/status.py b/status.py index 9d2a50d..dc5a23b 100644 --- a/status.py +++ b/status.py @@ -8,8 +8,8 @@ LOOP_SLEEP_TIME = 10 class Status: - def __init__(self, lnd, output, channels_file, keep_loop, show_fees): - self.lnd = lnd + def __init__(self, client, output, channels_file, keep_loop, show_fees): + self.client = client self.output = output self.channels_file = channels_file self.keep_loop = keep_loop @@ -37,15 +37,17 @@ class Status: def once(self): channels = self.read_file(self.channels_file) for channelID in channels: - if len(channelID) != 18: - continue - if not channelID.isnumeric(): - continue + # TODO: Fix this with short channel ids or convert between formats + # if len(channelID) != 18: + # continue + # if not channelID.isnumeric(): + # continue try: - response = self.lnd.get_edge(int(channelID)) - node1 = self.lnd.get_node(response.node1_pub) - node2 = self.lnd.get_node(response.node2_pub) + response = self.client.get_edge(channelID) + node1 = self.client.get_node(response.node1_pub) + node2 = self.client.get_node(response.node2_pub) + disabled = response.node1_policy.disabled or response.node2_policy.disabled self.print_channel(response, node1.alias, node2.alias, disabled) except grpc.RpcError as e: