Core Lightning implementation of BOLT #11 invoices - part 1

LIVE #17November 23, 2023

In this live we see how to write JSON RPC commands in Core Lightning. Specifically, we write 3 JSON RPC commands foo, my-invoice and my-commands. The foo command takes no argument and returns {"foo": {"bar":"baz"}}. We also modify it to return the node id. Then we look at the invoice command which takes arguments and write my-invoice command that takes arguments. Finally, we write my-commands command which lists the commands registered in JSON RPC interface.

Transcript with corrections and improvements

Hi everybody I hope you're doing well, let's get started this live session about Lightning Network invoices.

What I want to do today and in the next few months is to talk about BOLT #11 invoices, specifically how they are implemented in Core Lightning.

Let's look at the invoice command. It takes 3 mandatory arguments amount_msat, label and description and some optional ones:

invoice  amount_msat label description
         [expiry] [fallbacks] [preimage]
         [exposeprivatechannels] [cltv] [deschashonly]

If we run that command at the command line where l1-cli is an alias for lightning-cli --lightning-dir=/tmp/l1-regtest we get the following BOLT #11 invoice:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli invoice 10000 label-1 description-1
{
   "payment_hash": "de57fd9e5d2190c17d3e8d95f980ebdb5c6da7e67128f505033df1e6bfc5643a",
   "expires_at": 1701338404,
   "bolt11": "lnbcrt100n1pj47g4...r8rsyy",
   "payment_secret": "17ace961ae413115c8944c874f78f1834ab54b6234d009e7744a262d9c9acd5e",
   "created_index": 1,
   "warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}

What happens when we run the previous command is that we send a JSON RPC request like the following to the lightning-rpc socket file

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "invoice",
  "params": ["10000", "label-1", "description-1"]
}

and lightningd handles it and returns the JSON RPC response with the payload presented above.

The goals of the next live coding sessions is:

  1. to understand how this JSON output is generated,

  2. what are BOLT #11 specificities and

  3. what are Core Lightning specificities.

We've seen in other lives how to add JSON RPC commands to CLN using plugins.

Today, we are going to see how to write JSON RPC commands in Core Lightning.

Specifically, we'll write 3 JSON RPC commands foo, my-invoice and my-commands. The foo command takes no argument and returns {"foo": {"bar":"baz"}}. We'll also modify it to return the node id. Then we'll look at the invoice command which takes arguments and write my-invoice command that takes arguments. Finally, we'll write my-commands command which lists the commands registered in JSON RPC interface.

Compiling CLN with rust plugin disabled

In the lightning repository checked out at v23.08.1 (the latest release), we run the configure file, clean the repository from previous builds (if any) and we build it (-j4 because I have 4 CPUs on my machine running Ubuntu 22.04.2 LTS):

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ ./configure --disable-rust
...
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make clean
...
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make -j4
CC: cc -DBINTOPKGLIBEXECDIR="../libexec/c-lightning" -Wall -Wundef -Wmissing-prototypes -Wmissing-declarations -Wstrict-prototypes -Wold-style-definition -Werror -Wno-maybe-uninitialized -Wshadow=local -std=gnu11 -g -fstack-protector-strong -Og -I ccan -I external/libwally-core/include/ -I external/libwally-core/src/secp256k1/include/ -I external/jsmn/ -I external/libbacktrace/ -I external/gheap/ -I external/build-x86_64-linux-gnu/libbacktrace-build -I external/libsodium/src/libsodium/include -I external/libsodium/src/libsodium/include/sodium -I external/build-x86_64-linux-gnu/libsodium-build/src/libsodium/include -I . -I/usr/local/include  -I/usr/include/postgresql    -DSHACHAIN_BITS=48 -DJSMN_PARENT_LINKS  -DCOMPAT_V052=1 -DCOMPAT_V060=1 -DCOMPAT_V061=1 -DCOMPAT_V062=1 -DCOMPAT_V070=1 -DCOMPAT_V072=1 -DCOMPAT_V073=1 -DCOMPAT_V080=1 -DCOMPAT_V081=1 -DCOMPAT_V082=1 -DCOMPAT_V090=1 -DCOMPAT_V0100=1 -DCOMPAT_V0121=1  -DBUILD_ELEMENTS=1 -c -o
LD: cc   -Og  config.vars  -Lexternal/build-x86_64-linux-gnu -lwallycore -lsecp256k1 -ljsmn -lbacktrace -lsodium -L/usr/local/include -lm -lsqlite3 -lz  -L/usr/lib/x86_64-linux-gnu -lpq -o
...

Note that I have all the build dependencies already installed on my machine (See Installing from source).

In the directory lightningd the binary lightningd has been built among other like channeld.

peer_control.c, invoice.c and jsonrpc.c files

The 3 commands foo, my-invoice and my-commands that we are going to add to the JSON RPC interface will be written respectively in the files:

Adding a command to lightningd

Example of getinfo defined in lightning:lightningd/peer_control.c:

static struct command_result *json_getinfo(...) {...}
static const struct json_command getinfo_command = {...};
AUTODATA(json_command, &getinfo_command);

Some helpers to write commands:

The statement AUTODATA(...); is used to declare JSON RPC commands when the first argument is json_command as above.

Those JSON RPC commands are collected before the main function of lightningd binary runs. Then, in that main function a call to jsonrpc_setup function registers those commands to the JSON RPC interface.

