Create invoices with a Node.JS cli using lnmessage and commando

LNROOM #21August 18, 2023

In this video we write a Node.JS cli application that generates invoices by sending commando messages to a CLN node using lnmessage library.

Transcript with corrections and improvements

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

Start 2 Lightning nodes running on regtest

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

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

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

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

Install lnmessage

Let's install lnmessage:

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

Now in package.json we set type to module:

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

Connect to l2 with lnmessage

The beauty of lnmessage and commando plugin is that to get information from our node or to tell our node to do stuff we need to connect to the node. Once we are connected we use the capacity of nodes in the Lightning Network to send each other messages to send commando messages to our node with the right runes.

Let's see how we can connect ./invoice.js script to l2 node using lnmessage.

We need l2's node id, host and port. We get those informations by running the following command:

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

To connect to l2, in the invoice.js file, we instantiate the ln object with the class LnMessage to which we provide the previous information about the node l2 and also a tcp socket. Then, we call ln.connect() async method to connect to the node l2:

#!/usr/bin/env node

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

const ln = new LnMessage({
  remoteNodePublicKey: '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b',
  tcpSocket: new net.Socket(),
  ip: '127.0.0.1',
  port: 7272
})

const connected = await ln.connect()

Before we run that script, let's open another terminal in which we define the alias l2-cli and check that the node l2 has no peers:

# TERMINAL 2

◉ tony@tony:~/lnroom:
$ alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/lnroom:
$ l2-cli listpeers
{
   "peers": []
}

Now in the terminal 1, we run invoice.js script

# TERMINAL 1

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

and in the terminal 2 while ./invoice.js is running we check that l2 has a new peer (which is in fact ./invoice.js):

# TERMINAL 2

◉ tony@tony:~/lnroom:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "037b6daf05fa442584b4f6f47cc65d3740340c5a92944760364367f605756d16f2",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:60520"
         ],
         "features": "0000000000000000"
      }
   ]
}

Send a getinfo commando message to l2

Before we create invoices with ./invoice.js, let's send commando messages that ask the node l2 to run the getinfo command.

To do that we need a rune. Let's generate an unrestricted rune with commando-rune command:

◉ tony@tony:~/lnroom:
$ l2-cli commando-rune
{
   "rune": "k9KuYqPop1qtN4CmqFDVarwNrONhwc1MuIELzcUmmYw9MA==",
   "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."
}

Now we can modify our script and call ln.commando() method with the field method set to getinfo and the field rune set to the rune we got above:

#!/usr/bin/env node

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

const ln = new LnMessage({
  remoteNodePublicKey: '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b',
  tcpSocket: new net.Socket(),
  ip: '127.0.0.1',
  port: 7272
})

const connected = await ln.connect()

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

Back to our terminal we can get information about our the node l2 by running our script:

◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  id: '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b',
  alias: 'SLIMYTRAWL',
  color: '03ab4b',
  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: 7272 } ],
  version: 'v23.05.2',
  blockheight: 1,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}

Restrict the rune to only authorize the invoice method

As we are writing a command line application which only generates invoices, we don't want to use an unrestricted rune.

To generate a new rune restricted to the method invoice from the previous master rune, we run the following command:

◉ tony@tony:~/lnroom:
$ l1-cli -k commando-rune rune=k9KuYqPop1qtN4CmqFDVarwNrONhwc1MuIELzcUmmYw9MA== \restrictions='[["method=invoice"]]'
{
   "rune": "TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ==",
   "unique_id": "0"
}

Now, we replace in invoice.js the unrestricted run by this new rune:

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

This leads us to the error Not authorized: method is not equal to invoice when we try to run our script

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

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

Node.js v18.17.0

which is normal because we send a commando message that tries to run the command getinfo on the node l2 with a rune that allows only to run the invoice command.

Let's just modify our script to handle better those kind of error by using a try...catch block:

...
if (connected) {
  try {
    const getinfo = await ln.commando({
      method: 'getinfo',
      params: [],
      rune: 'TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ=='
    })
    console.log(getinfo)
  } catch (err) {
    console.error(err)
  }
  ln.disconnect()
}

Back to our terminal we check that the error is handled correctly:

◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  code: 19537,
  message: 'Not authorized: method is not equal to invoice'
}

Complete invoice.js to send invoice commando messages to l2

The invoice method takes 3 arguments amount_msat, label (must be unique) and description.

