#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [ # "pyln-client>=25.9.3" # ] # /// from pyln.client import Plugin, RpcError from threading import Timer import os import json import traceback plugin = Plugin() def load_state(path): try: state = json.loads(open(path, "r").read()) except Exception: print("Could not read state file, creating a new one.") return {"channels": {}} return state def save_state(path, state): """Atomically save the new state to the state_file.""" tmppath = path + ".tmp" with open(tmppath, "w") as f: f.write(json.dumps(state, indent=2)) os.rename(tmppath, path) def is_connectable(rpc, node_id): nodes = rpc.listnodes(node_id) if len(nodes) == 0: return False print(nodes) def maybe_open_channel(desired, rpc): peers = rpc.listpeers(desired["node_id"])["peers"] if "satoshi" in desired: desired["amount"] = "{}sat".format(desired["satoshi"]) del desired["satoshi"] if peers == []: # Need to connect first, and then open a channel # if not is_connectable(rpc, desired['node_id']): # print("No address known for {}, cannot connect.".format(desired['node_id'])) try: rpc.connect(desired["node_id"]) except RpcError as re: print("Could not connect to peer: {}".format(re.error)) return peer = rpc.listpeers(desired["node_id"])["peers"][0] else: peer = peers[0] channel_states = [c["state"] for c in peer["channels"]] if peer is None or len(peer["channels"]) == 0: # Just open it, we don't have one yet # TODO(cdecker) Check balance before actually opening rpc.fundchannel(**desired) elif "CHANNELD_NORMAL" in channel_states: # Already in the desired state, nothing to do. return elif channel_states == ["ONCHAIND"]: # If our only channel is in state ONCHAIND it's probably time # to open a new one rpc.connect(desired["node_id"]) rpc.fundchannel(**desired) def check_channels(plugin): """Load actual and desired states, and try to reconcile.""" state = load_state(plugin.state_file) print(state) for c in state["channels"].values(): try: maybe_open_channel(c, plugin.rpc) except Exception: plugin.log(f"Error attempting to open a channel with {c['node_id']}.") traceback.print_exc() Timer(30, check_channels, args=[plugin]).start() @plugin.method("addpersistentchannel") def add_persistent_channel(node_id, satoshi, plugin, feerate="normal", announce=True): """Add a persistent channel to the state map. The persistent-channels plugin will ensure that the channel is opened as soon as possible and re-opened should it get closed. The parameters are identical to `fundchannel`. """ state = load_state(plugin.state_file) state["channels"][node_id] = { "node_id": node_id, "satoshi": satoshi, "feerate": feerate, "announce": announce, } save_state(plugin.state_file, state) maybe_open_channel(state["channels"][node_id], plugin.rpc) @plugin.init() def init(options, configuration, plugin): # This is the file in which we'll store all of our state (mostly # desired channels for now) plugin.state_file = os.path.join( configuration["lightning-dir"], "persistent-channels.json" ) check_channels(plugin) plugin.run()