Let's list all those declarations using rg utility:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ rg 'AUTODATA\(json_command'
lightningd/plugin_control.c
325:AUTODATA(json_command, &plugin_control_command);

lightningd/ping.c
95:AUTODATA(json_command, &ping_command);

lightningd/dual_open_control.c
3544:AUTODATA(json_command, &queryrates_command);
3585:AUTODATA(json_command, &openchannel_init_command);
3586:AUTODATA(json_command, &openchannel_update_command);
3587:AUTODATA(json_command, &openchannel_signed_command);
3588:AUTODATA(json_command, &openchannel_bump_command);
3589:AUTODATA(json_command, &openchannel_abort_command);

lightningd/peer_control.c
2110:AUTODATA(json_command, &listpeers_command);
2163:AUTODATA(json_command, &staticbackup_command);
2223:AUTODATA(json_command, &listpeerchannels_command);
2428:AUTODATA(json_command, &disconnect_command);
2553:AUTODATA(json_command, &getinfo_command);
2659:AUTODATA(json_command, &waitblockheight_command);
2912:AUTODATA(json_command, &setchannel_command);
2976:AUTODATA(json_command, &dev_sign_last_tx);
3017:AUTODATA(json_command, &dev_fail_command);
3076:AUTODATA(json_command, &dev_reenable_commit);
3193:AUTODATA(json_command, &dev_forget_channel_command);

lightningd/signmessage.c
115:AUTODATA(json_command, &json_signmessage_cmd);
243:AUTODATA(json_command, &json_checkmessage_cmd);

lightningd/gossip_control.c
487:AUTODATA(json_command, &setleaserates_command);
536:AUTODATA(json_command, &addgossip_command);
565:AUTODATA(json_command, &dev_set_max_scids_encode_size);
608:AUTODATA(json_command, &dev_compact_gossip_store);
635:AUTODATA(json_command, &dev_gossip_set_time);

lightningd/invoice.c
1225:AUTODATA(json_command, &invoice_command);
1353:AUTODATA(json_command, &listinvoices_command);
1434:AUTODATA(json_command, &delinvoice_command);
1460:AUTODATA(json_command, &delexpiredinvoice_command);
1510:AUTODATA(json_command, &waitanyinvoice_command);
1556:AUTODATA(json_command, &waitinvoice_command);
1592:AUTODATA(json_command, &decodepay_command);
1841:AUTODATA(json_command, &createinvoice_command);
1883:AUTODATA(json_command, &preapproveinvoice_command);
1924:AUTODATA(json_command, &preapprovekeysend_command);
1978:AUTODATA(json_command, &signinvoice_command);

lightningd/closed_channel.c
119:AUTODATA(json_command, &listclosedchannels_command);

lightningd/closing_control.c
906:AUTODATA(json_command, &close_command);

lightningd/offer.c
130:AUTODATA(json_command, &createoffer_command);
192:AUTODATA(json_command, &listoffers_command);
231:AUTODATA(json_command, &disableoffer_command);
499:AUTODATA(json_command, &createinvreq_command);
536:AUTODATA(json_command, &payersign_command);
599:AUTODATA(json_command, &listinvoicerequests_command);
639:AUTODATA(json_command, &disableinvoicerequest_command);

lightningd/log.c
1164:AUTODATA(json_command, &getlog_command);

lightningd/jsonrpc.c
177:AUTODATA(json_command, &help_command);
231:AUTODATA(json_command, &stop_command);
315:AUTODATA(json_command, &dev_command);
1516:AUTODATA(json_command, &check_command);
1542:AUTODATA(json_command, &notifications_command);
1568:AUTODATA(json_command, &batching_command);

lightningd/channel_control.c
2070:AUTODATA(json_command, &splice_init_command);
2083:AUTODATA(json_command, &splice_update_command);
2091:AUTODATA(json_command, &splice_signed_command);
2143:AUTODATA(json_command, &dev_feerate_command);
2196:AUTODATA(json_command, &dev_quiesce_command);

lightningd/peer_htlcs.c
2989:AUTODATA(json_command, &dev_ignore_htlcs);
3120:AUTODATA(json_command, &listforwards_command);
3185:AUTODATA(json_command, &delforward_command);
3265:AUTODATA(json_command, &listhtlcs_command);

lightningd/configs.c
338:AUTODATA(json_command, &listconfigs_command);
631:AUTODATA(json_command, &setconfig_command);

lightningd/runes.c
399:AUTODATA(json_command, &showrunes_command);
592:AUTODATA(json_command, &creatrune_command);
600:AUTODATA(json_command, &invokerune_command);
701:AUTODATA(json_command, &blacklistrune_command);
709:AUTODATA(json_command, &destroyrune_command);
848:AUTODATA(json_command, &checkrune_command);

lightningd/wait.c
216:AUTODATA(json_command, &wait_command);

lightningd/connect_control.c
257:AUTODATA(json_command, &connect_command);
825:AUTODATA(json_command, &sendcustommsg_command);
837:AUTODATA(json_command, &dev_sendcustommsg_command);
862:AUTODATA(json_command, &dev_suppress_gossip);
884:AUTODATA(json_command, &dev_report_fds);

lightningd/hsm_control.c
283:AUTODATA(json_command, &makesecret_command);

lightningd/chaintopology.c
783:AUTODATA(json_command, &feerates_command);
810:AUTODATA(json_command, &parse_feerate_command);

lightningd/onion_message.c
245:AUTODATA(json_command, &sendonionmessage_command);
357:AUTODATA(json_command, &blindedpath_command);

lightningd/datastore.c
298:AUTODATA(json_command, &datastore_command);
306:AUTODATA(json_command, &deldatastore_command);
314:AUTODATA(json_command, &listdatastore_command);

lightningd/memdump.c
89:AUTODATA(json_command, &dev_memdump_command);
313:AUTODATA(json_command, &dev_memleak_command);

lightningd/opening_control.c
1552:AUTODATA(json_command, &fundchannel_start_command);
1560:AUTODATA(json_command, &fundchannel_cancel_command);
1569:AUTODATA(json_command, &fundchannel_complete_command);
1579:AUTODATA(json_command, &json_commitchan_command);

lightningd/pay.c
1374:AUTODATA(json_command, &sendonion_command);
1636:AUTODATA(json_command, &sendpay_command);
1683:AUTODATA(json_command, &waitsendpay_command);
1770:AUTODATA(json_command, &listsendpays_command);
1878:AUTODATA(json_command, &delpay_command);
1942:AUTODATA(json_command, &createonion_command);

wallet/reservation.c
147:AUTODATA(json_command, &reserveinputs_command);
230:AUTODATA(json_command, &unreserveinputs_command);
655:AUTODATA(json_command, &fundpsbt_command);
840:AUTODATA(json_command, &utxopsbt_command);

wallet/walletrpc.c
189:AUTODATA(json_command, &newaddr_command);
272:AUTODATA(json_command, &listaddrs_command);
428:AUTODATA(json_command, &listfunds_command);
505:AUTODATA(json_command, &dev_rescan_output_command);
610:AUTODATA(json_command, &listtransactions_command);
824:AUTODATA(json_command, &signpsbt_command);
860:AUTODATA(json_command, &setpsbtversion_command);
1022:AUTODATA(json_command, &sendpsbt_command);

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 by CLN repository and running the command start_ln:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ source contrib/startup_regtest.sh
lightning-cli is /home/tony/clnlive/lightning/cli/lightning-cli
lightningd is /home/tony/clnlive/lightning/lightningd/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:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 815172
[2] 815218
WARNING: eatmydata not found: install it for faster testing
Commands:
        l1-cli, l1-log,
        l2-cli, l2-log,
        bt-cli, stop_ln, fund_nodes

Note that lightningd is /home/tony/clnlive/lightning/lightningd/lightningd so we are using the lightningd we just compiled.

We can check that l1-cli is just an alias for lightning-cli with the base directory being /tmp/l1-regtest:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ alias l1-cli
alias l1-cli='/home/tony/clnlive/lightning/cli/lightning-cli --lightning-dir=/tmp/l1-regtest'

To be sure that we have at least a lightning node running on regtest, we can call the subcommand getinfo like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli getinfo
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7",
   "alias": "PEEVEDAUTO",
   "color": "0370e7",
   "num_peers": 0,
   "num_pending_channels": 0,
   "num_active_channels": 0,
   "num_inactive_channels": 0,
   "address": [],
   "binding": [
      {
         "type": "ipv4",
         "address": "127.0.0.1",
         "port": 7171
      }
   ],
   "version": "v23.08.1",
   "blockheight": 1,
   "network": "regtest",
   "fees_collected_msat": 0,
   "lightning-dir": "/tmp/l1-regtest/regtest",
   "our_features": {
      "init": "08a0000a0269a2",
      "node": "88a0000a0269a2",
      "channel": "",
      "invoice": "02000002024100"
   }
}

We can also generate a BOLT #11 invoice like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli invoice 10000 label-1 description-1
{
   "payment_hash": "4f36f7251095e4e3b2a940ee509d9686f3ebec598b88cacf3a983038980dbe69",
   "expires_at": 1701357720,
   "bolt11": "lnbcrt100n1pj47mscsp5ll2yqrk8ksdkzdlwpjylclfpun23y2c52zzjfqzptglqxhjvf00spp5fum0wfgsjhjw8v4fgrh9p8vksme7hmze3wyv4ne6nqcr3xqdhe5sdq4v3jhxcmjd9c8g6t0dcknzxqyjw5qcqp29qxpqysgqwl3awdug7qewzjtvhsjda5ysvysnju4nr2ncwmzhd4kn8czysw4ng5ustwq7qwgy074hehexuxudtn9zkjsu3gryke3pvufhea5cgjcql5z6au",
   "payment_secret": "ffd4400ec7b41b6137ee0c89fc7d21e4d5122b1450852480415a3e035e4c4bdf",
   "created_index": 1,
   "warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}

Write foo JSON RPC command in lightningd/peer_control.c file

In this section we write a foo command in lightning:lightningd/peer_control.c file. That command takes no argument and returns:

{"foo": {"bar":"baz"}}

To do so we take inspiration from the command getinfo defined in that same file.

We write that command just after the getinfo command.

We define a function json_foo that returns a command_result struct. Then we define a json_command struct foo_command where the third field is json_foo function. Finally, we use AUTODATA macros to register foo_command command:

/* clnlive #17 */

static struct command_result *json_foo(struct command *cmd,
                                       const char *buffer,
                                       const jsmntok_t *obj UNNEEDED,
                                       const jsmntok_t *params)
{
        struct json_stream *response;

        if (!param(cmd, buffer, params, NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_string(response, "bar", "baz");

        return command_success(cmd, response);
}

static const struct json_command foo_command = {
        "foo",
        "developer",
        json_foo,
        "foo clnlive #17"
};
AUTODATA(json_command, &foo_command);

We compile:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make -j4
...

Let's define l1-start alias that let us start the node l1 in background:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ alias l1-start='/home/tony/clnlive/lightning/lightningd/lightningd --lightning-dir=/tmp/l1-regtest --daemon'

Now we stop l1 node and restart it with the new compiled lightningd:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start

Now we can call our new foo JSON RPC command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "bar": "baz"
}

This is almost what we want to do. Now, let's set that object as the value of a field that we call foo. To do that we use the functions json_object_start and json_object_end like this:

static struct command_result *json_foo(...)
{
        ...
        response = json_stream_success(cmd);
        json_object_start(response, "foo");
        json_add_string(response, "bar", "baz");
        json_object_end(response);

        return command_success(cmd, response);
}
...

We compile:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make -j4
...

Let's restart l1 node with the new compiled lightningd:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start

Now we can call our new foo JSON RPC command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "foo": {
      "bar": "baz"
   }
}

Cool!

Finally, let's modify again json_foo function such that foo command returns the node id.

To do that we use the function json_add_node_id like this:

static struct command_result *json_foo(..)
{
        ...
        response = json_stream_success(cmd);
        json_add_node_id(response, "id", &cmd->ld->id);

        return command_success(cmd, response);
}

We compile, restart l1 node and call foo command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7"
}

Write my-invoice JSON RPC command in lightning/invoice.c file

In this section we write a my-invoice command in lightning:lightningd/invoice.c file. That command takes two arguments

  • amount_msat which is required and

  • description which is optional

and returns

{
   "amount_msat": <amount_msat>,
   "description": <description>
}

if description argument is provided or the following if not:

{
   "amount_msat": <amount_msat>
}

To do so we take inspiration from the command invoice defined in that same file.

We write that command just after the invoice command like this:

/* clnlive #17 */

