How Core Lightning plugins can communicate with each other?
In this live we implement a plugin that emits custom notifications foo to lightningd and another plugin which subscribes to those custom notifications foo. We do it with Python only and also with pyln-client package.
Transcript with corrections and improvements
CLN plugins can talk with each other using CLN's push-based notification mechanism and specifically custom notifications.
In this live, which is divided in two parts of 20 minutes of coding
followed by 10 minutes of chat, we'll implement a plugin that emits
custom notifications foo to lightningd and another plugin which
subscribes to those custom notifications foo.
In the first part we'll write the plugin using Python only. The
benefits of doing it without pyln-client first is that it allows us to
understand how the system works and that learning can be then applied
to other languages (as CLN plugins can be written in any languages).
In the second part we'll write (almost) the same plugins in Python,
but this time using pyln-client package.
Custom notifications and subscriptions
Before implementing anything, let's describe the system we'll build today.
We'll write two plugins:
foo-emit.pyplugin which:announces the
foocustom notification tolightningdandregisters the JSON-RPC command
foo-emit(which emitsfoocustom notifications) tolightningd.
foo-subscribe.pyplugin:subscribes to the
foocustom notification
Once both plugins are started on a lightning node, each time we call
the command foo-emit, foo-emit.py plugin sends a custom notification
foo to lightningd, then lightningd forwards that custom notification
foo to foo-subscribe.py plugin and finally foo-subscribe.py does
something with that custom notification foo:
emits `foo` forwards `foo`
┌───────────┐ custom notification ┌──────────┐ custom notification ┌────────────────┐
│foo-emit.py│--------------------->│lightningd│--------------------->│foo-subscribe.py│
└───────────┘ └──────────┘ └────────────────┘
CLN plugin mechanism stages
getmanifestrequestinitrequestio loop
foo-emit.py's reponse to getmanifest request
When the foo-emit.py plugin is started, it receives a getmanifest
request from lightningd like this one
{
"jsonrpc": "2.0",
"id": 182,
"method": "getmanifest",
"params": {
"allow-deprecated-apis": false
}
}
and since it wants to register the JSON-RPC foo-emit and to declare
the custom notification foo, it just have to relpy to that request
with a response that
sets the
rpcmethodsfield of theresultmember with thefoo-emitmethod andsets the
notificationsfield of theresultmember to the array[{"method": "foo"}]
like this:
{
"jsonrpc": "2.0",
"id": 182,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [{
"name": "foo-emit",
"usage": "usage",
"description": "description"
}],
"notifications": [{"method": "foo"}]
}
}
foo-subscribe.py's reponse to getmanifest request
When the foo-subscribe.py plugin is started, it receives a getmanifest
request from lightningd like this one
{
"jsonrpc": "2.0",
"id": 196,
"method": "getmanifest",
"params": {
"allow-deprecated-apis": false
}
}
and since it wants to subscribe to the custom notification foo, it
just have to reply to that request with a response that sets the
subscriptions field of the result member to the array ["foo"] like
this:
{
"jsonrpc": "2.0",
"id": 196,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"subscriptions": ["foo"]
}
}
foo notification as received by foo-subscribe.py
When lightningd forwards the custom notification foo, it wraps the
payload of the notification in an object that contains metadata about
the notification.
Specifically, when foo-emit.py plugin emits the following custom
notification foo to lightningd
{
"jsonrpc": "2.0",
"method": "foo",
"params": {
"foo": {
"bar": "baz"
}
}
}
foo-subscribe.py plugin receives the following notification forwaded
by lightningd with the sender plugin (foo-emit.py) sets in the origin
field of the params member:
{
"jsonrpc": "2.0",
"method": "foo",
"params": {
"origin": "foo-emit.py",
"payload": {
"foo": {
"bar": "baz"
}
}
}
}
Implementation in Python
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] 324321
[2] 324355
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'
foo-subscribe.py
foo-subscribe.py skeleton
Instead of writing the plugin from scratch we use parts of the code we
wrote during the first live which was about registering JSON-RPC
methods to lightningd and understanding CLN plugin system.
This way we can focus on how to subscribe to notification topics and how to handle notifications.
So we start with the file myplugin.py containing the following:
#!/usr/bin/env python
import sys
import json
# getmanifest
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": []
}
}
sys.stdout.write(json.dumps(manifest))
sys.stdout.flush()
# init
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
init = {
"jsonrpc": "2.0",
"id": req_id,
"result": {}
}
sys.stdout.write(json.dumps(init))
sys.stdout.flush()
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
In that script, we first receive the getmanifest request from
lightningd in our stdin stream, we extract its id and we construct the
getmanifest response (the plugin is dynamic, with no startup options and
register no JSON-RPC methods) that we send back to lightningd by
writing it to our stdout stream:
...
# getmanifest
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": []
}
}
sys.stdout.write(json.dumps(manifest))
sys.stdout.flush()
...
Then we handle the init request sent by lightningd in our stdin
stream:
...
# init
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
init = {
"jsonrpc": "2.0",
"id": req_id,
"result": {}
}
sys.stdout.write(json.dumps(init))
sys.stdout.flush()
...
And finally we start an I/O loop waiting for incoming request from
lightningd:
...
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
Subscribe to invoice_creation builtin notification topic
We want to subscribe to the foo custom notification. But before we do
that, let get something similar working that doesn't need the foo
custom notifications to "exist" to check that our system is working.
So, let's subscribe to the builtin notification topic invoice_creation
(which is sent each time we create an invoice) like this:
...
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"subscriptions": ["invoice_creation"]
}
}
...
And when we'll receive notifications for that topic, we'll write them
into the file /tmp/foo-subscribe like this:
for request in sys.stdin:
sys.stdin.readline() # "\n"
with open("/tmp/foo-subscribe", "a") as f:
f.write(request)
Note that we don't need to write any logic because the only
notifications we'll ever receive from lightningd are for
invoice_creation topic (due to our getmanifest response).
Note also that since we are handling notifications, we don't send any
responses to lightningd unlike what we did before with getmanifest and
init requests.
In our terminal now we can start our plugin, check that we have it running and create an invoice:
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/foo-subscribe.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg foo
324613 pts/0 S 0:00 python /home/tony/clnlive/foo-subscribe.py
324642 pts/0 S+ 0:00 rg foo
◉ tony@tony:~/clnlive:
$ l1-cli invoice 0.001btc inv pizza
{
"payment_hash": "49ac1bd3779ad9d4ad91d258b7e3150e6c682fd319aa23b81aa0d2105d124bd3",
"expires_at": 1685629291,
"bolt11": "lnbcrt1m1pjx7mhtsp5k2fd3lf6zwv2zpw3luq3x4t3k5ntg89q89w369enmmkeguxs5yzqpp5fxkph5mhntvaftv36fvt0cc4pekxst7nrx4z8wq65rfpqhgjf0fsdqgwp5h57npxqyjw5qcqp29qyysgqy54hdh9qqaexsh8g2vmpj9c8hzh8edwspkm9vss278dmp22yffpxv3apkfjkq8quru8mp8gtsdqmtf3p8xv8g9v2h8ar8jcvc8mc3mgpj4p5l6",
"payment_secret": "b292d8fd3a1398a105d1ff01135571b526b41ca0395d1d1733deed9470d0a104",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
As the node l1 is running foo-subscribe.py plugin which subscribes to
invoice_creation notifications and write them to the file
/tmp/foo-subscribe each time l1 creates an invoice, the file
/tmp/foo-subscribe contains the following invoice_creation
notification:
{"jsonrpc":"2.0","method":"invoice_creation","params":{"invoice_creation":{"msat":"100000000msat","preimage":"26d2b1252d066c80560c93fa3d99d35ca4aef03abe4859b9d3fb109d8345f317","label":"inv"}}}
Subscribe to foo custom notifications
Fine, our system is working, now let's replace the subscription to
invoice_creation to foo custom notification topic like this
...
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"subscriptions": ["foo"]
}
}
...
and restart foo-subscribe.py plugin:
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-subscribe.py
{
"command": "start",
"plugins": [...]
}
foo-emit.py
Let's start with the same Python script as before.
To register the JSON-RPC method foo-emit to lightningd we add it in
the array rpcmethods of the manifest answer to the getmanifest
request like this
...
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [{
"name": "foo-emit",
"usage": "usage",
"description": "description"
}]
}
}
...
and to declare the custom notification foo, we set the notifications
field of the result member to the array [{"method": "foo"}] in the
manifest answer like this:
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [{
"name": "foo-emit",
"usage": "usage",
"description": "description"
}],
"notifications": [{"method": "foo"}]
}
}
In our io loop, we are going to receive only foo-emit requests. And
each time we receive a foo-emit request we want to send a foo
notification to lightningd with its payload being {"foo": {"bar":
"baz"}}. To do this we modify foo-emit.py script like this:
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
foo_notification = {
"jsonrpc": "2.0",
"method": "foo",
"params": {"foo": {"bar": "baz"}}
}
sys.stdout.write(json.dumps(foo_notification))
sys.stdout.flush()
There something missing in what we wrote, thought it works "almost" correctly. Let's check that script and we'll improve it after.
In our terminal, let's start foo-emit.py plugin and check that we
have our two plugins running:
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-emit.py
{
"command": "start",
"plugins": [...,
{
"name": "/home/tony/clnlive/foo-subscribe.py",
"active": true,
"dynamic": true
},
{
"name": "/home/tony/clnlive/foo-emit.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg foo
324716 pts/0 S 0:00 python /home/tony/clnlive/foo-subscribe.py
324968 pts/0 S 0:00 python /home/tony/clnlive/foo-emit.py
324991 pts/0 S+ 0:00 rg foo
We can now call foo-emit command (which hangs):
◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
This has emitted a foo notification which has been forwarded to
foo-subscribe.py plugin which consequently wrote the notification in
the file /tmp/foo-subscribe that now contains the following
{"jsonrpc":"2.0","method":"invoice_creation","params":{"invoice_creation":{"msat":"100000000msat","preimage":"26d2b1252d066c80560c93fa3d99d35ca4aef03abe4859b9d3fb109d8345f317","label":"inv"}}}
{"jsonrpc":"2.0","method":"foo","params":{"origin":"foo-emit.py","payload":{"foo": {"bar": "baz"}}}}
and we can prettify the forwarded foo notification like this:
{
"jsonrpc": "2.0",
"method": "foo",
"params": {
"origin": "foo-emit.py",
"payload": {
"foo": {
"bar": "baz"
}
}
}
}
Why foo-emit command hangs?
This is because when we receive the foo-emit request in the io loop,
we notify lightningd with a foo custom notification but we "forget"
to reply to lightningd to the foo-emit request. So lightningd waits
for a response and the client hangs.
Let's fix that with a meaningful answer like 'foo' notification emited
(I should have wrote emitted! anyway) that we send back to lightningd:
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
foo_notification = {
"jsonrpc": "2.0",
"method": "foo",
"params": {"foo": {"bar": "baz"}}
}
sys.stdout.write(json.dumps(foo_notification))
sys.stdout.flush()
req_id = json.loads(request)["id"]
foo_emit_response = {
"jsonrpc": "2.0",
"id": req_id,
"result": {"notification": "'foo' notification emited"}
}
sys.stdout.write(json.dumps(foo_emit_response))
sys.stdout.flush()
Back to our terminal we can check that the command no longer hangs
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-emit.py
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
and that the foo notification has been forwaded to foo-subscribe.py
plugin which wrote the notification in the /tmp/foo-subscribe again:
{"jsonrpc":"2.0","method":"invoice_creation","params":{"invoice_creation":{"msat":"100000000msat","preimage":"26d2b1252d066c80560c93fa3d99d35ca4aef03abe4859b9d3fb109d8345f317","label":"inv"}}}
{"jsonrpc":"2.0","method":"foo","params":{"origin":"foo-emit.py","payload":{"foo": {"bar": "baz"}}}}
{"jsonrpc":"2.0","method":"foo","params":{"origin":"foo-emit.py","payload":{"foo": {"bar": "baz"}}}}
We are done with that first part.
Chat
Can the functionality of one plugin influence the behavior of another plugin?
Implementation in Python with pyln-client
Install pyln-client and restart 2 Lightning nodes running on regtest
Now we are going to write with pyln-client the plugins pyln-emit.py
and pyln-subscribe.py which do almost the same thing as we did in the
first part.
We start by stopping our nodes and bitcoind using commands provided by
the script lightning/contrib/startup_regtest.sh
◉ tony@tony:~/clnlive:
$ stop_ln
Lost connection to the RPC socket.Terminated
Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Terminated
[1]- Exit 143 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[2]+ Exit 143 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
◉ tony@tony:~/clnlive:
$ destroy_ln
◉ tony@tony:~/clnlive:
$ rm -r ~/.bitcoin/regtest/
then we install pyln-client in a Python virtual environment
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
finally we start two Lightning nodes running on the Bitcoin regtest
chain and check the alias of the command l1-cli:
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 325771
[2] 325813
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
pyln-subscribe.py
We want to subscribe to the foo custom notification. But before we do
that, let get something similar working that doesn't need the foo
custom notifications to "exist" to check that our system is working.
So, let's subscribe to the builtin notification topic invoice_creation
and each time we receive that notification we write the invoice
informations into the file /tmp/pyln-subscribe:
#!/usr/bin/env python
import json
from pyln.client import Plugin
plugin = Plugin()
@plugin.subscribe("invoice_creation")
def invoice_creation_func(plugin,invoice_creation,**kwargs):
with open("/tmp/pyln-subscribe", "a") as f:
f.write(json.dumps(invoice_creation))
plugin.run()
In our terminal now we can start our plugin, check that we have it running and create an invoice:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326051 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326073 pts/0 S+ 0:00 rg pyln
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli invoice 0.001btc inv-1 pizza
{
"payment_hash": "dc05a3a17aa300bc5e51a8e357bd2fc3aa300a544fabdad8a33c2463d0af0e42",
"expires_at": 1685631034,
"bolt11": "lnbcrt1m1pjx7ad6sp5ynt9cmltnled2ttmd2zjl9yagkmgtul2xpv5j95nky85g55k944spp5msz68gt65vqtchj34r3400f0cw4rqzj5f74a4k9r8sjx8590pepqdqgwp5h57npxqyjw5qcqp29qyysgqe9xfh49e85p6rven37rvhh0mhau842cr2qwrxhuy9qhmz24jvy6h5ca8lle9w7mwy93qh2tczhxjahatd52hjk6whgvyh0clfuwe62cqs2lyy3",
"payment_secret": "24d65c6feb9ff2d52d7b6a852f949d45b685f3ea3059491693b10f4452962d6b",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
As the node l1 is running pyln-subscribe.py plugin which subscribes to
invoice_creation notifications and write them to the file
/tmp/pyln-subscribe each time l1 creates an invoice, the file
/tmp/pyln-subscribe contains the following invoice_creation
notification:
{"msat": "100000000msat", "preimage": "4d8ce6a06146e0b6d4a5857f77ba312a3adfaebebf8d7068efd98428d6bfdd75", "label": "inv-1"}
Fine, our system is working, now let's replace the subscription to
invoice_creation to foo custom notification topic like this
#!/usr/bin/env python
import json
from pyln.client import Plugin
plugin = Plugin()
@plugin.subscribe("foo")
def foo_func(plugin,payload,**kwargs):
with open("/tmp/pyln-subscribe", "a") as f:
f.write(json.dumps(payload))
plugin.run()
and restart pyln-subscribe.py plugin and check that it is running:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326233 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326255 pts/0 S+ 0:00 rg pyln
pyln-emit.py
We can use notify method of the class Plugin to send notifications to
lightningd. The first argument is the method of the notification
(remember that a JSON-RPC notification is a JSON-RPC request without
any id member) and the second is the payload (what goes into the
params of the request).
With that said we can register foo-emite JSON-RPC method to lightningd
that sends foo custom notifications to lightningd with the payload
being {"foo": {"bar": "baz"}} like this:
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("foo-emit")
def foo_emit_func(plugin):
plugin.notify("foo", {"foo": {"bar": "baz"}})
plugin.run()
While foo-emit command is well defined, lightningd won't let us send
foo notifications without declaring them with add_notification_topic
method of the class Plugin like this:
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("foo-emit")
def foo_emit_func(plugin):
plugin.notify("foo", {"foo": {"bar": "baz"}})
plugin.add_notification_topic("foo")
plugin.run()
In our terminal, we can now start foo-emit.py plugin and check that we
have our two plugins running:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-emit.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
},
{
"name": "/home/tony/clnlive/pyln-emit.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326233 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326404 pts/0 S 0:00 python /home/tony/clnlive/pyln-emit.py
326426 pts/0 R+ 0:00 rg pyln
Let's run foo-emit command:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
null
This has emitted a foo notification which has been forwarded to
pyln-subscribe.py plugin which consequently wrote the payload of the
notification in the file /tmp/pyln-subscribe that now contains the
following
{"msat": "100000000msat", "preimage": "4d8ce6a06146e0b6d4a5857f77ba312a3adfaebebf8d7068efd98428d6bfdd75", "label": "inv-1"}
{"foo": {"bar": "baz"}}
As we did in the previous example, let's the command foo-emit returns a
meaningful information by adding a return statment in the function
that notifies lightningd:
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("foo-emit")
def foo_emit_func(plugin):
plugin.notify("foo", {"foo": {"bar": "baz"}})
return {"notification": "'foo' notification emited"}
plugin.add_notification_topic("foo")
plugin.run()
Back to our terminal, we restart pyln-emit.py plugin and run the
command foo-emit:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-emit.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
It worked correctly and the file /tmp/pyln-subscribe is now:
{"msat": "100000000msat", "preimage": "4d8ce6a06146e0b6d4a5857f77ba312a3adfaebebf8d7068efd98428d6bfdd75", "label": "inv-1"}
{"foo": {"bar": "baz"}}
{"foo": {"bar": "baz"}}
Finally, we also write the sender of the notification in the file
/tmp/pyln-subscribe by modifying how pyln-subscribe.py handles foo
custom notifications:
#!/usr/bin/env python
import json
from pyln.client import Plugin
plugin = Plugin()
@plugin.subscribe("foo")
def foo_func(plugin,origin,payload,**kwargs):
params = {
"origin": origin,
"payload": payload
}
with open("/tmp/pyln-subscribe", "a") as f:
f.write(json.dumps(params))
plugin.run()
Back to our terminal, we restart pyln-emit.py plugin and run the
command foo-emit:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
It worked correctly and the file /tmp/pyln-subscribe is now:
{"msat": "100000000msat", "preimage": "4d8ce6a06146e0b6d4a5857f77ba312a3adfaebebf8d7068efd98428d6bfdd75", "label": "inv-1"}
{"foo": {"bar": "baz"}}
{"foo": {"bar": "baz"}}
{"origin": "pyln-emit.py", "payload": {"foo": {"bar": "baz"}}}
We are done with the second part.
Terminal session
We ran the following commands in this order:
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/foo-subscribe.py
$ ps -ax | rg foo
$ l1-cli invoice 0.001btc inv pizza
$ l1-cli plugin start $(pwd)/foo-subscribe.py
$ l1-cli plugin start $(pwd)/foo-emit.py
$ ps -ax | rg foo
$ l1-cli foo-emit
$ l1-cli plugin start $(pwd)/foo-emit.py
$ l1-cli foo-emit
$ stop_ln
$ destroy_ln
$ rm -r ~/.bitcoin/regtest/
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install pyln-client
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
$ ps -ax | rg pyln
$ l1-cli invoice 0.001btc inv-1 pizza
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
$ ps -ax | rg pyln
$ l1-cli plugin start $(pwd)/pyln-emit.py
$ ps -ax | rg pyln
$ l1-cli foo-emit
$ l1-cli plugin start $(pwd)/pyln-emit.py
$ l1-cli foo-emit
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
$ l1-cli foo-emit
And below you can read the terminal session (command lines and outputs):
◉ 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] 324321
[2] 324355
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)/foo-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/foo-subscribe.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg foo
324613 pts/0 S 0:00 python /home/tony/clnlive/foo-subscribe.py
324642 pts/0 S+ 0:00 rg foo
◉ tony@tony:~/clnlive:
$ l1-cli invoice 0.001btc inv pizza
{
"payment_hash": "49ac1bd3779ad9d4ad91d258b7e3150e6c682fd319aa23b81aa0d2105d124bd3",
"expires_at": 1685629291,
"bolt11": "lnbcrt1m1pjx7mhtsp5k2fd3lf6zwv2zpw3luq3x4t3k5ntg89q89w369enmmkeguxs5yzqpp5fxkph5mhntvaftv36fvt0cc4pekxst7nrx4z8wq65rfpqhgjf0fsdqgwp5h57npxqyjw5qcqp29qyysgqy54hdh9qqaexsh8g2vmpj9c8hzh8edwspkm9vss278dmp22yffpxv3apkfjkq8quru8mp8gtsdqmtf3p8xv8g9v2h8ar8jcvc8mc3mgpj4p5l6",
"payment_secret": "b292d8fd3a1398a105d1ff01135571b526b41ca0395d1d1733deed9470d0a104",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-subscribe.py
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-emit.py
{
"command": "start",
"plugins": [...,
{
"name": "/home/tony/clnlive/foo-subscribe.py",
"active": true,
"dynamic": true
},
{
"name": "/home/tony/clnlive/foo-emit.py",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/clnlive:
$ ps -ax | rg foo
324716 pts/0 S 0:00 python /home/tony/clnlive/foo-subscribe.py
324968 pts/0 S 0:00 python /home/tony/clnlive/foo-emit.py
324991 pts/0 S+ 0:00 rg foo
◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
^C
◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/foo-emit.py
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
◉ tony@tony:~/clnlive:
$ stop_ln
Lost connection to the RPC socket.Terminated
Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Terminated
[1]- Exit 143 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[2]+ Exit 143 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
◉ tony@tony:~/clnlive:
$ destroy_ln
◉ tony@tony:~/clnlive:
$ rm -r ~/.bitcoin/regtest/
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 325771
[2] 325813
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326051 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326073 pts/0 S+ 0:00 rg pyln
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli invoice 0.001btc inv-1 pizza
{
"payment_hash": "dc05a3a17aa300bc5e51a8e357bd2fc3aa300a544fabdad8a33c2463d0af0e42",
"expires_at": 1685631034,
"bolt11": "lnbcrt1m1pjx7ad6sp5ynt9cmltnled2ttmd2zjl9yagkmgtul2xpv5j95nky85g55k944spp5msz68gt65vqtchj34r3400f0cw4rqzj5f74a4k9r8sjx8590pepqdqgwp5h57npxqyjw5qcqp29qyysgqe9xfh49e85p6rven37rvhh0mhau842cr2qwrxhuy9qhmz24jvy6h5ca8lle9w7mwy93qh2tczhxjahatd52hjk6whgvyh0clfuwe62cqs2lyy3",
"payment_secret": "24d65c6feb9ff2d52d7b6a852f949d45b685f3ea3059491693b10f4452962d6b",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326233 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326255 pts/0 S+ 0:00 rg pyln
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-emit.py
{
"command": "start",
"plugins": [ ...,
{
"name": "/home/tony/clnlive/pyln-subscribe.py",
"active": true,
"dynamic": true
},
{
"name": "/home/tony/clnlive/pyln-emit.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pyln
326233 pts/0 S 0:00 python /home/tony/clnlive/pyln-subscribe.py
326404 pts/0 S 0:00 python /home/tony/clnlive/pyln-emit.py
326426 pts/0 R+ 0:00 rg pyln
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
null
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-emit.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pyln-subscribe.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli foo-emit
{
"notification": "'foo' notification emited"
}
Source code
foo-emit.py
#!/usr/bin/env python
import sys
import json
# getmanifest
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [{
"name": "foo-emit",
"usage": "usage",
"description": "description"
}],
"notifications": [{"method": "foo"}]
}
}
sys.stdout.write(json.dumps(manifest))
sys.stdout.flush()
# init
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
init = {
"jsonrpc": "2.0",
"id": req_id,
"result": {}
}
sys.stdout.write(json.dumps(init))
sys.stdout.flush()
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
foo_notification = {
"jsonrpc": "2.0",
"method": "foo",
"params": {"foo": {"bar": "baz"}}
}
sys.stdout.write(json.dumps(foo_notification))
sys.stdout.flush()
req_id = json.loads(request)["id"]
foo_emit_response = {
"jsonrpc": "2.0",
"id": req_id,
"result": {"notification": "'foo' notification emited"}
}
sys.stdout.write(json.dumps(foo_emit_response))
sys.stdout.flush()
foo-subscribe.py
#!/usr/bin/env python
import sys
import json
# getmanifest
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
manifest = {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"subscriptions": ["foo"]
}
}
sys.stdout.write(json.dumps(manifest))
sys.stdout.flush()
# init
request = sys.stdin.readline()
sys.stdin.readline() # "\n"
req_id = json.loads(request)["id"]
init = {
"jsonrpc": "2.0",
"id": req_id,
"result": {}
}
sys.stdout.write(json.dumps(init))
sys.stdout.flush()
# io loop
for request in sys.stdin:
sys.stdin.readline() # "\n"
with open("/tmp/foo-subscribe", "a") as f:
f.write(request)
pyln-emit.py
#!/usr/bin/env python
from pyln.client import Plugin
plugin = Plugin()
@plugin.method("foo-emit")
def foo_emit_func(plugin):
plugin.notify("foo", {"foo": {"bar": "baz"}})
return {"notification": "'foo' notification emited"}
plugin.add_notification_topic("foo")
plugin.run()
pyln-subscribe.py
#!/usr/bin/env python
import json
from pyln.client import Plugin
plugin = Plugin()
@plugin.subscribe("foo")
def foo_func(plugin,origin,payload,**kwargs):
params = {
"origin": origin,
"payload": payload
}
with open("/tmp/pyln-subscribe", "a") as f:
f.write(json.dumps(params))
plugin.run()
foo-notification-forwarded
{
"jsonrpc": "2.0",
"method": "foo",
"params": {
"origin": "foo-emit.py",
"payload": {
"foo": {
"bar": "baz"
}
}
}
}
foo-subscribe
{"jsonrpc":"2.0","method":"invoice_creation","params":{"invoice_creation":{"msat":"100000000msat","preimage":"26d2b1252d066c80560c93fa3d99d35ca4aef03abe4859b9d3fb109d8345f317","label":"inv"}}}
{"jsonrpc":"2.0","method":"foo","params":{"origin":"foo-emit.py","payload":{"foo": {"bar": "baz"}}}}
{"jsonrpc":"2.0","method":"foo","params":{"origin":"foo-emit.py","payload":{"foo": {"bar": "baz"}}}}
pyln-subscribe
{"msat": "100000000msat", "preimage": "4d8ce6a06146e0b6d4a5857f77ba312a3adfaebebf8d7068efd98428d6bfdd75", "label": "inv-1"}
{"foo": {"bar": "baz"}}
{"foo": {"bar": "baz"}}
{"origin": "pyln-emit.py", "payload": {"foo": {"bar": "baz"}}}