We modify invoice.js file to send commando messages that run invoice method on the node l2:

...
if (connected) {
  try {
    const invoice = await ln.commando({
      method: 'invoice',
      params: {
        amount_msat: "10000",
        label: "inv",
        description: "pizza"
      },
      rune: 'TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ=='
    })
    console.log(invoice)
  } catch (err) {
    console.error(err)
  }
  ln.disconnect()
}

In the terminal, we see than we can generate an invoice by running that script:

◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  payment_hash: '4ae1919ccf13022860328cba8df8de044291ef6aa865601ad9351f45180b77c7',
  expires_at: 1692975366,
  bolt11: 'lnbcrt100n1pjdlp5xsp5u8zy3yafvd3vr8zvhgv77ance3kmpzcfkztktkfrxw2dh33u3w2qpp5ftser8x0zvpzscpj3jagm7x7q3pfrmm24pjkqxkex5052xqtwlrsdqgwp5h57npxqyjw5qcqp29qxpqysgq9v20764zw5ndex5m99h3v3uprxwdwljx4apst937s39xr755zqzsqgnc6z65tlgr6yq55kw0ddw5eh7gvyyepwxrjlg2gr77sqg7d9gp2lpfc8',
  payment_secret: 'e1c44893a96362c19c4cba19ef7678cc6db08b09b09765d9233394dbc63c8b94',
  warning_capacity: 'Insufficient incoming channel capacity to pay invoice'
}

Let's use fund_nodes command provided by the script lightning/contrib/startup_regtest.sh to fund a channel from the node l1 to the node l2:

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

Now the node l1 can pay the invoice we generated with invoice.js script:

◉ tony@tony:~/lnroom:
$ l1-cli pay lnbcrt100n1pjdlp5xsp5u8zy3yafvd3vr8zvhgv77ance3kmpzcfkztktkfrxw2dh33u3w2qpp5ftser8x0zvpzscpj3jagm7x7q3pfrmm24pjkqxkex5052xqtwlrsdqgwp5h57npxqyjw5qcqp29qxpqysgq9v20764zw5ndex5m99h3v3uprxwdwljx4apst937s39xr755zqzsqgnc6z65tlgr6yq55kw0ddw5eh7gvyyepwxrjlg2gr77sqg7d9gp2lpfc8
{
   "destination": "03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b",
   "payment_hash": "4ae1919ccf13022860328cba8df8de044291ef6aa865601ad9351f45180b77c7",
   "created_at": 1692370716.499,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "da22d040b5cd8456276c07a44f1557568c4e4eb84f902cbcc796881bea432988",
   "status": "complete"
}

If we run again invoice.js script we get the Duplicate label error because CLN requires that the label field in the arguments of the invoice method to be unique (which is not our case thought we've hard coded that value):

◉ tony@tony:~/lnroom:
$ ./invoice.js
{ code: 900, message: "Duplicate label 'inv'" }

Let's produce random labels for invoices using Math.random() function

...
if (connected) {
  try {
    const invoice = await ln.commando({
      method: 'invoice',
      params: {
        amount_msat: "10000",
        label: `inv-${Math.random()}`,
        description: "pizza"
      },
      rune: 'TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ=='
    })
    console.log(invoice)
  } catch (err) {
    console.error(err)
  }
  ln.disconnect()
}

and check in our terminal that it works as expected:

◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  payment_hash: 'a649975d22f292aaf583df80798a668a4671093930398b6b1cfb64d505e537b5',
  expires_at: 1692975564,
  bolt11: 'lnbcrt100n1pjdlp6vsp5gs2ma5s7e0v5cuqc5ywhlc66ngrsxt98cfedsu6xfckt8p4twcpqpp55eyewhfz72f24avrm7q8nznx3fr8zzfexquck6culdjd2p09x76sdqgwp5h57npxqyjw5qcqp29qxpqysgqhz07dvzcn4xyw8mh8nrt3zua52kduyxf0ynepfnvdz8puv6uz0j3crdunzpc06g3csa383ehqelh8pr2lvw8dr962n24ft4mquyfuxsqsjrsn8',
  payment_secret: '4415bed21ecbd94c7018a11d7fe35a9a07032ca7c272d873464e2cb386ab7602',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}

Last thing to do is to be able to pass the amount of the invoice and its description via command line arguments. This can be done like this:

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