static struct command_result *json_my_invoice(struct command *cmd,
                                              const char *buffer,
                                              const jsmntok_t *obj UNNEEDED,
                                              const jsmntok_t *params)
{
        struct amount_msat *msatoshi_val;
        const char *desc_val;
        struct json_stream *response;

        if (!param(cmd, buffer, params,
                   p_req("amount_msat|msatoshi", param_positive_msat_or_any, &msatoshi_val),
                   p_req("description", param_escaped_string, &desc_val),
                   NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        return command_success(cmd, response);
}

static const struct json_command my_invoice_command = {
        "my-invoice",
        "payment",
        json_my_invoice,
        "my-invoice clnlive #17"};
AUTODATA(json_command, &my_invoice_command);

We compile, restart l1 node and call my-invoice command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{}

Since we used p_req macro to set both arguments, if we don't pass the description argument to my-invoice command we get the following error:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000
{
   "code": -32602,
   "message": "missing required parameter: description"
}

Now let's fill that json response with the amount_msat field. To do that we use the functions json_add_amount_msat like this:

static struct command_result *json_my_invoice(...)
{
        ...
        if (!param(cmd, buffer, params,
                   p_req("amount_msat", param_msat, &msatoshi_val),
                   p_opt("description", param_escaped_string, &desc_val),
                   NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_amount_msat(response, "amount_msat", *msatoshi_val);
        if (desc_val)
                json_add_string(response, "description", desc_val);
        return command_success(cmd, response);
}
...

Note that we also modified the field name of the first argument and the function that handles it (param_positive_msat_or_any -> param_msat).

We compile, restart l1 node and call my-invoice command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{
   "amount_msat": 10000
}

And because we used param_msat function to set msatoshi_val variable and json_add_amount_msat, my-invoice command understands amount_msat argument ending with sat or btc as we see below:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10sat description
{
   "amount_msat": 10000
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 1btc description
{
   "amount_msat": 100000000000
}

Finally, we make the description argument optional. To do that we use p_opt macros (instead of p_req) like this:

static struct command_result *json_my_invoice(...)
{
        ...
        if (!param(cmd, buffer, params,
                   p_req("amount_msat", param_msat, &msatoshi_val),
                   p_opt("description", param_escaped_string, &desc_val),
                   NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_amount_msat(response, "amount_msat", *msatoshi_val);
        if (desc_val)
                json_add_string(response, "description", desc_val);
        return command_success(cmd, response);
}

We compile, restart l1 node and call my-invoice command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{
   "amount_msat": 10000,
   "description": "description"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000
{
   "amount_msat": 10000
}

Write my-commands JSON RPC command in lightningd/jsonrpc.c file

In this section we write a my-commands command in lightning:lightningd/jsonrpc.c file. That command takes no arguments and returns the following json object

{
   "commands": [
      "cmd-1",
      "cmd-2",
      ...
   ]
}

which list in the commands field all the registered commands by lightningd.

We define my-commands just after the help command like this where cmd->ld->jsonrpc->commands is an array of all the JSON RPC commands registered in lightningd and we return only the name field of the first one:

/* clnlive #17 */

static struct command_result *json_my_commands(struct command *cmd,
                                               const char *buffer,
                                               const jsmntok_t *obj UNNEEDED,
                                               const jsmntok_t *params)
{
        struct json_stream *response;
        struct json_command **commands;

        if (!param(cmd, buffer, params, NULL))
                return command_param_failed();

        commands = cmd->ld->jsonrpc->commands;

        response = json_stream_success(cmd);
        json_add_string(response, "command", commands[0]->name);

        return command_success(cmd, response);
}

static const struct json_command my_commands_command = {
        "my-commands",
        "developer",
        json_my_commands,
        "foo clnlive #17"
};
AUTODATA(json_command, &my_commands_command);

We compile, restart l1 node and call my-commands command like this

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "command": "feerates"
}

and we see that feerate is the first listed command in cmd->ld->jsonrpc->commands array.

Now we modify json_my_commands a bit to return the first listed command in a array. To do that we use the functions json_array_start and json_array_end like this:

static struct command_result *json_my_commands(...)
{
        ...
        response = json_stream_success(cmd);
        json_array_start(response, "commands");
        json_add_string(response, NULL, commands[0]->name);
        json_array_end(response);

        return command_success(cmd, response);
}

We compile, restart l1 node and call my-commands command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "feerates"
   ]
}

Interesting but we want to list all the JSON RCP commands registered in lightningd.

To do this we loop over cmd->ld->jsonrpc->commands array using tal_count macro to get the number of elements of that array like this:

static struct command_result *json_my_commands(...)
{
        ...
        response = json_stream_success(cmd);
        json_array_start(response, "commands");
        for (size_t i = 0; i < tal_count(commands); i++) {
                json_add_string(response, NULL, commands[i]->name);
        }
        json_array_end(response);

        return command_success(cmd, response);
}

We compile, restart l1 node and we list all the JSON RPC commands registered in lightningd by calling my-commands command like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "feerates",
      "parsefeerate",
      "splice_init",
      "splice_update",
      "splice_signed",
      "close",
      ...
      "bkpr-dumpincomecsv",
      "bkpr-channelsapy"
   ]
}

We are almost down playing with that command.

Now we filter those commands and list only the one with the plugin category.

Note that category in json_command struct has been changed in CLN v0.7.1 and can be "bitcoin", "channels", "network", "payment", "plugins", "utility", "developer" for native commands, or any other new category set by a plugin.

So, we can do that filtering like this using the macro streq:

static struct command_result *json_my_commands(...)
{
        ...
        response = json_stream_success(cmd);
        json_array_start(response, "commands");
        for (size_t i = 0; i < tal_count(commands); i++) {
                if (streq(commands[i]->category, "plugin"))
                        json_add_string(response, NULL, commands[i]->name);
        }
        json_array_end(response);

        return command_success(cmd, response);
}

We compile, restart l1 node and we list only the JSON RPC commands with the plugin category by calling my-commands like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "plugin",
      "emergencyrecover",
      "restorefrompeer",
      "keysend",
      "renepaystatus",
      "renepay",
      "getrawblockbyheight",
      "getchaininfo",
      "estimatefees",
      "sendrawtransaction",
      "getutxout",
      "commando",
      "commando-rune",
      "commando-listrunes",
      "commando-blacklist",
      "getroute",
      "listchannels",
      "listnodes",
      "listincoming",
      "offer",
      "invoicerequest",
      "decode",
      "paystatus",
      "listpays",
      "pay",
      "txprepare",
      "txdiscard",
      "txsend",
      "withdraw",
      "upgradewallet",
      "autocleaninvoice",
      "autoclean-status",
      "autoclean-once",
      "funderupdate",
      "multiwithdraw",
      "fundchannel",
      "multifundchannel",
      "sql",
      "listsqlschemas",
      "bkpr-listbalances",
      "bkpr-listaccountevents",
      "bkpr-inspect",
      "bkpr-listincome",
      "bkpr-dumpincomecsv",
      "bkpr-channelsapy"
   ]
}

Finally, we list only the commands foo, my-invoice and my-commands that we defined today. To do that we modify those commands such that their category is now clnlive like this:

/* lightningd/peer_control.c */
static const struct json_command foo_command = {
        "foo",
        "clnlive",
        json_foo,
        "foo clnlive #17"
};

/* lightningd/invoice.c */
static const struct json_command my_invoice_command = {
        "my-invoice",
        "clnlive",
        json_my_invoice,
        "my-invoice clnlive #17"};

/* lightningd/jsonrpc.c */
static const struct json_command my_commands_command = {
        "my-commands",
        "clnlive",
        json_my_commands,
        "foo clnlive #17"
};

In json_my_commands we want the category of the commands to be clnlive:

static struct command_result *json_my_commands(...)
{
        ...

        for (size_t i = 0; i < tal_count(commands); i++) {
                if (streq(commands[i]->category, "clnlive"))
                        json_add_string(response, NULL, commands[i]->name);
        }
        ...
}

We compile and restart l1 node and we list only the commands foo, my-invoice and my-commands that we defined today like this:

◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "my-invoice",
      "my-commands",
      "foo"
   ]
}

We are done!

Terminal session

We ran the following commands in this order:

$ cd lightning/
$ ./configure --disable-rust
$ make clean
$ make -j4
$ rg 'AUTODATA\(json_command'
$ source contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli getinfo
$ l1-cli invoice 10000 label-1 description-1
$ l1-cli stop
$ alias l1-start='/home/tony/clnlive/lightning/lightningd/lightningd --lightning-dir=/tmp/l1-regtest --daemon'
$ l1-start
$ l1-cli foo
$ l1-cli stop
$ l1-start
$ l1-cli foo
$ l1-cli stop
$ l1-start
$ l1-cli foo
$ l1-cli stop
$ l1-start
$ l1-cli foo
$ l1-cli stop
$ l1-start
$ l1-cli my-invoice 10000 description
$ l1-cli my-invoice 10000
$ l1-cli stop
$ l1-start
$ l1-cli my-invoice 10000 description
$ l1-cli my-invoice 10sat description
$ l1-cli my-invoice 1btc description
$ l1-cli stop
$ l1-start
$ l1-cli my-invoice 10000 description
$ l1-cli my-invoice 10000
$ l1-cli stop
$ l1-start
$ l1-cli my-invoice 10000 description
$ l1-cli my-invoice 10000
$ l1-cli stop
$ l1-start
$ l1-cli stop
$ l1-start
$ l1-cli my-commands
$ l1-cli stop
$ l1-start
$ l1-cli my-commands
$ l1-cli stop
$ l1-start
$ l1-cli my-commands
$ l1-cli stop
$ l1-start
$ l1-cli my-commands
$ l1-cli stop
$ l1-start
$ l1-cli my-commands
$ l1-cli stop
$ l1-start
$ l1-cli my-commands

And below you can read the terminal session (command lines and outputs):

