Introduction to commando and commando-rune

LIVE #9July 20, 2023

In this live we see how to create runes (restrictions tokens) with commando-rune command and how to use them with commando command in order to run authorized (by the rune) methods on a directly-connected peer. Then we write a nodejs cli app which runs the getinfo method on a directly-connected peer using lnmessage library. Finally, we write a nodejs cli app which creates invoices by running the invoice method on a directly-connected peer using lnmessage library. This gives use the opportunity to see how to add restrictions to runes.

Transcript with corrections and improvements

The commando RPC command lets us send messages to a directly-connected peer containing a request to run on the peer node.

The peer will allow us to run the method if it has provided us with a rune which allows it.

The peer uses the commando-rune RPC command to create such a rune (which is a base64 string).

Each rune contains a unique id (a number starting at 0), and can have restrictions inside it. Nobody can remove restrictions from a rune.

See rustyrussell/runes for more details.

Deprecated APIs (in the future maybe)?

l1 and l2

Start 2 Lightning nodes running on regtest

Let's start two Lightning nodes running on the Bitcoin regtest chain by sourcing the script lightning/contrib/startup_regtest.sh provided in CLN repository and by running the command start_ln:

◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
...
◉ tony@tony:~/clnlive:
$ start_ln
...

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

◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'

Connect l1 with l2

To let the node l2 use commando in order to run for instance the command getinfo on the node l1, both node l1 and l2 needs to be connected. We connect them using connect command provided by lightning/contrib/startup_regtest.sh script

◉ tony@tony:~/clnlive:
$ connect 1 2
{
   "id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "features": "08a0000a0269a2",
   "direction": "out",
   "address": {
      "type": "ipv4",
      "address": "127.0.0.1",
      "port": 7272
   }
}

and we list the peers of l1 node like this:

◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
   "peers": [
      {
         "id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:7272"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}

l1 creates an unrestricted rune

The node l1 creates an unrestricted rune using commando-rune like this (We can think of a rune as a token with restrictions that the node l1 can give to some application to get access to some commands of l1's node):

◉ tony@tony:~/clnlive:
$ l1-cli commando-rune
{
   "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
   "unique_id": "0",
   "warning_unrestricted_rune": "WARNING: This rune has no restrictions! Anyone who has access to this rune could drain funds from your node. Be careful when giving this to apps that you don't trust. Consider using the restrictions parameter to only allow access to specific rpc methods."
}

Note that as this rune has no restriction, if the node l1 gives it to a non trusted application, that application can do anything with the node, specifically, it can use the command withdraw to steal all the onchain funds of l1's node.

We can decode that rune using decode command:

◉ tony@tony:~/clnlive:
$ l1-cli -k decode string=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "type": "rune",
   "unique_id": "0",
   "string": "71355f48e7f7e28a066ba8005723a8ec697d48c4dac05a0e7f4c0f44df9b84c5:=0",
   "restrictions": [],
   "valid": true
}

l2 calls commando to getinfo on l1

As we are going to use commando command, let's write its signature first:

commando peer_id method [params] [rune]

Now, with l1's rune and its id

◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq -r .id
02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df

the node l2 can call the command commando in order to run the getinfo method on the node l1 like this:

◉ tony@tony:~/clnlive:
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df \getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
   "alias": "SLEEPYPHOTO",
   "color": "02139b",
   "num_peers": 1,
   "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.05.2",
   "blockheight": 1,
   "network": "regtest",
   "fees_collected_msat": 0,
   "lightning-dir": "/tmp/l1-regtest/regtest",
   "our_features": {
      "init": "08a0000a0269a2",
      "node": "88a0000a0269a2",
      "channel": "",
      "invoice": "02000002024100"
   }
}

l2 withdraws all l1's onchain funds

Let's use the command fund_nodes from lightning/contrib/startup_regtest.sh to fund l1 wallet and a channel from l1 to l2:

◉ tony@tony:~/clnlive:
$ fund_nodes
Mining into address bcrt1qefqgq474z47e0mx8kpcwhltwhsr3n0nesg49c9... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.

We can check the funds of the node l1 like this:

◉ tony@tony:~/clnlive:
$ l1-cli listfunds
{
   "outputs": [
      {
         "txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "output": 0,
         "amount_msat": 98999846000,
         "scriptpubkey": "00147d3dc7584ba33b8bf92c11668357e29977d99f21",
         "address": "bcrt1q057uwkzt5vach7fvz9ngx4lzn9man8epp8e5jc",
         "status": "confirmed",
         "blockheight": 103,
         "reserved": false
      }
   ],
   "channels": [
      {
         "peer_id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "state": "CHANNELD_NORMAL",
         "channel_id": "59e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1d",
         "short_channel_id": "103x1x1",
         "our_amount_msat": 1000000000,
         "amount_msat": 1000000000,
         "funding_txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "funding_output": 1
      }
   ]
}

Now, let's see how l2 can withdraw all l1's onchain funds using commando command, withdraw command and the unrestricted rune previously generated by l1.

First we need an address belonging to l2's wallet:

◉ tony@tony:~/clnlive:
$ l2-cli newaddr
{
   "bech32": "bcrt1qcsk9r09shnmwaeqxckwvgklkmsuwrmvt8r6euu"
}

Then we can run the following command which withdraw all the onchain fund of l1's node:

◉ tony@tony:~/clnlive:
$ l2-cli -k commando peer_id=02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df \method=withdraw \params='{"destination": "bcrt1qcsk9r09shnmwaeqxckwvgklkmsuwrmvt8r6euu", "satoshi": "all"}' \rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "tx": "020000000159e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1c0000000000fdffffff01b89de60500000000160014c42c51bcb0bcf6eee406c59cc45bf6dc38e1ed8b6c000000",
   "txid": "f5493851d1ee0bfe410d426c45850d26a41411dd524ee430389451d7f4b5b274",
   "psbt": "cHNidP8BAgQCAAAAAQMEbAAAAAEEAQEBBQEBAQYBAwH7BAIAAAAAAQDqAgAAAAABAZDi4Lh3qxCW2XZnhXIIdz1BF3lbYBtKlrklY2rTtfGfAAAAAAD9////Aiae5gUAAAAAFgAUfT3HWEujO4v5LBFmg1fimXfZnyFAQg8AAAAAACIAIE63d7Xc+sw1idoVUOMvIkeLz5SoRieGPZRwMnI0CJ2+AkcwRAIgWJvJVdV12C4Y2FusM7BVIfSSlJKR+ZKJdZ25GbynIfYCIBPApKs0nAENTd0H1p+5x9jtdkmnORx9rlaRoOlZGXiEASECBLOpXSa0gdYoygpFI2WqWorsuN6S1tpkorMHhv/mymdmAAAAAQEfJp7mBQAAAAAWABR9PcdYS6M7i/ksEWaDV+KZd9mfISICAkTdt1WBadiveioGL6SQAXheTDEWPYHoufQVkVtw4QxQRzBEAiBj+AcJ5+mmb0bnRmfmMzr9a9ecjq6IGRuI+SHUT4yongIgRVLsJ31TajY/mbWQxoZA+TDOl+htW4lsEt9P2bAVHDUBIgYCRN23VYFp2K96KgYvpJABeF5MMRY9gei59BWRW3DhDFAIfT3HWAAAAAABDiBZ4qiY/tavdeh5b1Ta79u76QcHH1Dog72UG7gDEONeHAEPBAAAAAABEAT9////AAEDCLid5gUAAAAAAQQWABTELFG8sLz27uQGxZzEW/bcOOHtiwz8CWxpZ2h0bmluZwQCAAEA"
}

As we are running the nodes on regtest, we need to mine a block to get that last transaction including in a block. We can do this like this:

◉ tony@tony:~/clnlive:
$ bt-cli -rpcwallet=default getnewaddress
bcrt1q8k3lut8j9nyh8wp7afwqzf88g6sdxtpdq5h0rc
◉ tony@tony:~/clnlive:
$ bt-cli generatetoaddress 1 bcrt1q8k3lut8j9nyh8wp7afwqzf88g6sdxtpdq5h0rc
[
  "0c06ae63b41ba69c5a5885424d4cc1e0c351ecdae1fdf71c968b0b0dd8f4c048"
]

Note that bt-cli is an alias for bitcoin-cli -regtest.

After l1 has retrieved the informations from bitcoind, we can check that l1's wallet is now empty:

◉ tony@tony:~/clnlive:
$ l1-cli listfunds
{
   "outputs": [],
   "channels": [
      {
         "peer_id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "state": "CHANNELD_NORMAL",
         "channel_id": "59e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1d",
         "short_channel_id": "103x1x1",
         "our_amount_msat": 1000000000,
         "amount_msat": 1000000000,
         "funding_txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "funding_output": 1
      }
   ]
}

Add restriction readonly to l1's unrestricted rune

Each new master rune is saved and can be listed with commando-listrunes command like this:

◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      }
   ]
}

We can add restrictions to runes using commando-rune command.

Let add the specific restriction readonly to the l1 unrestricted rune cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==:

◉ tony@tony:~/clnlive:
$ l1-cli -k commando-rune rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA== \restrictions=readonly
{
   "rune": "k6YhINmpUP99wRYmGA2x_gJjcDFhLDIzO3uU9bPu20E9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "0"
}

Note that this rune got the same unique id has its master rune.

By decoding that rune, we can see that this new rune authorized the methods:

  • starting by list like listpeers (method^list) but not listdatastore (method/listdatastore),

  • starting by get like getinfo (method^get),

  • and summary (method=summary):

◉ tony@tony:~/clnlive:
$ l1-cli decode k6YhINmpUP99wRYmGA2x_gJjcDFhLDIzO3uU9bPu20E9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl
{
   "type": "rune",
   "unique_id": "0",
   "string": "93a62120d9a950ff7dc11626180db1fe02637031612c32333b7b94f5b3eedb41:=0&method^list|method^get|method=summary&method/listdatastore",
   "restrictions": [
      {
         "alternatives": [
            "method^list",
            "method^get",
            "method=summary"
         ],
         "summary": "method (of command) starts with 'list' OR method (of command) starts with 'get' OR method (of command) equal to 'summary'"
      },
      {
         "alternatives": [
            "method/listdatastore"
         ],
         "summary": "method (of command) unequal to 'listdatastore'"
      }
   ],
   "valid": true
}

See lightning:doc/lightning-commando-rune.7.md for more information about the restriction format.

We don't do it, but if we fund again l1 wallet and try to withdraw all l1's onchain funds using l2 node and commando like we did before but with this readonly rune, it won't work.

l1 creates a new readonly rune

The restrictions to the runes can also be put at the creation of the master rune.

For instance, l1 can creates a new master rune with the readonly restriction like this:

◉ tony@tony:~/clnlive:
$ l1-cli commando-rune null readonly
{
   "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "1"
}

So far l1 node issued two master runes that we can list like this:

◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      },
      {
         "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
         "unique_id": "1",
         "restrictions": [
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "list",
                     "condition": "^",
                     "english": "method starts with list"
                  },
                  {
                     "fieldname": "method",
                     "value": "get",
                     "condition": "^",
                     "english": "method starts with get"
                  },
                  {
                     "fieldname": "method",
                     "value": "summary",
                     "condition": "=",
                     "english": "method equal to summary"
                  }
               ],
               "english": "method starts with list OR method starts with get OR method equal to summary"
            },
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "listdatastore",
                     "condition": "/",
                     "english": "method unequal to listdatastore"
                  }
               ],
               "english": "method unequal to listdatastore"
            }
         ],
         "restrictions_as_english": "method starts with list OR method starts with get OR method equal to summary AND method unequal to listdatastore"
      }
   ]
}

l1 blacklists the unrestricted rune

At some point, l1 may want to revoke some runes.

This is possible with the command commando-blacklist.

To do so l1 needs to use the unique id of the rune.

For instance, to revoke the unrestricted rune cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA== which got the unique id 0, l1 runs the following command:

◉ tony@tony:~/clnlive:
$ l1-cli commando-blacklist 0
{
   "blacklist": [
      {
         "start": 0,
         "end": 0
      }
   ]
}

Note that any rune that is a descendant of the master rune with unique id 0 will also be revokated.

We can check in the list of the runes that the rune with unique id 0 has now the field blacklisted set to true:

◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "blacklisted": true,
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      },
      {
         "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
         "unique_id": "1",
         "restrictions": [
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "list",
                     "condition": "^",
                     "english": "method starts with list"
                  },
                  {
                     "fieldname": "method",
                     "value": "get",
                     "condition": "^",
                     "english": "method starts with get"
                  },
                  {
                     "fieldname": "method",
                     "value": "summary",
                     "condition": "=",
                     "english": "method equal to summary"
                  }
               ],
               "english": "method starts with list OR method starts with get OR method equal to summary"
            },
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "listdatastore",
                     "condition": "/",
                     "english": "method unequal to listdatastore"
                  }
               ],
               "english": "method unequal to listdatastore"
            }
         ],
         "restrictions_as_english": "method starts with list OR method starts with get OR method equal to summary AND method unequal to listdatastore"
      }
   ]
}

If the node l2 try to call commando with that blacklisted rune as before, this is the error message it gets:

◉ tony@tony:~/clnlive:
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df \getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "code": 19537,
   "message": "Not authorized: Blacklisted rune"
}

Run getinfo method on the node l2 using lnmessage - getinfo.js

In this second part we write a nodejs cli app which runs the getinfo method on the node l2 (directly-connected peer) using lnmessage library.

The library lnmessage is able to connect to CLN nodes and to send commando requests to those nodes.

We use it to connect to the node l2. To do so we need the following informations about l2

◉ tony@tony:~/clnlive:
$ l2-cli getinfo | jq '.id, .binding'
"0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7"
[
  {
    "type": "ipv4",
    "address": "127.0.0.1",
    "port": 7272
  }
]

that we use to set the top variables NODE_ID, NODE_IP and NODE_PORT in getinfo.js file. Those variables are used to instantiate ln object with the class LnMessage. Finally, we can connect to the node l2 with ln.connect async method:

#!/usr/bin/env node

import LnMessage from 'lnmessage'
import net from 'net'

// node l2
const NODE_ID = '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7272'

const ln = new LnMessage({
  remoteNodePublicKey: NODE_ID,
  tcpSocket: new net.Socket(),
  ip: NODE_IP,
  port: NODE_PORT
})

await ln.connect()

Before we run that script, we install lnmessage

◉ tony@tony:~/clnlive:
$ npm i lnmessage

and add "type": "module" in package.json file:

{
  "type": "module",
  "dependencies": {
    "lnmessage": "^0.2.2"
  }
}

We also open another terminal where we define the alias l2-cli of the node l2 and we check that l2 has only one peer so far (being the node l1):

# TERMINAL 2
◉ tony@tony:~/clnlive:
$ alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/clnlive:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
         "connected": true,
         "num_channels": 1,
         "netaddr": [
            "127.0.0.1:50508"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}

Now in the terminal 1, we can run getinfo.js script and see that it hangs:

◉ tony@tony:~/clnlive:
$ ./getinfo.js
^C

This is normal because it is now connected to the node l2 and it is waiting to do something. Indeed, in the terminal 2 we can check that l2 has two peers one being getinfo.js:

# TERMINAL 2
◉ tony@tony:~/clnlive:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "0390d494b9b1f2b396ad79f7069b5230b6a7247c565926d9e6b077739d583c0855",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:48452"
         ],
         "features": "0000000000000000"
      },
      {
         "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
         "connected": true,
         "num_channels": 1,
         "netaddr": [
            "127.0.0.1:50508"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}

Now to use commando with lnmessage and run getinfo on the node l2, we need a rune created by l2 that allows getinfo method.

Let's create such a rune:

◉ tony@tony:~/clnlive:
$ l2-cli commando-rune null readonly
{
   "rune": "9xepUI9PBgd8dJhPUoPnelh2GPRL1TSD3-nf_jdGrzo9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "0"
}

Note that readonly runes allow more than the getinfo method.

Now we set the top variable RUNE with that rune. Then, after we connect to the node l2 we use ln.commando async method to run getinfo on the node l2 using that rune. Once done we print the information about l2. Finally we disconnect ourself and l2 with ln.disconnect method:

#!/usr/bin/env node

import LnMessage from 'lnmessage'
import net from 'net'

// node l2
const NODE_ID = '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7272'
// rune
const RUNE = '9xepUI9PBgd8dJhPUoPnelh2GPRL1TSD3-nf_jdGrzo9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl'

const ln = new LnMessage({
  remoteNodePublicKey: NODE_ID,
  tcpSocket: new net.Socket(),
  ip: NODE_IP,
  port: NODE_PORT
})

await ln.connect()

const getinfo = await ln.commando({
  method: 'getinfo',
  params: [],
  rune: RUNE
})

console.log(getinfo)

ln.disconnect()

Back to our terminal, we can run getinfo.js script and get the information about the node l2:

◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
  id: '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7',
  alias: 'VIOLETBOUNCE',
  color: '0365a1',
  num_peers: 2,
  num_pending_channels: 0,
  num_active_channels: 1,
  num_inactive_channels: 0,
  address: [],
  binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7272 } ],
  version: 'v23.05.2',
  blockheight: 109,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}