We are now able to generate a invoice of 10000msat with the label pizza like this:

◉ tony@tony:~/lnroom:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'b365891fa4a97e249006d8c7f3105a4ef033ce6e0194d0510d2bbad6b2ecec7e',
  expires_at: 1692975654,
  bolt11: 'lnbcrt100n1pjdlpaxsp5ks409l0kwhzx8xxnhmgawrwzhpk6dy9ft22wmkdnup8wzhev6msqpp5kdjcj8ay49lzfyqxmrrlxyz6fmcr8nnwqx2dq5gd9waddvhva3lqdqgwp5h57npxqyjw5qcqp29qxpqysgqs7vganej2j72yxz9m8ch3dft274a78adxemvsj4c8ucy0a9jjwgk2h0w53mgemedfjv8aakj8jrlhswy9e3uhckhntq570mdvcdecrqq8llvq7',
  payment_secret: 'b42af2fdf675c46398d3bed1d70dc2b86da690a95a94edd9b3e04ee15f2cd6e0',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}

Finally we do a bit of refactoring and we get the following script

#!/usr/bin/env node

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

const NODE_ID = '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b'
const NODE_IP = '127.0.0.1'
const NODE_PORT = 7272
const RUNE = 'TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ=='

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)
  try {
    const invoice = await ln.commando({
      method: 'invoice',
      params: {
        amount_msat: amountMsat,
        label: `inv-${Math.random()}`,
        description: description
      },
      rune: RUNE
    })
    console.log(invoice)
  } catch (err) {
    console.error(err)
  }
  ln.disconnect()
}

which works perfectly:

◉ tony@tony:~/lnroom:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'a04de18dd66d7c63b5bbfdc3d99441059932e270feb421b5ce3da5e8c5f6c4b7',
  expires_at: 1692975790,
  bolt11: 'lnbcrt100n1pjdlzpwsp5uujpz46n5cp5q78k7zrxnj3e892sgslspuljl3kvzfpcxpme3z6qpp55px7rrwkd47x8ddmlhpan9zpqkvn9cnsl66zrdww8kj7330kcjmsdqgwp5h57npxqyjw5qcqp29qxpqysgqs9hz2zhfn5wqczmslhjg4mvvuwamd4dcea8fl52r4nyy54t4d4fzf0mkx2u8jxd0pf63gxl2gll49axy0vwgzezwym46uxcmd3t39pgqwph3ph',
  payment_secret: 'e724115753a6034078f6f08669ca3939550443f00f3f2fc6cc124383077988b4',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}

We are done!

Terminal session

We ran the following commands in this order:

$ ls
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l2-cli
$ npm i lnmessage
$ l2-cli getinfo | jq '.id, .binding'
$ alias l2-cli
$ ./invoice.js
$ l2-cli commando-rune
$ ./invoice.js
$ l1-cli -k commando-rune rune=k9KuYqPop1qtN4CmqFDVarwNrONhwc1MuIELzcUmmYw9MA== restrictions='[["method=invoice"]]'
$ ./invoice.js
$ ./invoice.js
$ ./invoice.js
$ fund_nodes
$ l1-cli pay lnbcrt100n1pjdlp5xsp5u8zy3yafvd3vr8zvhgv77ance3kmpzcfkztktkfrxw2dh33u3w2qpp5ftser8x0zvpzscpj3jagm7x7q3pfrmm24pjkqxkex5052xqtwlrsdqgwp5h57npxqyjw5qcqp29qxpqysgq9v20764zw5ndex5m99h3v3uprxwdwljx4apst937s39xr755zqzsqgnc6z65tlgr6yq55kw0ddw5eh7gvyyepwxrjlg2gr77sqg7d9gp2lpfc8
$ ./invoice.js
$ ./invoice.js
$ ./invoice.js 10000 pizza
$ ./invoice.js 10000 pizza

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

◉ tony@tony:~/lnroom:
$ ls
lightning/  invoice.js  notes.org
◉ tony@tony:~/lnroom:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
  start_ln 3: start three nodes, l1, l2, l3
  connect 1 2: connect l1 and l2
  fund_nodes: connect all nodes with channels, in a row
  stop_ln: shutdown
  destroy_ln: remove ln directories
◉ tony@tony:~/lnroom:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 12892
[2] 12925
WARNING: eatmydata not found: install it for faster testing
Commands:
        l1-cli, l1-log,
        l2-cli, l2-log,
        bt-cli, stop_ln, fund_nodes
