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"}}