Get started with cln-grpc plugin

LIVE #15October 12, 2023

In this live we continue our tour about Core Lightning remote control with the builtin plugin cln-grpc. We write a Python application which generates invoices on our CLN node using the gRPC protocol.

Transcript with corrections and improvements

Over the past two months, we've been discussing how to remotely control a Core Lightning node.

First we presented the commando plugin. We saw how to control a CLN node with another CLN node using that plugin and the commando command.

Then we looked at lnmessage and lnsocket libraries that implement commando clients which can send commando messages to CLN nodes.

In v23.08, clnrest plugin which implements a REST interface to CLN node has been added. We did a demo of how to use that plugin in order to send HTTP requests to a CLN node to run JSON RPC commands on that node.

And today we are going to run JSON RPC commands in a remote node via the gRPC protocol using cln-grpc builtin plugin.

Note: I'm not a specialist of gRPC protocol, I know just enough to explain how to use it in the case of Core Lightning. So, maybe I won't use the right vocabulary, but anyway we are going to see how it works. If you think I'm doing something wrong, please correct me in the chat.

The goal of today is to write a Python script invoice.py that takes two arguments and generate a BOLT #11 invoice using cln-grpc plugin like this:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py 10000 pizza
lnbcrt100n1pjj0me...5zx6xh

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:
$ 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:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 968150
[2] 968193
WARNING: eatmydata not found: install it for faster testing
Commands:
        l1-cli, l1-log,
        l2-cli, l2-log,
        bt-cli, stop_ln, fund_nodes

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

◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-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:
$ l1-cli getinfo
{
   "id": "030da9d745d82472909034ab7caf11c101978f7d38e46487e69ae4e011ddfa5b78",
   "alias": "ANGRYAUTO",
   "color": "030da9",
   "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"
   }
}

Starting CLN node with cln-grpc plugin

Note that if we don't specify a port for cln-grpc plugin, the plugin is disable by default.

And in the previous script that we used, no port is specified, so cln-grpc plugin is not running on the node l1 nor in l2. We can check this by listing the running plugins like this

◉ tony@tony:~/clnlive:
$ l1-cli -F plugin list
command=list
plugins[0].name=/usr/local/libexec/c-lightning/plugins/autoclean
plugins[0].active=true
plugins[0].dynamic=false
plugins[1].name=/usr/local/libexec/c-lightning/plugins/chanbackup
plugins[1].active=true
plugins[1].dynamic=false
plugins[2].name=/usr/local/libexec/c-lightning/plugins/bcli
plugins[2].active=true
plugins[2].dynamic=false
plugins[3].name=/usr/local/libexec/c-lightning/plugins/commando
plugins[3].active=true
plugins[3].dynamic=false
plugins[4].name=/usr/local/libexec/c-lightning/plugins/funder
plugins[4].active=true
plugins[4].dynamic=true
plugins[5].name=/usr/local/libexec/c-lightning/plugins/topology
plugins[5].active=true
plugins[5].dynamic=false
plugins[6].name=/usr/local/libexec/c-lightning/plugins/keysend
plugins[6].active=true
plugins[6].dynamic=false
plugins[7].name=/usr/local/libexec/c-lightning/plugins/offers
plugins[7].active=true
plugins[7].dynamic=true
plugins[8].name=/usr/local/libexec/c-lightning/plugins/pay
plugins[8].active=true
plugins[8].dynamic=true
plugins[9].name=/usr/local/libexec/c-lightning/plugins/txprepare
plugins[9].active=true
plugins[9].dynamic=true
plugins[10].name=/usr/local/libexec/c-lightning/plugins/cln-renepay
plugins[10].active=true
plugins[10].dynamic=true
plugins[11].name=/usr/local/libexec/c-lightning/plugins/spenderp
plugins[11].active=true
plugins[11].dynamic=false
plugins[12].name=/usr/local/libexec/c-lightning/plugins/sql
plugins[12].active=true
plugins[12].dynamic=true
plugins[13].name=/usr/local/libexec/c-lightning/plugins/bookkeeper
plugins[13].active=true
plugins[13].dynamic=false

and filtering by grpc using rg utility like this

◉ tony@tony:~/clnlive:
$ l1-cli -F plugin list | rg grpc