◉ tony@tony:~/lnroom:
$ alias l2-cli
alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/lnroom:
$ npm i lnmessage

added 9 packages in 6s

5 packages are looking for funding
  run `npm fund` for details
◉ tony@tony:~/lnroom:
$ l2-cli getinfo | jq '.id, .binding'
"03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b"
[
  {
    "type": "ipv4",
    "address": "127.0.0.1",
    "port": 7272
  }
]
◉ tony@tony:~/lnroom:
$ alias l2-cli
alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/lnroom:
$ ./invoice.js
^C
◉ tony@tony:~/lnroom:
$ l2-cli commando-rune
{
   "rune": "k9KuYqPop1qtN4CmqFDVarwNrONhwc1MuIELzcUmmYw9MA==",
   "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:~/lnroom:
$ ./invoice.js
{
  id: '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b',
  alias: 'SLIMYTRAWL',
  color: '03ab4b',
  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: 7272 } ],
  version: 'v23.05.2',
  blockheight: 1,
  network: 'regtest',
  fees_collected_msat: 0,
  'lightning-dir': '/tmp/l2-regtest/regtest',
  our_features: {
    init: '08a0000a0269a2',
    node: '88a0000a0269a2',
    channel: '',
    invoice: '02000002024100'
  }
}
◉ tony@tony:~/lnroom:
$ l1-cli -k commando-rune rune=k9KuYqPop1qtN4CmqFDVarwNrONhwc1MuIELzcUmmYw9MA== \restrictions='[["method=invoice"]]'
{
   "rune": "TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ==",
   "unique_id": "0"
}
◉ tony@tony:~/lnroom:
$ ./invoice.js

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

