balanceofsatoshis
Version:
Lightning balance CLI
296 lines (245 loc) • 8.59 kB
JavaScript
const {isIP} = require('net');
const asyncAuto = require('async/auto');
const {bech32} = require('bech32');
const {addPeer} = require('ln-service');
const {getIdentity} = require('ln-service');
const {getNodeAlias} = require('ln-sync');
const {getPeers} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
const bech32CharLimit = 2000;
const {decode} = bech32;
const errorStatus = 'ERROR';
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
const okStatus = 'OK';
const parseUri = n => n.split('@');
const prefix = 'lnurl';
const sslProtocol = 'https:';
const tag = 'channelRequest';
const typeDefault = '0';
const types = [{name: 'Public', value: '0'}, {name: 'Private', value: '1'}];
const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
/** Request inbound channel from lnurl
{
ask: <Ask Function>
lnd: <Authenticated LND API Object>
lnurl: <Lnurl String>
logger: <Winston Logger Object>
request: <Request Function>
}
@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.ask) {
return cbk([400, 'ExpectedAskFunctionToRequestChannelFromLnurl']);
}
if (!args.lnurl) {
return cbk([400, 'ExpectedUrlToRequestChannelFromLnurl']);
}
try {
decode(asLnurl(args.lnurl), bech32CharLimit);
} catch (err) {
return cbk([400, 'FailedToDecodeLnurlToRequestChannel', {err}]);
}
if (decode(asLnurl(args.lnurl), bech32CharLimit).prefix !== prefix) {
return cbk([400, 'ExpectedLnUrlPrefixToRequestChannel']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToRequestChannelFromLnurl']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToRequestChannelFromLnurl']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToGetLnurlRequestChannel']);
}
return cbk();
},
// Get node identity public key
getIdentity: ['validate', ({}, cbk) => {
return getIdentity({lnd: args.lnd}, cbk);
}],
// Get the list of connected peers to determine if connection is needed
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
// Get accepted terms from the encoded url
getTerms: ['validate', ({}, cbk) => {
const {words} = decode(asLnurl(args.lnurl), bech32CharLimit);
const url = wordsAsUtf8(words);
return args.request({url, json: true}, (err, r, json) => {
if (!!err) {
return cbk([503, 'FailureGettingLnurlDataFromUrl', {err}]);
}
if (!json) {
return cbk([503, 'ExpectedJsonObjectReturnedInLnurlResponse']);
}
if (json.status === errorStatus) {
return cbk([503, 'UnexpectedServiceError', {err: json.reason}]);
}
if (!json.callback) {
return cbk([503, 'ExpectedCallbackInLnurlResponseJson']);
}
try {
new URL(json.callback);
} catch (err) {
return cbk([503, 'ExpectedValidLnurlResponseCallbackUrl', {err}]);
}
if ((new URL(json.callback)).protocol !== sslProtocol) {
return cbk([400, 'LnurlsThatSpecifyNonSslUrlsAreUnsupported']);
}
if (!json.k1) {
return cbk([503, 'ExpectedK1InLnurlChannelResponseJson']);
}
if (!json.tag) {
return cbk([503, 'ExpectedTagInLnurlChannelResponseJson']);
}
if (json.tag !== tag) {
return cbk([503, 'ExpectedTagToBeChannelRequestInLnurlResponse']);
}
if (!json.uri) {
return cbk([503, 'ExpectedUriInLnurlResponseJson']);
}
// uri: remote node address of form node_key@ip_address:port_number
const [id, socket] = parseUri(json.uri);
if (!isPublicKey(id)) {
return cbk([503, 'ExpectedValidPublicKeyIdInLnurlResponseJson']);
}
if (!socket) {
return cbk([503, 'ExpectedNetworkSocketAddressInLnurlResponse']);
}
return cbk(null, {id, socket, k1: json.k1, url: json.callback});
});
}],
// Get the node alias
getAlias: ['getTerms', ({getTerms}, cbk) => {
return getNodeAlias({id: getTerms.id, lnd: args.lnd}, cbk);
}],
// Connect to the peer returned in the lnurl response
connect: [
'getAlias',
'getPeers',
'getTerms',
({getAlias, getPeers, getTerms}, cbk) =>
{
// Exit early when the node is already connected
if (getPeers.peers.map(n => n.public_key).includes(getTerms.id)) {
return cbk();
}
args.logger.info({
connecting_to: {
alias: getAlias.alias || undefined,
public_key: getTerms.id,
socket: getTerms.socket,
},
});
return addPeer({
lnd: args.lnd,
public_key: getTerms.id,
socket: getTerms.socket,
},
cbk);
}],
// Select private or public mode for the channel
askPrivate: ['connect', 'getTerms', ({getTerms}, cbk) => {
return args.ask({
choices: types,
default: typeDefault,
message: 'Channel type?',
name: 'priv',
type: 'select',
},
({priv}) => cbk(null, priv));
}],
// Confirm that an inbound channel should be requested
ok: ['askPrivate', 'getAlias', ({askPrivate, getAlias}, cbk) => {
const node = getAlias.alias || getAlias.id;
const type = !!askPrivate ? 'a private' : 'an';
return args.ask({
default: true,
message: `Request ${type} inbound channel from ${node}?`,
name: 'ok',
type: 'confirm',
},
({ok}) => cbk(null, ok));
}],
// Send a signal to cancel the channel request
sendCancelation: [
'getIdentity',
'getTerms',
'ok',
({channel, getTerms, ok}, cbk) =>
{
// Exit early when user wants to proceed with the channel request
if (!!ok) {
return cbk();
}
return args.request({
json: true,
qs: {
cancel: Number(!ok),
k1: getTerms.k1,
remoteid: getIdentity.public_key,
},
url: getTerms.url,
},
(err, r, json) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorCancelingChannelRequest', {err}]);
}
if (!json) {
return cbk([503, 'ExpectedJsonObjectInCancelChannelResponse']);
}
if (json.status === errorStatus) {
return cbk([503, 'ChannelCancelReturnedErr', {err: json.reason}]);
}
if (json.status !== okStatus) {
return cbk([503, 'ExpectedOkStatusInCancelChannelResponse']);
}
return cbk([400, 'CanceledRequestForInboundChannel']);
});
}],
// Make the request to confirm a request for an inbound channel
sendConfirmation: [
'askPrivate',
'getIdentity',
'getTerms',
'ok',
({askPrivate, getIdentity, getTerms, ok}, cbk) =>
{
// Exit early when the user decides to cancel
if (!ok) {
return cbk();
}
return args.request({
json: true,
qs: {
k1: getTerms.k1,
private: askPrivate,
remoteid: getIdentity.public_key,
},
url: getTerms.url,
},
(err, r, json) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorRequestingLnurlChannel', {err}]);
}
if (!json) {
return cbk([503, 'ExpectedJsonObjectReturnedInChannelResponse']);
}
if (json.status === errorStatus) {
return cbk([503, 'ChannelRequestReturnedErr', {err: json.reason}]);
}
if (json.status !== okStatus) {
return cbk([503, 'ExpectedOkStatusInChannelRequestResponse']);
}
args.logger.info({requested_channel_open: true});
return cbk();
});
}],
},
returnResult({reject, resolve}, cbk));
});
};