Really cool!

Let's continue.

Creates invoices using lnmessage - invoice.js

In this part, we write a nodejs cli app which creates invoices by running the invoice method on the node l2 (directly-connected peer) using lnmessage library.

We can represent it using this diagram:

            pays invoices created
               by `invoice.js`
┌─────────┐                       ┌─────────┐
│ node l1 │<--------------------->│ node l2 │
└─────────┘                       └─────────┘
                                       ▲
                                       │ - connects using `lnmessage`
                                       │ - creates invoices via `commando`
                                ┌──────────────┐
                                │ ./invoice.js │
                                └──────────────┘

Specifically, we want invoice.js script to take two arguments, the amount of the invoice in msat and that the description of the invoice, such that we can create an invoice on the node l2 like this

◉ tony@tony:~/clnlive/:
$ ./invoice.js 10000 pizza
{..., bolt11: 'lnbcrt100n1...tll8phrt6j4atqqfv9p3u', ...}

and the node l1 can pay it like this:

◉ tony@tony:~/clnlive/:
$ l1-cli pay lnbcrt100n1...tll8phrt6j4atqqfv9p3u
...

Writing invoice.js script gives the opportunity to see how to add restrictions to a master rune.

Specifically, we will create a master rune that allows only the invoice method and then we will restrict that rune such that it allows only 5 invoice request per minute.

So with that last rune, we will be able to create only 5 invoices per minutes using invoice.js.

Let's copy getinfo.js file into the file invoice.js wrapping the commando call into an if statement and keeping the readonly rune created previously by l2:

#!/usr/bin/env node

import LnMessage from 'lnmessage'
import net from 'net'

// node l2
const NODE_ID = '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7272'
// rune
const RUNE = '9xepUI9PBgd8dJhPUoPnelh2GPRL1TSD3-nf_jdGrzo9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl'

const ln = new LnMessage({
  remoteNodePublicKey: NODE_ID,
  tcpSocket: new net.Socket(),
  ip: NODE_IP,
  port: NODE_PORT
})

const connected = await ln.connect()

if (connected) {
  const getinfo = await ln.commando({
    method: 'getinfo',
    params: [],
    rune: RUNE
  })
  console.log(getinfo)
}

ln.disconnect()

In our terminal we check that we didn't introduce errors and invoice.js returns l2's information:

◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  id: '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7',
  alias: 'VIOLETBOUNCE',
  color: '0365a1',
  num_peers: 2,
  num_pending_channels: 0,
  num_active_channels: 1,
  num_inactive_channels: 0,
  address: [],
  binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7272 } ],
  version: 'v23.05.2',
  blockheight: 109,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}

Now let the node l2 creates a new rune that allows only the method invoice:

◉ tony@tony:~/clnlive:
$ l2-cli commando-rune null '[["method=invoice"]]'
{
   "rune": "KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ==",
   "unique_id": "2"
}

Let's decode it to be sure that the restriction has been applied to the rune:

◉ tony@tony:~/clnlive:
$ l2-cli -k decode string=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ==
{
   "type": "rune",
   "unique_id": "2",
   "string": "2a0c46620e2e4f374733f500ca8be315f7fbab55b9e5f6275fe0aea3c4bd7692:=2&method=invoice",
   "restrictions": [
      {
         "alternatives": [
            "method=invoice"
         ],
         "summary": "method (of command) equal to 'invoice'"
      }
   ],
   "valid": true
}

In invoice.js we replace the rune with this new created one that allows only invoice method

...
// rune only `invoice`
const RUNE = 'KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ=='
...

and by running invoice.js script which try to run getinfo method on the node l2 we get the following Not authorized... error:

◉ tony@tony:~/clnlive:
$ ./invoice.js

node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^
{
  code: 19537,
  message: 'Not authorized: method is not equal to invoice'
}

Let's modify our if statement in invoice.js such that we do an invoice on l2 using commando with an amount of 10000 msat, the label being inv-0 and the description being Description:

...
if (connected) {
  const invoice = await ln.commando({
    method: 'invoice',
    params: {
      amount_msat: '10000',
      label: 'inv-0',
      description: 'Description'
    },
    rune: RUNE
  })
  console.log(invoice)
}
...

In our terminal we are now able to create an invoice on the node l2:

◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: '00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5',
  expires_at: 1690470883,
  bolt11: 'lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9',
  payment_secret: '3915ea5fb71c2ce27ddd3ef62df9b3526713815feedaa7010d42d354bd73fa14',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}

We can check that this invoice is indeed listed on the node l2

◉ tony@tony:~/clnlive:
$ l2-cli listinvoices
{
   "invoices": [
      {
         "label": "inv-0",
         "bolt11": "lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9",
         "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
         "amount_msat": 10000,
         "status": "unpaid",
         "description": "Description",
         "expires_at": 1690470883
      }
   ]
}

and let the node l1 pays that invoice:

◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9
{
   "destination": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
   "created_at": 1689866126.410,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "ad3f0d115c17947cf84e439f820b2e3cba3cf0ba9fa263fba2c031c522158e1b",
   "status": "complete"
}

COOL!

Let's make the label of the invoice unique (making it random is enough in our case to insure uniqueness) such that we can run multiple time invoice.js and create many invoices. In some way we want to spam l2.

...
if (connected) {
  const invoice = await ln.commando({
    method: 'invoice',
    params: {
      amount_msat: '10000',
      label: `inv-${Math.random()}`,
      description: 'Description'
    },
    rune: RUNE
  })
  console.log(invoice)
}
...

Back to our terminal, we can generate a new invoice on the node l2 with a random label:

◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: 'b11bb5daaf8b00c7eecf5d895861340182aaf1fcc96c27e366443e9e86e88780',
  expires_at: 1690471023,
  bolt11: 'lnbcrt100n1pjtjnl0sp5845wqv8xnfec6stlyzlswzqe5rhfetqd2kdvh6mdls3pgtltmedqpp5kydmtk403vqv0mk0tky4scf5qxp24u0ue9kz0cmxgslfaphgs7qqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqzu9wdpeqcm3qpyj25r4f6x0zk8rlmwtkrmm48ahykrm0tja456csawsc2enc2wanne4537vl07kkswsudp7e46hntupures7ssn6axqpuptyn9',
  payment_secret: '3d68e030e69a738d417f20bf070819a0ee9cac0d559acbeb6dfc22142febde5a',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ l2-cli listinvoices
{
   "invoices": [
      {
         "label": "inv-0",
         "bolt11": "lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9",
         "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
         "amount_msat": 10000,
         "status": "paid",
         "pay_index": 1,
         "amount_received_msat": 10000,
         "paid_at": 1689866127,
         "payment_preimage": "ad3f0d115c17947cf84e439f820b2e3cba3cf0ba9fa263fba2c031c522158e1b",
         "description": "Description",
         "expires_at": 1690470883
      },
      {
         "label": "inv-0.28556700488076636",
         "bolt11": "lnbcrt100n1pjtjnl0sp5845wqv8xnfec6stlyzlswzqe5rhfetqd2kdvh6mdls3pgtltmedqpp5kydmtk403vqv0mk0tky4scf5qxp24u0ue9kz0cmxgslfaphgs7qqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqzu9wdpeqcm3qpyj25r4f6x0zk8rlmwtkrmm48ahykrm0tja456csawsc2enc2wanne4537vl07kkswsudp7e46hntupures7ssn6axqpuptyn9",
         "payment_hash": "b11bb5daaf8b00c7eecf5d895861340182aaf1fcc96c27e366443e9e86e88780",
         "amount_msat": 10000,
         "status": "unpaid",
         "description": "Description",
         "expires_at": 1690471023
      }
   ]
}

As we have no rate restriction encoding in the rune we are using, we can run invoice.js as many time as we want. This can be done like this:

◉ tony@tony:~/clnlive:
$ for i in {1..5}; do ./invoice.js; done
{...,bolt11: 'lnbcrt100n1...4atcvkkxcpsqkva3',...}
{...,bolt11: 'lnbcrt100n1...h87s0p4hgpfkt2na',...}
{...,bolt11: 'lnbcrt100n1...swncp7awqpsm4ahl',...}
{...,bolt11: 'lnbcrt100n1...hecz2puacqtn57u4',...}
{...,bolt11: 'lnbcrt100n1...e0pp9uf8cp8f3f9c',...}

And we can do it again:

◉ tony@tony:~/clnlive:
$ for i in {1..5}; do ./invoice.js; done
{...,bolt11: 'lnbcrt100n1...dn3zcpwuspm0uuy3',...}
{...,bolt11: 'lnbcrt100n1...fx95vsnugqtgjgtc',...}
{...,bolt11: 'lnbcrt100n1...0fafe2wtqpp58e6t',...}
{...,bolt11: 'lnbcrt100n1...ufhm7lgfqp7mv9lp',...}
{...,bolt11: 'lnbcrt100n1...8jue4v26cpnyquxr',...}

As we want to protect the node l2 against spams, we create a new rune from the previous rune KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ== by adding a rate restriction. Specifically, we want to allow only 5 invoice requests per minute. We do this by running the following command:

◉ tony@tony:~/clnlive:
$ l2-cli -k commando-rune rune=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ== \restrictions='[["rate=5"]]'
{
   "rune": "wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU=",
   "unique_id": "2"
}

We can decode that new rune and check that the rate restriction has be taken into account:

◉ tony@tony:~/clnlive:
$ l2-cli -k decode string=wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU=
{
   "type": "rune",
   "unique_id": "2",
   "string": "c24b5402eb6d1168193603c79a587bf783952c19360cd2dbd01829cf27797ecd:=2&method=invoice&rate=5",
   "restrictions": [
      {
         "alternatives": [
            "method=invoice"
         ],
         "summary": "method (of command) equal to 'invoice'"
      },
      {
         "alternatives": [
            "rate=5"
         ],
         "summary": "rate (max per minute) equal to 5"
      }
   ],
   "valid": true
}

Let's use that new rune in invoice.js:

...
// rune only `invoice` and rate=5
const RUNE = 'wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU='
...

In our terminal we can create 5 invoices almost instantaneously

◉ tony@tony:~/clnlive:
$ for i in {1..5}; do ./invoice.js; done
{...,bolt11: 'lnbcrt100n1...rz530spgq7gxu5y',...}
{...,bolt11: 'lnbcrt100n1...tdye8qeqqccz55h',...}
{...,bolt11: 'lnbcrt100n1...2u8yt4yqqvqz8sr',...}
{...,bolt11: 'lnbcrt100n1...y7x7lvlqpky6sfc',...}
{...,bolt11: 'lnbcrt100n1...hszqn7mgpess5yn',...}

but can't create more due to the rate limite encoded in the new rune:

◉ tony@tony:~/clnlive:
$ ./invoice.js
node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^
{
  code: 19537,
  message: 'Not authorized: Rate of 5 per minute exceeded'
}

If we wait 1 minute, we can check that we can generate a new invoice again:

◉ tony@tony:~/clnlive:
$ ./invoice.js
{..., bolt11: 'lnbcrt100n1...axwqagcq4l5a07',...}

Runes are really powerful!

To end this demo, we want invoice.js script to take two positional arguments:

  1. the amount of the invoice in msat

  2. and the description of the invoice.

We can do this like this:

if (connected) {
  const  [amountMsat, description]  = process.argv.slice(2)
  const invoice = await ln.commando({
    method: 'invoice',
    params: {
      amount_msat: amountMsat,
      label: `inv-${Math.random()}`,
      description: description
    },
    rune: RUNE
  })
  console.log(invoice)
}

Now, we can create the following invoice of 10000 msat with the description pizza

◉ tony@tony:~/clnlive:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'c3b4f056c38f9beab43d3e9f28aa587e2bfa54114d442e87d0fa7d849cbd8a82',
  expires_at: 1690471534,
  bolt11: 'lnbcrt100n1pjtj50wsp55cdvjyqu58um03w3nhsmp7tupq7ntyl3hxesfr40dtcuwwycqw0qpp5cw60q4kr37d74dpa860j32jc0c4l54q3f4zzap7slf7cf89a32pqdqgwp5h57npxqyjw5qcqp29qxpqysgqkaf45jua4xqgn69p35urlgr6erj7ma0e580vewk4app6n954yy59e3r8amsnzpyfg8uatvezz4wl4ym8rumc6egyks2tqx9lfhqda9gpm2ghzd',
  payment_secret: 'a61ac9101ca1f9b7c5d19de1b0f97c083d3593f1b9b3048eaf6af1c73898039e',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}

and finally let the node l1 pay that pizza:

◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100n1pjtj50wsp55cdvjyqu58um03w3nhsmp7tupq7ntyl3hxesfr40dtcuwwycqw0qpp5cw60q4kr37d74dpa860j32jc0c4l54q3f4zzap7slf7cf89a32pqdqgwp5h57npxqyjw5qcqp29qxpqysgqkaf45jua4xqgn69p35urlgr6erj7ma0e580vewk4app6n954yy59e3r8amsnzpyfg8uatvezz4wl4ym8rumc6egyks2tqx9lfhqda9gpm2ghzd
{
   "destination": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "payment_hash": "c3b4f056c38f9beab43d3e9f28aa587e2bfa54114d442e87d0fa7d849cbd8a82",
   "created_at": 1689866776.403,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "68fe9b9a362b94a6c24c4fe9eb641e733cbf255f85939d93e6debe854c5a968a",
   "status": "complete"
}

We are done!

Terminal session

We ran the following commands in this order:

# TERMINAL 1
$ ls
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ connect 1 2
$ l1-cli listpeers
$ l1-cli commando-rune
$ l1-cli -k decode string=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
$ l1-cli getinfo | jq -r .id
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
$ fund_nodes
$ l1-cli listfunds
$ l2-cli newaddr
$ l2-cli -k commando peer_id=02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df method=withdraw params='{"destination": "bcrt1qcsk9r09shnmwaeqxckwvgklkmsuwrmvt8r6euu", "satoshi": "all"}' rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
$ bt-cli -rpcwallet=default getnewaddress
$ bt-cli generatetoaddress 1 bcrt1q8k3lut8j9nyh8wp7afwqzf88g6sdxtpdq5h0rc
$ l1-cli listfunds
$ l1-cli commando-listrunes
$ l1-cli -k commando-rune rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA== restrictions=readonly
$ l1-cli decode k6YhINmpUP99wRYmGA2x_gJjcDFhLDIzO3uU9bPu20E9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl
$ l1-cli commando-rune null readonly
$ l1-cli commando-listrunes
$ l1-cli commando-blacklist 0
$ l1-cli commando-listrunes
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
$ l2-cli getinfo | jq '.id, .binding'
$ alias l2-cli
$ npm i lnmessage
$ ./getinfo.js
$ l2-cli commando-rune null readonly
$ ./getinfo.js
$ ./invoice.js
$ l2-cli commando-rune null '[["method=invoice"]]'
$ l2-cli -k decode string=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ==
$ ./invoice.js
$ ./invoice.js
$ l2-cli listinvoices
$ l1-cli pay lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9
$ ./invoice.js
$ l2-cli listinvoices
$ for i in {1..5}; do ./invoice.js; done
$ for i in {1..5}; do ./invoice.js; done
$ l2-cli -k commando-rune rune=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ== restrictions='[["rate=5"]]'
$ l2-cli -k decode string=wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU=
$ for i in {1..5}; do ./invoice.js; done
$ ./invoice.js
$ ./invoice.js
$ ./invoice.js 10000 pizza
$ l1-cli pay lnbcrt100n1pjtj50wsp55cdvjyqu58um03w3nhsmp7tupq7ntyl3hxesfr40dtcuwwycqw0qpp5cw60q4kr37d74dpa860j32jc0c4l54q3f4zzap7slf7cf89a32pqdqgwp5h57npxqyjw5qcqp29qxpqysgqkaf45jua4xqgn69p35urlgr6erj7ma0e580vewk4app6n954yy59e3r8amsnzpyfg8uatvezz4wl4ym8rumc6egyks2tqx9lfhqda9gpm2ghzd

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