Node.js v18.17.0
◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  code: 19537,
  message: 'Not authorized: method is not equal to invoice'
}
◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  payment_hash: '4ae1919ccf13022860328cba8df8de044291ef6aa865601ad9351f45180b77c7',
  expires_at: 1692975366,
  bolt11: 'lnbcrt100n1pjdlp5xsp5u8zy3yafvd3vr8zvhgv77ance3kmpzcfkztktkfrxw2dh33u3w2qpp5ftser8x0zvpzscpj3jagm7x7q3pfrmm24pjkqxkex5052xqtwlrsdqgwp5h57npxqyjw5qcqp29qxpqysgq9v20764zw5ndex5m99h3v3uprxwdwljx4apst937s39xr755zqzsqgnc6z65tlgr6yq55kw0ddw5eh7gvyyepwxrjlg2gr77sqg7d9gp2lpfc8',
  payment_secret: 'e1c44893a96362c19c4cba19ef7678cc6db08b09b09765d9233394dbc63c8b94',
  warning_capacity: 'Insufficient incoming channel capacity to pay invoice'
}
◉ tony@tony:~/lnroom:
$ fund_nodes
Mining into address bcrt1q3g2uakt0tqdmn5aypr3k8cl5l3659ez2ue9wge... 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:~/lnroom:
$ l1-cli pay lnbcrt100n1pjdlp5xsp5u8zy3yafvd3vr8zvhgv77ance3kmpzcfkztktkfrxw2dh33u3w2qpp5ftser8x0zvpzscpj3jagm7x7q3pfrmm24pjkqxkex5052xqtwlrsdqgwp5h57npxqyjw5qcqp29qxpqysgq9v20764zw5ndex5m99h3v3uprxwdwljx4apst937s39xr755zqzsqgnc6z65tlgr6yq55kw0ddw5eh7gvyyepwxrjlg2gr77sqg7d9gp2lpfc8
{
   "destination": "03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b",
   "payment_hash": "4ae1919ccf13022860328cba8df8de044291ef6aa865601ad9351f45180b77c7",
   "created_at": 1692370716.499,
   "parts": 1,
   "amount_msat": 10000,
   "amount_sent_msat": 10000,
   "payment_preimage": "da22d040b5cd8456276c07a44f1557568c4e4eb84f902cbcc796881bea432988",
   "status": "complete"
}
◉ tony@tony:~/lnroom:
$ ./invoice.js
{ code: 900, message: "Duplicate label 'inv'" }
◉ tony@tony:~/lnroom:
$ ./invoice.js
{
  payment_hash: 'a649975d22f292aaf583df80798a668a4671093930398b6b1cfb64d505e537b5',
  expires_at: 1692975564,
  bolt11: 'lnbcrt100n1pjdlp6vsp5gs2ma5s7e0v5cuqc5ywhlc66ngrsxt98cfedsu6xfckt8p4twcpqpp55eyewhfz72f24avrm7q8nznx3fr8zzfexquck6culdjd2p09x76sdqgwp5h57npxqyjw5qcqp29qxpqysgqhz07dvzcn4xyw8mh8nrt3zua52kduyxf0ynepfnvdz8puv6uz0j3crdunzpc06g3csa383ehqelh8pr2lvw8dr962n24ft4mquyfuxsqsjrsn8',
  payment_secret: '4415bed21ecbd94c7018a11d7fe35a9a07032ca7c272d873464e2cb386ab7602',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/lnroom:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'b365891fa4a97e249006d8c7f3105a4ef033ce6e0194d0510d2bbad6b2ecec7e',
  expires_at: 1692975654,
  bolt11: 'lnbcrt100n1pjdlpaxsp5ks409l0kwhzx8xxnhmgawrwzhpk6dy9ft22wmkdnup8wzhev6msqpp5kdjcj8ay49lzfyqxmrrlxyz6fmcr8nnwqx2dq5gd9waddvhva3lqdqgwp5h57npxqyjw5qcqp29qxpqysgqs7vganej2j72yxz9m8ch3dft274a78adxemvsj4c8ucy0a9jjwgk2h0w53mgemedfjv8aakj8jrlhswy9e3uhckhntq570mdvcdecrqq8llvq7',
  payment_secret: 'b42af2fdf675c46398d3bed1d70dc2b86da690a95a94edd9b3e04ee15f2cd6e0',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
◉ tony@tony:~/lnroom:
$ ./invoice.js 10000 pizza
{
  payment_hash: 'a04de18dd66d7c63b5bbfdc3d99441059932e270feb421b5ce3da5e8c5f6c4b7',
  expires_at: 1692975790,
  bolt11: 'lnbcrt100n1pjdlzpwsp5uujpz46n5cp5q78k7zrxnj3e892sgslspuljl3kvzfpcxpme3z6qpp55px7rrwkd47x8ddmlhpan9zpqkvn9cnsl66zrdww8kj7330kcjmsdqgwp5h57npxqyjw5qcqp29qxpqysgqs9hz2zhfn5wqczmslhjg4mvvuwamd4dcea8fl52r4nyy54t4d4fzf0mkx2u8jxd0pf63gxl2gll49axy0vwgzezwym46uxcmd3t39pgqwph3ph',
  payment_secret: 'e724115753a6034078f6f08669ca3939550443f00f3f2fc6cc124383077988b4',
  warning_deadends: 'Insufficient incoming capacity, once dead-end peers were excluded'
}
# TERMINAL 2

◉ tony@tony:~/lnroom:
$ alias l2-cli='lightning-cli --lightning-dir=/tmp/l2-regtest'
◉ tony@tony:~/lnroom:
$ l2-cli listpeers
{
   "peers": []
}
◉ tony@tony:~/lnroom:
$ l2-cli listpeers
{
   "peers": [
      {
         "id": "037b6daf05fa442584b4f6f47cc65d3740340c5a92944760364367f605756d16f2",
         "connected": true,
         "num_channels": 0,
         "netaddr": [
            "127.0.0.1:60520"
         ],
         "features": "0000000000000000"
      }
   ]
}

Source code

invoice.js

#!/usr/bin/env node

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

const NODE_ID = '03ab4b3226cc22c3ebc4cdaecf4ecc0ca8e2238d4392b4d900f4546328cb616e6b'
const NODE_IP = '127.0.0.1'
const NODE_PORT = 7272
const RUNE = 'TUNWsckQn6U5p4Tz3mkmWve0OqtBPlAQ3MnyXr3jvZE9MCZtZXRob2Q9aW52b2ljZQ=='

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)
  try {
    const invoice = await ln.commando({
      method: 'invoice',
      params: {
        amount_msat: amountMsat,
        label: `inv-${Math.random()}`,
        description: description
      },
      rune: RUNE
    })
    console.log(invoice)
  } catch (err) {
    console.error(err)
  }
  ln.disconnect()
}

package.json

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

Resources