LNROOM #17 - Write a Core Lightning plugin in Javascript
In this episode, we write a Core Lightning plugin in Javascript that registers a JSON-RPC method. To do so we use
clightningjsJavascript package.
Transcript with corrections and improvements¶
Let's add the method mymethod to CLN by writing a dynamic Javascript plugin called myplugin.js using clightningjs
When we start the plugin myplugin.js with the option foo_opt set to BAR like this
where l1-cli is an alias for lightning-cli --lightning-dir=/tmp/l1-regtest, we expect mymethod method called with the parameters foo1=bar1 and foo2=bar2 to gives us the following
â—‰ tony@tony:~/lnroom:
$ l1-cli -k mymethod 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.
Core Lightning plugin system¶
Before implementing that plugin, let's describe Core Lightning plugin system.
Once lightningd has started the plugin as a subprocess, the plugin and lightningd communicate using pipes. Specifically,
lightningdwrites to the stdin stream of the plugin to send it messages and- the plugin writes to its stdout stream to send messages to
lightningd.
To understand each other they use the JSON-RPC protocol.
That communcation can be represented like this:
Now that we know how lightningd and plugins communicate with each other, let's take a look at the life cycle of a plugin like myplugin.js which registers a JSON-RPC method called mymethod:
-
When
lightningdstartsmyplugin.js, it sends thegetmanifestrequest tomyplugin.js.myplugin.jsreplies tolightningdwith a response containing the declaration of the startup optionfoo_optand the information to registermymethodJSON-RPC method. -
Then
lightningdsends theinitrequest tomyplugin.js. This tellsmyplugin.jsthatlightningdis ready to communicate. Thatinitrequest contains informations that the plugin might needs to work correctly. For instance,myplugin.jsneeds to know the value of the startup optionfoo_optand to know the Unix socket file to use to connect to the node and to send JSON-RPC requests in order to retrieve the node id. Those informations are available in theinitrequest. -
Then
myplugin.jsstarts an IO loop and waits in its stdin stream incoming requests fromlightningd:- (a). A client sends a
mymethodrequest tolightningd. Asmyplugin.jshas registeredmymethodJSON-RPC method tolightningd,lightningdforwards that request tomyplugin.js, - (b).
myplugin.jsreceives that request, builds a response and sends it back tolightningd, - ©. Finally,
lightningdforwards that response to the client, - (d). We repeat (a) to ©.
- (a). A client sends a
Setup¶
Here is my setup:
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:~/lnroom:
$ source lightning/contrib/startup_regtest.sh
...
â—‰ tony@tony:~/lnroom:
$ start_ln
...
We can check that l1-cli is just an alias for lightning-cli with the base directory being /tmp/l1-regtest:
Installing and using clightningjs¶
To install clightningjs we run the following command:
In the file myplugin.js, we require the class Plugin from clightningjs, then we instantiate the object plugin with the class Plugin and finally we start the I/O loop with plugin.start():
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
plugin.start();
Once we've made myplugin.js file executable, we can start the plugin myplugin.js like this (thought it does nothing):
â—‰ tony@tony:~/lnroom:
$ chmod +x myplugin.js
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...
{
"name": "/home/tony/lnroom/myplugin.js",
"active": true,
"dynamic": true
}
]
}
Declare mymethod JSON-RPC method¶
Now let's declare mymethod method which always returns the json object {"foo": "bar"}:
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
function mymethodFunc(params) {
return {'foo', 'bar'}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.start();
Let's restart the plugin
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
and call mymethod method by running the following command:
Not so bad. We know how to register JSON-RPC commands.
Let's continue.
Add foo_opt startup option to myplugin.js¶
We can declare startup options to lightningd with addOption method of plugin object. After we receive the init request from lightningd, clightningjs takes care to put the startup options we've declared to lightningd in the object plugin.options.
For instance, we can declare the startup option foo_opt with plugin.addOption and we can access foo_opt option with plugin.options in mymethodFunc function:
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
function mymethodFunc(params) {
const fooOpt = plugin.options['foo_opt'];
return {"foo_opt": fooOpt}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.addOption('foo_opt', 'bar', 'description');
plugin.start();
We restart the plugin and call mymethod method:
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"foo_opt": {
"default": "bar",
"description": "description",
"type": "string",
"value": "bar"
}
}
We got the object lightningd sent us in the init request.
If we just want the value of foo_opt option we change myplugin.js to this:
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
function mymethodFunc(params) {
const fooOpt = plugin.options['foo_opt'].value;
return {"foo_opt": fooOpt}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.addOption('foo_opt', 'bar', 'description');
plugin.start();
Now let's restart myplugin.js plugin with the startup option foo_opt set to BAR and call mymethod method:
â—‰ tony@tony:~/lnroom:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"foo_opt": "BAR"
}
Great! We know how to declare startup options and use them.
Get the node id of the node l1 running myplugin.js¶
Let's see how we can get the node id of l1 node, the node running the plugin.
We can get that node id by doing a getinfo RPC call to the node.
Indeed, in the I/O loop of our plugin (started with plugin.start()) the plugin first answers to the getmanifest request and then answers to the init request. Just before sending the init response, clightningjs instantiate plugin.rpc property with the class RpcWrapper. This allows us to do JSON-RPC call to the node running the plugin like this:
As plugin.rpc.call method is async we have to declare mymethodFunc async. So we modify myplugin.js like this in order to return the node id of the node l1 running the plugin:
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
async function mymethodFunc(params) {
const fooOpt = plugin.options['foo_opt'].value;
const getinfo = await plugin.rpc.call('getinfo', {});
return {"node_id": getinfo['id']}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.addOption('foo_opt', 'bar', 'description');
plugin.start();
Back to our terminal, we restart our plugin and call mymethod method which returns l1's node id:
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"node_id": "027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6"
}
We can check that we got the correct id by calling directly getinfo method:
â—‰ tony@tony:~/lnroom:
$ l1-cli getinfo | jq -r .id
027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6
Complete implementation¶
Now we just have to put all the pieces together like this:
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
async function mymethodFunc(params) {
const fooOpt = plugin.options['foo_opt'].value;
const getinfo = await plugin.rpc.call('getinfo', {});
return {
"node_id": getinfo['id'],
"options": {
"foo_opt": fooOpt
},
"cli_params": params
};
}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.addOption('foo_opt', 'bar', 'description');
plugin.start();
Note that if we pass positional parameters to mymethod, the argument params of mymethodFunc will be an array and if we pass key/value pair parameters to mymethod, the argument params of mymethodFunc will be an object.
Therefore, after restarting our plugin myplugin.js with the startup option foo_opt set to BAR like this
â—‰ tony@tony:~/lnroom:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
and calling mymethod method with key/value pairs foo1=bar1 and foo2=bar2 we get the following:
â—‰ tony@tony:~/lnroom:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"node_id": "027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6",
"options": {
"foo_opt": "BAR"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
We are done!
Terminal session¶
We ran the following commands in this order:
$ ls
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ npm install clightningjs
$ chmod +x myplugin.js
$ l1-cli plugin start $(pwd)/myplugin.js
$ l1-cli plugin start $(pwd)/myplugin.js
$ l1-cli mymethod
$ l1-cli plugin start $(pwd)/myplugin.js
$ l1-cli mymethod
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
$ l1-cli mymethod
$ l1-cli plugin start $(pwd)/myplugin.js
$ l1-cli mymethod
$ l1-cli getinfo | jq -r .id
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
And below you can read the terminal session (command lines and outputs):
â—‰ tony@tony:~/lnroom:
$ ls
lightning/ myplugin.js notes.org
â—‰ 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
â—‰ tony@tony:~/lnroom:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 1468431
[2] 1468465
WARNING: eatmydata not found: install it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
â—‰ tony@tony:~/lnroom:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
â—‰ tony@tony:~/lnroom:
$ npm install clightningjs
â—‰ tony@tony:~/lnroom:
$ chmod +x myplugin.js
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...
{
"name": "/home/tony/lnroom/myplugin.js",
"active": true,
"dynamic": true
}
]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"foo": "bar"
}
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"foo_opt": {
"default": "bar",
"description": "description",
"type": "string",
"value": "bar"
}
}
â—‰ tony@tony:~/lnroom:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"foo_opt": "BAR"
}
â—‰ tony@tony:~/lnroom:
$ l1-cli plugin start $(pwd)/myplugin.js
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli mymethod
{
"node_id": "027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6"
}
â—‰ tony@tony:~/lnroom:
$ l1-cli getinfo | jq -r .id
027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6
â—‰ tony@tony:~/lnroom:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/myplugin.js foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
â—‰ tony@tony:~/lnroom:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"node_id": "027e52e2cf16cec77b60d0e252f0f299cb05917c2db6f508aa327a1a45376233c6",
"options": {
"foo_opt": "BAR"
},
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
}
}
Source code¶
myplugin.js¶
#!/usr/bin/env node
const Plugin = require('clightningjs');
const plugin = new Plugin();
async function mymethodFunc(params) {
const fooOpt = plugin.options['foo_opt'].value;
const getinfo = await plugin.rpc.call('getinfo', {});
return {
"node_id": getinfo['id'],
"options": {
"foo_opt": fooOpt
},
"cli_params": params
};
}
plugin.addMethod('mymethod', mymethodFunc, 'usage', 'description');
plugin.addOption('foo_opt', 'bar', 'description');
plugin.start();