# TERMINAL 1
◉ tony@tony:~/clnlive:
$ ls
clnlive-scratch/  lightning/  getinfo.js  invoice.js  notes.org
◉ 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.
[1] 598681
[2] 598719
WARNING: eatmydata not found: instal 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:
$ connect 1 2
{
   "id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "features": "08a0000a0269a2",
   "direction": "out",
   "address": {
      "type": "ipv4",
      "address": "127.0.0.1",
      "port": 7272
   }
}
◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
   "peers": [
      {
         "id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:7272"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-rune
{
   "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
   "unique_id": "0",
   "warning_unrestricted_rune": "WARNING: This rune has no restrictions! Anyone who has access to this rune could drain funds from your node. Be careful when giving this to apps that you don't trust. Consider using the restrictions parameter to only allow access to specific rpc methods."
}
◉ tony@tony:~/clnlive:
$ l1-cli -k decode string=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "type": "rune",
   "unique_id": "0",
   "string": "71355f48e7f7e28a066ba8005723a8ec697d48c4dac05a0e7f4c0f44df9b84c5:=0",
   "restrictions": [],
   "valid": true
}
◉ tony@tony:~/clnlive:
$ l1-cli getinfo | jq -r .id
02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df
◉ tony@tony:~/clnlive:
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
   "alias": "SLEEPYPHOTO",
   "color": "02139b",
   "num_peers": 1,
   "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.05.2",
   "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:
$ fund_nodes
Mining into address bcrt1qefqgq474z47e0mx8kpcwhltwhsr3n0nesg49c9... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.
◉ tony@tony:~/clnlive:
$ l1-cli listfunds
{
   "outputs": [
      {
         "txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "output": 0,
         "amount_msat": 98999846000,
         "scriptpubkey": "00147d3dc7584ba33b8bf92c11668357e29977d99f21",
         "address": "bcrt1q057uwkzt5vach7fvz9ngx4lzn9man8epp8e5jc",
         "status": "confirmed",
         "blockheight": 103,
         "reserved": false
      }
   ],
   "channels": [
      {
         "peer_id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "state": "CHANNELD_NORMAL",
         "channel_id": "59e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1d",
         "short_channel_id": "103x1x1",
         "our_amount_msat": 1000000000,
         "amount_msat": 1000000000,
         "funding_txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "funding_output": 1
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l2-cli newaddr
{
   "bech32": "bcrt1qcsk9r09shnmwaeqxckwvgklkmsuwrmvt8r6euu"
}
◉ tony@tony:~/clnlive:
$ l2-cli -k commando peer_id=02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df \method=withdraw \params='{"destination": "bcrt1qcsk9r09shnmwaeqxckwvgklkmsuwrmvt8r6euu", "satoshi": "all"}' \rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "tx": "020000000159e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1c0000000000fdffffff01b89de60500000000160014c42c51bcb0bcf6eee406c59cc45bf6dc38e1ed8b6c000000",
   "txid": "f5493851d1ee0bfe410d426c45850d26a41411dd524ee430389451d7f4b5b274",
   "psbt": "cHNidP8BAgQCAAAAAQMEbAAAAAEEAQEBBQEBAQYBAwH7BAIAAAAAAQDqAgAAAAABAZDi4Lh3qxCW2XZnhXIIdz1BF3lbYBtKlrklY2rTtfGfAAAAAAD9////Aiae5gUAAAAAFgAUfT3HWEujO4v5LBFmg1fimXfZnyFAQg8AAAAAACIAIE63d7Xc+sw1idoVUOMvIkeLz5SoRieGPZRwMnI0CJ2+AkcwRAIgWJvJVdV12C4Y2FusM7BVIfSSlJKR+ZKJdZ25GbynIfYCIBPApKs0nAENTd0H1p+5x9jtdkmnORx9rlaRoOlZGXiEASECBLOpXSa0gdYoygpFI2WqWorsuN6S1tpkorMHhv/mymdmAAAAAQEfJp7mBQAAAAAWABR9PcdYS6M7i/ksEWaDV+KZd9mfISICAkTdt1WBadiveioGL6SQAXheTDEWPYHoufQVkVtw4QxQRzBEAiBj+AcJ5+mmb0bnRmfmMzr9a9ecjq6IGRuI+SHUT4yongIgRVLsJ31TajY/mbWQxoZA+TDOl+htW4lsEt9P2bAVHDUBIgYCRN23VYFp2K96KgYvpJABeF5MMRY9gei59BWRW3DhDFAIfT3HWAAAAAABDiBZ4qiY/tavdeh5b1Ta79u76QcHH1Dog72UG7gDEONeHAEPBAAAAAABEAT9////AAEDCLid5gUAAAAAAQQWABTELFG8sLz27uQGxZzEW/bcOOHtiwz8CWxpZ2h0bmluZwQCAAEA"
}
◉ tony@tony:~/clnlive:
$ bt-cli -rpcwallet=default getnewaddress
bcrt1q8k3lut8j9nyh8wp7afwqzf88g6sdxtpdq5h0rc
◉ tony@tony:~/clnlive:
$ bt-cli generatetoaddress 1 bcrt1q8k3lut8j9nyh8wp7afwqzf88g6sdxtpdq5h0rc
[
  "0c06ae63b41ba69c5a5885424d4cc1e0c351ecdae1fdf71c968b0b0dd8f4c048"
]
◉ tony@tony:~/clnlive:
$ l1-cli listfunds
{
   "outputs": [],
   "channels": [
      {
         "peer_id": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
         "connected": true,
         "state": "CHANNELD_NORMAL",
         "channel_id": "59e2a898fed6af75e8796f54daefdbbbe907071f50e883bd941bb80310e35e1d",
         "short_channel_id": "103x1x1",
         "our_amount_msat": 1000000000,
         "amount_msat": 1000000000,
         "funding_txid": "1c5ee31003b81b94bd83e8501f0707e9bbdbefda546f79e875afd6fe98a8e259",
         "funding_output": 1
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli -k commando-rune rune=cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA== restrictions=readonly
{
   "rune": "k6YhINmpUP99wRYmGA2x_gJjcDFhLDIzO3uU9bPu20E9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "0"
}
◉ tony@tony:~/clnlive:
$ l1-cli decode k6YhINmpUP99wRYmGA2x_gJjcDFhLDIzO3uU9bPu20E9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl
{
   "type": "rune",
   "unique_id": "0",
   "string": "93a62120d9a950ff7dc11626180db1fe02637031612c32333b7b94f5b3eedb41:=0&method^list|method^get|method=summary&method/listdatastore",
   "restrictions": [
      {
         "alternatives": [
            "method^list",
            "method^get",
            "method=summary"
         ],
         "summary": "method (of command) starts with 'list' OR method (of command) starts with 'get' OR method (of command) equal to 'summary'"
      },
      {
         "alternatives": [
            "method/listdatastore"
         ],
         "summary": "method (of command) unequal to 'listdatastore'"
      }
   ],
   "valid": true
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-rune null readonly
{
   "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "1"
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      },
      {
         "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
         "unique_id": "1",
         "restrictions": [
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "list",
                     "condition": "^",
                     "english": "method starts with list"
                  },
                  {
                     "fieldname": "method",
                     "value": "get",
                     "condition": "^",
                     "english": "method starts with get"
                  },
                  {
                     "fieldname": "method",
                     "value": "summary",
                     "condition": "=",
                     "english": "method equal to summary"
                  }
               ],
               "english": "method starts with list OR method starts with get OR method equal to summary"
            },
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "listdatastore",
                     "condition": "/",
                     "english": "method unequal to listdatastore"
                  }
               ],
               "english": "method unequal to listdatastore"
            }
         ],
         "restrictions_as_english": "method starts with list OR method starts with get OR method equal to summary AND method unequal to listdatastore"
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-blacklist 0
{
   "blacklist": [
      {
         "start": 0,
         "end": 0
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli commando-listrunes
{
   "runes": [
      {
         "rune": "cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==",
         "blacklisted": true,
         "unique_id": "0",
         "restrictions": [],
         "restrictions_as_english": ""
      },
      {
         "rune": "JoQiAUQc0e-480qdA_7kkMPIICbO9jJYSAWprrrWXuI9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
         "unique_id": "1",
         "restrictions": [
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "list",
                     "condition": "^",
                     "english": "method starts with list"
                  },
                  {
                     "fieldname": "method",
                     "value": "get",
                     "condition": "^",
                     "english": "method starts with get"
                  },
                  {
                     "fieldname": "method",
                     "value": "summary",
                     "condition": "=",
                     "english": "method equal to summary"
                  }
               ],
               "english": "method starts with list OR method starts with get OR method equal to summary"
            },
            {
               "alternatives": [
                  {
                     "fieldname": "method",
                     "value": "listdatastore",
                     "condition": "/",
                     "english": "method unequal to listdatastore"
                  }
               ],
               "english": "method unequal to listdatastore"
            }
         ],
         "restrictions_as_english": "method starts with list OR method starts with get OR method equal to summary AND method unequal to listdatastore"
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l2-cli commando 02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df getinfo null cTVfSOf34ooGa6gAVyOo7Gl9SMTawFoOf0wPRN-bhMU9MA==
{
   "code": 19537,
   "message": "Not authorized: Blacklisted rune"
}
◉ tony@tony:~/clnlive:
$ l2-cli getinfo | jq '.id, .binding'
"0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7"
[
  {
    "type": "ipv4",
    "address": "127.0.0.1",
    "port": 7272
  }
]
◉ tony@tony:~/clnlive:
$ alias l2-cli
alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/clnlive:
$ npm i lnmessage

added 9 packages in 4s

5 packages are looking for funding
  run `npm fund` for details
◉ tony@tony:~/clnlive:
$ ./getinfo.js
^C
◉ tony@tony:~/clnlive:
$ l2-cli commando-rune null readonly
{
   "rune": "9xepUI9PBgd8dJhPUoPnelh2GPRL1TSD3-nf_jdGrzo9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
   "unique_id": "0"
}
◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
  id: '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7',
  alias: 'VIOLETBOUNCE',
  color: '0365a1',
  num_peers: 2,
  num_pending_channels: 0,
  num_active_channels: 1,
  num_inactive_channels: 0,
  address: [],
  binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7272 } ],
  version: 'v23.05.2',
  blockheight: 109,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  id: '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7',
  alias: 'VIOLETBOUNCE',
  color: '0365a1',
  num_peers: 2,
  num_pending_channels: 0,
  num_active_channels: 1,
  num_inactive_channels: 0,
  address: [],
  binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7272 } ],
  version: 'v23.05.2',
  blockheight: 109,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}
◉ tony@tony:~/clnlive:
$ l2-cli commando-rune null '[["method=invoice"]]'
{
   "rune": "KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ==",
   "unique_id": "2"
}
◉ tony@tony:~/clnlive:
$ l2-cli -k decode string=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ==
{
   "type": "rune",
   "unique_id": "2",
   "string": "2a0c46620e2e4f374733f500ca8be315f7fbab55b9e5f6275fe0aea3c4bd7692:=2&method=invoice",
   "restrictions": [
      {
         "alternatives": [
            "method=invoice"
         ],
         "summary": "method (of command) equal to 'invoice'"
      }
   ],
   "valid": true
}
◉ tony@tony:~/clnlive:
$ ./invoice.js

node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^
{
  code: 19537,
  message: 'Not authorized: method is not equal to invoice'
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: '00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5',
  expires_at: 1690470883,
  bolt11: 'lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9',
  payment_secret: '3915ea5fb71c2ce27ddd3ef62df9b3526713815feedaa7010d42d354bd73fa14',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ l2-cli listinvoices
{
   "invoices": [
      {
         "label": "inv-0",
         "bolt11": "lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9",
         "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
         "amount_msat": 10000,
         "status": "unpaid",
         "description": "Description",
         "expires_at": 1690470883
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9
{
   "destination": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
   "created_at": 1689866126.410,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "ad3f0d115c17947cf84e439f820b2e3cba3cf0ba9fa263fba2c031c522158e1b",
   "status": "complete"
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: 'b11bb5daaf8b00c7eecf5d895861340182aaf1fcc96c27e366443e9e86e88780',
  expires_at: 1690471023,
  bolt11: 'lnbcrt100n1pjtjnl0sp5845wqv8xnfec6stlyzlswzqe5rhfetqd2kdvh6mdls3pgtltmedqpp5kydmtk403vqv0mk0tky4scf5qxp24u0ue9kz0cmxgslfaphgs7qqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqzu9wdpeqcm3qpyj25r4f6x0zk8rlmwtkrmm48ahykrm0tja456csawsc2enc2wanne4537vl07kkswsudp7e46hntupures7ssn6axqpuptyn9',
  payment_secret: '3d68e030e69a738d417f20bf070819a0ee9cac0d559acbeb6dfc22142febde5a',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ l2-cli listinvoices
{
   "invoices": [
      {
         "label": "inv-0",
         "bolt11": "lnbcrt100n1pjtjnmrsp58y275hahrskwylwa8mmzm7dn2fn38q2lamd2wqgdgtf4f0tnlg2qpp5qpyr8qxsjra6r2wfw6yhje3vrt743afl0ta6nw907jmjv5e0ttjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm5mfzzjrdh5z9w9mmwl4nzhe49dd4rd956r2pw2m4pzdv28qjgen9dwa2hepkyu20lwkn47qsrjqg9uksu64v4h333prtn050lj2vwspvkxyn9",
         "payment_hash": "00483380d090fba1a9c9768979662c1afd58f53f7afba9b8aff4b726532f5ae5",
         "amount_msat": 10000,
         "status": "paid",
         "pay_index": 1,
         "amount_received_msat": 10000,
         "paid_at": 1689866127,
         "payment_preimage": "ad3f0d115c17947cf84e439f820b2e3cba3cf0ba9fa263fba2c031c522158e1b",
         "description": "Description",
         "expires_at": 1690470883
      },
      {
         "label": "inv-0.28556700488076636",
         "bolt11": "lnbcrt100n1pjtjnl0sp5845wqv8xnfec6stlyzlswzqe5rhfetqd2kdvh6mdls3pgtltmedqpp5kydmtk403vqv0mk0tky4scf5qxp24u0ue9kz0cmxgslfaphgs7qqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqzu9wdpeqcm3qpyj25r4f6x0zk8rlmwtkrmm48ahykrm0tja456csawsc2enc2wanne4537vl07kkswsudp7e46hntupures7ssn6axqpuptyn9",
         "payment_hash": "b11bb5daaf8b00c7eecf5d895861340182aaf1fcc96c27e366443e9e86e88780",
         "amount_msat": 10000,
         "status": "unpaid",
         "description": "Description",
         "expires_at": 1690471023
      }
   ]
}
◉ tony@tony:~/clnlive:
$ for i in {1..5}; do ./invoice.js; done
{
  payment_hash: '22d26f262d9661ea7d3a5b6de1770ec7e912e40d5f0325af461b0ae70aa2f2d6',
  expires_at: 1690471065,
  bolt11: 'lnbcrt100n1pjtj5qesp56p0n8jvf46gs6ykxcn66swcssfq39u6m72ads5qrllrvfg8erdsspp5ytfx7f3djes75lf6tdk7zacwcl539eqdtupjtt6xrv9wwz4z7ttqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqnnqgv8wpf0q7z5qe3a65ng3qc047mhm7m8276pscxjvw8lu8zqh8tc7pjnx62n9hu3lpnkvkg3ercuzw9g0jlm2qwneq3k4atcvkkxcpsqkva3',
  payment_secret: 'd05f33c989ae910d12c6c4f5a83b10824112f35bf2bad85003ffc6c4a0f91b61',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: 'f3210ca2c6312712bfafb3cead7096a0db1da39d9d8014d880727f23f84d1ae2',
  expires_at: 1690471066,
  bolt11: 'lnbcrt100n1pjtj5q6sp5h7xgtp3myzce5hne7n72ahmeufnklxnqwj5uxxgvw5kpx9uumcxqpp57vssegkxxyn390a0k0826uyk5rd3mguankqpfkyqwflj87zdrt3qdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq5zmur44mqqrakyyd0tnd2h8zjlat7cexutz0j4tsrla5rqpq79fkmnspkyzxy8gw74ge3clcv7jc53fgquf8psp93mv3vzh87s0p4hgpfkt2na',
  payment_secret: 'bf8c85863b20b19a5e79f4fcaedf79e2676f9a6074a9c3190c752c13179cde0c',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '16b01c1e7003c07bbc1ba1db080ec30a9fdbf2b3cc3ddec3e272e92ae133b78e',
  expires_at: 1690471066,
  bolt11: 'lnbcrt100n1pjtj5q6sp5q09zkznejwwc6advkwtn3x63ls7kvsu04v8ce9alxha7s2d6dx3qpp5z6cpc8nsq0q8h0qm58dssrkrp20ahu4nes7aaslzwt5j4cfnk78qdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq5shj6xgc36kydpnmzafkars3nxxmram2ndg7cpuhrqcw2y525pf9qeu0f08pdrd0hp7jzp3wp3lcrngpf0pwsw6c5valfmswncp7awqpsm4ahl',
  payment_secret: '03ca2b0a79939d8d75acb397389b51fc3d66438fab0f8c97bf35fbe829ba69a2',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '801cfb72c1ccec60d61c3c55501342c201dd96775536b498dfb23fe4757ca220',
  expires_at: 1690471067,
  bolt11: 'lnbcrt100n1pjtj5qmsp593mdwt4dx4s8ez3f344lwasj8suxw947qhvgvrzg7z9pvrj29g2spp5sqw0kukpenkxp4su8324qy6zcgqam9nh25mtfxxlkgl7gatu5gsqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq8s8gn49pe0uk2ln9l2wreqlmh4ea4ae4gr6h4tzvx2g2438hvempckm2jlq5c97v8zdn2phsv653wvg2v4k07gdhyvak4fhecz2puacqtn57u4',
  payment_secret: '2c76d72ead35607c8a298d6bf776123c386716be05d8860c48f08a160e4a2a15',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: 'dbfb3af0f1aa33932e6e5277132ab7892fa8fe714646db69f8ecc0e45ec769f2',
  expires_at: 1690471067,
  bolt11: 'lnbcrt100n1pjtj5qmsp5utuzs6667t0rjdcgtjc5wlj3j0qnn35zm9mzquylzg50q0eacwlqpp5m0an4u834geextnw2fm3x24h3yh63ln3gerdk60canqwghk8d8eqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqx6g88n6e60n8ayvva8aswfuumvrhsd9vpagsa3g5y8nwmulcsmmscf4zmz42vkdqft7kujzqytfr8qmexs9859fhm447nue0pp9uf8cp8f3f9c',
  payment_secret: 'e2f8286b5af2de3937085cb1477e5193c139c682d97620709f1228f03f3dc3be',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ for i in {1..5}; do ./invoice.js; done
{
  payment_hash: '6fd136adc408f1613a35a83730d483eb673d493f0c30f8e87ba75bd8e26b3dff',
  expires_at: 1690471072,
  bolt11: 'lnbcrt100n1pjtj5pqsp5fkq8lg80ytflncgy5v7taln25y2fddanxrkykha4vp7vfr87gggqpp5dlgndtwyprckzw344qmnp4yradnn6jflpsc036rm5ada3cnt8hlsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqwjrekw9nfk7pxrgdnxmaryxmrgp796k74lw8vdjngmfksd6q5vs3s8cnpd60mjz86rplxzzz2u7809sfywcf8f969y6aakdn3zcpwuspm0uuy3',
  payment_secret: '4d807fa0ef22d3f9e104a33cbefe6aa11496b7b330ec4b5fb5607cc48cfe4210',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '531733963c102db1432a5dedf665ed767368afe22cd93c0383385d5bf91fb534',
  expires_at: 1690471072,
  bolt11: 'lnbcrt100n1pjtj5pqsp5mk3waty734t4wwhalty00shk3etrt2fdqqz2w8xx2r94t58va2rqpp52vtn893uzqkmzse2thklve0dweek3tlz9nvncqur8pw4h7glk56qdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq4mmnccnf4mzhr0l0mdpuk45u7fnq259yr77lk9fcq6zzddl2lhvrqcj92p7z4des6z76a68sz532dap5hvdr0f92n7pu55fx95vsnugqtgjgtc',
  payment_secret: 'dda2eeac9e8d57573afdfac8f7c2f68e5635a92d0004a71cc650cb55d0ecea86',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '6bfc6395dfc8e0251fc1c0264ee95a441a0c2b0be2c6f52bb8b89ef2b4a0d3f9',
  expires_at: 1690471073,
  bolt11: 'lnbcrt100n1pjtj5ppsp582r4crj2vpecve8wqpfjhh0dvulmtyezq4w8503qzfqg3nksus5qpp5d07x89wlersz287pcqnya626gsdqc2ctutr022achz009d9q60usdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqf8ytfflgl84tjst3tjphjn9e8yck79fwzpc8h7l54v03yjec5vqn29ltyr08gmvpzpgg4kf2w0xm0j4f40c8rwh9y44l650fafe2wtqpp58e6t',
  payment_secret: '3a875c0e4a60738664ee00532bdded673fb59322055c7a3e20124088ced0e428',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: 'fa5468d5a9654cb397d366df3280d5c4a4dcebaa217c25cd5dfc66fd9be3c988',
  expires_at: 1690471073,
  bolt11: 'lnbcrt100n1pjtj5ppsp5v4hgmvpwkgzj2zl74xqml6p322t8vv9u4unfdqujl9dr0x058zgqpp5lf2x34dfv4xt897nvm0n9qx4cjjde6a2y97ztn2al3n0mxlrexyqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq55fh4yqykjk8z3mxamg92adnjmua386d72f9dd4qea2cmtwk0dzj9theyjk3npxxatnp7tesceu4dhzn4d630p4cq2f75yufhm7lgfqp7mv9lp',
  payment_secret: '656e8db02eb205250bfea981bfe83152967630bcaf26968392f95a3799f43890',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '39356e5fba55b1458bbdb92776e029684fd7a7b355962ca727b77adaa0e983a5',
  expires_at: 1690471074,
  bolt11: 'lnbcrt100n1pjtj5pzsp5y8tfpkwq5cljzjpd7537ktrpgchveqkd2wxclmaql3tmxrdvcxrqpp58y6kuha62kc5tzaahynhdcpfdp8a0fan2ktzefe8kaad4g8fswjsdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq9z9yuy0h9wlg7j48rucmr3yvt7eg9nh8uhv2k2gtjaahu8txenu44jn992e5gzpcrdj4km7aqyp400n2d5vh279hl56sm98jue4v26cpnyquxr',
  payment_secret: '21d690d9c0a63f21482df523eb2c61462ecc82cd538d8fefa0fc57b30dacc186',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ l2-cli -k commando-rune rune=KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ== restrictions='[["rate=5"]]'
{
   "rune": "wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU=",
   "unique_id": "2"
}
◉ tony@tony:~/clnlive:
$ l2-cli -k decode string=wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU=
{
   "type": "rune",
   "unique_id": "2",
   "string": "c24b5402eb6d1168193603c79a587bf783952c19360cd2dbd01829cf27797ecd:=2&method=invoice&rate=5",
   "restrictions": [
      {
         "alternatives": [
            "method=invoice"
         ],
         "summary": "method (of command) equal to 'invoice'"
      },
      {
         "alternatives": [
            "rate=5"
         ],
         "summary": "rate (max per minute) equal to 5"
      }
   ],
   "valid": true
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: '0dfc3229c67fc082f1f04dd9444ceab6133015b1665b12b3338f1824af6b8b50',
  expires_at: 1690471329,
  bolt11: 'lnbcrt100n1pjtj5fpsp5q2rkpw0q0yjhd4yc9f2he5anfz7u5eke4cryd3pyt3hv64kwx36qpp5ph7ry2wx0lqg9u0sfhv5gn82kcfnq9d3ved39ven3uvzftmt3dgqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqhtlr75n5ysy6f2x8v72w5fxd7a0932t4xehxnc4csmwyzhtsnrwrdcwhef5kukprztsvvmzjamgpdszdntum9wy53u0f6ytrz530spgq7gxu5y',
  payment_secret: '028760b9e0792576d4982a557cd3b348bdca66d9ae0646c4245c6ecd56ce3474',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ for i in {1..4}; do ./invoice.js; done
{
  payment_hash: 'fc85941b31d5ea4046a8b78d27b93b7e3d39267b70ecd35911ee0dc8b0fdf30b',
  expires_at: 1690471342,
  bolt11: 'lnbcrt100n1pjtj5fwsp5x2wl0u0ldsse82t0xul5f32ftn59pyh8gxm2lxptg7qvt7ufre4qpp5ljzegxe36h4yq34gk7xj0wfm0c7njfnmwrkdxkg3acxu3v8a7v9sdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq756gczk0rvseegze95yraykwnf95sq040wzh9r9n33r2qa2p5yujsdfpwzrhj4kuqjs5n7xu547cydhnchtqnjahfg8s8lutdye8qeqqccz55h',
  payment_secret: '329df7f1ff6c2193a96f373f44c5495ce85092e741b6af982b4780c5fb891e6a',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: '6652e61f380abf47473c7135cad782f938ef2ae90201ec28ff208f54944b5c21',
  expires_at: 1690471343,
  bolt11: 'lnbcrt100n1pjtj5f0sp5ykl4p09tmsgwqynresswltrshympynmqm55zqny5zv5vq7829dyspp5vefwv8ecp2l5w3euwy6u44uzlyuw72hfqgq7c28lyz84f9zttsssdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqfrx83erpp7vqhxf2dukr95r0amalzw5ljx8yj87wuxaqy9dwnyszusferelkj4lxj4nt8t07lfzg27fs5wct0t6q7uy2psr2u8yt4yqqvqz8sr',
  payment_secret: '25bf50bcabdc10e01263cc20efac70b936124f60dd28204c941328c078ea2b49',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: 'dcfb803fbd5c20ff1519e0b64c72055a760ac04eaf488e9748c494520af6fca8',
  expires_at: 1690471343,
  bolt11: 'lnbcrt100n1pjtj5f0sp5whh78aey955e26uf96gz8czajtal39jkk4nej7uv32n4ql3u5l2spp5mnacq0aatss079geuzmycus9tfmq4szw4ayga96gcj29yzhklj5qdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgq6lfdcd9gyzsx8tvntz5q5wyrdqrlkqunyer8q3u242lw5dk3h2y5ecfpup5mn4an9rk8yaghvlarhmfwtu2qfs488l3457ky7x7lvlqpky6sfc',
  payment_secret: '75efe3f7242d29956b892e9023e05d92fbf89656b567997b8c8aa7507e3ca7d5',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
{
  payment_hash: 'a621134f1bf1b1421a02bffbced8c465f41819de1f3aaadcc3b9413697e61b6e',
  expires_at: 1690471344,
  bolt11: 'lnbcrt100n1pjtj5fssp5dy6fkx0f3havnt5y533v4ttpqlkmnqt8xz9vzsk3dzyjtsejt4mqpp55cs3xncm7xc5yxszhlauakxyvh6psxw7rua24hxrh9qnd9lxrdhqdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqm6g4ylzuf26edpu3c8hcwucs5yj00wwszp5rqr07zekl8rm8468jjr3wsqfsu0uefn82xeuzgk6gemy20pre0z8ftxadpjzhszqn7mgpess5yn',
  payment_secret: '69349b19e98dfac9ae84a462caad6107edb98167308ac142d1688925c3325d76',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^
{
  code: 19537,
  message: 'Not authorized: Rate of 5 per minute exceeded'
}
◉ tony@tony:~/clnlive:
$ ./invoice.js
{
  payment_hash: '3f66fc5b60e9b07db2425c24983e035ff1a6a4dc4197bc8acf7d601a466237aa',
  expires_at: 1690471446,
  bolt11: 'lnbcrt100n1pjtj5vksp54e4vxc4fryn6mm6rv55dkgqd5jl0rdxw0lv87gfht3k9wmqwuwrspp58an0ckmqaxc8mvjztsjfs0srtlc6dfxugxtmezk004sp53nzx74qdqjg3jhxcmjd9c8g6t0dcxqyjw5qcqp29qxpqysgqgf09csj3r98g5u69hnghzau6qtls0uj3d3q737qfu6r6md76kmanq9lsj7ypn2jjqsphm4af2gzdlkff4zdva3dj2d37p3tuaxwqagcq4l5a07',
  payment_secret: 'ae6ac362a91927adef436528db200da4bef1b4ce7fd87f21375c6c576c0ee387',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'c3b4f056c38f9beab43d3e9f28aa587e2bfa54114d442e87d0fa7d849cbd8a82',
  expires_at: 1690471534,
  bolt11: 'lnbcrt100n1pjtj50wsp55cdvjyqu58um03w3nhsmp7tupq7ntyl3hxesfr40dtcuwwycqw0qpp5cw60q4kr37d74dpa860j32jc0c4l54q3f4zzap7slf7cf89a32pqdqgwp5h57npxqyjw5qcqp29qxpqysgqkaf45jua4xqgn69p35urlgr6erj7ma0e580vewk4app6n954yy59e3r8amsnzpyfg8uatvezz4wl4ym8rumc6egyks2tqx9lfhqda9gpm2ghzd',
  payment_secret: 'a61ac9101ca1f9b7c5d19de1b0f97c083d3593f1b9b3048eaf6af1c73898039e',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100n1pjtj50wsp55cdvjyqu58um03w3nhsmp7tupq7ntyl3hxesfr40dtcuwwycqw0qpp5cw60q4kr37d74dpa860j32jc0c4l54q3f4zzap7slf7cf89a32pqdqgwp5h57npxqyjw5qcqp29qxpqysgqkaf45jua4xqgn69p35urlgr6erj7ma0e580vewk4app6n954yy59e3r8amsnzpyfg8uatvezz4wl4ym8rumc6egyks2tqx9lfhqda9gpm2ghzd
{
   "destination": "0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7",
   "payment_hash": "c3b4f056c38f9beab43d3e9f28aa587e2bfa54114d442e87d0fa7d849cbd8a82",
   "created_at": 1689866776.403,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "68fe9b9a362b94a6c24c4fe9eb641e733cbf255f85939d93e6debe854c5a968a",
   "status": "complete"
}
# TERMINAL 2
◉ tony@tony:~/clnlive:
$ alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/clnlive:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
         "connected": true,
         "num_channels": 1,
         "netaddr": [
            "127.0.0.1:50508"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}
◉ tony@tony:~/clnlive:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "0390d494b9b1f2b396ad79f7069b5230b6a7247c565926d9e6b077739d583c0855",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:48452"
         ],
         "features": "0000000000000000"
      },
      {
         "id": "02139b8774bf4b02881154c1753e49cc6bb6632593b3c3839bc073159d02c672df",
         "connected": true,
         "num_channels": 1,
         "netaddr": [
            "127.0.0.1:50508"
         ],
         "features": "08a0000a0269a2"
      }
   ]
}

Source code

getinfo.js

#!/usr/bin/env node

import LnMessage from 'lnmessage'
import net from 'net'

// node l2
const NODE_ID = '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7272'
// rune
const RUNE = '9xepUI9PBgd8dJhPUoPnelh2GPRL1TSD3-nf_jdGrzo9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl'

const ln = new LnMessage({
  remoteNodePublicKey: NODE_ID,
  tcpSocket: new net.Socket(),
  ip: NODE_IP,
  port: NODE_PORT
})

await ln.connect()

const getinfo = await ln.commando({
  method: 'getinfo',
  params: [],
  rune: RUNE
})

console.log(getinfo)

ln.disconnect()

invoice.js

#!/usr/bin/env node

import LnMessage from 'lnmessage'
import net from 'net'

// node l2
const NODE_ID = '0365a17e5faee910000a822f74deb926bda01671debfe46bbc66dddd3c18ae8ad7'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7272'
// rune only `invoice`
// const RUNE = 'KgxGYg4uTzdHM_UAyovjFff7q1W55fYnX-Cuo8S9dpI9MiZtZXRob2Q9aW52b2ljZQ=='

// rune only `invoice` and rate=5
const RUNE = 'wktUAuttEWgZNgPHmlh794OVLBk2DNLb0Bgpzyd5fs09MiZtZXRob2Q9aW52b2ljZSZyYXRlPTU='

const ln = new LnMessage({
  remoteNodePublicKey: NODE_ID,
  tcpSocket: new net.Socket(),
  ip: NODE_IP,
  port: NODE_PORT
})

const connected = await ln.connect()

if (connected) {
  const  [amountMsat, description]  = process.argv.slice(2)
  const invoice = await ln.commando({
    method: 'invoice',
    params: {
      amount_msat: amountMsat,
      label: `inv-${Math.random()}`,
      description: description
    },
    rune: RUNE
  })
  console.log(invoice)
}

ln.disconnect()

package.json

{
  "type": "module",
  "dependencies": {
    "lnmessage": "^0.2.2"
  }
}

Resources