lnsocket

Rust · BOLT 8 · Core Lightning Commando

lnsocket.

Open an encrypted connection to any Lightning node, then call Core Lightning RPCs over it, from Rust or from your shell.

libcargo add lnsocket
clicargo install --git https://github.com/jb55/lnsocket-rs --features cli
lnrpc -n mynode
$ lnrpc -n mynodemynode> getinfo{  "id": "02a1d3…c3f9",  "alias": "mynode",  "num_active_channels": 9,  "blockheight": 921337,  …}mynode> listinvoices {"label": "coffee for two"}{  "invoices": [    { "label": "coffee for two", "status": "paid", … }  ]}mynode> nosuchmethoderror: nosuchmethod: Unknown command 'nosuchmethod' (code -32601)mynode> listfunds listpeerchannels… both results, sent together over the same socketmynode> 

One connection, many calls. Output is illustrative and trimmed.

lnrpc

Commando from your shell, one call or a whole session

lnrpc is a port of the C lnsocket tool of the same name. It calls Core Lightning RPCs over Commando using a rune, so you can reach your node from anywhere it's reachable on the Lightning port. No SSH, no lightning-cli on the box.

save a node

Name it once

Nodes live in ~/.config/lnrpc/config.toml, written with mode 0600 because it holds runes. The first one you add becomes the default.

$ lnrpc config add mynode 02a1…c3f9 ln.example.com:9735 <rune>
$ lnrpc config list
* mynode  02a1…c3f9@ln.example.com:9735
one-shot

Call it by name

Params are a JSON argument after the method. Pass several methods to send them concurrently over one connection, printed in order.

$ lnrpc getinfo
$ lnrpc -n othernode listinvoices '{"label":"foo"}'
$ lnrpc getinfo listfunds listpeerchannels
interactive

Leave out the method to get a session

Run lnrpc (or lnrpc -n othernode) with no method and it connects once, then reads method [params] lines at a mynode> prompt.

  • Type JSON as is. A word starting with { or [ runs to the end of its JSON value, so {"label": "coffee for two"} needs no shell quoting.
  • Line editing and history, kept in ~/.local/state/lnrpc/history.
  • Errors stay on their line. A bad method or bad JSON prints an error and the next prompt comes back.
  • Ctrl+C drops the line, or stops waiting on a slow call. Ctrl+D, exit or quit ends the session.
piped

Or script it

Lines from a pipe or a file run the same way without a prompt. Blank lines and # comments are skipped, and the exit status is non-zero if any call failed.

$ printf 'getinfo\nlistfunds\n' | lnrpc -n mynode

$ cat checks.txt
# morning check
listpeerchannels
listinvoices {"status": "unpaid"}
$ lnrpc -n mynode < checks.txt

A call is never sent twice. If the connection drops while a call is in flight, lnrpc doesn't resend it. It prints an error saying the call may or may not have run on the node, so a pay can't go out twice behind your back. A session reconnects for the next line.

the library

A Lightning socket you drive yourself

lnsocket is an async tokio library. It does the BOLT 8 Noise handshake and the init exchange, then reads and writes typed Lightning messages. CommandoClient runs Core Lightning RPCs over that same encrypted connection.

src/main.rs
use std::str::FromStr;

use bitcoin::secp256k1::{PublicKey, SecretKey, rand};
use lnsocket::{CommandoClient, LNSocket};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), lnsocket::Error> {
    let node = PublicKey::from_str("02...").expect("node id");
    let key = SecretKey::new(&mut rand::thread_rng());

    // BOLT 8 handshake and init, then Commando over the same socket
    let sock = LNSocket::connect_and_init(key, node, "ln.example.com:9735")
        .await?;
    let commando = CommandoClient::spawn(sock, "your-rune");

    let info = commando.call("getinfo", json!({})).await?;
    println!("{}", info["alias"]);
    Ok(())
}

LNSocket

Connects over TCP, runs the Noise_XK handshake from BOLT 8, and reads and writes typed BOLT 1 messages. Your own messages go through read_custom.

CommandoClient

A background task owns the socket and matches replies to calls by request id, so calls can run concurrently. It has timeouts, reconnects with backoff, and a retry policy per call (RetryPolicy::Never for anything that spends).

Tor

.onion addresses go through the SOCKS proxy at 127.0.0.1:9050 automatically, or one you choose with TorConfig.

Small on purpose

No TLS and no global state. You own your keys, your policies and your backpressure.