which outputs nothing. So we know that cln-grpc is not running on l1 node.

Let's stop l1 node

◉ tony@tony:~/clnlive:
$ l1-cli stop
"Shutdown complete"

and start it again specifying a port in the option --grpc-port in order to have cln-grpc plugin running

◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --grpc-port=3030 --daemon

Now we verify that cln-grpc plugin is running like this:

◉ tony@tony:~/clnlive:
$ l1-cli -F plugin list | rg grpc
plugins[13].name=/usr/local/libexec/c-lightning/plugins/cln-grpc

The cln-grpc plugin is listening on port 3030 and we can check that using nc utility like this:

◉ tony@tony:~/clnlive:
$ nc -z -v 127.0.0.1 3030
Connection to 127.0.0.1 3030 port [tcp/*] succeeded!

Now, what we want to do is to connect to cln-grpc with a gRPC channel and send it.

  1. getinfo requests and then

  2. invoice requests.

Copy pem files

When we started l1 node with cln-grpc we also generated certificate files to authenticate and encrypt gRPC channels with our node:

◉ tony@tony:~/clnlive:
$ ls /tmp/l1-regtest/regtest/*.pem
/tmp/l1-regtest/regtest/ca-key.pem      /tmp/l1-regtest/regtest/client.pem
/tmp/l1-regtest/regtest/ca.pem          /tmp/l1-regtest/regtest/server-key.pem
/tmp/l1-regtest/regtest/client-key.pem  /tmp/l1-regtest/regtest/server.pem

To use those certificates in our Python script, we copy them in the current directory:

◉ tony@tony:~/clnlive:
$ cp /tmp/l1-regtest/regtest/client*.pem /tmp/l1-regtest/regtest/ca.pem .
◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  lightning/  ca.pem  client-key.pem  client.pem  invoice.py  notes.org

cln-grpc proto files

As far as I understand gRPC protocol, services are defined once in proto files and we use those proto files to derive library bindings for different languages.

Let's take a look to the proto files used by cln-grpc plugins which defined the CLN methods that can be used with that plugin:

◉ tony@tony:~/clnlive:
$ ls lightning/cln-grpc/proto/
node.proto  primitives.proto

cln-grpc/proto/node.proto

lightning:cln-grpc/proto/node.proto

...
service Node {
        rpc Getinfo(GetinfoRequest) returns (GetinfoResponse) {}
        rpc ListPeers(ListpeersRequest) returns (ListpeersResponse) {}
        rpc ListFunds(ListfundsRequest) returns (ListfundsResponse) {}
        rpc SendPay(SendpayRequest) returns (SendpayResponse) {}
        rpc ListChannels(ListchannelsRequest) returns (ListchannelsResponse) {}
        rpc AddGossip(AddgossipRequest) returns (AddgossipResponse) {}
        rpc AutoCleanInvoice(AutocleaninvoiceRequest) returns (AutocleaninvoiceResponse) {}
        rpc CheckMessage(CheckmessageRequest) returns (CheckmessageResponse) {}
        rpc Close(CloseRequest) returns (CloseResponse) {}
        rpc ConnectPeer(ConnectRequest) returns (ConnectResponse) {}
        rpc CreateInvoice(CreateinvoiceRequest) returns (CreateinvoiceResponse) {}
        rpc Datastore(DatastoreRequest) returns (DatastoreResponse) {}
        rpc CreateOnion(CreateonionRequest) returns (CreateonionResponse) {}
        rpc DelDatastore(DeldatastoreRequest) returns (DeldatastoreResponse) {}
        rpc DelExpiredInvoice(DelexpiredinvoiceRequest) returns (DelexpiredinvoiceResponse) {}
        rpc DelInvoice(DelinvoiceRequest) returns (DelinvoiceResponse) {}
        rpc Invoice(InvoiceRequest) returns (InvoiceResponse) {}
        rpc ListDatastore(ListdatastoreRequest) returns (ListdatastoreResponse) {}
        rpc ListInvoices(ListinvoicesRequest) returns (ListinvoicesResponse) {}
        rpc SendOnion(SendonionRequest) returns (SendonionResponse) {}
        rpc ListSendPays(ListsendpaysRequest) returns (ListsendpaysResponse) {}
        rpc ListTransactions(ListtransactionsRequest) returns (ListtransactionsResponse) {}
        rpc Pay(PayRequest) returns (PayResponse) {}
        rpc ListNodes(ListnodesRequest) returns (ListnodesResponse) {}
        rpc WaitAnyInvoice(WaitanyinvoiceRequest) returns (WaitanyinvoiceResponse) {}
        rpc WaitInvoice(WaitinvoiceRequest) returns (WaitinvoiceResponse) {}
        rpc WaitSendPay(WaitsendpayRequest) returns (WaitsendpayResponse) {}
        rpc NewAddr(NewaddrRequest) returns (NewaddrResponse) {}
        rpc Withdraw(WithdrawRequest) returns (WithdrawResponse) {}
        rpc KeySend(KeysendRequest) returns (KeysendResponse) {}
        rpc FundPsbt(FundpsbtRequest) returns (FundpsbtResponse) {}
        rpc SendPsbt(SendpsbtRequest) returns (SendpsbtResponse) {}
        rpc SignPsbt(SignpsbtRequest) returns (SignpsbtResponse) {}
        rpc UtxoPsbt(UtxopsbtRequest) returns (UtxopsbtResponse) {}
        rpc TxDiscard(TxdiscardRequest) returns (TxdiscardResponse) {}
        rpc TxPrepare(TxprepareRequest) returns (TxprepareResponse) {}
        rpc TxSend(TxsendRequest) returns (TxsendResponse) {}
        rpc ListPeerChannels(ListpeerchannelsRequest) returns (ListpeerchannelsResponse) {}
        rpc ListClosedChannels(ListclosedchannelsRequest) returns (ListclosedchannelsResponse) {}
        rpc DecodePay(DecodepayRequest) returns (DecodepayResponse) {}
        rpc Decode(DecodeRequest) returns (DecodeResponse) {}
        rpc Disconnect(DisconnectRequest) returns (DisconnectResponse) {}
        rpc Feerates(FeeratesRequest) returns (FeeratesResponse) {}
        rpc FundChannel(FundchannelRequest) returns (FundchannelResponse) {}
        rpc GetRoute(GetrouteRequest) returns (GetrouteResponse) {}
        rpc ListForwards(ListforwardsRequest) returns (ListforwardsResponse) {}
        rpc ListPays(ListpaysRequest) returns (ListpaysResponse) {}
        rpc ListHtlcs(ListhtlcsRequest) returns (ListhtlcsResponse) {}
        rpc Ping(PingRequest) returns (PingResponse) {}
        rpc SendCustomMsg(SendcustommsgRequest) returns (SendcustommsgResponse) {}
        rpc SetChannel(SetchannelRequest) returns (SetchannelResponse) {}
        rpc SignInvoice(SigninvoiceRequest) returns (SigninvoiceResponse) {}
        rpc SignMessage(SignmessageRequest) returns (SignmessageResponse) {}
        rpc Stop(StopRequest) returns (StopResponse) {}
        rpc PreApproveKeysend(PreapprovekeysendRequest) returns (PreapprovekeysendResponse) {}
        rpc PreApproveInvoice(PreapproveinvoiceRequest) returns (PreapproveinvoiceResponse) {}
        rpc StaticBackup(StaticbackupRequest) returns (StaticbackupResponse) {}
}

message GetinfoRequest {
}

message GetinfoResponse {
        bytes id = 1;
        optional string alias = 2;
        bytes color = 3;
        uint32 num_peers = 4;
        uint32 num_pending_channels = 5;
        uint32 num_active_channels = 6;
        uint32 num_inactive_channels = 7;
        string version = 8;
        string lightning_dir = 9;
        optional GetinfoOur_features our_features = 10;
        uint32 blockheight = 11;
        string network = 12;
        Amount fees_collected_msat = 13;
        repeated GetinfoAddress address = 14;
        repeated GetinfoBinding binding = 15;
        optional string warning_bitcoind_sync = 16;
        optional string warning_lightningd_sync = 17;
}
...
message InvoiceRequest {
        AmountOrAny amount_msat = 10;
        string description = 2;
        string label = 3;
        optional uint64 expiry = 7;
        repeated string fallbacks = 4;
        optional bytes preimage = 5;
        optional uint32 cltv = 6;
        optional bool deschashonly = 9;
}

message InvoiceResponse {
        string bolt11 = 1;
        bytes payment_hash = 2;
        bytes payment_secret = 3;
        uint64 expires_at = 4;
        optional uint64 created_index = 10;
        optional string warning_capacity = 5;
        optional string warning_offline = 6;
        optional string warning_deadends = 7;
        optional string warning_private_unused = 8;
        optional string warning_mpp = 9;
}
...

cln-grpc/proto/primitives.proto

lightning:cln-grpc/proto/primitives.proto

...
message Amount {
        uint64 msat = 1;
}
...
message AmountOrAny {
        oneof value {
                Amount amount = 1;
                bool any = 2;
        }
}
...

Generate gRPC bindings for Python with protoc

In that section with generate gRPC bindings for Python using protoc to which we pass the previous cln-grpc proto files as argument.

First we install grpcio-tools in a Python virtual environment:

◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install grpcio-tools
...

Then, from node.proto file we generate the Python files node_pb2_grpc.py and node_pb2.py using grpc_tools.protoc module:

(.venv) ◉ tony@tony:~/clnlive:
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/node.proto \  --python_out=. \  --grpc_python_out=. \  --experimental_allow_proto3_optional
(.venv) ◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  ca.pem          client.pem  node_pb2_grpc.py  notes.org
lightning/        client-key.pem  invoice.py  node_pb2.py

And similarly from primitives.proto file we generate the Python file primitives_pb2.py:

(.venv) ◉ tony@tony:~/clnlive:
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/primitives.proto \  --python_out=. \  --experimental_allow_proto3_optional
(.venv) ◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  ca.pem          client.pem  node_pb2_grpc.py  notes.org
lightning/        client-key.pem  invoice.py  node_pb2.py       primitives_pb2.py

Getinfo

Here is our invoice.py script which connects to l1 node via a gRPC channel importing node_pb2 module and NodeStub from node_pb2_grpc, defined by the Python bindings generated in the previous section and using the self-signed certificates generated by cln-grpc plugin.

#!/usr/bin/env python

from pathlib import Path
from node_pb2_grpc import NodeStub
import node_pb2
import grpc

p = Path(".")
cert_path = p / "client.pem"
key_path = p / "client-key.pem"
ca_cert_path = p / "ca.pem"

creds = grpc.ssl_channel_credentials(
    root_certificates=ca_cert_path.open("rb").read(),
    private_key=key_path.open("rb").read(),
    certificate_chain=cert_path.open("rb").read()
)

channel = grpc.secure_channel(
        "localhost:3030",
        creds,
        options=(("grpc.ssl_target_name_override", "cln"),)
)
stub = NodeStub(channel)

print(stub.Getinfo(node_pb2.GetinfoRequest()))

Now we can do a getinfo request on our node via a gRPC channel like this:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
id: "\003\r\251\327E\330$r\220\2204\253|\257\021\301\001\227\217}8\344d\207\346\232\344\340\021\335\372[x"
alias: "ANGRYAUTO"
color: "\003\r\251"
version: "v23.08.1"
lightning_dir: "/tmp/l1-regtest/regtest"
our_features {
  init: "\010\240\000\n\002i\242"
  node: "\210\240\000\n\002i\242"
  invoice: "\002\000\000\002\002A\000"
}
blockheight: 1
network: "regtest"
fees_collected_msat {
}
binding {
  item_type: IPV6
  address: "127.0.0.1"
  port: 7171
}

If we want we can print out only the alias of l1 node by modifying invoice.py script like this

...
print(stub.Getinfo(node_pb2.GetinfoRequest()).alias)

and running it this way:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
ANGRYAUTO

Invoice

Let's continue and modify invoice.py script such that we do an invoice request on l1 node via a gRPC channel and print out the response we get:

...
inv = stub.Invoice(node_pb2.InvoiceRequest(
    amount_msat=node_pb2.primitives__pb2.AmountOrAny(
        amount=node_pb2.primitives__pb2.Amount(
            msat=10000
        )),
    label="label",
    description="description"
))

print(inv)

Let's run that script:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
bolt11: "lnbcrt100n1pjjszzmsp5aqy5k8ka2fcqfkmavfk2v6z367jkt48ftmhg6hvt9hprdzt6je3spp59uqphe4e6zduvg4khfghyucgzt2f7fmpra73dx4c3a9kf5qn7psqdqjv3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqtv68pqzfy4htxdz9quhv2jzmwm8eujxsfaxql0n47td274ymjhpycyjn60hj4p688lh0t05jk6vhn4rukt4xttwpf5yx9d6duy09a0gqvyv9f5"
payment_hash: "/\000\033\346\271\320\233\306\"\266\272Qrs\010\022\324\237\'a\037}\026\232\270\217Kd\320\023\360`"
payment_secret: "\350\tK\036\335Rp\004\333}bl\246hQ\327\245e\324\351^\356\215]\213-\3026\211z\226c"
expires_at: 1697727195
created_index: 1
warning_capacity: "Insufficient incoming channel capacity to pay invoice"

If we just want to print the BOLT #11 invoice string, we can do this by printing the bolt11 property of the inv object like this:

...
inv = stub.Invoice(node_pb2.InvoiceRequest(
    amount_msat=node_pb2.primitives__pb2.AmountOrAny(
        amount=node_pb2.primitives__pb2.Amount(
            msat=10000
        )),
    label="label-1",
    description="description"
))

print(inv.bolt11)

Note that we also modified the label parameter to not conflict with the previous one already registered by l1 node.

Let's generate a BOLT #11 on l1 node like this:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
lnbcrt100n1pjjsz97sp58q5vkxahpxyrhxd560a4akc82xxmmf50rukdc9lpmtlyeq300f0qpp5f625gpfjn84tqhxmfxnz7v53rnu8kseyq3vzdnzpqjllsy40ng9qdqjv3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq6nglnq8wcr5vc7dszv2arhm42tar64zcrduf694u0jdtwy5vshx38r5fe6gpzcnljudp8vz2tw8tpvdpp9ynzek9csuxk08k24cjh4sqhmyz2f

Finally, let's modify our script such that it takes two arguments, the first one being the amount in msat and the second one being the invoice's description and it generates a random label for the invoice request:

...
inv = stub.Invoice(node_pb2.InvoiceRequest(
    amount_msat=node_pb2.primitives__pb2.AmountOrAny(
        amount=node_pb2.primitives__pb2.Amount(
            msat=int(sys.argv[1])
        )),
    label=f"label-{random.random()}",
    description=sys.argv[2]
))

print(inv.bolt11)

Here we are, we can generate a BOLT #11 invoice for a pizza which costs 10000msat connecting to l1 node via Grcp like this:

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py 10000 pizza
lnbcrt100n1pjjszfzsp5ztx99xmaxy8ggzlfkufa00pglefrrlqaxfmpzaeejg34ke07nxcspp5wn6pseckunnlhsymwap4zfvnw4y8v83syp95qycl46xkjq8y5ykqdqgwp5h57npxqyjw5qcqp29qxpqysgqxulngjvd3hrq3yt0ddw7436syff74zyaxvsd3xdwesx59gg0uvuppmcle3l2dxf79q2qpghv5z35kqj0fdx578745w60a8wken7ws6spyl69th

We are done!

Terminal session

We ran the following commands in this order:

$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli getinfo
$ l1-cli -F plugin list
$ l1-cli -F plugin list | rg grpc
$ l1-cli stop
$ lightningd --lightning-dir=/tmp/l1-regtest --grpc-port=3030 --daemon
$ l1-cli getinfo
$ l1-cli -F plugin list | rg grpc
$ nc -z -v 127.0.0.1 3030
$ ls /tmp/l1-regtest/regtest/*.pem
$ cp /tmp/l1-regtest/regtest/client*.pem /tmp/l1-regtest/regtest/ca.pem .
$ ls
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install grpcio-tools
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/node.proto \  --python_out=. \  --grpc_python_out=. \  --experimental_allow_proto3_optional
$ ls
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/primitives.proto \  --python_out=. \  --experimental_allow_proto3_optional
$ ls
$ ./invoice.py
$ ./invoice.py
$ ./invoice.py
$ ./invoice.py
$ ./invoice.py 10000 pizza

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

◉ tony@tony:~/clnlive:
$ 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:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 968150
[2] 968193
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:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
   "id": "030da9d745d82472909034ab7caf11c101978f7d38e46487e69ae4e011ddfa5b78",
   "alias": "ANGRYAUTO",
   "color": "030da9",
   "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:
$ l1-cli -F plugin list
command=list
plugins[0].name=/usr/local/libexec/c-lightning/plugins/autoclean
plugins[0].active=true
plugins[0].dynamic=false
plugins[1].name=/usr/local/libexec/c-lightning/plugins/chanbackup
plugins[1].active=true
plugins[1].dynamic=false
plugins[2].name=/usr/local/libexec/c-lightning/plugins/bcli
plugins[2].active=true
plugins[2].dynamic=false
plugins[3].name=/usr/local/libexec/c-lightning/plugins/commando
plugins[3].active=true
plugins[3].dynamic=false
plugins[4].name=/usr/local/libexec/c-lightning/plugins/funder
plugins[4].active=true
plugins[4].dynamic=true
plugins[5].name=/usr/local/libexec/c-lightning/plugins/topology
plugins[5].active=true
plugins[5].dynamic=false
plugins[6].name=/usr/local/libexec/c-lightning/plugins/keysend
plugins[6].active=true
plugins[6].dynamic=false
plugins[7].name=/usr/local/libexec/c-lightning/plugins/offers
plugins[7].active=true
plugins[7].dynamic=true
plugins[8].name=/usr/local/libexec/c-lightning/plugins/pay
plugins[8].active=true
plugins[8].dynamic=true
plugins[9].name=/usr/local/libexec/c-lightning/plugins/txprepare
plugins[9].active=true
plugins[9].dynamic=true
plugins[10].name=/usr/local/libexec/c-lightning/plugins/cln-renepay
plugins[10].active=true
plugins[10].dynamic=true
plugins[11].name=/usr/local/libexec/c-lightning/plugins/spenderp
plugins[11].active=true
plugins[11].dynamic=false
plugins[12].name=/usr/local/libexec/c-lightning/plugins/sql
plugins[12].active=true
plugins[12].dynamic=true
plugins[13].name=/usr/local/libexec/c-lightning/plugins/bookkeeper
plugins[13].active=true
plugins[13].dynamic=false
◉ tony@tony:~/clnlive:
$ l1-cli -F plugin list | rg grpc
◉ tony@tony:~/clnlive:
$ l1-cli stop
"Shutdown complete"
◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --grpc-port=3030 --daemon
[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:
$ l1-cli getinfo
{
   "id": "030da9d745d82472909034ab7caf11c101978f7d38e46487e69ae4e011ddfa5b78",
   "alias": "ANGRYAUTO",
   "color": "030da9",
   "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:
$ l1-cli -F plugin list | rg grpc
plugins[13].name=/usr/local/libexec/c-lightning/plugins/cln-grpc
◉ tony@tony:~/clnlive:
$ nc -z -v 127.0.0.1 3030
Connection to 127.0.0.1 3030 port [tcp/*] succeeded!
◉ tony@tony:~/clnlive:
$ ls /tmp/l1-regtest/regtest/*.pem
/tmp/l1-regtest/regtest/ca-key.pem      /tmp/l1-regtest/regtest/client.pem
/tmp/l1-regtest/regtest/ca.pem          /tmp/l1-regtest/regtest/server-key.pem
/tmp/l1-regtest/regtest/client-key.pem  /tmp/l1-regtest/regtest/server.pem
◉ tony@tony:~/clnlive:
$ cp /tmp/l1-regtest/regtest/client*.pem /tmp/l1-regtest/regtest/ca.pem .
◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  lightning/  ca.pem  client-key.pem  client.pem  invoice.py  notes.org
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install grpcio-tools
...
(.venv) ◉ tony@tony:~/clnlive:
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/node.proto \  --python_out=. \  --grpc_python_out=. \  --experimental_allow_proto3_optional
(.venv) ◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  ca.pem          client.pem  node_pb2_grpc.py  notes.org
lightning/        client-key.pem  invoice.py  node_pb2.py
(.venv) ◉ tony@tony:~/clnlive:
$ python -m grpc_tools.protoc \  -I lightning/cln-grpc/proto \  lightning/cln-grpc/proto/primitives.proto \  --python_out=. \  --experimental_allow_proto3_optional
(.venv) ◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  ca.pem          client.pem  node_pb2_grpc.py  notes.org
lightning/        client-key.pem  invoice.py  node_pb2.py       primitives_pb2.py
(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
id: "\003\r\251\327E\330$r\220\2204\253|\257\021\301\001\227\217}8\344d\207\346\232\344\340\021\335\372[x"
alias: "ANGRYAUTO"
color: "\003\r\251"
version: "v23.08.1"
lightning_dir: "/tmp/l1-regtest/regtest"
our_features {
  init: "\010\240\000\n\002i\242"
  node: "\210\240\000\n\002i\242"
  invoice: "\002\000\000\002\002A\000"
}
blockheight: 1
network: "regtest"
fees_collected_msat {
}
binding {
  item_type: IPV6
  address: "127.0.0.1"
  port: 7171
}

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
ANGRYAUTO
(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
bolt11: "lnbcrt100n1pjjszzmsp5aqy5k8ka2fcqfkmavfk2v6z367jkt48ftmhg6hvt9hprdzt6je3spp59uqphe4e6zduvg4khfghyucgzt2f7fmpra73dx4c3a9kf5qn7psqdqjv3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqtv68pqzfy4htxdz9quhv2jzmwm8eujxsfaxql0n47td274ymjhpycyjn60hj4p688lh0t05jk6vhn4rukt4xttwpf5yx9d6duy09a0gqvyv9f5"
payment_hash: "/\000\033\346\271\320\233\306\"\266\272Qrs\010\022\324\237\'a\037}\026\232\270\217Kd\320\023\360`"
payment_secret: "\350\tK\036\335Rp\004\333}bl\246hQ\327\245e\324\351^\356\215]\213-\3026\211z\226c"
expires_at: 1697727195
created_index: 1
warning_capacity: "Insufficient incoming channel capacity to pay invoice"

(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py
lnbcrt100n1pjjsz97sp58q5vkxahpxyrhxd560a4akc82xxmmf50rukdc9lpmtlyeq300f0qpp5f625gpfjn84tqhxmfxnz7v53rnu8kseyq3vzdnzpqjllsy40ng9qdqjv3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq6nglnq8wcr5vc7dszv2arhm42tar64zcrduf694u0jdtwy5vshx38r5fe6gpzcnljudp8vz2tw8tpvdpp9ynzek9csuxk08k24cjh4sqhmyz2f
(.venv) ◉ tony@tony:~/clnlive:
$ ./invoice.py 10000 pizza
lnbcrt100n1pjjszfzsp5ztx99xmaxy8ggzlfkufa00pglefrrlqaxfmpzaeejg34ke07nxcspp5wn6pseckunnlhsymwap4zfvnw4y8v83syp95qycl46xkjq8y5ykqdqgwp5h57npxqyjw5qcqp29qxpqysgqxulngjvd3hrq3yt0ddw7436syff74zyaxvsd3xdwesx59gg0uvuppmcle3l2dxf79q2qpghv5z35kqj0fdx578745w60a8wken7ws6spyl69th

Source code

invoice.py

#!/usr/bin/env python

from pathlib import Path
from node_pb2_grpc import NodeStub
import node_pb2
import grpc
import random
import sys

p = Path(".")
cert_path = p / "client.pem"
key_path = p / "client-key.pem"
ca_cert_path = p / "ca.pem"

creds = grpc.ssl_channel_credentials(
    root_certificates=ca_cert_path.open("rb").read(),
    private_key=key_path.open("rb").read(),
    certificate_chain=cert_path.open("rb").read()
)

channel = grpc.secure_channel(
        "localhost:3030",
        creds,
        options=(("grpc.ssl_target_name_override", "cln"),)
)
stub = NodeStub(channel)

# print(stub.Getinfo(node_pb2.GetinfoRequest()).alias)

inv = stub.Invoice(node_pb2.InvoiceRequest(
    amount_msat=node_pb2.primitives__pb2.AmountOrAny(
        amount=node_pb2.primitives__pb2.Amount(
            msat=int(sys.argv[1])
        )),
    label=f"label-{random.random()}",
    description=sys.argv[2]
))

print(inv.bolt11)

Resources