◉ tony@tony:~/clnlive:
$ cd lightning/
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ ./configure --disable-rust
...
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make clean
...
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ make -j4
CC: cc -DBINTOPKGLIBEXECDIR="../libexec/c-lightning" -Wall -Wundef -Wmissing-prototypes -Wmissing-declarations -Wstrict-prototypes -Wold-style-definition -Werror -Wno-maybe-uninitialized -Wshadow=local -std=gnu11 -g -fstack-protector-strong -Og -I ccan -I external/libwally-core/include/ -I external/libwally-core/src/secp256k1/include/ -I external/jsmn/ -I external/libbacktrace/ -I external/gheap/ -I external/build-x86_64-linux-gnu/libbacktrace-build -I external/libsodium/src/libsodium/include -I external/libsodium/src/libsodium/include/sodium -I external/build-x86_64-linux-gnu/libsodium-build/src/libsodium/include -I . -I/usr/local/include  -I/usr/include/postgresql    -DSHACHAIN_BITS=48 -DJSMN_PARENT_LINKS  -DCOMPAT_V052=1 -DCOMPAT_V060=1 -DCOMPAT_V061=1 -DCOMPAT_V062=1 -DCOMPAT_V070=1 -DCOMPAT_V072=1 -DCOMPAT_V073=1 -DCOMPAT_V080=1 -DCOMPAT_V081=1 -DCOMPAT_V082=1 -DCOMPAT_V090=1 -DCOMPAT_V0100=1 -DCOMPAT_V0121=1  -DBUILD_ELEMENTS=1 -c -o
LD: cc   -Og  config.vars  -Lexternal/build-x86_64-linux-gnu -lwallycore -lsecp256k1 -ljsmn -lbacktrace -lsodium -L/usr/local/include -lm -lsqlite3 -lz  -L/usr/lib/x86_64-linux-gnu -lpq -o
...
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ rg 'AUTODATA\(json_command'
lightningd/plugin_control.c
325:AUTODATA(json_command, &plugin_control_command);

lightningd/ping.c
95:AUTODATA(json_command, &ping_command);

lightningd/dual_open_control.c
3544:AUTODATA(json_command, &queryrates_command);
3585:AUTODATA(json_command, &openchannel_init_command);
3586:AUTODATA(json_command, &openchannel_update_command);
3587:AUTODATA(json_command, &openchannel_signed_command);
3588:AUTODATA(json_command, &openchannel_bump_command);
3589:AUTODATA(json_command, &openchannel_abort_command);

lightningd/peer_control.c
2110:AUTODATA(json_command, &listpeers_command);
2163:AUTODATA(json_command, &staticbackup_command);
2223:AUTODATA(json_command, &listpeerchannels_command);
2428:AUTODATA(json_command, &disconnect_command);
2553:AUTODATA(json_command, &getinfo_command);
2659:AUTODATA(json_command, &waitblockheight_command);
2912:AUTODATA(json_command, &setchannel_command);
2976:AUTODATA(json_command, &dev_sign_last_tx);
3017:AUTODATA(json_command, &dev_fail_command);
3076:AUTODATA(json_command, &dev_reenable_commit);
3193:AUTODATA(json_command, &dev_forget_channel_command);

lightningd/signmessage.c
115:AUTODATA(json_command, &json_signmessage_cmd);
243:AUTODATA(json_command, &json_checkmessage_cmd);

lightningd/gossip_control.c
487:AUTODATA(json_command, &setleaserates_command);
536:AUTODATA(json_command, &addgossip_command);
565:AUTODATA(json_command, &dev_set_max_scids_encode_size);
608:AUTODATA(json_command, &dev_compact_gossip_store);
635:AUTODATA(json_command, &dev_gossip_set_time);

lightningd/invoice.c
1225:AUTODATA(json_command, &invoice_command);
1353:AUTODATA(json_command, &listinvoices_command);
1434:AUTODATA(json_command, &delinvoice_command);
1460:AUTODATA(json_command, &delexpiredinvoice_command);
1510:AUTODATA(json_command, &waitanyinvoice_command);
1556:AUTODATA(json_command, &waitinvoice_command);
1592:AUTODATA(json_command, &decodepay_command);
1841:AUTODATA(json_command, &createinvoice_command);
1883:AUTODATA(json_command, &preapproveinvoice_command);
1924:AUTODATA(json_command, &preapprovekeysend_command);
1978:AUTODATA(json_command, &signinvoice_command);

lightningd/closed_channel.c
119:AUTODATA(json_command, &listclosedchannels_command);

lightningd/closing_control.c
906:AUTODATA(json_command, &close_command);

lightningd/offer.c
130:AUTODATA(json_command, &createoffer_command);
192:AUTODATA(json_command, &listoffers_command);
231:AUTODATA(json_command, &disableoffer_command);
499:AUTODATA(json_command, &createinvreq_command);
536:AUTODATA(json_command, &payersign_command);
599:AUTODATA(json_command, &listinvoicerequests_command);
639:AUTODATA(json_command, &disableinvoicerequest_command);

lightningd/log.c
1164:AUTODATA(json_command, &getlog_command);

lightningd/jsonrpc.c
177:AUTODATA(json_command, &help_command);
231:AUTODATA(json_command, &stop_command);
315:AUTODATA(json_command, &dev_command);
1516:AUTODATA(json_command, &check_command);
1542:AUTODATA(json_command, &notifications_command);
1568:AUTODATA(json_command, &batching_command);

lightningd/channel_control.c
2070:AUTODATA(json_command, &splice_init_command);
2083:AUTODATA(json_command, &splice_update_command);
2091:AUTODATA(json_command, &splice_signed_command);
2143:AUTODATA(json_command, &dev_feerate_command);
2196:AUTODATA(json_command, &dev_quiesce_command);

lightningd/peer_htlcs.c
2989:AUTODATA(json_command, &dev_ignore_htlcs);
3120:AUTODATA(json_command, &listforwards_command);
3185:AUTODATA(json_command, &delforward_command);
3265:AUTODATA(json_command, &listhtlcs_command);

lightningd/configs.c
338:AUTODATA(json_command, &listconfigs_command);
631:AUTODATA(json_command, &setconfig_command);

lightningd/runes.c
399:AUTODATA(json_command, &showrunes_command);
592:AUTODATA(json_command, &creatrune_command);
600:AUTODATA(json_command, &invokerune_command);
701:AUTODATA(json_command, &blacklistrune_command);
709:AUTODATA(json_command, &destroyrune_command);
848:AUTODATA(json_command, &checkrune_command);

lightningd/wait.c
216:AUTODATA(json_command, &wait_command);

lightningd/connect_control.c
257:AUTODATA(json_command, &connect_command);
825:AUTODATA(json_command, &sendcustommsg_command);
837:AUTODATA(json_command, &dev_sendcustommsg_command);
862:AUTODATA(json_command, &dev_suppress_gossip);
884:AUTODATA(json_command, &dev_report_fds);

lightningd/hsm_control.c
283:AUTODATA(json_command, &makesecret_command);

lightningd/chaintopology.c
783:AUTODATA(json_command, &feerates_command);
810:AUTODATA(json_command, &parse_feerate_command);

lightningd/onion_message.c
245:AUTODATA(json_command, &sendonionmessage_command);
357:AUTODATA(json_command, &blindedpath_command);

lightningd/datastore.c
298:AUTODATA(json_command, &datastore_command);
306:AUTODATA(json_command, &deldatastore_command);
314:AUTODATA(json_command, &listdatastore_command);

lightningd/memdump.c
89:AUTODATA(json_command, &dev_memdump_command);
313:AUTODATA(json_command, &dev_memleak_command);

lightningd/opening_control.c
1552:AUTODATA(json_command, &fundchannel_start_command);
1560:AUTODATA(json_command, &fundchannel_cancel_command);
1569:AUTODATA(json_command, &fundchannel_complete_command);
1579:AUTODATA(json_command, &json_commitchan_command);

lightningd/pay.c
1374:AUTODATA(json_command, &sendonion_command);
1636:AUTODATA(json_command, &sendpay_command);
1683:AUTODATA(json_command, &waitsendpay_command);
1770:AUTODATA(json_command, &listsendpays_command);
1878:AUTODATA(json_command, &delpay_command);
1942:AUTODATA(json_command, &createonion_command);

wallet/reservation.c
147:AUTODATA(json_command, &reserveinputs_command);
230:AUTODATA(json_command, &unreserveinputs_command);
655:AUTODATA(json_command, &fundpsbt_command);
840:AUTODATA(json_command, &utxopsbt_command);

