Register a JSON-RPC method to Core Lightning using pyln-client Python package
In this live, we register a JSON-RPC method in Core Lightning using pyln-client Python package. As pyln-client uses Python decorators to declare JSON-RPC methods, in the second part we try to understand how they work by playing with 2 examples, the second one being very close to the way pyln-client uses them.
Transcript with corrections and improvements
Introduction
Hi everybody, I'm really happy to be with you for another live.
To make this session more interactive, we will divide it into three parts of about 20 minutes of coding and 10 minutes of chat.
In the first live we tried to understand CLN Plugin mechanism by building a plugin in Python from scratch. In some way, we used Python as a "proxy" language to demonstrate how plugins work but we could have done it the same way with any other general purpose language.
Today we'll start by registering the same JSON-RPC method in Core
Lightning as in the first live but using pyln-client Python package.
This is faster to write as it takes only 25% of the LOC that we wrote last time to get the plugin working the same way.
As pyln-client uses Python decorators to declare JSON-RPC methods, in
the second part we'll try to understand how they work by playing with
2 examples, the second one being very close to the way pyln-client
uses them.
Finally we'll look at Plugin.method method implementation (We haven't
seen this part, but I'll talk about it in a video that will be posted
soon).
Let's go.
Register myplugin method to CLN using pyln-client
Let's add the method myplugin to CLN by writing a dynamic
Python plugin called myplugin.py using pyln-client.
When we start the plugin myplugin.py with the option foo_opt set to
BAR like this
◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.py foo_opt=BAR
where l1-cli is an alias for lightning-cli
--lightning-dir=/tmp/l1-regtest, we expect myplugin method called with
the parameters foo1=bar1 and foo2=bar2 to gives us the following
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar1 foo2=bar2
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "BAR"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
where the value of node_id is the ID of the node l1.
Setup
Here is my setup:
◉ tony@tony:~/clnlive:
$ lightningd --version | xargs printf "lightningd %s\n" && python --version && lsb_release -ds
lightningd v23.02.2
Python 3.10.6
Ubuntu 22.04.2 LTS
Start 2 Lightning nodes running on regtest
Let's start two Lightning nodes running on the Bitcoin regtest chain
by sourcing the script lightning/contrib/startup_regtest.sh provided
in CLN repository and by running the command start_ln:
◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
start_ln 3: start three nodes, l1, l2, l3
connect 1 2: connect l1 and l2
fund_nodes: connect all nodes with channels, in a row
stop_ln: shutdown
destroy_ln: remove ln directories
◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 108917
[2] 108952
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
We can check that l1-cli is just an alias for lightning-cli with the
base directory being /tmp/l1-regtest:
◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Installing and using pyln-client
Note that during the live we got the Python error ModuleNotFoundError:
No module named 'pyln' when we tried to use pyln-client installed in
a Python virtual environment.
I described here what we did, why we got that error (not in the video) and how not do to that mistake again (not in the video).
In the executable file myplugin.py, we import the class Plugin from
the module pyln.client, then we instantiate the object plugin with
the class Plugin and finally we start the I/O loop with plugin.run():
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
plugin.run()
This is the minimum we need to have the plugin working (thought it does nothing).
Now, as we haven't installed pyln-client yet (note that it is not
installed globlally in my computer), if we try to start myplugin.py
plugin we get the following error:
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
Traceback (most recent call last):
File "/home/tony/clnlive/myplugin.py", line 3, in <module>
from pyln.client import Plugin
ModuleNotFoundError: No module named 'pyln'
{
"code": -3,
"message": "/home/tony/clnlive/myplugin.py: exited before replying to getmanifest"
}
Let's install pyln-client in the virtual environment .venv using pip
like this:
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ which python
/home/tony/clnlive/.venv/bin/python
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
Successfully installed PySocks-1.7.1 asn1crypto-1.5.1 base58-2.1.1 bitstring-3.1.9 cffi-1.15.1 coincurve-17.0.0 cryptography-36.0.2 pycparser-2.21 pyln-bolt7-1.0.246 pyln-client-23.2 pyln-proto-23.2
We can list the newly installed packages in our virtual environment that we have activated:
(.venv) ◉ tony@tony:~/clnlive:
$ pip list
Package Version
------------ -------
asn1crypto 1.5.1
base58 2.1.1
bitstring 3.1.9
cffi 1.15.1
coincurve 17.0.0
cryptography 36.0.2
pip 22.0.2
pycparser 2.21
pyln-bolt7 1.0.246
pyln-client 23.2
pyln-proto 23.2
PySocks 1.7.1
setuptools 59.6.0
Now let's try to start myplugin.py again:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
Traceback (most recent call last):
File "/home/tony/clnlive/myplugin.py", line 3, in <module>
from pyln.client import Plugin
ModuleNotFoundError: No module named 'pyln'
{
"code": -3,
"message": "/home/tony/clnlive/myplugin.py: exited before replying to getmanifest"
}
During the live I didn't understand why we got that error thought
.venv virtual environment was activated with pyln-client installed and
the Python binary used in that environment was
/home/tony/clnlive/.venv/bin/python. In order not to waste too much
time at this stage of the live, I decided to change the shebang of
myplugin.py file from
#!/usr/bin/env python
to
#!/home/tony/clnlive/.venv/bin/python
which tells the Python script to use the modules installed in .venv
virtual environment. We deactivated .venv virtual environment and
succeeded to start myplugin.py plugin. It worked perfectly and we
moved on.
Now let's take look at the mistake I made.
When we tell lightningd to start a plugin, it spawned it as a child
process. Child processes inherit their environment variables from
their parent. Now, remember that we started lightningd (with
start_ln) before activating .venv environment. So
/home/tony/clnlive/.venv/bin directory didn't appear at the beginning
of PATH environment variable of lightningd process, and so neither
appeared in PATH environment variable of the child process and finally
/home/tony/clnlive/.venv/bin/python couldn't be found.
What could we have done differently to develop our plugin without
changing the shebang #!/usr/bin/env python?
We could have push
/home/tony/clnlive/.venv/binat the beginning ofPATHenvironment variable before startinglightningd(for instance by activating.venvvirtual environment before) or,we could have install
pyln-clientglobally (without using Python virtual environments).
Declare myplugin JSON-RPC method
The file myplugin.py being
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
plugin.run()
we can start the plugin like this
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
and verify that it is indeed running by checking for its process:
◉ tony@tony:~/clnlive:
$ ps -ax | rg myplugin
109548 pts/1 S 0:00 /home/tony/clnlive/.venv/bin/python /home/tony/clnlive/myplugin.py
109555 pts/1 D+ 0:00 rg myplugin
When we started the plugin, the expression plugin.run() did two things:
first it answered to the
getmanifestrequest sent bylightningd, saying something like this: "I'm declaring no options, no methods, no notifications, nothing" and thenit answered to the
initrequest sent bylightningdsaying something like this: "I have no specific initialization to do, we can talk together now"
and after this the plugin started waiting for incoming data in its
stdin.
So with no surprise, as myplugin has not been registered in that
communication, we see that lightningd doesn't know about myplugin
method:
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"code": -32601,
"message": "Unknown command 'myplugin'"
}
Now let's declare myplugin method that takes no arguments and returns
always the json object {"foo": "bar"}:
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin):
return {"foo": "bar"}
plugin.run()
Let's restart the plugin
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
and call myplugin method by running the following command:
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo": "bar"
}
Not so bad. We know how to register JSON-RPC commands.
Let's continue.
Add foo_opt startup option to plugin.py
The class Plugin defined the method get_option that let us get the
options we pass when we start the plugin.
In the function myplugin_func we assigned the variable foo_opt to
the startup option that we get using get_option method and we return
its value in a dictionary like this:
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin):
foo_opt = plugin.get_option("foo_opt")
return {"foo_opt": foo_opt}
plugin.run()
We restart the plugin
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
and we call myplugin method which produces the following error:
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"code": -32600,
"message": "Error while processing myplugin: No option with name foo_opt registered",
"traceback": "Traceback (most recent call last):\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 639, in _dispatch_request\n result = self._exec_func(method.func, request)\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 616, in _exec_func\n ret = func(*ba.args, **ba.kwargs)\n File \"/home/tony/clnlive/myplugin.py\", line 9, in myplugin_func\n foo_opt = plugin.get_option(\"foo_opt\")\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 435, in get_option\n raise ValueError(\"No option with name {} registered\".format(name))\nValueError: No option with name foo_opt registered\n"
}
This is totally normal. We tried to use a startup option that we
didn't declared in the getmanifest response we sent to lightningd. If
we had more complicated error that we need to fix, we could print in a
more readable way the error traceback like this:
◉ tony@tony:~/clnlive:
$ python
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Traceback (most recent call last):\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 639, in _dispatch_request\n result = self._exec_func(method.func, request)\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 616, in _exec_func\n ret = func(*ba.args, **ba.kwargs)\n File \"/home/tony/clnlive/myplugin.py\", line 9, in myplugin_func\n foo_opt = plugin.get_option(\"foo_opt\")\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 435, in get_option\n raise ValueError(\"No option with name {} registered\".format(name))\nValueError: No option with name foo_opt registered\n")
Traceback (most recent call last):
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 639, in _dispatch_request
result = self._exec_func(method.func, request)
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 616, in _exec_func
ret = func(*ba.args, **ba.kwargs)
File "/home/tony/clnlive/myplugin.py", line 9, in myplugin_func
foo_opt = plugin.get_option("foo_opt")
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 435, in get_option
raise ValueError("No option with name {} registered".format(name))
ValueError: No option with name foo_opt registered
>>>
◉ tony@tony:~/clnlive:
Let's fix that error by declaring the startup option foo_opt with bar
as default value using add_option method:
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin):
foo_opt = plugin.get_option("foo_opt")
return {"foo_opt": foo_opt}
plugin.add_option(name="foo_opt",
default="bar",
description="'foo_opt description")
plugin.run()
We restart the plugin
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
...
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
...
and we call myplugin method that should return bar, the default value
of foo_opt startup option:
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo_opt": "bar"
}
To be sure that this is working correctly, let's restart myplugin.py
plugin with the startup option foo_opt set to BAR
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
...
◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.py foo_opt=BAR
...
and verify that calling myplugin method returns BAR as value for
foo_opt option:
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo_opt": "BAR"
}
Great! We know how to declare startup options and use them.
Pass cli parameters
To take into account cli parameters we have to modify myplugin_func
function signature.
To add the two optional cli parameters foo1 and foo2 with their
default value being respectively foo1 and foo2, we modify
myplugin_func as follow (note that we've also modify the returned
value):
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin,foo1="foo1", foo2="foo2"):
foo_opt = plugin.get_option("foo_opt")
return {
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": foo_opt
},
"cli_params": {
"foo1": foo1,
"foo2": foo2
}
}
plugin.add_option(name="foo_opt",
default="bar",
description="'foo_opt description")
plugin.run()
We restart the plugin
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
...
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
...
and call myplugin method passing the key/value parameters foo1=bar1
and foo2=bar2 which returned the expected json object with the cli
parameters set correctly:
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar1 foo2=bar2
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
To be sure we did it the right way, we can call it again with the
key/value parameters foo1=bar_1 and foo2=bar_2:
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar_1",
"foo2": "bar_2"
}
}
Cool! We know how to pass cli parameters.
Get the node id of the node l1 running plugin.py
Finally we complete myplugin method so that it also returns the node
id of the node running the plugin.
We can get the node id via a call to getinfo method like this
◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq .id
"0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f"
and this call can also be done using pyln-client.
Indeed, in the I/O loop of our plugin (started with plugin.run()) the
plugin first answers to the getmanifest request and then answers to the
init request. Just before sending the init response, pyln-client
instantiate plugin.rpc property with the class LightningRpc. This
allows us to do JSON-RPC call to the node running the plugin.
For instance, the following expression returns the node id:
plugin.rpc.getinfo()["id"]
So we can modify our plugin like this
#!/home/tony/clnlive/.venv/bin/python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin,foo1="foo1", foo2="foo2"):
node_id = plugin.rpc.getinfo()["id"]
foo_opt = plugin.get_option("foo_opt")
return {
"node_id": node_id,
"options": {
"foo_opt": foo_opt
},
"cli_params": {
"foo1": foo1,
"foo2": foo2
}
}
plugin.add_option(name="foo_opt",
default="bar",
description="'foo_opt description")
plugin.run()
and after restarting the plugin
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
...
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
...
we can call myplugin method like this
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
{
"node_id": "0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar_1",
"foo2": "bar_2"
}
}
and check that the node id is the same as in:
◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq .id
"0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f"
We are done with this part.
Chat
Are there clients for other languages?
I read in the docs that plugin lightningd communication is done via stdin/stdout. Does it give the best performances we can get? Is there other ways to do the communication?
I don't know.
Python decorators
Motivation
Python decorators are used in pyln-client and in CLN tests which
are written in Python and uses pytest.
So if we want either to understand how pyln-client is implemented or
we want to read/write tests for CLN, knowing about Python decorators
can be useful.
For instance if we search for the occurences of @pytest in lightning
repository we get 448 hits:
◉ tony@tony:~/work/repos/lightning:[git»(HEAD detached at v23.02.2)]
$ rg '^@pytest'
contrib/pyln-testing/pyln/testing/fixtures.py
25:@pytest.fixture(scope="session")
46:@pytest.fixture(autouse=True)
69:@pytest.fixture
110:@pytest.fixture
121:@pytest.fixture
126:@pytest.fixture
189:@pytest.fixture
209:@pytest.fixture
426:@pytest.fixture(autouse=True)
453:@pytest.fixture
614:@pytest.fixture
622:@pytest.fixture
629:@pytest.fixture
external/lnprototest/tests/conftest.py
27:@pytest.fixture() # type: ignore
35:@pytest.fixture()
49:@pytest.fixture()
...
And to be specific here we have test_reconnect_sender_add test which is written composing decorators:
@pytest.mark.developer
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
def test_reconnect_sender_add(node_factory):
disconnects = ['-WIRE_COMMITMENT_SIGNED',
'+WIRE_COMMITMENT_SIGNED',
'-WIRE_REVOKE_AND_ACK',
'+WIRE_REVOKE_AND_ACK']
if EXPERIMENTAL_DUAL_FUND:
disconnects = ['=WIRE_COMMITMENT_SIGNED'] + disconnects
# Feerates identical so we don't get gratuitous commit to update them
l1 = node_factory.get_node(disconnect=disconnects,
may_reconnect=True,
feerates=(7500, 7500, 7500, 7500))
l2 = node_factory.get_node(may_reconnect=True)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.fundchannel(l2, 10**6)
amt = 200000000
inv = l2.rpc.invoice(amt, 'testpayment', 'desc')
rhash = inv['payment_hash']
assert only_one(l2.rpc.listinvoices('testpayment')['invoices'])['status'] == 'unpaid'
route = [{'amount_msat': amt, 'id': l2.info['id'], 'delay': 5, 'channel': first_scid(l1, l2)}]
# This will send commit, so will reconnect as required.
l1.rpc.sendpay(route, rhash, payment_secret=inv['payment_secret'])
# Should have printed this for every reconnect.
for i in range(0, len(disconnects)):
l1.daemon.wait_for_log('Already have funding locked in')
A simple decorator example
Decorators are a Python mechanism that allows to modify a function definition by applying it a function (called a decorator) that takes a function as argument and returns another function that becomes the new function definition.
Do do that Python uses the @ sign at the beginning of line followed by
a decorator function.
For instance assuming bar is a function that take a function of one
argument as argument and return a function with the same signature, we
can use it as decorator function to modify the definition of foo
function like this:
@bar
def foo(x):
print(f"'{x}' in 'foo'")
If we send the previous snippet to the Python interpreter, the
function foo will be set to something like this:
# this is done by Python, we don't write this assignment
foo = bar(foo)
Now, let's do it for real and define the decorator bar as the identy
function which take a function f and return f like this:
def bar(f):
return f
@bar
def foo(x):
print(f"'{x}' in 'foo'")
When we send the previous snippet to the Python interpreter, the
function foo is set to bar(foo) which is exactly foo and we get the
following:
>>> foo("cln")
'cln' in 'foo'
Nothing fancy here but it's a good start.
Let's modify bar by defining in its body a function bar_inner with the
same signature as foo, that first prints something and then calls f
passed as argument of bar and finally bar return that new function
bar_inner:
def bar(f):
def bar_inner(x):
print(f"'{x}' in 'bar_inner'")
f(x)
return bar_inner
@bar
def foo(x):
print(f"'{x}' in 'foo'")
When we send the previous snippet to the Python interpreter, the
function foo is set to bar(foo) which is bar_inner and we get the
following:
>>> foo("cln")
'cln' in 'bar_inner'
'cln' in 'foo'
Using decorators to build a dictionary
In the previous example, the decorator bar takes a function as
argument and returns a function and is used with the syntax @bar which
is not exactly the same as
@plugin.method("myplugin")
def myplugin_func(...):
...
return {...}
that we used previously to register the JSON-RPC method myplugin using
the class Plugin defined in pyln-client package.
Let's have a look to an example closer to what we did to define
myplugin JSON-RPC method.
Python decorators mechanism allows also the expression after the @
sign to be a function name (for instance make_decorator) followed by
the arguments to be passed to that function enclosed by parentheses
(...) like this:
@make_decorator("myplugin")
def method_func(x):
return {"x_field": x}
In that case, make_decorator must be a function that takes one string
as argument and return a function which is a decorator (that means
that takes a function as argument and return a function with the same
signature).
Hence, assuming make_decorator is well defined, if we send the
previous snippet to the Python interpreter, the function method_func
will be set to something like this:
# this is done by Python, we don't write this assignment
method_func = make_decorator("myplugin")(method_func)
Now, let's define make_decorator such that the function that it
returns (decorator) is in charge to add to the gobal dictionary methods
a key/value pair where the key is rpc_method_name (argument of
make_decorator function) and the value is f (its argument which will
be method_func in our example):
methods = {}
def make_decorator(rpc_method_name):
def decorator(f):
methods[rpc_method_name] = f
return f
return decorator
@make_decorator("myplugin")
def method_func(x):
return {"x_field": x}
When we send the previous snippet to the Python interpreter, an entry
for myplugin is added to methods dictionary its value being
method_func function and method_func is left unmodified. So we have
the following:
>>> method_func
<function method_func at 0x7f26d69bdcf0>
>>> methods
{'myplugin': <function method_func at 0x7f26d69bdcf0>}
>>> methods['myplugin']("XXXXXXXXX")
{'x_field': 'XXXXXXXXX'}
Now we can make a parallel between the function make_decorator of the
previous example and the method Plugin.method of Plugin class which uses
Plugin.add_method method to add method_name entry to self.methods
property its value being a Method object instantiated using the
argument passed to Plugin.add_method:
class Plugin(object):
...
def add_method(self, name: str, func: Callable[..., Any],
background: bool = False,
category: Optional[str] = None,
desc: Optional[str] = None,
long_desc: Optional[str] = None,
deprecated: bool = False) -> None:
"""..."""
if name in self.methods:
raise ValueError(
"Name {} is already bound to a method.".format(name)
)
# Register the function with the name
method = Method(
name, func, MethodType.RPCMETHOD, category, desc, long_desc,
deprecated
)
method.background = background
self.methods[name] = method
...
def method(self, method_name: str, category: Optional[str] = None,
desc: Optional[str] = None,
long_desc: Optional[str] = None,
deprecated: bool = False) -> JsonDecoratorType:
"""..."""
def decorator(f: Callable[..., JSONType]) -> Callable[..., JSONType]:
self.add_method(method_name,
f,
background=False,
category=category,
desc=desc,
long_desc=long_desc,
deprecated=deprecated)
return f
return decorator
We are done!
Chat
I'm not very clear on the kind of things we use lightning plugin for.
As far as I understand, CLN plugin systems is part of the design of
the software and many builtin features offer by CLN are implemented as
plugins. For instance pay command is implemented in the pay plugin.
You can find more ideas in https://github.com/lightningd/plugins.
Terminal session
We ran the following commands in this order:
$ lightningd --version | xargs printf "lightningd %s\n" && python --version && lsb_release -ds
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/myplugin.py
$ python -m venv .venv
$ source .venv/bin/activate
$ which python
$ pip install pyln-client
$ pip list
$ l1-cli plugin start $(pwd)/myplugin.py
$ deactivate
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ ps -ax | rg myplugin
$ l1-cli myplugin
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli myplugin
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli myplugin
$ python
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli myplugin
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.py foo_opt=BAR
$ l1-cli myplugin
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ ps -ax | rg myplugin
$ l1-cli myplugin
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli -k myplugin foo1=bar1 foo2=bar2
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
$ l1-cli getinfo
$ l1-cli getinfo | jq .id
$ l1-cli plugin stop $(pwd)/myplugin.py
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli getinfo | jq .id
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
And below you can read the terminal session (command lines and outputs):
◉ tony@tony:~/clnlive:
$ lightningd --version | xargs printf "lightningd %s\n" && python --version && lsb_release -ds
lightningd v23.02.2
Python 3.10.6
Ubuntu 22.04.2 LTS
◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
start_ln 3: start three nodes, l1, l2, l3
connect 1 2: connect l1 and l2
fund_nodes: connect all nodes with channels, in a row
stop_ln: shutdown
destroy_ln: remove ln directories
◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 108917
[2] 108952
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
Traceback (most recent call last):
File "/home/tony/clnlive/myplugin.py", line 3, in <module>
from pyln.client import Plugin
ModuleNotFoundError: No module named 'pyln'
{
"code": -3,
"message": "/home/tony/clnlive/myplugin.py: exited before replying to getmanifest"
}
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ which python
/home/tony/clnlive/.venv/bin/python
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
Collecting pyln-client
Using cached pyln_client-23.2-py3-none-any.whl (29 kB)
Collecting pyln-bolt7>=1.0
Using cached pyln_bolt7-1.0.246-py3-none-any.whl (18 kB)
Collecting pyln-proto>=0.12
Using cached pyln_proto-23.2-py3-none-any.whl (31 kB)
Collecting PySocks<2.0.0,>=1.7.1
Using cached PySocks-1.7.1-py3-none-any.whl (16 kB)
Collecting base58<3.0.0,>=2.1.1
Using cached base58-2.1.1-py3-none-any.whl (5.6 kB)
Collecting coincurve<18.0.0,>=17.0.0
Using cached coincurve-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.3 MB)
Collecting cryptography<37.0.0,>=36.0.1
Using cached cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl (3.6 MB)
Collecting bitstring<4.0.0,>=3.1.9
Using cached bitstring-3.1.9-py3-none-any.whl (38 kB)
Collecting asn1crypto
Using cached asn1crypto-1.5.1-py2.py3-none-any.whl (105 kB)
Collecting cffi>=1.3.0
Using cached cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (441 kB)
Collecting pycparser
Using cached pycparser-2.21-py2.py3-none-any.whl (118 kB)
Installing collected packages: bitstring, asn1crypto, PySocks, pyln-bolt7, pycparser, base58, cffi, cryptography, coincurve, pyln-proto, pyln-client
Successfully installed PySocks-1.7.1 asn1crypto-1.5.1 base58-2.1.1 bitstring-3.1.9 cffi-1.15.1 coincurve-17.0.0 cryptography-36.0.2 pycparser-2.21 pyln-bolt7-1.0.246 pyln-client-23.2 pyln-proto-23.2
(.venv) ◉ tony@tony:~/clnlive:
$ pip list
Package Version
------------ -------
asn1crypto 1.5.1
base58 2.1.1
bitstring 3.1.9
cffi 1.15.1
coincurve 17.0.0
cryptography 36.0.2
pip 22.0.2
pycparser 2.21
pyln-bolt7 1.0.246
pyln-client 23.2
pyln-proto 23.2
PySocks 1.7.1
setuptools 59.6.0
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
Traceback (most recent call last):
File "/home/tony/clnlive/myplugin.py", line 3, in <module>
from pyln.client import Plugin
ModuleNotFoundError: No module named 'pyln'
{
"code": -3,
"message": "/home/tony/clnlive/myplugin.py: exited before replying to getmanifest"
}
(.venv) ◉ tony@tony:~/clnlive:
$ deactivate
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg myplugin
109548 pts/1 S 0:00 /home/tony/clnlive/.venv/bin/python /home/tony/clnlive/myplugin.py
109555 pts/1 D+ 0:00 rg myplugin
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"code": -32601,
"message": "Unknown command 'myplugin'"
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo": "bar"
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"code": -32600,
"message": "Error while processing myplugin: No option with name foo_opt registered",
"traceback": "Traceback (most recent call last):\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 639, in _dispatch_request\n result = self._exec_func(method.func, request)\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 616, in _exec_func\n ret = func(*ba.args, **ba.kwargs)\n File \"/home/tony/clnlive/myplugin.py\", line 9, in myplugin_func\n foo_opt = plugin.get_option(\"foo_opt\")\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 435, in get_option\n raise ValueError(\"No option with name {} registered\".format(name))\nValueError: No option with name foo_opt registered\n"
}
◉ tony@tony:~/clnlive:
$ python
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Traceback (most recent call last):\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 639, in _dispatch_request\n result = self._exec_func(method.func, request)\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 616, in _exec_func\n ret = func(*ba.args, **ba.kwargs)\n File \"/home/tony/clnlive/myplugin.py\", line 9, in myplugin_func\n foo_opt = plugin.get_option(\"foo_opt\")\n File \"/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py\", line 435, in get_option\n raise ValueError(\"No option with name {} registered\".format(name))\nValueError: No option with name foo_opt registered\n")
Traceback (most recent call last):
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 639, in _dispatch_request
result = self._exec_func(method.func, request)
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 616, in _exec_func
ret = func(*ba.args, **ba.kwargs)
File "/home/tony/clnlive/myplugin.py", line 9, in myplugin_func
foo_opt = plugin.get_option("foo_opt")
File "/home/tony/clnlive/.venv/lib/python3.10/site-packages/pyln/client/plugin.py", line 435, in get_option
raise ValueError("No option with name {} registered".format(name))
ValueError: No option with name foo_opt registered
>>>
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo_opt": "bar"
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.py foo_opt=BAR
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"foo_opt": "BAR"
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg myplugin
110334 pts/1 S 0:00 /home/tony/clnlive/.venv/bin/python /home/tony/clnlive/myplugin.py
110357 pts/1 S+ 0:00 rg myplugin
◉ tony@tony:~/clnlive:
$ l1-cli myplugin
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar1 foo2=bar2
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
{
"node_id": "029999ce6a1b74cf077f503b81bb4d95af34114363bdac5922a9b353ce0449bac4",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar_1",
"foo2": "bar_2"
}
}
◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f",
"alias": "VIOLETWATER",
"color": "034107",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq .id
"0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f"
◉ tony@tony:~/clnlive:
$ l1-cli plugin stop $(pwd)/myplugin.py
{
"command": "stop",
"result": "Successfully stopped myplugin.py."
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
...
{
"name": "/home/tony/clnlive/myplugin.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq .id
"0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f"
◉ tony@tony:~/clnlive:
$ l1-cli -k myplugin foo1=bar_1 foo2=bar_2
{
"node_id": "0341075d01cd6bd67d2410ffaa5607649fcebce23efc0fe7faef882742b0ef3f3f",
"options": {
"foo_opt": "bar"
},
"cli_params": {
"foo1": "bar_1",
"foo2": "bar_2"
}
}
pyln-client source code
As we briefly looked at Plugin class implementation, here is Plugin.run
method source code
class Plugin(object):
...
def run(self) -> None:
# If we are not running inside lightningd we'll print usage
# and some information about the plugin.
if os.environ.get('LIGHTNINGD_PLUGIN', None) != '1':
return self.print_usage()
partial = b""
for l in self.stdin.buffer:
partial += l
msgs = partial.split(b'\n\n')
if len(msgs) < 2:
continue
partial = self._multi_dispatch(msgs)
and Plugin.method method source code:
class Plugin(object):
...
def method(self, method_name: str, category: Optional[str] = None,
desc: Optional[str] = None,
long_desc: Optional[str] = None,
deprecated: bool = False) -> JsonDecoratorType:
"""Decorator to add a plugin method to the dispatch table.
Internally uses add_method.
"""
def decorator(f: Callable[..., JSONType]) -> Callable[..., JSONType]:
self.add_method(method_name,
f,
background=False,
category=category,
desc=desc,
long_desc=long_desc,
deprecated=deprecated)
return f
return decorator
Even if we didn't look at Plugin._init method (the method that does the instantiation that allows to do RPC calls with the node running the plugin) during the live, let's reproduce its source code, :
class Plugin(object):
...
def _init(self, options: Dict[str, JSONType],
configuration: Dict[str, JSONType],
request: Request) -> JSONType:
def verify_str(d: Dict[str, JSONType], key: str) -> str:
v = d.get(key)
if not isinstance(v, str):
raise ValueError("Wrong argument to init: expected {key} to be"
" a string, got {v}".format(key=key, v=v))
return v
def verify_bool(d: Dict[str, JSONType], key: str) -> bool:
v = d.get(key)
if not isinstance(v, bool):
raise ValueError("Wrong argument to init: expected {key} to be"
" a bool, got {v}".format(key=key, v=v))
return v
self.rpc_filename = verify_str(configuration, 'rpc-file')
self.lightning_dir = verify_str(configuration, 'lightning-dir')
path = os.path.join(self.lightning_dir, self.rpc_filename)
self.rpc = LightningRpc(path)
self.startup = verify_bool(configuration, 'startup')
for name, value in options.items():
self.options[name]['value'] = value
# Dispatch the plugin's init handler if any
if self.child_init:
return self._exec_func(self.child_init, request)
return None
Source code
myplugin.py
See Intalling and using pyln-client for a discussion about Python virtual environments.
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("myplugin")
def myplugin_func(plugin,foo1="foo1", foo2="foo2"):
node_id = plugin.rpc.getinfo()["id"]
foo_opt = plugin.get_option("foo_opt")
return {
"node_id": node_id,
"options": {
"foo_opt": foo_opt
},
"cli_params": {
"foo1": foo1,
"foo2": foo2
}
}
plugin.add_option(name="foo_opt",
default="bar",
description="'foo_opt description")
plugin.run()
decorators.py
def bar(f):
def bar_inner(x):
print(f"'{x}' in 'bar_inner'")
f(x)
return bar_inner
@bar
def foo(x):
print(f"'{x}' in 'foo'")
foo("cln")
# foo = bar(foo)
methods = {}
def make_decorator(rpc_method_name):
def decorator(f):
methods[rpc_method_name] = f
return f
return decorator
@make_decorator("myplugin")
def method_func(x):
return {"x_field": x}
# method_func = make_decorator("myplugin")(method_func)