Core lightning rpc_command hook, pay command and BOLT11 invoice
In this live we write a plugin that limits the amount a node can send (using the builtin pay
command) to a BOLT11 invoice. This is possible thanks to Core Lightning hook system and specifically the hook rpc_command
.
Transcript with corrections and improvements
In this live we write a plugin that limits the amount a node can send
(using the builtin pay
command) to a BOLT11 invoice.
Specifically, we write the plugin pay-up-to.py
such that when we start
it with the startup option limit
set to 0.001btc
for instance, we
can't pay invoices higher than that threshold. Here an example where
l1-cli
and l2-cli
are aliases for lightning-cli
command with
--lightning-dir
set respectively to /tmp/l1-regtest
and
/tmp/l2-regtest
:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv "too expensive pizza"
lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
rpc_command hook
The hook system allows plugins to register to some events that can
happened in lightningd
and ask lightningd
to be consulted to decide
what to do next.
A list of those events can be found in the documentation.
Regarding the rpc_command
hook, when a plugin registers to it, each
time a client send a JSON-RPC request to lightningd
, lightningd
forwards it to the plugin and waits for the plugin to tell it what to
do next. The plugin can answer to lightningd
in 4 ways:
I don't care about that request do what you were supposed to do,
I modified the request, now do what you were supposed to do but with the modified request,
Take that response and give it to the client,
Take that error and give it to the client.
This can be visualize like this:
(if registered to
rcp_command hook)
sends a forwards the
JSON-RPC request JSON-RPC request
┌───────┐------------------>┌──────────┐------------------------>┌───────────┐
│client │ │lightningd│ │a-plugin.py│
└───────┘<------------------└──────────┘<------------------------└───────────┘
- result.result ("continue")
- result.replace (...)
- result.return.result (...)
- result.return.error (...)
Let's write some code.
Install pyln-client and start 2 Lightning nodes running on regtest
Let's install pyln-client
in a Python virtual environment
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
and start two Lightning nodes running on the Bitcoin regtest
chain and check the alias of the command l1-cli
:
(.venv) ◉ 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
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1163253
[2] 1163288
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Register to rpc_command hook
We use the Python decorators @plugin.hook("rpc_command")
to register
to rpc_command
hook. The function bellow that decorators is used to
build the payload of the JSON-RPC response we send back to
lightningd
each time we receive a rpc_command
request. By returning
the dictionary {"result": "continue"}
, the plugin tells lightningd
"I
don't care about that request do what you were supposed to do":
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
return {"result": "continue"}
plugin.run()
We jump back in our terminal and start our pay-up-to.py
plugin like
this:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/pay-up-to.py",
"active": true,
"dynamic": true
}
]
}
We use ps
to check that our plugin is running:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pay-up
1163640 pts/0 S 0:00 python /home/tony/clnlive/pay-up-to.py
1163658 pts/0 S+ 0:00 rg pay-up
And finally, we call the command getinfo
and check that everything is
working correctly:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
While this is working we can't really see that the getinfo
request has
been forwarded to pay-up-to.py
plugin.
Make the commands hang 2 seconds
Let's make the commands hang 2 seconds each time they are used by a client. This will make our example more tangible:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
time.sleep(2)
return {"result": "continue"}
Let's restart our plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
Now when we call any JSON-RPC command we can see that they hang during
2 seconds before returning the expected response. This is the case
for instance for l1-cli getinfo
and l1-cli listpeers
.
Take over l1 node
So far the plugin let lightningd
take care of the answer even when we
made it slow.
Now, we modify rpc_command_func
like this
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
return {"return": {"result": {"BOOM": "I took over your node"}}}
so that each time a client sends a JSON-RPC request it will get the following response:
{
"BOOM": "I took over your node"
}
Let's restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and send the requests getinfo
, listpeers
and stop
:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli stop
{
"BOOM": "I took over your node"
}
The hook rpc_command
hook is so powerful, that we've just made our
node useless. We can't even stop it with the stop
command.
Let's stop the node l1
by killing its associated process and restart
it with lightningd
command:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163255 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l1-regtest
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1163998 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ kill -9 1163255
lightning/contrib/startup_regtest.sh: line 85: 1163255 Killed $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[1]- Exit 137 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
(.venv) ◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
We can check using ps
that both nodes l1
and l2
are running again:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1164016 ? Ss 0:00 lightningd --lightning-dir=/tmp/l1-regtest --daemon
1164074 pts/0 S+ 0:00 rg lightningd
Take over getinfo
Now we'll see how to just take over the command getinfo
and let
lightningd
taking care of the other JSON-RPC commands.
This is possible by filtering on the method
field of rpc_command
argument in the function rpc_command_func
.
How can we know that rpc_command
has a method
field?
Let's find out.
We modify rpc_command_func
in order to print rpc_command
value
representation in the file /tmp/pay-up-to
and we'll see the presence
of that field:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
with open("/tmp/pay-up-to", "a") as f:
f.write(repr(rpc_command) + "\n\n")
return {"result": "continue"}
Let's restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and call the commands getinfo
and listpeers
like this:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
...
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
Now we can check that the request getinfo
and listpeers
have been
written into the file /tmp/pay-up-to
:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164167', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'getinfo', 'id': 'cli:getinfo#1164167', 'params': []}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164226', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'listpeers', 'id': 'cli:listpeers#1164226', 'params': []}
Note: I don't know why we also have notifications
requests written in
that file.
Now we take over the getinfo
command only:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "getinfo":
return {"return": {"result": {"BOOM": "I took 'getinfo'"}}}
return {"result": "continue"}
Let's check in our terminal that we have implemented the expected behavior. We restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and call getinfo
and listpeers
commands:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over 'getinfo'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
Chat
Can plugins be written in any language?
Have I understood correctly? The plugin is controling the result of getinfo but nothing else in the node.
Hooks and getmanifest
Before we continue writing our pay-up-to.py
plugin, let's take a look
at the getmanifest
request/response between lightningd
and the plugin
when we start it.
When we start pay-up-to.py
plugin, we receive (the plugin) a
getmanifest
request like this one:
{
"jsonrpc": "2.0",
"id": 182,
"method": "getmanifest",
"params": {
"allow-deprecated-apis": false
}
}
In order to register to rpc_command
hook we add {"name":
"rpc_command"}
to the array hooks
in the field result
of the response
to the getmanifest
. So, our response to the getmanifest
request looks
like this:
{
"jsonrpc": "2.0",
"id": 182,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"hooks": [{ "name": "rpc_command" }]
}
}
In the case of the plugin pay-up-to.py
, as we are using pyln-client
package, pyln-client
does it for us.
Open a channel between the node l1 and l2
As we are going to "modify" the command pay and try it to pay bolt11
invoices, we need a channel open between the node l1
and l2
. We do
this using the commands connect
and fund_nodes
provided by the script
contrib/startup_regtest.sh
from the lightning
repository:
(.venv) ◉ tony@tony:~/clnlive:
$ connect 1 2
{
"id": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ fund_nodes
Mining into address bcrt1qlz7phgee89vg23fs54an94phxd9au5az9vn6rn... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.
Params type - array or object
We've seen before that the rpc_command
argument in the function
rpc_command_func
corresponds to the JSON-RPC request that lightningd
forwards to us. The params
field of that request contained the
parameters of the command and can be an ordered array or a key/value
pairs object.
Let's observe that in the case of the pay
command.
To do so we modify pay-up-to.py
like this
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
with open("/tmp/pay-up-to", "a") as f:
f.write(repr(rpc_command) + "\n\n")
return {"result": "continue"}
plugin.run()
such that every JSON-RPC requests sent to lightningd
will be written in
the file /tmp/pay-up-to
.
Back to our terminal, we restart our plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
Let's call the pay
command with ordered arguments (fake arguments):
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv amountofinv
{
"code": -32602,
"message": "amount_msat|msatoshi: should be a millisatoshi amount: invalid token '\"amountofinv\"'"
}
We don't care about the errors, what we want to is the type of params
field in the pay
request. We can check, by looking at the file
/tmp/pay-up-to
, that in the case of ordered arguments, params
is an
array:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165357', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165357', 'params': ['bolt11inv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165409', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165409', 'params': ['bolt11inv', 'amountofinv']}
Now if we use -k
flag and pass the argument by key/value pairs like this
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k pay bolt11=bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
we see that params
field in the JSON-RPC request is now a key/value
pairs object:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165478', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165478', 'params': {'bolt11': 'bolt11inv'}}
In the plugin pay-up-to.py
, we'll treat only the case where params
is
an array with only one element: the bolt11 invoice.
Take over pay
Let's keep implementing our plugin.
As we want to modify the pay
command, let's start by writing the logic
that make the plugin takes over the pay
command:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
return {"return": {"result": {"BOOM": "I took 'pay'"}}}
return {"result": "continue"}
We restart our plugin and check that those changes are effective:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
...
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "I took over 'pay'"
}
bolt11 argument in pay command
In the plugin pay-up-to.py
, we treat only the case where params
is
an array with only one element: the bolt11 invoice.
That means we don't treat the case where the bolt11 invoice has no
amount specified. If we treat that case we would have to check for a
second argument amount passed to the pay
command.
Let's check if we can retrieve the bolt11 invoice:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
return {"return": {"result": {"BOOM": bolt11}}}
return {"result": "continue"}
We restart our plugin and check that those changes are effective:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "bolt11"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay boltfoo
{
"BOOM": "boltfoo"
}
Human readable part of bolt11
I reproduce here part of bolt11 specification (Human-Readable Part, Creative Commons Attribution 4.0 International License)
The format for a Lightning invoice uses bech32 encoding.
[...]
The human-readable part of a Lightning invoice consists of two sections:
prefix
:ln
+ BIP-0173 currency prefix (e.g.lnbc
for Bitcoin mainnet,lntb
for Bitcoin testnet,lntbs
for Bitcoin signet, andlnbcrt
for Bitcoin regtest)amount
: optional number in that currency, followed by an optionalmultiplier
letter. The unit encoded here is the 'social' convention of a payment unit -- in the case of Bitcoin the unit is 'bitcoin' NOT satoshis.
The following multiplier
letters are defined:
m
(milli): multiply by 0.001u
(micro): multiply by 0.000001n
(nano): multiply by 0.000000001p
(pico): multiply by 0.000000000001
Examples
Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w
Please send $3 for a cup of nonsense (ナンセンス 1杯) to the same peer, within 1 minute
lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny
Now send $24 for an entire list of things (hashed)
lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7
Limit pay command to 0.001btc
Now we use the function amount
that takes a bolt11 invoice with a
mandatory amount and a mandatory multiplier (This is mandatory in our
function but optional in bolt11 specification) to limit the amount we
can pay to a bolt11 invoice.
We "hard code" the limit to 0.001btc
and our plugin is now:
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > 0.001:
return {"return":
{"result":
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}}}
return {"result": "continue"}
plugin.run()
Note that the fields in the dictionary returned are all hard coded. We'll change this in a moment.
Back to our terminal, we restart pay-up-to.py
plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
The node l2
creates a bolt11 invoice with an amount below the
threshold 0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-1 "foo"
{
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"expires_at": 1686840846,
"bolt11": "lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq",
"payment_secret": "9a13873900982a3a80013cc9451d23cf921f63c608efb1bb21f9802f16e2ab89",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
and so the node l1
can pay that invoice:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"created_at": 1686236056.629,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "46947a15026a544ceff6b528c4983bdf5d19534f2ac0e37057c527e803e723ac",
"status": "complete"
}
Now the node l2
creates a bolt11 invoice with an amount of 0.002btc
superior to the threshold 0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-2 "foo"
{
"payment_hash": "a50b36cf359e47bb08487f6dd759a2c38b076de4aff4c67ca1bab450e85d15f4",
"expires_at": 1686840875,
"bolt11": "lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf",
"payment_secret": "a3d6461c7f53edac1fac6eb0a78bdca50da3b364c742d8b228eb3dcd65605472",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
such that the node l1
can't pay that invoice as we can see below:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
Let's modify rpc_command_func
such that the field invoice_too_high
in
the payload response sent back to lightningd
contains the amount of
the bolt11 invoice which is superior to the threshold 0.001btc
:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > 0.001:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}}}
return {"result": "continue"}
Now we can restart our plugin, let the node l2
generates an invoice
with an amount of 0.002btc
(too high) and check that the plugin
pay-up-to.py
stops the payment and returns the too high amount of the
invoice:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-3 "foo"
{
"payment_hash": "a861020907fdbeae59397aceb098a1e83bfe95e14a75fa36dd7aadbec908bd75",
"expires_at": 1686840947,
"bolt11": "lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6",
"payment_secret": "789272c7650f96b7ca74e7ec84465ccaf36bf7099ccf30e174607a68a489a092",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
Chat
It seems easy to add plugins!
As I can check visually the amount why would I need a plugin like pay-up-to.py?
You can add custom metrics about the node activity.
(not in the video) Limit pay command to 'limit' which can be defined at plugin startup time
The last thing we can do to make our plugin more capable is to add a
startup option, let say limit
, that is used to set the threshold of
the amount we can pay to an invoice. So far it was an hard coded
value equal to 0.001btc
.
With pyln-client
package, this can be done
using
add_option
method of thePlugin
class to add a new startup option andusing
get_option
method of thePlugin
class to get the value of some options set after theinit
request/response communication betweenlightningd
and the plugin when the plugin is started.
So the plugin pay-up-to.py
is now implemented like this:
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
limit = float(plugin.get_option("limit").strip("btc"))
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > limit:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": str(limit) + "btc",
"bolt11": bolt11
}}}
return {"result": "continue"}
plugin.add_option(name="limit",
default="0.001btc",
description="pay bolt11 invoice up to 'limit'")
plugin.run()
Note that we also modified bolt11
value in the payload returned by
rpc_command_func
function.
Back to our terminal, we can start pay-up-to.py
plugin with limit
startup option set to 0.001btc
:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
{
"command": "start",
"plugins": [...]
}
And we can check that the plugin works as expected:
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-4 "foo"
{
"payment_hash": "9c72b37ea796bf24420bb35084aedaeec78b2f6d17e205ddab8341b7b745b9bf",
"expires_at": 1686842009,
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex",
"payment_secret": "c5ccc10b077525af28894e0c4b8dd8015857fed14e7fc332f198611f7e660e07",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-5 "foo"
{
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"expires_at": 1686842048,
"bolt11": "lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk",
"payment_secret": "c491bd72e651fe04ed5f9891665b4f40c901fbf4c47981d6e9e82d89b7a40cf2",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"created_at": 1686237261.677,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "200fd3cf48f433dfddc32a3e16d9fd022fde5c53aaa4dad94cc3e9767ff2d545",
"status": "complete"
}
We are done!
Terminal session
We ran the following commands in this order:
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install pyln-client
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ ps -ax | rg pay-up
$ l1-cli getinfo
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli stop
$ ps -ax | rg lightningd
$ kill -9 1163255
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
$ ps -ax | rg lightningd
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ connect 1 2
$ fund_nodes
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli pay bolt11inv
$ l1-cli pay bolt11inv amountofinv
$ l1-cli -k pay bolt11=bolt11inv
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli pay bolt11
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli pay bolt11
$ l1-cli pay boltfoo
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l2-cli invoice 0.0001btc inv-1 "foo"
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
$ l2-cli invoice 0.002btc inv-2 "foo"
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l2-cli invoice 0.002btc inv-3 "foo"
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
$ l2-cli invoice 0.002btc inv-4 "foo"
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
$ l2-cli invoice 0.0001btc inv-5 "foo"
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
And below you can read the terminal session (command lines and outputs):
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
(.venv) ◉ 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
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1163253
[2] 1163288
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/pay-up-to.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pay-up
1163640 pts/0 S 0:00 python /home/tony/clnlive/pay-up-to.py
1163658 pts/0 S+ 0:00 rg pay-up
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli stop
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163255 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l1-regtest
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1163998 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ kill -9 1163255
lightning/contrib/startup_regtest.sh: line 85: 1163255 Killed $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[1]- Exit 137 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
(.venv) ◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1164016 ? Ss 0:00 lightningd --lightning-dir=/tmp/l1-regtest --daemon
1164074 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over 'getinfo'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ connect 1 2
{
"id": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ fund_nodes
Mining into address bcrt1qlz7phgee89vg23fs54an94phxd9au5az9vn6rn... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv amountofinv
{
"code": -32602,
"message": "amount_msat|msatoshi: should be a millisatoshi amount: invalid token '\"amountofinv\"'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k pay bolt11=bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 1,
"num_pending_channels": 0,
"num_active_channels": 1,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 108,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "I took over 'pay'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "bolt11"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay boltfoo
{
"BOOM": "boltfoo"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-1 "foo"
{
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"expires_at": 1686840846,
"bolt11": "lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq",
"payment_secret": "9a13873900982a3a80013cc9451d23cf921f63c608efb1bb21f9802f16e2ab89",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"created_at": 1686236056.629,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "46947a15026a544ceff6b528c4983bdf5d19534f2ac0e37057c527e803e723ac",
"status": "complete"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-2 "foo"
{
"payment_hash": "a50b36cf359e47bb08487f6dd759a2c38b076de4aff4c67ca1bab450e85d15f4",
"expires_at": 1686840875,
"bolt11": "lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf",
"payment_secret": "a3d6461c7f53edac1fac6eb0a78bdca50da3b364c742d8b228eb3dcd65605472",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-3 "foo"
{
"payment_hash": "a861020907fdbeae59397aceb098a1e83bfe95e14a75fa36dd7aadbec908bd75",
"expires_at": 1686840947,
"bolt11": "lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6",
"payment_secret": "789272c7650f96b7ca74e7ec84465ccaf36bf7099ccf30e174607a68a489a092",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-4 "foo"
{
"payment_hash": "9c72b37ea796bf24420bb35084aedaeec78b2f6d17e205ddab8341b7b745b9bf",
"expires_at": 1686842009,
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex",
"payment_secret": "c5ccc10b077525af28894e0c4b8dd8015857fed14e7fc332f198611f7e660e07",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-5 "foo"
{
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"expires_at": 1686842048,
"bolt11": "lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk",
"payment_secret": "c491bd72e651fe04ed5f9891665b4f40c901fbf4c47981d6e9e82d89b7a40cf2",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"created_at": 1686237261.677,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "200fd3cf48f433dfddc32a3e16d9fd022fde5c53aaa4dad94cc3e9767ff2d545",
"status": "complete"
}
Source code
pay-up-to.py
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
limit = float(plugin.get_option("limit").strip("btc"))
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > limit:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": str(limit) + "btc",
"bolt11": bolt11
}}}
return {"result": "continue"}
plugin.add_option(name="limit",
default="0.001btc",
description="pay bolt11 invoice up to 'limit'")
plugin.run()
pay-up-to
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164167', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'getinfo', 'id': 'cli:getinfo#1164167', 'params': []}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164226', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'listpeers', 'id': 'cli:listpeers#1164226', 'params': []}
...
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165357', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165357', 'params': ['bolt11inv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165409', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165409', 'params': ['bolt11inv', 'amountofinv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165478', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165478', 'params': {'bolt11': 'bolt11inv'}}
...