wallet/walletrpc.c
189:AUTODATA(json_command, &newaddr_command);
272:AUTODATA(json_command, &listaddrs_command);
428:AUTODATA(json_command, &listfunds_command);
505:AUTODATA(json_command, &dev_rescan_output_command);
610:AUTODATA(json_command, &listtransactions_command);
824:AUTODATA(json_command, &signpsbt_command);
860:AUTODATA(json_command, &setpsbtversion_command);
1022:AUTODATA(json_command, &sendpsbt_command);
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ source contrib/startup_regtest.sh
lightning-cli is /home/tony/clnlive/lightning/cli/lightning-cli
lightningd is /home/tony/clnlive/lightning/lightningd/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:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 815172
[2] 815218
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:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ alias l1-cli
alias l1-cli='/home/tony/clnlive/lightning/cli/lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli getinfo
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7",
   "alias": "PEEVEDAUTO",
   "color": "0370e7",
   "num_peers": 0,
   "num_pending_channels": 0,
   "num_active_channels": 0,
   "num_inactive_channels": 0,
   "address": [],
   "binding": [
      {
         "type": "ipv4",
         "address": "127.0.0.1",
         "port": 7171
      }
   ],
   "version": "v23.08.1",
   "blockheight": 1,
   "network": "regtest",
   "fees_collected_msat": 0,
   "lightning-dir": "/tmp/l1-regtest/regtest",
   "our_features": {
      "init": "08a0000a0269a2",
      "node": "88a0000a0269a2",
      "channel": "",
      "invoice": "02000002024100"
   }
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli invoice 10000 label-1 description-1
{
   "payment_hash": "4f36f7251095e4e3b2a940ee509d9686f3ebec598b88cacf3a983038980dbe69",
   "expires_at": 1701357720,
   "bolt11": "lnbcrt100n1pj47mscsp5ll2yqrk8ksdkzdlwpjylclfpun23y2c52zzjfqzptglqxhjvf00spp5fum0wfgsjhjw8v4fgrh9p8vksme7hmze3wyv4ne6nqcr3xqdhe5sdq4v3jhxcmjd9c8g6t0dcknzxqyjw5qcqp29qxpqysgqwl3awdug7qewzjtvhsjda5ysvysnju4nr2ncwmzhd4kn8czysw4ng5ustwq7qwgy074hehexuxudtn9zkjsu3gryke3pvufhea5cgjcql5z6au",
   "payment_secret": "ffd4400ec7b41b6137ee0c89fc7d21e4d5122b1450852480415a3e035e4c4bdf",
   "created_index": 1,
   "warning_capacity": "Insufficient incoming channel capacity to pay invoice"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
[1]-  Done                    test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--network=$network" "--lightning-dir=/tmp/l$i-$network" "--bitcoin-datadir=$PATH_TO_BITCOIN" "--database-upgrade=true"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ alias l1-start='/home/tony/clnlive/lightning/lightningd/lightningd --lightning-dir=/tmp/l1-regtest --daemon'
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7",
   "alias": "PEEVEDAUTO",
   "color": "0370e7",
   "num_peers": 0,
   "num_pending_channels": 0,
   "num_active_channels": 0,
   "num_inactive_channels": 0,
   "address": [],
   "binding": [
      {
         "type": "ipv4",
         "address": "127.0.0.1",
         "port": 7171
      }
   ],
   "version": "v23.08.1-modded",
   "blockheight": 1,
   "network": "regtest",
   "fees_collected_msat": 0,
   "lightning-dir": "/tmp/l1-regtest/regtest",
   "our_features": {
      "init": "08a0000a0269a2",
      "node": "88a0000a0269a2",
      "channel": "",
      "invoice": "02000002024100"
   }
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "bar": "baz"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "foo": {
      "bar": "baz"
   }
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli foo
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000
{
   "code": -32602,
   "message": "missing required parameter: description"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{
   "amount_msat": 10000
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10sat description
{
   "amount_msat": 10000
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 1btc description
{
   "amount_msat": 100000000000
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{
   "amount_msat": 10000,
   "description": "description"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000
Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.Lost connection to the RPC socket.lightning-cli: reading response: socket closed
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
lightning-cli: Connecting to 'lightning-rpc': Connection refused
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000 description
{
   "amount_msat": 10000,
   "description": "description"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-invoice 10000
{
   "amount_msat": 10000
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
Cannot add duplicate command foo
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
lightning-cli: Connecting to 'lightning-rpc': Connection refused
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "id": "0370e7cead9fbb0845ef7dff5b7b4a2c4eab8b722456ed0bca7747ff680c3de0f7"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "command": "feerates"
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "feerates"
   ]
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "feerates",
      "parsefeerate",
      "splice_init",
      "splice_update",
      "splice_signed",
      "close",
      "openchannel_init",
      "openchannel_update",
      "openchannel_signed",
      "openchannel_bump",
      "openchannel_abort",
      "listclosedchannels",
      "connect",
      "sendcustommsg",
      "dev-sendcustommsg",
      "sendonionmessage",
      "blindedpath",
      "setleaserates",
      "addgossip",
      "makesecret",
      "invoice",
      "my-invoice",
      "listinvoices",
      "delinvoice",
      "delexpiredinvoice",
      "waitanyinvoice",
      "waitinvoice",
      "decodepay",
      "createinvoice",
      "preapproveinvoice",
      "preapprovekeysend",
      "signinvoice",
      "help",
      "my-commands",
      "stop",
      "check",
      "notifications",
      "batching",
      "getlog",
      "fundchannel_start",
      "fundchannel_cancel",
      "fundchannel_complete",
      "recoverchannel",
      "sendonion",
      "sendpay",
      "waitsendpay",
      "listsendpays",
      "delpay",
      "createonion",
      "listpeers",
      "staticbackup",
      "listpeerchannels",
      "disconnect",
      "getinfo",
      "foo",
      "waitblockheight",
      "setchannel",
      "listforwards",
      "delforward",
      "listhtlcs",
      "plugin",
      "showrunes",
      "createrune",
      "invokerune",
      "blacklistrune",
      "destroyrune",
      "checkrune",
      "wait",
      "listconfigs",
      "setconfig",
      "datastore",
      "deldatastore",
      "listdatastore",
      "ping",
      "createoffer",
      "listoffers",
      "disableoffer",
      "createinvoicerequest",
      "payersign",
      "listinvoicerequests",
      "disableinvoicerequest",
      "signmessage",
      "checkmessage",
      "newaddr",
      "dev-listaddrs",
      "listfunds",
      "dev-rescan-outputs",
      "listtransactions",
      "signpsbt",
      "setpsbtversion",
      "sendpsbt",
      "reserveinputs",
      "unreserveinputs",
      "fundpsbt",
      "utxopsbt",
      "emergencyrecover",
      "restorefrompeer",
      "keysend",
      "getrawblockbyheight",
      "getchaininfo",
      "estimatefees",
      "sendrawtransaction",
      "getutxout",
      "commando",
      "commando-rune",
      "commando-listrunes",
      "commando-blacklist",
      "getroute",
      "listchannels",
      "listnodes",
      "listincoming",
      "offer",
      "invoicerequest",
      "decode",
      "paystatus",
      "listpays",
      "pay",
      "renepaystatus",
      "renepay",
      "autocleaninvoice",
      "autoclean-status",
      "autoclean-once",
      "funderupdate",
      "multiwithdraw",
      "fundchannel",
      "multifundchannel",
      "sql",
      "listsqlschemas",
      "txprepare",
      "txdiscard",
      "txsend",
      "withdraw",
      "upgradewallet",
      "bkpr-listbalances",
      "bkpr-listaccountevents",
      "bkpr-inspect",
      "bkpr-listincome",
      "bkpr-dumpincomecsv",
      "bkpr-channelsapy"
   ]
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "plugin",
      "emergencyrecover",
      "restorefrompeer",
      "keysend",
      "renepaystatus",
      "renepay",
      "getrawblockbyheight",
      "getchaininfo",
      "estimatefees",
      "sendrawtransaction",
      "getutxout",
      "commando",
      "commando-rune",
      "commando-listrunes",
      "commando-blacklist",
      "getroute",
      "listchannels",
      "listnodes",
      "listincoming",
      "offer",
      "invoicerequest",
      "decode",
      "paystatus",
      "listpays",
      "pay",
      "txprepare",
      "txdiscard",
      "txsend",
      "withdraw",
      "upgradewallet",
      "autocleaninvoice",
      "autoclean-status",
      "autoclean-once",
      "funderupdate",
      "multiwithdraw",
      "fundchannel",
      "multifundchannel",
      "sql",
      "listsqlschemas",
      "bkpr-listbalances",
      "bkpr-listaccountevents",
      "bkpr-inspect",
      "bkpr-listincome",
      "bkpr-dumpincomecsv",
      "bkpr-channelsapy"
   ]
}
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-start
◉ tony@tony:~/clnlive/lightning:[git»(HEAD detached at v23.08.1)]
$ l1-cli my-commands
{
   "commands": [
      "my-invoice",
      "my-commands",
      "foo"
   ]
}

Core Lightning source code

getinfo

json_getinfo in lightning:lightningd/peer_control.c

static struct command_result *json_getinfo(struct command *cmd,
                                           const char *buffer,
                                           const jsmntok_t *obj UNNEEDED,
                                           const jsmntok_t *params)
{
        struct json_stream *response;
        struct peer *peer;
        struct channel *channel;
        unsigned int pending_channels = 0, active_channels = 0,
                     inactive_channels = 0, num_peers = 0;
        size_t count_announceable;
        struct peer_node_id_map_iter it;

        if (!param(cmd, buffer, params, NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_node_id(response, "id", &cmd->ld->id);
        json_add_string(response, "alias", (const char *)cmd->ld->alias);
        json_add_hex_talarr(response, "color", cmd->ld->rgb);

        /* Add some peer and channel stats */
        for (peer = peer_node_id_map_first(cmd->ld->peers, &it);
             peer;
             peer = peer_node_id_map_next(cmd->ld->peers, &it)) {
                num_peers++;

                list_for_each(&peer->channels, channel, list) {
                        if (channel->state == CHANNELD_AWAITING_LOCKIN
                                        || channel->state == DUALOPEND_AWAITING_LOCKIN
                                        || channel->state == DUALOPEND_OPEN_INIT) {
                                pending_channels++;
                        } else if (channel_active(channel)) {
                                active_channels++;
                        } else {
                                inactive_channels++;
                        }
                }
        }
        json_add_num(response, "num_peers", num_peers);
        json_add_num(response, "num_pending_channels", pending_channels);
        json_add_num(response, "num_active_channels", active_channels);
        json_add_num(response, "num_inactive_channels", inactive_channels);

        /* Add network info */
        json_array_start(response, "address");
        if (cmd->ld->listen) {
                /* These are the addresses we're announcing */
                count_announceable = tal_count(cmd->ld->announceable);
                for (size_t i = 0; i < count_announceable; i++)
                        json_add_address(response, NULL, cmd->ld->announceable+i);

                /* Add discovered IPs if we announce them.
                 * Also see `create_node_announcement` in `gossip_generation.c`. */
                if ((cmd->ld->config.ip_discovery == OPT_AUTOBOOL_AUTO && count_announceable == 0) ||
                     cmd->ld->config.ip_discovery == OPT_AUTOBOOL_TRUE) {
                        if (cmd->ld->discovered_ip_v4 != NULL &&
                                        !wireaddr_arr_contains(
                                                cmd->ld->announceable,
                                                cmd->ld->discovered_ip_v4))
                                json_add_address(response, NULL,
                                                 cmd->ld->discovered_ip_v4);
                        if (cmd->ld->discovered_ip_v6 != NULL &&
                                        !wireaddr_arr_contains(
                                                cmd->ld->announceable,
                                                cmd->ld->discovered_ip_v6))
                                json_add_address(response, NULL,
                                                 cmd->ld->discovered_ip_v6);
                }
                json_array_end(response);

                /* This is what we're actually bound to. */
                json_array_start(response, "binding");
                for (size_t i = 0; i < tal_count(cmd->ld->binding); i++)
                        json_add_address_internal(response, NULL,
                                        cmd->ld->binding+i);
        }
        json_array_end(response);

        json_add_string(response, "version", version());
        /* If we're still syncing, put the height we're up to here, so
         * they can see progress!  Otherwise use the height gossipd knows
         * about, so tests work properly. */
        if (!topology_synced(cmd->ld->topology)) {
                json_add_num(response, "blockheight",
                             get_block_height(cmd->ld->topology));
        } else {
                json_add_num(response, "blockheight",
                             cmd->ld->gossip_blockheight);
        }
        json_add_string(response, "network", chainparams->network_name);
        json_add_amount_msat(response,
                             "fees_collected_msat",
                             wallet_total_forward_fees(cmd->ld->wallet));
        json_add_string(response, "lightning-dir", cmd->ld->config_netdir);

        if (!cmd->ld->topology->bitcoind->synced)
                json_add_string(response, "warning_bitcoind_sync",
                                "Bitcoind is not up-to-date with network.");
        else if (!topology_synced(cmd->ld->topology))
                json_add_string(response, "warning_lightningd_sync",
                                "Still loading latest blocks from bitcoind.");

        u8 **bits = cmd->ld->our_features->bits;
        json_object_start(response, "our_features");
        json_add_hex_talarr(response, "init",
                        featurebits_or(cmd,
                                       bits[INIT_FEATURE],
                                       bits[GLOBAL_INIT_FEATURE]));
        json_add_hex_talarr(response, "node", bits[NODE_ANNOUNCE_FEATURE]);
        json_add_hex_talarr(response, "channel", bits[CHANNEL_FEATURE]);
        json_add_hex_talarr(response, "invoice", bits[BOLT11_FEATURE]);
        json_object_end(response);

        return command_success(cmd, response);
}

static const struct json_command getinfo_command = {
        "getinfo",
        "utility",
        json_getinfo,
        "Show information about this node"
};
AUTODATA(json_command, &getinfo_command);

invoice

json_invoice in lightning:lightningd/invoice.c

static struct command_result *json_invoice(struct command *cmd,
                                           const char *buffer,
                                           const jsmntok_t *obj UNNEEDED,
                                           const jsmntok_t *params)
{
        const jsmntok_t *fallbacks;
        struct amount_msat *msatoshi_val;
        struct invoice_info *info;
        const char *desc_val;
        const u8 **fallback_scripts = NULL;
        u64 *expiry;
        struct sha256 rhash;
        struct secret payment_secret;
        struct preimage *preimage;
        u32 *cltv;
        struct jsonrpc_request *req;
        struct plugin *plugin;
        bool *hashonly;
        const size_t inv_max_label_len = 128;
#if DEVELOPER
        const jsmntok_t *routes;
#endif

        info = tal(cmd, struct invoice_info);
        info->cmd = cmd;

        if (!param(cmd, buffer, params,
                   p_req("amount_msat|msatoshi", param_positive_msat_or_any, &msatoshi_val),
                   p_req("label", param_label, &info->label),
                   p_req("description", param_escaped_string, &desc_val),
                   p_opt_def("expiry", param_u64, &expiry, 3600*24*7),
                   p_opt("fallbacks", param_array, &fallbacks),
                   p_opt("preimage", param_preimage, &preimage),
                   p_opt("exposeprivatechannels", param_chanhints,
                         &info->chanhints),
                   p_opt_def("cltv", param_number, &cltv,
                             cmd->ld->config.cltv_final),
                   p_opt_def("deschashonly", param_bool, &hashonly, false),
#if DEVELOPER
                   p_opt("dev-routes", param_array, &routes),
#endif
                   NULL))
                return command_param_failed();

        if (strlen(info->label->s) > inv_max_label_len) {
                return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
                                    "Label '%s' over %zu bytes", info->label->s, inv_max_label_len);
        }

        if (strlen(desc_val) > BOLT11_FIELD_BYTE_LIMIT && !*hashonly) {
                return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
                                    "Descriptions greater than %d bytes "
                                    "not yet supported "
                                    "(description length %zu)",
                                    BOLT11_FIELD_BYTE_LIMIT,
                                    strlen(desc_val));
        }

        if (fallbacks) {
                size_t i;
                const jsmntok_t *t;

                fallback_scripts = tal_arr(cmd, const u8 *, fallbacks->size);
                json_for_each_arr(i, t, fallbacks) {
                        struct command_result *r;

                        r = parse_fallback(cmd, buffer, t, &fallback_scripts[i]);
                        if (r)
                                return r;
                }
        }

        if (preimage)
                info->payment_preimage = *preimage;
        else
                /* Generate random secret preimage. */
                randombytes_buf(&info->payment_preimage,
                                sizeof(info->payment_preimage));
        /* Generate preimage hash. */
        sha256(&rhash, &info->payment_preimage, sizeof(info->payment_preimage));
        /* Generate payment secret. */
        invoice_secret(&info->payment_preimage, &payment_secret);

        info->b11 = new_bolt11(info, msatoshi_val);
        info->b11->chain = chainparams;
        info->b11->timestamp = time_now().ts.tv_sec;
        info->b11->payment_hash = rhash;
        info->b11->receiver_id = cmd->ld->id;
        info->b11->min_final_cltv_expiry = *cltv;
        info->b11->expiry = *expiry;
        info->b11->description = tal_steal(info->b11, desc_val);
        /* BOLT #11:
         * * `h` (23): `data_length` 52. 256-bit description of purpose of payment (SHA256).
         *...
         * A writer:
         *...
         *    - MUST include either exactly one `d` or exactly one `h` field.
         */
        if (*hashonly) {
                info->b11->description_hash = tal(info->b11, struct sha256);
                sha256(info->b11->description_hash, desc_val, strlen(desc_val));
        } else
                info->b11->description_hash = NULL;
        info->b11->payment_secret = tal_dup(info->b11, struct secret,
                                            &payment_secret);
        info->b11->features = tal_dup_talarr(info->b11, u8,
                                             cmd->ld->our_features
                                             ->bits[BOLT11_FEATURE]);

#if DEVELOPER
        info->b11->routes = unpack_routes(info->b11, buffer, routes);
#else
        info->b11->routes = NULL;
#endif
        if (fallback_scripts)
                info->b11->fallbacks = tal_steal(info->b11, fallback_scripts);

        /* We can't generate routehints without listincoming. */
        plugin = find_plugin_for_command(cmd->ld, "listincoming");
        if (!plugin) {
                return invoice_complete(info, true,
                                        false, false, false, false, false);
        }

        req = jsonrpc_request_start(info, "listincoming",
                                    cmd->id, plugin->non_numeric_ids,
                                    command_log(cmd),
                                    NULL, listincoming_done,
                                    info);
        jsonrpc_request_end(req);
        plugin_request_send(plugin, req);
        return command_still_pending(cmd);
}

static const struct json_command invoice_command = {
        "invoice",
        "payment",
        json_invoice,
        "Create an invoice for {msatoshi} with {label} "
        "and {description} with optional {expiry} seconds "
        "(default 1 week), optional {fallbacks} address list"
        "(default empty list) and optional {preimage} "
        "(default autogenerated)"};
AUTODATA(json_command, &invoice_command);

help

json_help in lightning:lightningd/jsonrpc.c

static struct command_result *json_help(struct command *cmd,
                                        const char *buffer,
                                        const jsmntok_t *obj UNNEEDED,
                                        const jsmntok_t *params)
{
        struct json_stream *response;
        const char *cmdname;
        struct json_command **commands;
        const struct json_command *one_cmd;

        if (!param(cmd, buffer, params,
                   p_opt("command", param_string, &cmdname),
                   NULL))
                return command_param_failed();

        commands = cmd->ld->jsonrpc->commands;
        if (cmdname) {
                one_cmd = find_command(commands, cmdname);
                if (!one_cmd)
                        return command_fail(cmd, JSONRPC2_METHOD_NOT_FOUND,
                                            "Unknown command %s",
                                            cmdname);
                if (!cmd->ld->deprecated_apis && one_cmd->deprecated)
                        return command_fail(cmd, JSONRPC2_METHOD_NOT_FOUND,
                                            "Deprecated command %s",
                                            cmdname);
        } else
                one_cmd = NULL;

        asort(commands, tal_count(commands), compare_commands_name, NULL);

        response = json_stream_success(cmd);
        json_array_start(response, "help");
        for (size_t i = 0; i < tal_count(commands); i++) {
                if (!one_cmd || one_cmd == commands[i])
                        json_add_help_command(cmd, response, commands[i]);
        }
        json_array_end(response);

        /* Tell cli this is simple enough to be formatted flat for humans */
        json_add_string(response, "format-hint", "simple");

        return command_success(cmd, response);
}

command, json_command, json_stream and jsonrpc structs

command in lightning:lightningd/jsonrpc.h

/* Context for a command (from JSON, but might outlive the connection!). */
/* FIXME: move definition into jsonrpc.c */
struct command {
        /* Off list jcon->commands */
        struct list_node list;
        /* The global state */
        struct lightningd *ld;
        /* The 'id' which we need to include in the response. */
        const char *id;
        /* If 'id' needs to be quoted (i.e. it's a string) */
        bool id_is_string;
        /* What command we're running (for logging) */
        const struct json_command *json_cmd;
        /* The connection, or NULL if it closed. */
        struct json_connection *jcon;
        /* Does this want notifications? */
        bool send_notifications;
        /* Have we been marked by command_still_pending?  For debugging... */
        bool pending;
        /* Tell param() how to process the command */
        enum command_mode mode;
        /* Have we started a json stream already?  For debugging. */
        struct json_stream *json_stream;
        /* Optional output field filter. */
        struct json_filter *filter;
};

json_command in lightning:lightningd/jsonrpc.h

struct json_command {
        const char *name;
        const char *category;
        struct command_result *(*dispatch)(struct command *,
                                           const char *buffer,
                                           const jsmntok_t *obj,
                                           const jsmntok_t *params);
        const char *description;
        bool deprecated;
        const char *verbose;
};

json_stream in lightning:common/json_stream.h

struct json_stream {
        struct json_out *jout;

        /* Who is writing to this buffer now; NULL if nobody is. */
        struct command *writer;

        /* Who is io_writing from this buffer now: NULL if nobody is. */
        struct io_conn *reader;
        struct io_plan *(*reader_cb)(struct io_conn *conn,
                                     struct json_stream *js,
                                     void *arg);
        void *reader_arg;
        size_t len_read;

        /* If non-NULL, reflects the current filter position */
        struct json_filter *filter;

        /* Where to log I/O */
        struct logger *log;
};

jsonrpc in lightning:lightningd/jsonrpc.c

/**
 * `jsonrpc` encapsulates the entire state of the JSON-RPC interface,
 * including a list of methods that the interface supports (can be
 * appended dynamically, e.g., for plugins, and logs. It also serves
 * as a convenient `tal`-parent for all JSON-RPC related allocations.
 */
struct jsonrpc {
        struct io_listener *rpc_listener;
        struct json_command **commands;

        /* Map from json command names to usage strings: we don't put this inside
         * struct json_command as it's good practice to have those const. */
        STRMAP(const char *) usagemap;
};

Command parameters

param in lightning:common/json_param.c

bool param(struct command *cmd, const char *buffer,
           const jsmntok_t tokens[], ...)
{
        struct param *params = tal_arr(tmpctx, struct param, 0);
        const char *name;
        va_list ap;
        bool allow_extra = false;

        va_start(ap, tokens);
        while ((name = va_arg(ap, const char *)) != NULL) {
                enum param_style style = va_arg(ap, enum param_style);
                param_cbx cbx = va_arg(ap, param_cbx);
                void *arg = va_arg(ap, void *);
                if (streq(name, "")) {
                        allow_extra = true;
                        continue;
                }
                param_add(&params, name, style, cbx, arg);
        }
        va_end(ap);

        if (command_usage_only(cmd)) {
                check_params(params);
                command_set_usage(cmd, param_usage(cmd, params));
                return false;
        }

        /* Always return false if we're simply checking command parameters;
         * normally this returns true if all parameters are valid. */
        return param_arr(cmd, buffer, tokens, params, allow_extra) == NULL
                && !command_check_only(cmd);
}

p_req in lightning:common/json_param.h

/*
 * Add a required parameter.
 */
#define p_req(name, cbx, arg)                                \
              name"",                                        \
              PARAM_REQUIRED,                                \
              (param_cbx)(cbx),                              \
              (arg) + 0*sizeof((cbx)((struct command *)NULL, \
                               (const char *)NULL,           \
                               (const char *)NULL,           \
                               (const jsmntok_t *)NULL,      \
                               (arg)) == (struct command_result *)NULL)

p_opt in lightning:common/json_param.h

/*
 * Add an optional parameter.  *arg is set to NULL if it isn't found.
 */
#define p_opt(name, cbx, arg)                                   \
              name"",                                           \
              PARAM_OPTIONAL,                                   \
              (param_cbx)(cbx),                                 \
              ({ *arg = NULL;                                   \
                 (arg) + 0*sizeof((cbx)((struct command *)NULL, \
                                  (const char *)NULL,           \
                                  (const char *)NULL,           \
                                  (const jsmntok_t *)NULL,      \
                                  (arg)) == (struct command_result *)NULL); })

param_positive_msat_or_any in lightning:lightningd/invoice.c

static struct command_result *param_positive_msat_or_any(struct command *cmd,
                                                         const char *name,
                                                         const char *buffer,
                                                         const jsmntok_t *tok,
                                                         struct amount_msat **msat)
{
        if (json_tok_streq(buffer, tok, "any")) {
                *msat = NULL;
                return NULL;
        }
        *msat = tal(cmd, struct amount_msat);
        if (parse_amount_msat(*msat, buffer + tok->start, tok->end - tok->start)
            && !amount_msat_eq(**msat, AMOUNT_MSAT(0)))
                return NULL;

        return command_fail_badparam(cmd, name, buffer, tok,
                                     "should be positive msat or 'any'");
}

param_msat in lightning:common/json_param.c

struct command_result *param_msat(struct command *cmd, const char *name,
                                  const char *buffer, const jsmntok_t *tok,
                                  struct amount_msat **msat)
{
        *msat = tal(cmd, struct amount_msat);
        if (parse_amount_msat(*msat, buffer + tok->start, tok->end - tok->start))
                return NULL;

        return command_fail_badparam(cmd, name, buffer, tok,
                                     "should be a millisatoshi amount");
}

param_escaped_string in lightning:common/json_param.c

struct command_result *param_escaped_string(struct command *cmd,
                                            const char *name,
                                            const char * buffer,
                                            const jsmntok_t *tok,
                                            const char **str)
{
        if (tok->type == JSMN_STRING) {
                struct json_escape *esc;
                /* jsmn always gives us ~ well-formed strings. */
                esc = json_escape_string_(cmd, buffer + tok->start,
                                          tok->end - tok->start);
                *str = json_escape_unescape(cmd, esc);
                if (*str)
                        return NULL;
        }
        return command_fail_badparam(cmd, name, buffer, tok,
                                     "should be a string (without \\u)");
}

command_param_failed in lightning:lightningd/jsonrpc.c

struct command_result *command_param_failed(void)
{
        return &param_failed;
}

json_stream_success and command_success

json_stream_success in lightning:lightningd/jsonrpc.c

struct json_stream *json_stream_success(struct command *cmd)
{
        struct json_stream *r = json_start(cmd);
        json_object_start(r, "result");

        /* We have results?  OK, start filtering */
        if (cmd->filter)
                json_stream_attach_filter(r, cmd->filter);
        return r;
}

command_success in lightning:lightningd/jsonrpc.c

struct command_result *command_success(struct command *cmd,
                                       struct json_stream *result)
{
        assert(cmd);
        assert(cmd->json_stream == result);

        /* Filter will get upset if we close "result" object it didn't
         * see! */
        if (cmd->filter) {
                const char *err = json_stream_detach_filter(tmpctx, result);
                if (err)
                        json_add_string(result, "warning_parameter_filter",
                                        err);
        }

        json_object_end(result);
        json_object_end(result);

        return command_raw_complete(cmd, result);
}

common/json_stream.c

json_add_node_id in lightning:common/json_stream.c

void json_add_node_id(struct json_stream *response,
                      const char *fieldname,
                      const struct node_id *id)
{
        json_add_hex(response, fieldname, id->k, sizeof(id->k));
}

json_add_amount_msat in lightning:common/json_stream.c

void json_add_amount_msat(struct json_stream *result,
                          const char *msatfieldname,
                          struct amount_msat msat)
{
        assert(strends(msatfieldname, "_msat"));
        json_add_u64(result, msatfieldname, msat.millisatoshis); /* Raw: low-level helper */
}

json_add_string in lightning:common/json_stream.c

void json_add_string(struct json_stream *js,
                     const char *fieldname,
                     const char *str TAKES)
{
        if (json_filter_ok(js->filter, fieldname))
                json_out_addstr(js->jout, fieldname, str);
        if (taken(str))
                tal_free(str);
}

json_object_start in lightning:common/json_stream.c

void json_object_start(struct json_stream *js, const char *fieldname)
{
        if (json_filter_down(&js->filter, fieldname))
                json_out_start(js->jout, fieldname, '{');
}

json_object_end in lightning:common/json_stream.c

void json_object_end(struct json_stream *js)
{
        if (json_filter_up(&js->filter))
                json_out_end(js->jout, '}');
}

json_array_start in lightning:common/json_stream.c

void json_array_start(struct json_stream *js, const char *fieldname)
{
        if (json_filter_down(&js->filter, fieldname))
                json_out_start(js->jout, fieldname, '[');
}

json_array_end in lightning:common/json_stream.c

void json_array_end(struct json_stream *js)
{
        if (json_filter_up(&js->filter))
                json_out_end(js->jout, ']');
}

AUTODATA

AUTODATA in lightning:common/autodata.h

/* This uses GCC's constructor attribute */
#define AUTODATA(name, ptr)                                               \
        static __attribute__((constructor)) NEEDED                        \
        void CPPMAGIC_GLUE2(register_one_##name,__COUNTER__)(void) {      \
                register_autotype_##name(ptr);                            \
        }

tal_count and streq in ccan

tal_count in ccan:ccan/tal/tal.h

/**
 * tal_count - get the count of objects in a tal object.
 * @ptr: The tal allocated object (or NULL)
 *
 * Returns 0 if @ptr is NULL.  Note that if the allocation was done as a
 * different type to @ptr, the result may not match the @count argument
 * (or implied 1) of that allocation!
 */
#define tal_count(p) (tal_bytelen(p) / sizeof(*p))

streq in ccan:ccan/str/str.h

/**
 * streq - Are two strings equal?
 * @a: first string
 * @b: first string
 *
 * This macro is arguably more readable than "!strcmp(a, b)".
 *
 * Example:
 *      if (streq(somestring, ""))
 *              printf("String is empty!\n");
 */
#define streq(a,b) (strcmp((a),(b)) == 0)

Source code

json_foo

/* clnlive #17 */

static struct command_result *json_foo(struct command *cmd,
                                       const char *buffer,
                                       const jsmntok_t *obj UNNEEDED,
                                       const jsmntok_t *params)
{
        struct json_stream *response;

        if (!param(cmd, buffer, params, NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_node_id(response, "id", &cmd->ld->id);
        /* json_object_start(response, "foo"); */
        /* json_add_string(response, "bar", "baz"); */
        /* json_object_end(response); */

        return command_success(cmd, response);
}

static const struct json_command foo_command = {
        "foo",
        "clnlive",
        json_foo,
        "foo clnlive #17"
};
AUTODATA(json_command, &foo_command);

json_my_invoice

/* clnlive #17 */

static struct command_result *json_my_invoice(struct command *cmd,
                                              const char *buffer,
                                              const jsmntok_t *obj UNNEEDED,
                                              const jsmntok_t *params)
{
        struct amount_msat *msatoshi_val;
        const char *desc_val;
        struct json_stream *response;

        if (!param(cmd, buffer, params,
                   p_req("amount_msat", param_msat, &msatoshi_val),
                   p_opt("description", param_escaped_string, &desc_val),
                   NULL))
                return command_param_failed();

        response = json_stream_success(cmd);
        json_add_amount_msat(response, "amount_msat", *msatoshi_val);
        if (desc_val)
                json_add_string(response, "description", desc_val);
        return command_success(cmd, response);
}

static const struct json_command my_invoice_command = {
        "my-invoice",
        "clnlive",
        json_my_invoice,
        "my-invoice clnlive #17"};
AUTODATA(json_command, &my_invoice_command);

json_my_commands

/* clnlive #17 */

static struct command_result *json_my_commands(struct command *cmd,
                                               const char *buffer,
                                               const jsmntok_t *obj UNNEEDED,
                                               const jsmntok_t *params)
{
        struct json_stream *response;
        struct json_command **commands;

        if (!param(cmd, buffer, params, NULL))
                return command_param_failed();

        commands = cmd->ld->jsonrpc->commands;

        response = json_stream_success(cmd);
        json_array_start(response, "commands");
        for (size_t i = 0; i < tal_count(commands); i++) {
                if (streq(commands[i]->category, "clnlive"))
                        json_add_string(response, NULL, commands[i]->name);
        }
        json_array_end(response);

        return command_success(cmd, response);
}

static const struct json_command my_commands_command = {
        "my-commands",
        "clnlive",
        json_my_commands,
        "foo clnlive #17"
};
AUTODATA(json_command, &my_commands_command);

clnlive-17.patch

From f96e4d67e9c6582023595a137687f78b4fdaf13a Mon Sep 17 00:00:00 2001
From: Tony Aldon <tony@tonyaldon.com>
Date: Fri, 24 Nov 2023 06:03:01 +0100
Subject: [PATCH] clnlive #17

---
 lightningd/invoice.c      | 31 +++++++++++++++++++++++++++++++
 lightningd/jsonrpc.c      | 35 +++++++++++++++++++++++++++++++++++
 lightningd/peer_control.c | 29 +++++++++++++++++++++++++++++
 3 files changed, 95 insertions(+)

diff --git a/lightningd/invoice.c b/lightningd/invoice.c
index cc7580a4f..f882ece98 100644
--- a/lightningd/invoice.c
+++ b/lightningd/invoice.c
@@ -1224,6 +1224,37 @@ static const struct json_command invoice_command = {
        "(default autogenerated)"};
 AUTODATA(json_command, &invoice_command);

+/* clnlive #17 */
+
+static struct command_result *json_my_invoice(struct command *cmd,
+                                             const char *buffer,
+                                             const jsmntok_t *obj UNNEEDED,
+                                             const jsmntok_t *params)
+{
+       struct amount_msat *msatoshi_val;
+       const char *desc_val;
+       struct json_stream *response;
+
+       if (!param(cmd, buffer, params,
+                  p_req("amount_msat", param_msat, &msatoshi_val),
+                  p_opt("description", param_escaped_string, &desc_val),
+                  NULL))
+               return command_param_failed();
+
+       response = json_stream_success(cmd);
+       json_add_amount_msat(response, "amount_msat", *msatoshi_val);
+       if (desc_val)
+               json_add_string(response, "description", desc_val);
+       return command_success(cmd, response);
+}
+
+static const struct json_command my_invoice_command = {
+       "my-invoice",
+       "clnlive",
+       json_my_invoice,
+       "my-invoice clnlive #17"};
+AUTODATA(json_command, &my_invoice_command);
+
 static void json_add_invoices(struct json_stream *response,
                              struct wallet *wallet,
                              const struct json_escape *label,
diff --git a/lightningd/jsonrpc.c b/lightningd/jsonrpc.c
index d3aa23b12..f1f8ae7de 100644
--- a/lightningd/jsonrpc.c
+++ b/lightningd/jsonrpc.c
@@ -176,6 +176,41 @@ static const struct json_command help_command = {
 };
 AUTODATA(json_command, &help_command);

+/* clnlive #17 */
+
+static struct command_result *json_my_commands(struct command *cmd,
+                                              const char *buffer,
+                                              const jsmntok_t *obj UNNEEDED,
+                                              const jsmntok_t *params)
+{
+       struct json_stream *response;
+       struct json_command **commands;
+
+       if (!param(cmd, buffer, params, NULL))
+               return command_param_failed();
+
+       commands = cmd->ld->jsonrpc->commands;
+
+       response = json_stream_success(cmd);
+       json_array_start(response, "commands");
+       for (size_t i = 0; i < tal_count(commands); i++) {
+               if (streq(commands[i]->category, "clnlive"))
+                       json_add_string(response, NULL, commands[i]->name);
+       }
+       json_array_end(response);
+
+
+       return command_success(cmd, response);
+}
+
+static const struct json_command my_commands_command = {
+       "my-commands",
+       "clnlive",
+       json_my_commands,
+       "foo clnlive #17"
+};
+AUTODATA(json_command, &my_commands_command);
+
 static struct command_result *json_stop(struct command *cmd,
                                        const char *buffer,
                                        const jsmntok_t *obj UNNEEDED,
diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c
index 9455d3e0a..b177e05a4 100644
--- a/lightningd/peer_control.c
+++ b/lightningd/peer_control.c
@@ -2552,6 +2552,35 @@ static const struct json_command getinfo_command = {
 };
 AUTODATA(json_command, &getinfo_command);

+/* clnlive #17 */
+
+static struct command_result *json_foo(struct command *cmd,
+                                      const char *buffer,
+                                      const jsmntok_t *obj UNNEEDED,
+                                      const jsmntok_t *params)
+{
+       struct json_stream *response;
+
+       if (!param(cmd, buffer, params, NULL))
+               return command_param_failed();
+
+       response = json_stream_success(cmd);
+       json_add_node_id(response, "id", &cmd->ld->id);
+       /* json_object_start(response, "foo"); */
+       /* json_add_string(response, "bar", "baz"); */
+       /* json_object_end(response); */
+
+       return command_success(cmd, response);
+}
+
+static const struct json_command foo_command = {
+       "foo",
+       "clnlive",
+       json_foo,
+       "foo clnlive #17"
+};
+AUTODATA(json_command, &foo_command);
+
 /* Wait for at least a specific blockheight, then return, or time out.  */
 struct waitblockheight_waiter {
        /* struct lightningd::waitblockheight_commands.  */
--
2.34.1

Resources