balanceofsatoshis
Version:
Lightning balance CLI
400 lines (339 loc) • 13.4 kB
JavaScript
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const {bold} = require('colorette');
const {formatTokens} = require('ln-sync');
const {getChannels} = require('ln-service');
const {getClosedChannels} = require('ln-service');
const {getForwards} = require('ln-service');
const {getHeight} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {getNode} = require('ln-service');
const {getNodeAlias} = require('ln-sync');
const {returnResult} = require('asyncjs-util');
const size = require('window-size');
const {chartAliasForPeer} = require('./../display');
const {getIcons} = require('./../display');
const isRelevantForward = require('./is_relevant_forward');
const isRelevantSource = require('./is_relevant_source');
const {lndCredentials} = require('./../lnd');
const {isArray} = Array;
const lastTime = times => !times.length ? null : new Date(max(...times));
const limit = 99999;
const {max} = Math;
const {min} = Math;
const msPerDay = 1000 * 60 * 60 * 24;
const notNull = array => array.filter(n => n !== null);
const {now} = Date;
const numDays = 1;
const {parse} = Date;
const sort = (a, b) => a > b ? 1 : ((b > a) ? -1 : 0);
const sortsForEarning = ['earned_in', 'earned_out', 'earned_total'];
const sortsForCapital = ['inbound', 'liquidity', 'outbound'];
const tokensAsBigTokens = tokens => !!tokens ? (tokens / 1e8).toFixed(8) : '';
const uniq = arr => Array.from(new Set(arr));
const wideSizeCols = 155;
/** Get recent forwarding activity
{
[days]: <Days Number>
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
[is_monochrome]: <Mute Colors Bool>
[is_table]: <Return Results As Table Bool>
lnd: <Authenticated LND API Object>
[sort]: <Sort By Field String>
[tags]: [<Tag Identifier String>]
}
@returns via cbk or Promise
{
peers: [{
alias: <Peer Alias String>
earned_inbound_fees: <Earned Inbound Fee Tokens Number>
earned_outbound_fees: <Earned Outbound Fee Tokens Number>
last_inbound_at: <Last Inbound Forward At ISO 8601 Date String>
last_outbound_at: <Last Forward At ISO 8601 Date String>
liquidity_inbound: <Inbound Liquidity Big Tokens Number>
outbound_liquidity: <Outbound Liquidity Big Tokens Number>
public_key: <Public Key String>
}]
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.fs) {
return cbk([400, 'ExpectedFsMethodsToGetForwardingInformation']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToGetForwardingInformation']);
}
const sorts = [].concat(sortsForCapital).concat(sortsForEarning);
if (!!args.sort && !sorts.includes(args.sort)) {
return cbk([400, 'ExpectedKnownSortToSortForwards', {sorts}]);
}
if (!isArray(args.tags)) {
return cbk([400, 'ExpectedArrayOfTagsToGetForwardingRecords']);
}
return cbk();
},
// Get channels
getChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.lnd}, cbk);
}],
// Get closed channels
getClosed: ['validate', ({}, cbk) => {
return getClosedChannels({lnd: args.lnd}, cbk);
}],
// Get forwards
getForwards: ['validate', ({}, cbk) => {
const before = new Date().toISOString();
const pastMs = (args.days || numDays) * msPerDay;
const after = new Date(now() - pastMs).toISOString();
return getForwards({after, before, limit, lnd: args.lnd}, cbk);
}],
// Get current block height
getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
// Get node icons
getIcons: ['validate', ({}, cbk) => getIcons({fs: args.fs}, cbk)],
// Get pending channels
getPending: ['validate', ({}, cbk) => {
return getPendingChannels({lnd: args.lnd}, cbk);
}],
// Consolidate closed and open channels
channels: [
'getChannels',
'getClosed',
({getChannels, getClosed}, cbk) =>
{
const channels = []
.concat(getChannels.channels)
.concat(getClosed.channels)
.filter(n => !!n.id && !!n.partner_public_key);
return cbk(null, channels);
}],
// Forwards from peers
sendingFromPeers: [
'channels',
'getForwards',
({channels, getForwards}, cbk) =>
{
const forwardingChannels = channels.filter(({id}) => {
return !!getForwards.forwards.find(n => n.incoming_channel === id);
});
return cbk(null, forwardingChannels.map(n => n.partner_public_key));
}],
// Forwards to peers
sendingToPeers: [
'channels',
'getForwards',
({channels, getForwards}, cbk) =>
{
const forwardingChannels = channels.filter(({id}) => {
return !!getForwards.forwards.find(n => n.outgoing_channel === id);
});
return cbk(null, forwardingChannels.map(n => n.partner_public_key));
}],
// Node metadata
nodes: [
'sendingFromPeers',
'sendingToPeers',
({sendingFromPeers, sendingToPeers}, cbk) =>
{
const nodes = uniq([].concat(sendingFromPeers).concat(sendingToPeers));
return asyncMap(nodes, (id, cbk) => {
return getNodeAlias({id, lnd: args.lnd}, cbk);
},
cbk);
}],
// Forwards
forwards: [
'channels',
'getChannels',
'getForwards',
'getIcons',
'getPending',
'nodes',
({
channels,
getChannels,
getForwards,
getIcons,
getPending,
nodes,
},
cbk) =>
{
const peers = nodes.map(node => {
// Get the channels that are associated with this peer
const nodeChannels = channels
.filter(n => n.partner_public_key === node.id);
const forwards = getForwards.forwards.filter(forward => {
return isRelevantForward({
all_channels: channels,
from: args.from,
incoming_channel: forward.incoming_channel,
node_channels: nodeChannels,
outgoing_channel: forward.outgoing_channel,
to: args.to,
});
});
const sources = getForwards.forwards.filter(forward => {
return isRelevantSource({
all_channels: channels,
from: args.from,
incoming_channel: forward.incoming_channel,
node_channels: nodeChannels,
outgoing_channel: forward.outgoing_channel,
to: args.to,
});
});
const forwardTimes = forwards.map(n => parse(n.created_at));
const inboundTimes = sources.map(n => parse(n.created_at));
const lastOut = lastTime(forwardTimes);
const lastIn = lastTime(inboundTimes);
const connected = getChannels.channels
.filter(n => n.partner_public_key === node.id);
const active = connected.filter(n => n.is_active);
const isHidden = !active.find(n => !n.is_private) && !!active.length;
const pending = getPending.pending_channels
.filter(n => n.is_opening)
.filter(n => n.partner_public_key === node.id);
const hasHtlcChannel = connected
.find(n => !!n.pending_payments.length);
const local = [].concat(nodeChannels).concat(pending)
.filter(n => !!n.local_balance)
.reduce((sum, n) => sum + n.local_balance, Number());
const remote = [].concat(nodeChannels).concat(pending)
.filter(n => !!n.remote_balance)
.reduce((sum, n) => sum + n.remote_balance, Number());
const isDisconnected = !connected.length && !pending.length;
const nodeIcons = getIcons.nodes.find(n => n.public_key === node.id);
return {
alias: node.alias,
earned_inbound_fees: sources.reduce((sum, n) => sum + n.fee, 0),
earned_outbound_fees: forwards.reduce((sum, n) => sum + n.fee, 0),
icons: !!nodeIcons ? nodeIcons.icons : undefined,
is_disconnected: isDisconnected || undefined,
is_forwarding: hasHtlcChannel || undefined,
is_inactive: !isDisconnected && !active.length || undefined,
is_pending: !!pending.length || undefined,
is_private: !!isHidden || undefined,
last_inbound_at: !lastIn ? undefined : lastIn.toISOString(),
last_outbound_at: !lastOut ? undefined : lastOut.toISOString(),
liquidity_inbound: remote,
liquidity_outbound: local,
public_key: node.id,
};
});
const sorted = peers
.sort((a, b) => {
if (args.sort === 'earned_in') {
if (a.earned_inbound_fees !== b.earned_inbound_fees) {
return a.earned_inbound_fees - b.earned_inbound_fees;
} else {
return b.earned_outbound_fees - a.earned_outbound_fees;
}
}
if (args.sort === 'earned_out') {
if (a.earned_outbound_fees !== b.earned_outbound_fees) {
return a.earned_outbound_fees - b.earned_outbound_fees;
} else {
return b.earned_inbound_fees - a.earned_inbound_fees;
}
}
if (args.sort === 'earned_total') {
const aTotal = a.earned_inbound_fees + a.earned_outbound_fees;
const bTotal = b.earned_inbound_fees + b.earned_outbound_fees;
return aTotal - bTotal;
}
if (args.sort === 'inbound') {
return a.liquidity_inbound - b.liquidity_inbound;
}
if (args.sort === 'liquidity') {
const aTotal = a.liquidity_inbound + a.liquidity_outbound;
const bTotal = b.liquidity_inbound + b.liquidity_outbound;
return aTotal - bTotal;
}
if (args.sort === 'outbound') {
return a.liquidity_outbound - b.liquidity_outbound;
}
const aEvents = [a.last_outbound_at, a.last_inbound_at];
const bEvents = [b.last_outbound_at, b.last_inbound_at];
const [lastA] = aEvents.filter(n => !!n).sort().reverse();
const [lastB] = bEvents.filter(n => !!n).sort().reverse();
return sort(lastA, lastB);
})
.filter(peer => {
return peer.earned_inbound_fees || peer.earned_outbound_fees;
})
.filter(peer => {
// Exit early when there is no tag filter
if (!args.tags.length) {
return true;
}
const {nodes} = getIcons;
const node = nodes.find(n => n.public_key === peer.public_key);
// Exit early when there is no matching tagged node
if (!node) {
return false;
}
return !!args.tags.find(tag => node.aliases.includes(tag));
});
return cbk(null, {peers: sorted});
}],
// Final forwards table
allForwards: ['forwards', ({forwards}, cbk) => {
if (!args.is_table) {
return cbk(null, {peers: forwards.peers});
}
const isWideSize = !size || size.get().width > wideSizeCols;
return cbk(null, {
peers: forwards.peers,
rows: []
.concat([notNull([
'Alias',
'Earned In',
'Earned Out',
'Inbound',
'Outbound',
!!isWideSize ? 'Public Key' : null,
]).map(n => !args.is_monochrome ? bold(n) : n)])
.concat(forwards.peers.map(peer => {
return notNull([
chartAliasForPeer({
alias: peer.alias,
icons: peer.icons,
is_disconnected: peer.is_disconnected,
is_forwarding: peer.is_forwarding,
is_inactive: peer.is_inactive,
is_pending: peer.is_pending,
is_private: peer.is_private,
public_key: peer.public_key,
}).display,
formatTokens({
is_monochrome: args.is_monochrome,
tokens: peer.earned_inbound_fees
}).display,
formatTokens({
is_monochrome: args.is_monochrome,
tokens: peer.earned_outbound_fees,
}).display,
formatTokens({
is_monochrome: args.is_monochrome,
tokens: peer.liquidity_inbound,
}).display,
formatTokens({
is_monochrome: args.is_monochrome,
tokens: peer.liquidity_outbound,
}).display,
!!isWideSize ? peer.public_key : null,
]);
})),
});
}],
},
returnResult({reject, resolve, of: 'allForwards'}, cbk));
});
};