Subscribe to connect notifications with pyln-client
In this episode we write a Python plugin with pyln-client
package which subscribes to the notification topics connect
and invoice_creation
.
Transcript with corrections and improvements
In this episode we write a Python plugin with pyln-client
package
which subscribes to the notification topics connect
and
invoice_creation
.
getmanifest request
connect
notifications are JSON-RPC requests we receive (if we've
subscribed to) from lightningd
each time we connect to another peer.
They have no id
field and its params
field contains the information
about the node we just connected to. Here is an example:
{
"jsonrpc": "2.0",
"method": "connect",
"params": {
"id": "0271aecf3075ce72f2df479b60fc1db98d4d780c66fa495013614054554e1bb2bf",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
}
invoice_creation
notifications are JSON-RPC requests we receive (if we've
subscribed to) from lightningd
each time we create an invoice. They
have no id
field and the params
field contains the invoice details.
Here is an example:
{
"jsonrpc": "2.0",
"method": "invoice_creation",
"params": {
"invoice_creation": {
"msat": "100000000msat",
"preimage": "88042f007f02283571abbc40aca8b4302643415e85c71413177ef139b4276970",
"label": "inv"
}
}
}
When the plugin is started, it receives a getmanifest
request from
lightningd
like this one
{
"jsonrpc": "2.0",
"id": 187,
"method": "getmanifest",
"params": {
"allow-deprecated-apis": false
}
}
and if the plugin wants to subscribe to connect
and invoice_creation
notifications topics, it just have to reply to that request with a
response that sets the subscriptions
field of the result
member to the
array ["connect", "invoice_creation"]
like this:
{
"jsonrpc": "2.0",
"id": 187,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"subscriptions": ["connect", "invoice_creation"]
}
}
This is what we are going to do with pyln-client
Python package.
Install pyln-client
Let's install pyln-client
in .venv
Python virtual environment:
◉ tony@tony:~/lnroom:
$ python -m venv .venv
◉ tony@tony:~/lnroom:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/lnroom:
$ pip install pyln-client
...
Our setup for this episode is:
(.venv) ◉ tony@tony:~/lnroom:
$ ./setup.sh
Ubuntu 22.04.2 LTS
Python 3.10.6
lightningd v23.02.2
pyln-client 23.5
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
:
(.venv) ◉ tony@tony:~/lnroom:
$ 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
(.venv) ◉ tony@tony:~/lnroom:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1460972
[2] 1461006
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
:
(.venv) ◉ tony@tony:~/lnroom:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Subscribe to connect
In the file myplugin.py
we have a working plugin written with
pyln-client
(thought it does nothing so far):
#!/usr/bin/env python
from pyln.client import Plugin
import json
plugin = Plugin()
plugin.run()
We use the line @plugin.subscribe("connect")
to tell pyln-client
that
we want to subscribe to connect
notification topic. And the function
defined after that line will be used to handle connect
notifications
received from lightningd
. In our case we write the params of the
connect
notification (which are the function arguments id
, direction
and address
) that we receive in the file /tmp/plugin
:
So our plugin is now:
#!/usr/bin/env python
from pyln.client import Plugin
import json
plugin = Plugin()
@plugin.subscribe("connect")
def connect_func(plugin,id, direction, address, **kwargs):
params = {
"id": id,
"direction": direction,
"address": address
}
with open("/tmp/myplugin", "a") as myplugin:
myplugin.write("connect params: " + json.dumps(params) + "\n")
plugin.run()
Let's start our pyln-client
plugin and connect the node l1
and l2
:
(.venv) ◉ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/lnroom:
$ l2-cli -F getinfo | rg 'id|binding'
id=039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0
binding[0].type=ipv4
binding[0].address=127.0.0.1
binding[0].port=7272
(.venv) ◉ tony@tony:~/lnroom:
$ l1-cli connect 039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0@127.0.0.1:7272
{
"id": "039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
We check that the file /tmp/plugin
contains
connect params: {"id": "039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0", "direction": "out", "address": {"type": "ipv4", "address": "127.0.0.1", "port": 7272}}
indicating that we've successfully subscribed to connect
notification
topic.
Subscribe to invoice_creation
Let's modify our plugin to subscribe to invoice_creation
notification
topic:
#!/usr/bin/env python
from pyln.client import Plugin
import json
plugin = Plugin()
@plugin.subscribe("connect")
def connect_func(plugin,id, direction, address, **kwargs):
params = {
"id": id,
"direction": direction,
"address": address
}
with open("/tmp/myplugin", "a") as myplugin:
myplugin.write("connect params: " + json.dumps(params) + "\n")
@plugin.subscribe("invoice_creation")
def invoice_creation_func(plugin, invoice_creation, **kwargs):
params = {
"invoice_creation": invoice_creation
}
with open("/tmp/myplugin", "a") as myplugin:
myplugin.write("invoice_creation params: " + json.dumps(params) + "\n")
plugin.run()
Back to our terminal, we restart the plugin and create an invoice with
the node l1
:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/myplugin-pyln.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/lnroom:
$ l1-cli invoice 0.001btc inv pizza
{
"payment_hash": "fde8a7df1924d00e96312d765117088d97bd1886b310b14a93a342902f6a7482",
"expires_at": 1684848791,
"bolt11": "lnbcrt1m1pjx8pshsp5wx6c04s24ak9yj2f90ph4j7wlsu460x0xqt5e6d2sgknh6u23skqpp5lh520hceyngqa93394m9z9cg3ktm6xyxkvgtzj5n5dpfqtm2wjpqdqgwp5h57npxqyjw5qcqp29qyysgq3ewslg3tc38uexd0dy7mm5nqzhml85sap777scpynudjvxkamkv5srr343y8uvzsyjy4lc0ff3t3uxgxly0l4msyr9yk5kskuplqewspglvvs9",
"payment_secret": "71b587d60aaf6c5249492bc37acbcefc395d3ccf30174ce9aa822d3beb8a8c2c",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
The file /tmp/plugin
is now:
connect params: {"id": "039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0", "direction": "out", "address": {"type": "ipv4", "address": "127.0.0.1", "port": 7272}}
invoice_creation params: {"invoice_creation": {"msat": "100000000msat", "preimage": "4ec62babb7d48f494d9945cdfdbd942f387467da6b804d84c0428b49bbdf0844", "label": "inv"}}
We are done!
Terminal session
We ran the following commands in this order:
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install pyln-client
$ ./setup.sh
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/myplugin.py
$ l2-cli -F getinfo | rg 'id|binding'
$ l1-cli connect 039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0@127.0.0.1:7272
$ l1-cli plugin start $(pwd)/myplugin.py
$ l1-cli invoice 0.001btc inv pizza
And below you can read the terminal session (command lines and outputs):
◉ tony@tony:~/lnroom:
$ python -m venv .venv
◉ tony@tony:~/lnroom:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/lnroom:
$ pip install pyln-client
Collecting pyln-client
Using cached pyln_client-23.5-py3-none-any.whl (35 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.5-py3-none-any.whl
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 PySocks<2.0.0,>=1.7.1
Using cached PySocks-1.7.1-py3-none-any.whl (16 kB)
Collecting bitstring<4.0.0,>=3.1.9
Using cached bitstring-3.1.9-py3-none-any.whl (38 kB)
Collecting base58<3.0.0,>=2.1.1
Using cached base58-2.1.1-py3-none-any.whl (5.6 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.5 pyln-proto-23.5
(.venv) ◉ tony@tony:~/lnroom:
$ ./setup.sh
Ubuntu 22.04.2 LTS
Python 3.10.6
lightningd v23.02.2
pyln-client 23.5
(.venv) ◉ tony@tony:~/lnroom:
$ 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
(.venv) ◉ tony@tony:~/lnroom:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1460972
[2] 1461006
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:~/lnroom:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
(.venv) ◉ tony@tony:~/lnroom:
$ 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/lnroom/myplugin.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/lnroom:
$ l2-cli -F getinfo | rg 'id|binding'
id=039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0
binding[0].type=ipv4
binding[0].address=127.0.0.1
binding[0].port=7272
(.venv) ◉ tony@tony:~/lnroom:
$ l1-cli connect 039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0@127.0.0.1:7272
{
"id": "039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
(.venv) ◉ tony@tony:~/lnroom:
$ 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/lnroom/myplugin.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/lnroom:
$ l1-cli invoice 0.001btc inv pizza
{
"payment_hash": "fde8a7df1924d00e96312d765117088d97bd1886b310b14a93a342902f6a7482",
"expires_at": 1684848791,
"bolt11": "lnbcrt1m1pjx8pshsp5wx6c04s24ak9yj2f90ph4j7wlsu460x0xqt5e6d2sgknh6u23skqpp5lh520hceyngqa93394m9z9cg3ktm6xyxkvgtzj5n5dpfqtm2wjpqdqgwp5h57npxqyjw5qcqp29qyysgq3ewslg3tc38uexd0dy7mm5nqzhml85sap777scpynudjvxkamkv5srr343y8uvzsyjy4lc0ff3t3uxgxly0l4msyr9yk5kskuplqewspglvvs9",
"payment_secret": "71b587d60aaf6c5249492bc37acbcefc395d3ccf30174ce9aa822d3beb8a8c2c",
"warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
Source code
myplugin.py
#!/usr/bin/env python
from pyln.client import Plugin
import json
plugin = Plugin()
@plugin.subscribe("connect")
def connect_func(plugin,id, direction, address, **kwargs):
params = {
"id": id,
"direction": direction,
"address": address
}
with open("/tmp/myplugin", "a") as myplugin:
myplugin.write("connect params: " + json.dumps(params) + "\n")
@plugin.subscribe("invoice_creation")
def invoice_creation_func(plugin, invoice_creation, **kwargs):
params = {
"invoice_creation": invoice_creation
}
with open("/tmp/myplugin", "a") as myplugin:
myplugin.write("invoice_creation params: " + json.dumps(params) + "\n")
plugin.run()
setup.sh
#!/usr/bin/env bash
ubuntu=$(lsb_release -ds)
lightningd=$(lightningd --version | xargs printf "lightningd %s\n")
python=$(python --version)
pyln_client=$(pip list | rg pyln-client)
printf "%s\n%s\n%s\n%s\n" "$ubuntu" "$python" "$lightningd" "$pyln_client"
connect.json
{
"jsonrpc": "2.0",
"method": "connect",
"params": {
"id": "0271aecf3075ce72f2df479b60fc1db98d4d780c66fa495013614054554e1bb2bf",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
}
invoice_creation.json
{
"jsonrpc": "2.0",
"method": "invoice_creation",
"params": {
"invoice_creation": {
"msat": "100000000msat",
"preimage": "88042f007f02283571abbc40aca8b4302643415e85c71413177ef139b4276970",
"label": "inv"
}
}
}
myplugin
connect params: {"id": "039b780a84d36584ac0d7c84d10a962a1c5b35d2775650454c2a843368970932b0", "direction": "out", "address": {"type": "ipv4", "address": "127.0.0.1", "port": 7272}}
invoice_creation params: {"invoice_creation": {"msat": "100000000msat", "preimage": "4ec62babb7d48f494d9945cdfdbd942f387467da6b804d84c0428b49bbdf0844", "label": "inv"}}