lnurl-node
Node.js implementation of lnurl. The purpose of this project is to provide an easy and flexible lnurl server that you can run as a stand-alone process (via CLI) or integrated with your own custom node.js application (via API).
Optionally, your lnurl server can authorize other applications (offline or otherwise). Possible use-cases include offline Lightning Network ATMs (e.g. Bleskomat), static QR codes for receiving donations, authentication mechanism for web sites or web services (login / registration / 2FA).
This project attempts to maintain backwards compatibility for any features, methods, options, hooks, and events which are documented here.
- Specification Support
- Installation
- Command-line interface
- API
- Tags and Parameters
- Hooks
- Events
- Supported Lightning Network Backends
- Configuring Data Store
- Debugging
- Tests
- Changelog
- License
Specification Support
The LNURL specification is divided into separate documents called "LUDs". These documents can be found in the lnurl-rfc repository.
The following is a list of LUDs which this module already (or plans to) support:
- [x] LUD-01 - encode/decode
- [x] LUD-02 - channelRequest
- [x] LUD-03 - withdrawRequest
- [x] LUD-04 - auth
- [x] LUD-06 - payRequest
- [ ] LUD-08 - Fast withdrawRequest
- [x] LUD-09 - successAction in payRequest
- [ ] LUD-10 - aes successAction in payRequest
- [x] LUD-12 - Comments in payRequest
- [ ] LUD-16 - Lightning Address
- [ ] LUD-17 - New URI schema prefixes
Installation
If you wish to use this module as a CLI tool, install it globally via npm:
npm install -g lnurl
Add to your application via npm
:
npm install lnurl --save
This will install lnurl
and add it to your application's package.json
file.
Command-line interface
This section assumes that you have lnurl
installed globally and that it is available on your current user's PATH.
CLI: help
To view the help menu:
lnurl --help
CLI: encode
Encode a URL:
lnurl encode "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"
Expected output:
lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns
This command also accepts piped input. For example:
echo -n "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df" \
| lnurl encode
CLI: decode
Decode an lnurl:
lnurl decode "lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns"
Expected output:
https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df
This command also accepts piped input. For example:
echo -n "lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns" \
| lnurl decode
CLI: generateNewUrl
To generate a new lnurl that a client application can then use:
lnurl generateNewUrl \
--host "localhost" \
--port "3000" \
--endpoint "/lnurl" \
--store.backend "knex" \
--store.config '{"client":"postgres","connection":{"host":"127.0.0.1","user":"postgres","password":"example","database":"lnurl_example"}}' \
--tag "withdrawRequest" \
--params '{"minWithdrawable":10000,"maxWithdrawable":10000,"defaultDescription":""}'
See Tags and Parameters for a full list of possible tags and params.
Alternatively, a configuration file can be used:
lnurl generateNewUrl \
--configFile ./config.json \
--tag "withdrawRequest" \
--params '{"minWithdrawable":10000,"maxWithdrawable":10000,"defaultDescription":""}'
Example output:
{
"encoded": "lnurl1dp68gup69uhkcmmrv9kxsmmnwsarxvpsxqhkcmn4wfkr7ufavvexxvpk893rswpjxcmnvctyvgexzen9xvmkycnxv33rvdtrvy6xzv3ex43xzve5vvexgwfj8yenxvm9xaskzdmpxuexywt9893nqvcly0lgs",
"secret": "c2c069b882676adb2afe37bbfdb65ca4a295ba34c2d929333e7aa7a72b9e9c03",
"url": "http://localhost:3000/lnurl?q=c2c069b882676adb2afe37bbfdb65ca4a295ba34c2d929333e7aa7a72b9e9c03"
}
It is possible to set the number of uses allowed for the new URL:
lnurl generateNewUrl \
--configFile ./config.json \
--tag "withdrawRequest" \
--params '{"minWithdrawable":10000,"maxWithdrawable":10000,"defaultDescription":""}' \
--uses 3
Set --uses
equal to 0
to allow the URL to be used an unlimited number of times.
For a list of available options:
lnurl generateNewUrl --help
It is also possible to generate lnurls in other ways:
- generateNewUrl - API method
CLI: server
Start an lnurl application server with the following command:
lnurl server \
--host "localhost" \
--port "3000" \
--lightning.backend "dummy" \
--lightning.config '{}'
- The example above uses the "dummy" LN backend. For details about how to connect to a real LN backend, see Supported Lightning Network Backends
- By default the lnurl server stores data in memory - which is fine for development and testing. But once you plan to run it in production, it is recommended that you use a proper data store - see Configuring Data Store.
- To enable debugging messages, see the Debugging section of this readme.
Alternatively, a configuration file can be used:
lnurl server --configFile ./config.json
To print all available options for the server command:
lnurl server --help
API
encode
encode(url)
Encode a url as a bech32-encoded string.
Usage:
const lnurl = require('lnurl');
const encoded = lnurl.encode('https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df');
console.log(encoded);
Expected output:
"lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns"
decode
decode(url)
Decode a bech32-encoded lnurl.
Usage:
const lnurl = require('lnurl');
const decoded = lnurl.decode('lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns');
console.log(decoded);
Expected output:
"https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"
createServer
createServer([options])
Create and initialize an instance of the lnurl server.
Usage:
const lnurl = require('lnurl');
const server = lnurl.createServer({
host: 'localhost',
port: 3000,
auth: {
apiKeys: [
{
id: '46f8cab814de07a8a65f',
key: 'ee7678f6fa5ab9cf3aa23148ef06553edd858a09639b3687113a5d5cdb5a2a67',
encoding: 'hex',
},
],
},
lightning: {
backend: 'dummy',
config: {},
},
});
- The example above uses the "dummy" LN backend. For details about how to connect to a real LN backend, see Supported Lightning Network Backends
- By default the lnurl server stores data in memory - which is fine for development and testing. But once you plan to run it in production, it is recommended that you use a proper data store - see Configuring Data Store.
- To enable debugging messages, see the Debugging section of this readme.
createServer: options
Below is the full list of options that can be passed to the createServer
method.
{
// The host for the web server:
host: 'localhost',
// The port for the web server:
port: 3000,
// Whether or not to start listening when the server is created:
listen: true,
// The URL where the server is externally reachable (e.g "https://your-lnurl-server.com"):
url: null,
// The URI path of the web API end-point:
endpoint: '/lnurl',
// See list of possible LN backends here:
// https://github.com/chill117/lnurl-node#supported-lightning-network-backends
lightning: {
// The name of the LN backend to use:
backend: 'dummy',
// Configuration options to pass to LN backend:
config: {},
},
store: {
// Name of store backend ('knex', 'memory'):
backend: 'memory',
// Configuration options to pass to store:
config: {},
},
payRequest: {
// A number greater than 0 indicates the maximum length of comments.
// Setting this to 0 ignores comments.
//
// Note that there is a generally accepted limit (2000 characters)
// to the length of URLs; see:
// https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
//
// Since comments are sent as a query parameter to the callback URL,
// this limit should be set to a maximum of 1000 to be safe.
commentAllowed: 500,
// Default metadata to be sent in response object:
metadata: '[["text/plain", "lnurl-node"]]',
},
}
- To use a custom lightning backend with your server see Custom Lightning Network Backend.
generateNewUrl
generateNewUrl(tag, params)
To generate a new lnurl that a client application can then use:
const tag = 'payRequest';
const params = {
minSendable: 10000,
maxSendable: 200000,
metadata: '[["text/plain", "lnurl-node"]]',
commentAllowed: 500,
};
server.generateNewUrl(tag, params).then(result => {
const { encoded, secret, url } = result;
console.log({ encoded, secret, url });
}).catch(error => {
console.error(error);
});
Expected output:
{
"encoded": "lnurl1dp68gup69uhkcmmrv9kxsmmnwsarxvpsxqhkcmn4wfkr7ufavvexxvpk893rswpjxcmnvctyvgexzen9xvmkycnxv33rvdtrvy6xzv3ex43xzve5vvexgwfj8yenxvm9xaskzdmpxuexywt9893nqvcly0lgs",
"secret": "c2c069b882676adb2afe37bbfdb65ca4a295ba34c2d929333e7aa7a72b9e9c03",
"url": "http://localhost:3000/lnurl?q=c2c069b882676adb2afe37bbfdb65ca4a295ba34c2d929333e7aa7a72b9e9c03"
}
See Tags and Parameters for a full list of possible tags and params.
It is possible to set the number of uses allowed for the new URL:
const tag = 'payRequest';
const params = {
minSendable: 10000,
maxSendable: 200000,
metadata: '[["text/plain", "lnurl-node"]]',
commentAllowed: 500,
};
const options = {
uses: 3,
};
server.generateNewUrl(tag, params, options).then(result => {
const { encoded, secret, url } = result;
console.log({ encoded, secret, url });
}).catch(error => {
console.error(error);
});
Set uses
equal to 0
to allow the URL to be used an unlimited number of times.
It is also possible to generate lnurls in other ways:
Tags and Parameters
Below you will find all tags and their associated params.
channelRequest
:
name | type | notes |
---|---|---|
localAmt |
integer (sats) |
> 0 |
pushAmt |
integer (sats) |
<= localAmt
|
login
:
none
payRequest
:
name | type | notes |
---|---|---|
minSendable |
integer (msats) |
> 0 |
maxSendable |
integer (msats) |
>= minSendable
|
metadata |
string |
stringified JSON |
commentAllowed |
integer |
character limit for comments (max. 1000), set to 0 to disallow comments |
withdrawRequest
:
name | type | notes |
---|---|---|
minWithdrawable |
integer (msats) |
> 0 |
maxWithdrawable |
integer (msats) |
>= minWithdrawable
|
defaultDescription |
string |
Hooks
It is possible to further customize your lnurl server by using hooks to run custom application code at key points in the server application flow.
- status
- url:process
- login
- channelRequest:validate
- channelRequest:info
- channelRequest:action
- payRequest:validate
- payRequest:info
- payRequest:action
- withdrawRequest:validate
- withdrawRequest:info
- withdrawRequest:action
How to use a hook:
const lnurl = require('lnurl');
const server = lnurl.createServer();
const { HttpError } = require('lnurl/lib');
// The callback signature can vary depending upon the hook used:
server.bindToHook('HOOK', function(arg1, arg2, arg3, next) {
// Fail the request by calling next with an error:
next(new Error('Your custom error message'));
// Use the HttpError constructor to pass the error to the response object:
next(new HttpError('Custom error sent in the response object', 400/* status code */));
// Or call next without any arguments to continue with the request:
next();
});
Hook: status
This hook is called when a request is received at the web server's /status
end-point.
Example OK status response:
server.bindToHook('status', function(req, res, next) {
// Call next() with no arguments to continue with the normal status flow.
next();
});
Example failed status response:
server.bindToHook('status', function(req, res, next) {
// Or, call next with an error to fail the request:
next(new HttpError('Service temporarily unavailable', 503));
});
Hook: url:process
This hook is called when a request is received at the web server's LNURL end-point.
Example modifying the request object:
server.bindToHook('url:process', function(req, res, next) {
req.query.defaultDescription = 'custom default description';
next();// Call next() with no arguments to continue the request.
});
Example rejecting the request:
server.bindToHook('url:process', function(req, res, next) {
// Call next() with an error to fail the request:
next(new HttpError('Failed check', 400));
});
Hook: login
login
The lnurl-auth subprotocol allows users to login/authenticate with your service. You can use the login hook as shown here to execute your own custom code whenever there is a successful login/authentication attempt for your server.
server.bindToHook('login', function(key, next) {
// This code is executed when the lnurl-auth checks have passed (e.g valid signature provided).
// `key` is the public linking key which has just authenticated.
// Perform asynchronous code such as database calls here.
// Call next() without any arguments to continue with the request:
next();
});
Hook: channelRequest:validate
Hook: payRequest:validate
Hook: withdrawRequest:validate
channelRequest:validate
payRequest:validate
withdrawRequest:validate
These hooks are called when validating the parameters provided when creating a new URL. For example, when calling server.generateNewUrl(tag, params)
.
server.bindToHook('channelRequest:validate', function(params, next) {
// Throw an error to prevent the creation of the new URL:
next(new Error('Invalid params!'));
// Call next() without any arguments to continue with the creation of the new URL:
next();
});
Hook: channelRequest:info
Hook: payRequest:info
Hook: withdrawRequest:info
channelRequest:info
payRequest:info
withdrawRequest:info
These hooks are called when the initial request is made to the LNURL end-point. The initial request occurs when a wallet app first scans a QR code containing an LNURL. The wallet app makes the initial request for more information about the tag and other parameters associated with the LNURL it just scanned.
server.bindToHook('channelRequest:info', function(secret, params, next) {
// `secret` is the k1 value that when hashed gives the unique `hash`
// associated with an LNURL in the data store.
// `params` are the parameters provided when the URL was created.
// Throw an error to fail the request:
next(new HttpError('Custom error sent in the response object', 400/* status code */));
// Call next() without any arguments to continue with the request:
next();
});
Hook: channelRequest:action
Hook: payRequest:action
Hook: withdrawRequest:action
channelRequest:action
payRequest:action
withdrawRequest:action
These hooks are called when the second request is made to the LNURL end-point. This request occurs when the wallet app wants to complete the action associated with the LNURL it scanned and made an initial request for previously.
-
channelRequest:action
- Wallet app sends its node ID and whether or not to make the channel private:-
remoteid
- remote node ID (public key) to which the server should open a channel -
private
-0
or1
-
-
payRequest:action
- Wallet sends the amount it wants to pay and an optional comment:-
amount
- amount the server should use when generating a new invoice
-
-
withdrawRequest:action
- Wallet sends a bolt11 invoice that the server should pay:-
pr
- bolt11 invoice
-
server.bindToHook('channelRequest:action', function(secret, params, next) {
// `secret` is the k1 value that when hashed gives the unique `hash`
// associated with an LNURL in the data store.
// `params` are the parameters provided when the URL was created plus
// the parameters provided in the request to the server.
// Throw an error to fail the request:
next(new HttpError('Custom error sent in the response object', 400/* status code */));
// Call next() without any arguments to continue with the request:
next();
});
Note that these hooks are executed before the server calls the LN backend method. So if an error is thrown here, a channel will not be opened; a new invoice will not be generated; the provided invoice will not be paid.
Events
- login
- channelRequest:action:processed
- channelRequest:action:failed
- payRequest:action:processed
- payRequest:action:failed
- withdrawRequest:action:processed
- withdrawRequest:action:failed
The server
object extends from the event emitter class. It is possible to listen for events as follows:
const lnurl = require('lnurl');
const server = lnurl.createServer();
server.on('EVENT', function(event) {
// The event object varies depending upon the event type.
});
Event: login
This event is emitted after a successful login attempt.
server.on('login', function(event) {
const { key, hash } = event;
// `key` - the public key as provided by the LNURL wallet app
// `hash` - the hash of the secret for the LNURL used to login
});
Event: channelRequest:action:processed
This event is emitted after a successful call to the LN backend's openChannel
method.
server.on('channelRequest:action:processed', function(event) {
const { secret, params, result } = event;
// `result` is the non-normalized response object from the LN backend
// So this will vary depending upon the backend used.
});
Event: payRequest:action:processed
This event is emitted after a successful call to the LN backend's addInvoice
method.
server.on('payRequest:action:processed', function(event) {
const { secret, params, result } = event;
const { id, invoice } = result;
// `id` - non-standard reference ID for the new invoice, can be NULL if none provided
// `invoice` - bolt11 invoice
});
Event: withdrawRequest:action:processed
This event is emitted after a successful call to the LN backend's payInvoice
method.
server.on('withdrawRequest:action:processed', function(event) {
const { secret, params, result } = event;
const { id } = result;
// `id` - non-standard reference ID for the payment, can be NULL if none provided
});
Event: channelRequest:action:failed
This event is emitted after a failed call to the LN backend's openChannel
method.
server.on('channelRequest:action:failed', function(event) {
const { secret, params, error } = event;
// `error` - error from the LN backend
});
Event: payRequest:action:failed
This event is emitted after a failed call to the LN backend's addInvoice
method.
server.on('payRequest:action:failed', function(event) {
const { secret, params, error } = event;
// `error` - error from the LN backend
});
Event: withdrawRequest:action:failed
This event is emitted after a failed call to the LN backend's payInvoice
method.
server.on('withdrawRequest:action:failed', function(event) {
const { secret, params, error } = event;
// `error` - error from the LN backend
});
Supported Lightning Network Backends
See lightning-backends for a list of supported Lightning Network backends and their corresponding configuration options.
Configuring Data Store
By default the lnurl server will store data in memory - which is not ideal for several reasons. It is strongly recommended that you configure a proper data store for your server. This module supports PostgreSQL.
PostgreSQL
To use PostgreSQL as your data store you will need to install the postgres module and knex wherever you are running your lnurl server:
npm install knex pg
Then you can run your server via the API as follows:
const lnurl = require('lnurl');
const server = lnurl.createServer({
// ...
store: {
backend: 'knex',
config: {
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'lnurl_server',
password: '',
database: 'lnurl_server',
},
},
},
// ...
});
Or via the CLI:
lnurl server \
--store.backend="knex" \
--store.config='{"client":"postgres","connection":{"host":"127.0.0.1","user":"lnurl_server","password":"","database":"lnurl_server"}}'
Debugging
This module uses debug to output debug messages to the console. To output all debug messages, run your node app with the DEBUG
environment variable:
DEBUG=lnurl* node your-app.js
Or if using the CLI interface:
DEBUG=lnurl* lnurl server
Tests
To run all tests:
npm test
Changelog
See CHANGELOG.md
License
This software is MIT licensed:
A short, permissive software license. Basically, you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. There are many variations of this license in use.