Write a Core Lightning plugin in Rust
In this episode, we write a Core Lightning plugin in Rust that registers a JSON-RPC method. To do so we use cln-plugin
and cln-rpc
Rust crates.
Transcript with corrections and improvements
Let's add the method mymethod
to CLN by writing a dynamic
Rust plugin called myplugin
using cln-plugin and cln-rpc crates.
When we start the plugin myplugin
with the option foo_opt
set to
BAR
like this
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin foo_opt=BAR
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/myplugin:
$ 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,
lightningd
writes to the stdin stream of the plugin to send it messages andthe 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:
-> stdin
| |
lightningd plugin
| |
<- stdout
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
which registers a JSON-RPC method called mymethod
:
When
lightningd
startsmyplugin
, it sends thegetmanifest
request tomyplugin
.myplugin
replies tolightningd
with a response containing the declaration of the startup optionfoo_opt
and the information to registermymethod
JSON-RPC method.Then
lightningd
sends theinit
request tomyplugin
. This tellsmyplugin
thatlightningd
is ready to communicate. Thatinit
request contains informations that the plugin might needs to work correctly. For instance,myplugin
needs to know the value of the startup optionfoo_opt
and 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 theinit
request.Then
myplugin
starts an IO loop and waits in its stdin stream incoming requests fromlightningd
:(a). A client sends a
mymethod
request tolightningd
. Asmyplugin
has registeredmymethod
JSON-RPC method tolightningd
,lightningd
forwards that request tomyplugin
,(b).
myplugin
receives that request, builds a response and sends it back tolightningd
,(c). Finally,
lightningd
forwards that response to the client,(d). We repeat (a) to (c).
Setup
Here is my setup:
lightningd v23.05.2
cln-plugin 0.1.4
cln-rpc 0.1.3
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:~/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
:
◉ tony@tony:~/lnroom:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Plugin skeleton
In the file main.rs
(in the subdirectory myplugin
) we have the
skeleton of a dynamic CLN plugin that can be run by lightningd
thought
it does nothing:
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.dynamic()
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
To start that plugin we first need to build it like this:
◉ tony@tony:~/lnroom:
$ cd myplugin/
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
...
Now we can run it like this:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"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/target/debug/myplugin",
"active": true,
"dynamic": true
}
]
}
To check that the plugin is running we can check for a process for that program:
◉ tony@tony:~/lnroom/myplugin:
$ ps -ax | rg myplu
391290 pts/1 Sl 0:00 /home/tony/lnroom/myplugin/target/debug/myplugin
391339 pts/1 S+ 0:00 rg myplu
Declare mymethod JSON-RPC method
Now let's declare mymethod
method which always returns the json object
{"foo": "bar"}
:
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.dynamic()
.rpcmethod("mymethod", "Description", mymethod_func)
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
async fn mymethod_func(_p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
Ok(json!({
"foo": "bar"
}))
}
Let's compile and restart the plugin like this
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 5.39s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
and call mymethod
method by running the following command:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo": "bar"
}
Add cli parameters to mymethod
In this part we focus on cli parameters handling.
Those parameters are avaible in the v
argument of the mymethod_func
.
Here is how we get foo1
and foo2
values if they are passed as
parameters to the JSON-RPC method mymethod
:
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.dynamic()
.rpcmethod("mymethod", "Description", mymethod_func)
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
async fn mymethod_func(_p: Plugin<()>, v: serde_json::Value) -> Result<serde_json::Value, Error> {
Ok(json!({
"foo1": v["foo1"],
"foo2": v["foo2"]
}))
}
Let's compile and restart the plugin:
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 4.17s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
Now we call mymethod
with no argument, 1 argument (foo1
) and 2
arguments (foo1
and foo2
):
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo1": null,
"foo2": null
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k mymethod foo1=bar1
{
"foo1": "bar1",
"foo2": null
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"foo1": "bar1",
"foo2": "bar2"
}
Let's continue.
Add foo_opt startup option to myplugin
To declare the startup option foo_opt
(with default value bar
) and
returns its value when we call the method mymethod
we modify main.rs
file like this:
use cln_plugin::options::{ConfigOption, Value};
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.option(ConfigOption::new(
"foo_opt",
Value::String("bar".to_string()),
"Description"
))
.dynamic()
.rpcmethod("mymethod", "Description", mymethod_func)
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
async fn mymethod_func(p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
let foo_opt = p.option("foo_opt").unwrap();
Ok(json!({
"foo_opt": foo_opt.as_str()
}))
}
Let's compile and restart the plugin:
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 6.06s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
Now when we call mymethod
we get the default value bar
for the startup
option foo_opt
:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo_opt": "bar"
}
Finally, we stop the plugin and restart it with the startup option
foo_opt
set to BAR
and when we call mymethod
we get BAR
for the
startup option foo_opt
:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin stop myplugin
{
"command": "stop",
"result": "Successfully stopped myplugin."
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo_opt": "BAR"
}
Let's continue.
Get the node id of the node l1 running myplugin
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.
To do that RPC call we'll use cln-rpc
crate. But to use that crate we
first need the path of the Unix socket file of the node l1
which we'll
use to send a getinfo
RPC request.
The needed informations to build that path have been sent by
lightningd
with the init
request when we started the plugin. We can
access these informations by calling p.configuration()
method where p
is the plugin argument in mymethod_func
:
use std::path::Path;
use cln_plugin::options::{ConfigOption, Value};
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
...
async fn mymethod_func(p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
let conf = p.configuration();
let socket_path = Path::new(&conf.lightning_dir).join(&conf.rpc_file);
Ok(json!({
"socket_path": socket_path,
}))
}
Let's compile and restart the plugin:
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 3.77s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
Now we can check calling mymethod
that we are able to get the l1
's
socket:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"socket_path": "/tmp/l1-regtest/regtest/lightning-rpc"
}
With that socket path we can now get the node id of the node l1
like
this:
use cln_rpc::{model::GetinfoRequest, ClnRpc, Request, Response};
use std::path::Path;
use cln_plugin::options::{ConfigOption, Value};
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
...
async fn mymethod_func(p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
let conf = p.configuration();
let socket_path = Path::new(&conf.lightning_dir).join(&conf.rpc_file);
let mut client = ClnRpc::new(socket_path).await?;
let getinfo = client.call(Request::Getinfo(GetinfoRequest {})).await?;
let node_id = match getinfo {
Response::Getinfo(getinfo_response) => getinfo_response.id,
_ => panic!()
};
Ok(json!({
"node_id": node_id,
}))
}
Let's compile and restart the plugin:
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 18.01s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
Finally, we can check that we get l1
's node id using cln-rpc
crate:
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"node_id": "03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46"
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli getinfo | jq -r .id
03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46
Let's continue.
Complete implementation
Now we just have to put all the pieces together like this:
use cln_rpc::{model::GetinfoRequest, ClnRpc, Request, Response};
use std::path::Path;
use cln_plugin::options::{ConfigOption, Value};
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.option(ConfigOption::new(
"foo_opt",
Value::String("bar".to_string()),
"Description"
))
.dynamic()
.rpcmethod("mymethod", "Description", mymethod_func)
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
async fn mymethod_func(p: Plugin<()>, v: serde_json::Value) -> Result<serde_json::Value, Error> {
let foo_opt = p.option("foo_opt").unwrap();
let conf = p.configuration();
let socket_path = Path::new(&conf.lightning_dir).join(&conf.rpc_file);
let mut client = ClnRpc::new(socket_path).await?;
let getinfo = client.call(Request::Getinfo(GetinfoRequest {})).await?;
let node_id = match getinfo {
Response::Getinfo(getinfo_response) => getinfo_response.id,
_ => panic!()
};
Ok(json!({
"node_id": node_id,
"options": {
"foo_opt": foo_opt.as_str()
},
"cli_params": {
"foo1": v["foo1"],
"foo2": v["foo2"]
}
}))
}
Therefore, after compiling and restarting our plugin with the startup
option foo_opt
set to BAR
like this
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 8.84s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin 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/myplugin:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
},
"node_id": "03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46",
"options": {
"foo_opt": "BAR"
}
}
We are done!
Terminal session
We ran the following commands in this order:
$ ls
$ tree myplugin/
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ cd myplugin/
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ ps -ax | rg myplu
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ l1-cli mymethod
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ l1-cli mymethod
$ l1-cli -k mymethod foo1=bar1
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ l1-cli mymethod
$ l1-cli plugin stop myplugin
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin foo_opt=BAR
$ l1-cli mymethod
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ l1-cli mymethod
$ cargo build
$ l1-cli plugin start $(pwd)/target/debug/myplugin
$ l1-cli mymethod
$ l1-cli getinfo | jq -r .id
$ cargo build
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin 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/ notes.org
◉ tony@tony:~/lnroom:
$ tree myplugin/
myplugin/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
◉ 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] 389311
[2] 389345
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:
$ cd myplugin/
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Updating crates.io index
Compiling proc-macro2 v1.0.66
Compiling unicode-ident v1.0.11
Compiling autocfg v1.1.0
Compiling memchr v2.5.0
Compiling futures-core v0.3.28
Compiling libc v0.2.147
Compiling pin-project-lite v0.2.10
Compiling serde v1.0.171
Compiling quote v1.0.31
Compiling syn v2.0.26
Compiling slab v0.4.8
Compiling futures-channel v0.3.28
Compiling futures-sink v0.3.28
Compiling futures-task v0.3.28
Compiling tokio v1.29.1
Compiling futures-util v0.3.28
Compiling cc v1.0.79
Compiling socket2 v0.4.9
Compiling num_cpus v1.16.0
Compiling mio v0.8.8
Compiling pin-utils v0.1.0
Compiling bytes v1.4.0
Compiling secp256k1-sys v0.6.1
Compiling rustix v0.38.4
Compiling once_cell v1.18.0
Compiling futures-io v0.3.28
Compiling tracing-core v0.1.31
Compiling aho-corasick v1.0.2
Compiling cfg-if v1.0.0
Compiling serde_json v1.0.103
Compiling linux-raw-sys v0.4.3
Compiling anyhow v1.0.72
Compiling bitflags v2.3.3
Compiling serde_derive v1.0.171
Compiling futures-macro v0.3.28
Compiling tokio-macros v2.1.0
Compiling regex-syntax v0.7.4
Compiling regex-automata v0.3.3
Compiling tracing v0.1.37
Compiling log v0.4.19
Compiling ryu v1.0.15
Compiling itoa v1.0.9
Compiling regex v1.9.1
Compiling tokio-util v0.7.8
Compiling bitcoin_hashes v0.11.0
Compiling secp256k1 v0.24.3
Compiling is-terminal v0.4.9
Compiling futures-executor v0.3.28
Compiling termcolor v1.2.0
Compiling humantime v2.1.0
Compiling bech32 v0.9.1
Compiling futures v0.3.28
Compiling tokio-stream v0.1.14
Compiling bitcoin v0.29.2
Compiling env_logger v0.10.0
Compiling hex v0.4.3
Compiling cln-plugin v0.1.4
Compiling cln-rpc v0.1.3
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 1m 15s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"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/target/debug/myplugin",
"active": true,
"dynamic": true
}
]
}
◉ tony@tony:~/lnroom/myplugin:
$ ps -ax | rg myplu
391290 pts/1 Sl 0:00 /home/tony/lnroom/myplugin/target/debug/myplugin
391339 pts/1 S+ 0:00 rg myplu
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 5.39s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo": "bar"
}
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 4.17s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo1": null,
"foo2": null
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k mymethod foo1=bar1
{
"foo1": "bar1",
"foo2": null
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"foo1": "bar1",
"foo2": "bar2"
}
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 6.06s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo_opt": "bar"
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin stop myplugin
{
"command": "stop",
"result": "Successfully stopped myplugin."
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"foo_opt": "BAR"
}
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 3.77s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"socket_path": "/tmp/l1-regtest/regtest/lightning-rpc"
}
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 18.01s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli plugin start $(pwd)/target/debug/myplugin
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli mymethod
{
"node_id": "03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46"
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli getinfo | jq -r .id
03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46
◉ tony@tony:~/lnroom/myplugin:
$ cargo build
Compiling myplugin v0.1.0 (/home/tony/lnroom/myplugin)
Finished dev [unoptimized + debuginfo] target(s) in 8.84s
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/target/debug/myplugin foo_opt=BAR
{
"command": "start",
"plugins": [...]
}
◉ tony@tony:~/lnroom/myplugin:
$ l1-cli -k mymethod foo1=bar1 foo2=bar2
{
"cli_params": {
"foo1": "bar1",
"foo2": "bar2"
},
"node_id": "03cff86fa2d70a0666668db91a64fc7f318450994448df2a248c30c40d73d77f46",
"options": {
"foo_opt": "BAR"
}
}
Source code
main.rc
use cln_rpc::{model::GetinfoRequest, ClnRpc, Request, Response};
use std::path::Path;
use cln_plugin::options::{ConfigOption, Value};
use serde_json::json;
use cln_plugin::{Builder, Error, Plugin};
use tokio::io::{stdin,stdout};
#[tokio::main]
async fn main() -> Result<(), Error> {
let state = ();
if let Some(plugin) = Builder::new(stdin(), stdout())
.option(ConfigOption::new(
"foo_opt",
Value::String("bar".to_string()),
"Description"
))
.dynamic()
.rpcmethod("mymethod", "Description", mymethod_func)
.start(state)
.await?
{
plugin.join().await
} else {
Ok(())
}
}
async fn mymethod_func(p: Plugin<()>, v: serde_json::Value) -> Result<serde_json::Value, Error> {
let foo_opt = p.option("foo_opt").unwrap();
let conf = p.configuration();
let socket_path = Path::new(&conf.lightning_dir).join(&conf.rpc_file);
let mut client = ClnRpc::new(socket_path).await?;
let getinfo = client.call(Request::Getinfo(GetinfoRequest {})).await?;
let node_id = match getinfo {
Response::Getinfo(getinfo_response) => getinfo_response.id,
_ => panic!()
};
Ok(json!({
"node_id": node_id,
"options": {
"foo_opt": foo_opt.as_str()
},
"cli_params": {
"foo1": v["foo1"],
"foo2": v["foo2"]
}
}))
}
Cargo.toml
[package]
name = "myplugin"
version = "0.1.0"
edition = "2021"
[dependencies]
cln-plugin = "0.1.4"
cln-rpc = "0.1.3"
serde_json = "1.0.102"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", ] }