balanceofsatoshis
Version:
Lightning balance CLI
496 lines (404 loc) • 14.4 kB
JavaScript
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const {bolden} = require('@alexbosworth/html2unicode');
const {decodeChanId} = require('bolt07');
const {getAutopilot} = require('ln-service');
const {getBackups} = require('ln-service');
const {getChainFeeRate} = require('ln-service');
const {getChannel} = require('ln-service');
const {getChannels} = require('ln-service');
const {getClosedChannels} = require('ln-service');
const {getInvoice} = require('ln-service');
const {getInvoices} = require('ln-service');
const {getNetwork} = require('ln-sync');
const {getNetworkGraph} = require('ln-service');
const {getPayments} = require('ln-sync');
const {getWalletInfo} = require('ln-service');
const {italicize} = require('@alexbosworth/html2unicode');
const moment = require('moment');
const {parsePaymentRequest} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {authenticatedLnd} = require('./../lnd');
const channelsAsReportActivity = require('./channels_as_report_activity');
const {currencyForNetwork} = require('./../network');
const {getBalance} = require('./../balances');
const {getForwards} = require('./../network');
const reportOverview = require('./report_overview');
const afterMs = 1000 * 60 * 60 * 24;
const centsPerDollar = 100;
const defaultConfTarget = 6;
const fiat = 'USD';
const formatAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
const limit = 1000;
const msPerBlock = 1000 * 60 * 10;
const {now} = Date;
const rateProvider = 'coingecko';
const sumOf = arr => arr.reduce((sum, n) => n + sum, 0);
const styled = 'styled';
/** Get report
{
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
[node]: <Node Name String>
request: <Request Function>
[style]: <Style Type String>
}
@returns via cbk
{}
*/
module.exports = ({fs, node, request, style}, cbk) => {
return asyncAuto({
// Get authenticated lnd connection
getLnd: cbk => authenticatedLnd({node}, cbk),
// Get balance
getBalance: ['getLnd', ({getLnd}, cbk) => {
return getBalance({node, lnd: getLnd.lnd}, cbk);
}],
// Get forwards
getForwards: ['getLnd', ({getLnd}, cbk) => {
return getForwards({fs, lnd: getLnd.lnd, tags: []}, cbk);
}],
// Get autopilot status
getAutopilot: ['getLnd', ({getLnd}, cbk) => {
return getAutopilot({lnd: getLnd.lnd}, (err, res) => {
if (!!err) {
return cbk(null, {});
}
return cbk(null, res);
});
}],
// Get backups
getBackups: ['getLnd', ({getLnd}, cbk) => {
return getBackups({lnd: getLnd.lnd}, cbk);
}],
// Get channels
getChannels: ['getLnd', ({getLnd}, cbk) => {
return getChannels({lnd: getLnd.lnd}, cbk);
}],
// Get closed channels
getClosed: ['getLnd', ({getLnd}, cbk) => {
return getClosedChannels({lnd: getLnd.lnd}, cbk);
}],
// Get chain fee rate
getChainFee: ['getLnd', ({getLnd}, cbk) => {
return getChainFeeRate({
lnd: getLnd.lnd,
confirmation_target: defaultConfTarget,
},
(err, res) => {
if (!!err) {
return cbk();
}
return cbk(null, res);
});
}],
// Get network graph
getGraph: ['getLnd', ({getLnd}, cbk) => {
return getNetworkGraph({lnd: getLnd.lnd}, cbk);
}],
// Get wallet info
getInfo: ['getLnd', ({getLnd}, cbk) => {
return getWalletInfo({lnd: getLnd.lnd}, cbk);
}],
// Get invoices
getInvoices: ['getLnd', ({getLnd}, cbk) => {
return getInvoices({lnd: getLnd.lnd}, cbk);
}],
// Get network
getNetwork: ['getLnd', ({getLnd}, cbk) => {
return getNetwork({lnd: getLnd.lnd}, cbk);
}],
// Get payments
getPayments: ['getLnd', ({getLnd}, cbk) => {
return getPayments({
limit,
after: new Date(now() - afterMs).toISOString(),
lnd: getLnd.lnd,
},
cbk);
}],
// Currency
currency: ['getInfo', ({getInfo}, cbk) => {
const {currency} = currencyForNetwork({chains: getInfo.chains});
return cbk(null, currency);
}],
// Get rebalances
getRebalances: [
'getInfo',
'getLnd',
'getPayments',
({getInfo, getLnd, getPayments}, cbk) =>
{
const rebalances = getPayments.payments.slice().reverse()
.filter(payment => now() - Date.parse(payment.created_at) < afterMs)
.filter(payment => payment.destination === getInfo.public_key);
return asyncMap(rebalances, (rebalance, cbk) => {
return getInvoice({id: rebalance.id, lnd: getLnd.lnd}, (err, res) => {
if (!!err) {
return cbk(err);
}
const [outHop] = rebalance.hops;
const [payment] = res.payments;
if (!payment) {
return cbk(null, {
created_at: rebalance.created_at,
fee: rebalance.fee,
out_peer: outHop,
tokens: rebalance.tokens,
});
}
return getChannel({
id: payment.in_channel,
lnd: getLnd.lnd,
},
(err, channel) => {
if (!!err) {
return cbk(null, {
created_at: rebalance.created_at,
fee: rebalance.fee,
out_peer: outHop,
tokens: rebalance.tokens,
});
}
const inPeer = channel.policies.find(policy => {
return policy.public_key !== getInfo.public_key;
});
return cbk(null, {
created_at: rebalance.created_at,
fee: rebalance.fee,
in_peer: inPeer.public_key,
out_peer: outHop,
tokens: rebalance.tokens,
});
});
});
},
cbk);
}],
report: [
'currency',
'getAutopilot',
'getBackups',
'getBalance',
'getChainFee',
'getChannels',
'getForwards',
'getGraph',
'getInfo',
'getInvoices',
'getNetwork',
'getPayments',
'getRebalances',
({
currency,
getAutopilot,
getBackups,
getBalance,
getChainFee,
getChannels,
getClosed,
getForwards,
getGraph,
getInfo,
getInvoices,
getNetwork,
getPayments,
getRebalances,
}, cbk) =>
{
const activity = [];
const currentHeight = getInfo.current_block_height;
const {nodes} = getGraph;
const findNode = pk => nodes.find(n => n.public_key === pk) || {};
const {report} = reportOverview({
currency,
alias: getInfo.alias,
balance: getBalance.balance,
chain_fee: !getChainFee ? undefined : getChainFee.tokens_per_vbyte,
channel_balance: getBalance.channel_balance,
latest_block_at: getInfo.latest_block_at,
public_key: getInfo.public_key,
});
const channelsActivity = channelsAsReportActivity({
now,
backups: getBackups.channels,
chain: {
currency,
height: getInfo.current_block_height,
network: getNetwork.network,
},
channels: getChannels.channels.slice().reverse(),
days: 1,
nodes: getGraph.nodes,
});
channelsActivity.activity.forEach(n => activity.push(n));
getInvoices.invoices.slice().reverse()
.filter(invoice => !!invoice.confirmed_at)
.filter(invoice => now() - Date.parse(invoice.confirmed_at) < afterMs)
.filter(invoice => {
const isToSelf = getPayments.payments.find(n => n.id === invoice.id);
return !isToSelf;
})
.forEach(invoice => {
const elements = [];
const received = invoice.received;
elements.push({
subtitle: moment(invoice.confirmed_at).fromNow(),
title: getInfo.alias || getInfo.public_key,
});
elements.push({
action: 'Received payment',
});
elements.push({
is_hidden: !invoice.description,
details: `"${invoice.description}"`,
});
elements.push({
details: `Received: ${formatAsBigUnit(received)} ${currency}`,
});
return activity.push({elements, date: invoice.confirmed_at});
});
getRebalances.forEach(rebalance => {
const elements = [];
const {fee} = rebalance;
const {tokens} = rebalance;
const amount = `${formatAsBigUnit(tokens)} ${currency}`;
const inHop = rebalance.in_peer;
const outHop = rebalance.out_peer;
const inNode = getGraph.nodes.find(n => n.public_key === inHop);
const outNode = getGraph.nodes.find(n => n.public_key === outHop);
const inbound = (inNode || {}).alias || (inNode || {}).public_key;
const outbound = (outNode || {}).alias || (outNode || {}).public_key;
elements.push({
subtitle: moment(rebalance.created_at).fromNow(),
title: getInfo.alias || getInfo.public_key,
});
elements.push({action: 'Rebalance'});
elements.push({
details: `Increased inbound liquidity on ${outbound} by ${amount}`,
});
if (!!inbound) {
elements.push({
details: `Decreased inbound liquidity on ${inbound}`,
});
}
elements.push({
details: `Fee: ${formatAsBigUnit(fee)} ${currency}`,
});
return activity.push({elements, date: rebalance.created_at});
});
getPayments.payments.slice().reverse()
.filter(payment => now() - Date.parse(payment.created_at) < afterMs)
.filter(payment => payment.destination !== getInfo.public_key)
.forEach(payment => {
const elements = [];
const node = findNode(payment.destination);
const {request} = payment;
elements.push({
subtitle: moment(payment.created_at).fromNow(),
title: node.alias || payment.destination,
});
elements.push({action: 'Sent payment'});
if (payment.request) {
elements.push({
details: `"${parsePaymentRequest({request}).description}"`,
});
}
elements.push({
details: `Sent: ${formatAsBigUnit(payment.tokens)} ${currency}`,
});
if (!!payment.fee) {
elements.push({
details: `Fee: ${formatAsBigUnit(payment.fee)} ${currency}`,
});
}
return activity.push({elements, date: payment.created_at});
});
getClosed.channels
.filter(channel => currentHeight - channel.close_confirm_height < 160)
.forEach(channel => {
const closeHeight = channel.close_confirm_height;
const node = findNode(channel.partner_public_key);
const msSinceClose = (currentHeight - closeHeight) * msPerBlock;
const channels = getChannels.channels
.filter(n => n.partner_public_key === channel.partner_public_key);
const elements = [];
const date = moment(now() - msSinceClose);
elements.push({
subtitle: date.fromNow(),
title: node.alias || channel.partner_public_key,
});
elements.push({
action: 'Channel closed',
});
const remoteBalance = channels.map(n => n.remote_balance);
const localBalance = channels.map(n => n.local_balance);
const inbound = formatAsBigUnit(sumOf(remoteBalance));
const outbound = formatAsBigUnit(sumOf(localBalance));
const inboundLiquidity = `${inbound} ${currency} inbound`;
const outboundLiquidity = `${outbound} ${currency} outbound`;
elements.push({
details: `Liquidity now ${inboundLiquidity}, ${outboundLiquidity}`,
});
return activity.push({elements, date: date.toISOString()});
});
getForwards.peers.slice().reverse().forEach(peer => {
const lastActivity = [peer.last_inbound_at, peer.last_outbound_at];
const elements = [];
const [last] = lastActivity.sort();
elements.push({
subtitle: moment(last).fromNow(),
title: peer.alias,
});
elements.push({action: 'Routing activity'});
if (!!peer.earned_inbound_fees) {
const inbound = formatAsBigUnit(peer.earned_inbound_fees);
elements.push({
details: `Earned from inbound routing: ${inbound} ${currency}`,
});
}
if (!!peer.earned_outbound_fees) {
const outbound = formatAsBigUnit(peer.earned_outbound_fees);
elements.push({
details: `Earned from outbound routing: ${outbound} ${currency}`,
});
}
const inbound = formatAsBigUnit(peer.liquidity_inbound);
elements.push({
details: `Inbound liquidity: ${inbound} ${currency}`,
});
const outbound = formatAsBigUnit(peer.liquidity_outbound);
elements.push({
details: `Outbound liquidity: ${outbound} ${currency}`,
});
return activity.push({elements, date: last});
});
if (!!activity.length) {
report.push({});
report.push({title: 'Recent Activity:'});
}
activity.sort((a, b) => a.date > b.date ? -1 : 1);
activity.forEach(({elements}) => {
report.push({});
return elements.forEach(element => report.push(element))
});
const renderReport = (lines) => {
return lines
.filter(n => !n.is_hidden)
.map(({action, details, subtitle, title}) => {
const elements = [
!!title && style === styled ? bolden(title) : title,
subtitle,
details,
!!action && style === styled ? italicize(action) : action,
];
return elements.filter(n => !!n).join(' ');
})
.join('\n');
}
return cbk(null, renderReport(report));
}],
},
returnResult({of: 'report'}, cbk));
};