def test_argument_coercion(): p = Plugin(autopatch=False) def test1(msat: Millisatoshi): assert isinstance(msat, Millisatoshi) ba = p._bind_kwargs(test1, {"msat": "100msat"}, None) test1(*ba.args) ba = p._bind_pos(test1, ["100msat"], None) test1(*ba.args, **ba.kwargs)
#!/usr/bin/env python3 from lightning import Plugin, Millisatoshi from packaging import version from collections import namedtuple import lightning import json import requests import threading import time plugin = Plugin(autopatch=True) have_utf8 = False # __version__ was introduced in 0.0.7.1, with utf8 passthrough support. try: if version.parse(lightning.__version__) >= version.parse("0.0.7.1"): have_utf8 = True except Exception: pass Charset = namedtuple('Charset', ['double_left', 'left', 'bar', 'mid', 'right', 'double_right', 'empty']) if have_utf8: draw = Charset('╟', '├', '─', '┼', '┤', '╢', '║') else: draw = Charset('#', '[', '-', '/', ']', '#', '|') class PriceThread(threading.Thread): def __init__(self): super().__init__()
from lightning import Plugin, RpcError from random import choice from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from time import sleep, time import heapq import json import os import random import string import threading Base = declarative_base() plugin = Plugin() exclusions = [] temporary_exclusions = {} class Probe(Base): __tablename__ = "probes" id = Column(Integer, primary_key=True) destination = Column(String) route = Column(String) error = Column(String) erring_channel = Column(String) failcode = Column(Integer) payment_hash = Column(String) started_at = Column(DateTime)
import sys from flask import Flask, render_template from flask_bootstrap import Bootstrap from flask_wtf import FlaskForm from io import BytesIO from lightning import LightningRpc, Plugin from os.path import join from random import random from time import time from wtforms import StringField, SubmitField, IntegerField from wtforms.validators import Required, NumberRange plugin = Plugin() class DonationForm(FlaskForm): """Form for donations """ amount = IntegerField("Enter how many Satoshis you want to donate!", validators=[Required(), NumberRange(min=1, max=16666666)]) description = StringField("Leave a comment (displayed publically)") submit = SubmitField('Donate') def make_base64_qr_code(bolt11): qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=4,
#!/usr/bin/env python3 from lightning import Plugin import json import psutil import subprocess import threading import time import os try: # C-lightning v0.7.2 plugin = Plugin(dynamic=False) except: plugin = Plugin() class ChildPlugin(object): def __init__(self, path, plugin): self.path = path self.plugin = plugin self.status = 'stopped' self.proc = None self.iolock = threading.Lock() self.decoder = json.JSONDecoder() self.manifest = None self.init = None self.reader = None def watch(self): last = os.path.getmtime(self.path) while True:
#!/usr/bin/env python3 """Simple plugin to test the openchannel_hook. We just refuse to let them open channels with an odd amount of millisatoshis. """ from lightning import Plugin, Millisatoshi plugin = Plugin() @plugin.hook('openchannel') def on_openchannel(openchannel, plugin, **kwargs): print("{} VARS".format(len(openchannel.keys()))) for k in sorted(openchannel.keys()): print("{}={}".format(k, openchannel[k])) if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 2 == 1: return { 'result': 'reject', 'error_message': "I don't like odd amounts" } return {'result': 'continue'} plugin.run()
#!/usr/bin/env python3 from lightning import Plugin plugin = Plugin(autopatch=True) @plugin.method("hello") def hello(plugin, name="world"): """This is the documentation string for the hello-function. It gets reported as the description when registering the function as a method with `lightningd`. """ greeting = plugin.get_option('greeting') s = '{} {}'.format(greeting, name) plugin.log(s) return s @plugin.method("init") def init(options, configuration, plugin): plugin.log("Plugin helloworld.py initialized") @plugin.subscribe("connect") def on_connect(plugin, id, address): plugin.log("Received connect event for peer {}".format(id)) @plugin.subscribe("disconnect")
#!/usr/bin/env python3 from lightning import Plugin from prometheus_client import start_http_server, CollectorRegistry from prometheus_client.core import InfoMetricFamily, GaugeMetricFamily from sys import exit plugin = Plugin() class BaseLnCollector(object): def __init__(self, rpc, registry): self.rpc = rpc self.registry = registry class NodeCollector(BaseLnCollector): def collect(self): info = self.rpc.getinfo() info_labels = {k: v for k, v in info.items() if isinstance(v, str)} node_info_fam = InfoMetricFamily( 'node', 'Static node information', labels=info_labels.keys(), ) node_info_fam.add_metric(info_labels, info_labels) yield node_info_fam class FundsCollector(BaseLnCollector): def collect(self): funds = self.rpc.listfunds()
#!/usr/bin/env python3 from lightning import Plugin plugin = Plugin(autopatch=True) @plugin.method("hello") def hello(plugin, name="world"): """This is the documentation string for the hello-function. It gets reported as the description when registering the function as a method with `lightningd`. """ greeting = plugin.get_option('greeting') s = '{} {}'.format(greeting, name) plugin.log(s) return s @plugin.init() def init(options, configuration, plugin): plugin.log("Plugin helloworld.py initialized") @plugin.subscribe("connect") def on_connect(plugin, id, address): plugin.log("Received connect event for peer {}".format(id))
#!/usr/bin/env python3 """This plugin is used to check that plugin options are parsed properly. The plugin offers 3 options, one of each supported type. """ from lightning import Plugin plugin = Plugin() @plugin.init() def init(configuration, options, plugin): for name, val in options.items(): plugin.log("option {} {} {}".format(name, val, type(val))) plugin.add_option('str_opt', 'i am a string', 'an example string option') plugin.add_option('int_opt', 7, 'an example int type option', opt_type='int') plugin.add_option('bool_opt', True, 'an example bool type option', opt_type='bool') plugin.run()
def connect(self, candidates, balance=1000000, dryrun=False): pdf = self.calculate_statistics(candidates) connection_dict = self.calculate_proposed_channel_capacities(pdf, balance) for nodeid, fraction in connection_dict.items(): try: satoshis = math.ceil(balance * fraction) print("Try to open channel with a capacity of {} to node {}".format(satoshis, nodeid)) if not dryrun: self.__rpc_interface.connect(nodeid) self.__rpc_interface.fundchannel(nodeid, satoshis) except ValueError as e: print("Could not open a channel to {} with capacity of {}. Error: {}".format(nodeid, satoshis, str(e))) plugin = Plugin() @plugin.init() def init(configuration, options, plugin): plugin.num_channels = int(options['autopilot-num-channels']) plugin.percent = int(options['autopilot-percent']) plugin.min_capacity_sat = int(options['autopilot-min-channel-size-msat']) / 1000 plugin.autopilot = CLightning_autopilot(plugin.rpc) @plugin.method('autopilot-run-once') def run_once(plugin, dryrun=False): # Let's start by inspecting the current state of the node funds = plugin.rpc.listfunds()
def test_bind_kwargs(): p = Plugin(autopatch=False) req = object() params = {'name': 'World'} def test1(name): assert name == 'World' bound = p._bind_kwargs(test1, params, req) test1(*bound.args, **bound.kwargs) def test2(name, plugin): assert name == 'World' assert plugin == p bound = p._bind_kwargs(test2, params, req) test2(*bound.args, **bound.kwargs) def test3(plugin, name): assert name == 'World' assert plugin == p bound = p._bind_kwargs(test3, params, req) test3(*bound.args, **bound.kwargs) def test4(plugin, name, request): assert name == 'World' assert plugin == p assert request == req bound = p._bind_kwargs(test4, params, req) test4(*bound.args, **bound.kwargs) def test5(request, name, plugin): assert name == 'World' assert plugin == p assert request == req bound = p._bind_kwargs(test5, params, req) test5(*bound.args, **bound.kwargs) def test6(request, name, plugin, answer=42): assert name == 'World' assert plugin == p assert request == req assert answer == 42 bound = p._bind_kwargs(test6, params, req) test6(*bound.args, **bound.kwargs) # Now mix in a catch-all parameter that needs to be assigned def test6(request, name, plugin, *args, **kwargs): assert name == 'World' assert plugin == p assert request == req assert args == () assert kwargs == {'answer': 42} bound = p._bind_kwargs(test6, {'name': 'World', 'answer': 42}, req) test6(*bound.args, **bound.kwargs)
def test_positional_inject(): p = Plugin() rdict = Request(plugin=p, req_id=1, method='func', params={ 'a': 1, 'b': 2, 'kwa': 3, 'kwb': 4 }) rarr = Request( plugin=p, req_id=1, method='func', params=[1, 2, 3, 4], ) def pre_args(plugin, a, b, kwa=3, kwb=4): assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def in_args(a, plugin, b, kwa=3, kwb=4): assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def post_args(a, b, plugin, kwa=3, kwb=4): assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def post_kwargs(a, b, kwa=3, kwb=4, plugin=None): assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def in_multi_args(a, request, plugin, b, kwa=3, kwb=4): assert request in [rarr, rdict] assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def in_multi_mix_args(a, plugin, b, request=None, kwa=3, kwb=4): assert request in [rarr, rdict] assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4) def extra_def_arg(a, b, c, d, e=42): """ Also uses a different name for kwa and kwb """ assert (a, b, c, d, e) == (1, 2, 3, 4, 42) def count(plugin, count, request): assert count == 42 and plugin == p funcs = [pre_args, in_args, post_args, post_kwargs, in_multi_args] for func, request in itertools.product(funcs, [rdict, rarr]): p._exec_func(func, request) p._exec_func(extra_def_arg, rarr) p._exec_func(count, Request( plugin=p, req_id=1, method='func', params=[42], )) # This should fail since it is missing one positional argument with pytest.raises(TypeError): p._exec_func(count, Request(plugin=p, req_id=1, method='func', params=[]))
To avoid this behevior I thought of making hand checks before making a Rpc call in a slot (i.e. doing this for each Rpc call since a GUI is event-driven), or override the `call` method which raises exceptions to quiet the RPC exception and open a dialog for the user to understand what's happening. I chose the second method. """ def call(self, method, payload=None): """Original call method with Qt-style exception handling""" try: return super(HackedLightningRpc, self).call(method, payload) except RpcError as e: QMessageBox.warning(None, "RPC error", str(e)) pass return False # Rpc call failed plugin = Plugin() @plugin.init() def init(options, configuration, plugin): notify2.init("lightning-qt") path = os.path.join(plugin.lightning_dir, plugin.rpc_filename) # See above the docstring for rationale plugin.rpc = HackedLightningRpc(path) @plugin.method("gui") def gui(plugin): """Launches the Qt GUI""" app = QApplication([]) win = MainWindow(plugin)
def test_methods_errors(): """A bunch of tests that should fail calling the methods.""" call_list = [] p = Plugin(autopatch=False) # Fails because we haven't added the method yet request = {'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': {}} with pytest.raises(ValueError): p._dispatch(request) assert call_list == [] @p.method("test1") def test1(name): call_list.append(test1) # Attempting to add it twice should fail with pytest.raises(ValueError): p.add_method("test1", test1) # Fails because it is missing the 'name' argument request = {'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': {}} with pytest.raises(TypeError): p._dispatch(request) assert call_list == [] # The same with positional arguments request = {'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': []} with pytest.raises(TypeError): p._dispatch(request) assert call_list == [] # Fails because we have a non-matching argument request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': { 'name': 'World', 'extra': 1 } } with pytest.raises(TypeError): p._dispatch(request) assert call_list == [] request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': ['World', 1] } with pytest.raises(TypeError): p._dispatch(request) assert call_list == []
#!/usr/bin/env python3 from lightning import Plugin plugin = Plugin() @plugin.method("utf8") def echo(plugin, utf8): assert '\\u' not in utf8 return {'utf8': utf8} plugin.run()
def test_simple_methods(): """Test the dispatch of methods, with a variety of bindings. """ call_list = [] p = Plugin(autopatch=False) @p.method("test1") def test1(name): """Has a single positional argument.""" assert name == 'World' call_list.append(test1) request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': { 'name': 'World' } } p._dispatch(request) assert call_list == [test1] @p.method("test2") def test2(name, plugin): """Also asks for the plugin instance. """ assert plugin == p call_list.append(test2) request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test2', 'params': { 'name': 'World' } } p._dispatch(request) assert call_list == [test1, test2] @p.method("test3") def test3(name, request): """Also asks for the request instance. """ assert request is not None call_list.append(test3) request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test3', 'params': { 'name': 'World' } } p._dispatch(request) assert call_list == [test1, test2, test3] @p.method("test4") def test4(name): """Try the positional arguments.""" assert name == 'World' call_list.append(test4) request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test4', 'params': ['World'] } p._dispatch(request) assert call_list == [test1, test2, test3, test4] @p.method("test5") def test5(name, request, plugin): """Try the positional arguments, mixing in the request and plugin.""" assert name == 'World' assert request is not None assert p == plugin call_list.append(test5) request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test5', 'params': ['World'] } p._dispatch(request) assert call_list == [test1, test2, test3, test4, test5] answers = [] @p.method("test6") def test6(name, answer=42): """This method has a default value for one of its params""" assert name == 'World' answers.append(answer) call_list.append(test6) # Both calls should work (with and without the default param request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test6', 'params': ['World'] } p._dispatch(request) assert call_list == [test1, test2, test3, test4, test5, test6] assert answers == [42] request = { 'id': 1, 'jsonrpc': '2.0', 'method': 'test6', 'params': ['World', 31337] } p._dispatch(request) assert call_list == [test1, test2, test3, test4, test5, test6, test6] assert answers == [42, 31337]
#!/usr/bin/env python3 """Simple plugin to allow testing while closing of HTLC is delayed. """ from lightning import Plugin import time plugin = Plugin() @plugin.hook('invoice_payment') def on_payment(payment, plugin): time.sleep(float(plugin.get_option('holdtime'))) return {} plugin.add_option('holdtime', '10', 'The time to hold invoice for.') plugin.run()
#!/usr/bin/env python3 from lightning import Plugin from lib.channel_filters import filter_channels plugin = Plugin(autopatch=True) @plugin.method("filterchannels") def filterchannels(plugin, filter_name, amount=0, unit='msat'): """Filter channels with `filter_name` as pending, active, closed, greater_than, or less_than with optional `amount` and `unit` (msat, sat, mbtc, or btc) """ return filter_channels(plugin.rpc, filter_name, amount, unit) @plugin.init() def init(options, configuration, plugin): print("Plugin filter_channel.py initialized") plugin.run()
#!/usr/bin/env python3 """This plugin is used to check that db_write calls are working correctly. """ from lightning import Plugin, RpcError import sqlite3 plugin = Plugin() plugin.sqlite_pre_init_cmds = [] plugin.initted = False @plugin.init() def init(configuration, options, plugin): if not plugin.get_option('dblog-file'): raise RpcError("No dblog-file specified") plugin.conn = sqlite3.connect(plugin.get_option('dblog-file'), isolation_level=None) plugin.log("replaying pre-init data:") plugin.conn.execute("PRAGMA foreign_keys = ON;") print(plugin.sqlite_pre_init_cmds) plugin.conn.execute("BEGIN TRANSACTION;") for c in plugin.sqlite_pre_init_cmds: plugin.conn.execute(c) plugin.log("{}".format(c)) plugin.conn.execute("COMMIT;") plugin.initted = True
#!/usr/bin/env python3 from lightning import Plugin, Millisatoshi, RpcError import re import time import uuid plugin = Plugin() # When draining 100% we must account (not pay) for an additional HTLC fee. # Currently there is no way of getting the exact number before the fact, # so we try and error until it is high enough, or take the exception text. HTLC_FEE_NUL = Millisatoshi('0sat') HTLC_FEE_STP = Millisatoshi('10sat') HTLC_FEE_MIN = Millisatoshi('100sat') HTLC_FEE_MAX = Millisatoshi('100000sat') HTLC_FEE_PAT = re.compile("^.* HTLC fee: ([0-9]+sat).*$") 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 )
#!/usr/bin/env python3 from lightning import Plugin, Millisatoshi, RpcError from datetime import datetime import time import uuid plugin = Plugin() def setup_routing_fees(plugin, route, msatoshi, payload): delay = int(plugin.get_option('cltv-final')) for r in reversed(route): r['msatoshi'] = msatoshi.millisatoshis r['amount_msat'] = msatoshi 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 += (msatoshi * ch['fee_per_millionth'] + 10**6 - 1) // 10**6 # integer math trick to round up if ch['source'] == payload['nodeid']: fee += payload['msatoshi'] msatoshi += fee delay += ch['delay'] r['direction'] = int(ch['channel_flags']) % 2 def find_worst_channel(route, nodeid): worst = None
#!/usr/bin/env python3 """Simple plugin to test the dynamic behavior. A plugin started with dynamic to False cannot be controlled after lightningd has been started. """ from lightning import Plugin plugin = Plugin(dynamic=False) @plugin.init() def init(configuration, options, plugin): plugin.log("Static plugin initialized.") @plugin.method('hello') def reject(plugin): """Mark a given node_id as reject for future connections. """ return "Hello, you cannot stop me without stopping lightningd" plugin.run()
#!/usr/bin/env python3 """This plugin is used to check that db_write calls are working correctly. """ from lightning import Plugin, RpcError import sqlite3 plugin = Plugin() plugin.sqlite_pre_init_cmds = [] plugin.initted = False @plugin.init() def init(configuration, options, plugin): if not plugin.get_option('dblog-file'): raise RpcError("No dblog-file specified") plugin.conn = sqlite3.connect(plugin.get_option('dblog-file'), isolation_level=None) plugin.log("replaying pre-init data:") for c in plugin.sqlite_pre_init_cmds: plugin.conn.execute(c) plugin.log("{}".format(c)) plugin.initted = True plugin.log("initialized") @plugin.hook('db_write') def db_write(plugin, writes): if not plugin.initted: plugin.log("deferring {} commands".format(len(writes))) plugin.sqlite_pre_init_cmds += writes else: