Write a Core Lightning plugin in Rust

LNROOM #19July 17, 2023

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,

  1. lightningd writes to the stdin stream of the plugin to send it messages and

  2. 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:

          ->  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:

  1. When lightningd starts myplugin, it sends the getmanifest request to myplugin. myplugin replies to lightningd with a response containing the declaration of the startup option foo_opt and the information to register mymethod JSON-RPC method.

  2. Then lightningd sends the init request to myplugin. This tells myplugin that lightningd is ready to communicate. That init request contains informations that the plugin might needs to work correctly. For instance, myplugin needs to know the value of the startup option foo_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 the init request.

  3. Then myplugin starts an IO loop and waits in its stdin stream incoming requests from lightningd:

    • (a). A client sends a mymethod request to lightningd. As myplugin has registered mymethod JSON-RPC method to lightningd, lightningd forwards that request to myplugin,

    • (b). myplugin receives that request, builds a response and sends it back to lightningd,

    • (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", ] }

Resources