UNPKG

ln-telegram

Version:
243 lines (205 loc) 7.46 kB
const asyncAuto = require('async/auto'); const asyncDetect = require('async/detect'); const asyncFilter = require('async/filter'); const asyncMap = require('async/map'); const asyncUntil = require('async/until'); const {getBorderCharacters} = require('table'); const {getInvoices} = require('ln-service'); const {getPayment} = require('ln-service'); const renderTable = require('table').table; const {getForwards} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {checkAccess} = require('./../authentication'); const {formatTokens} = require('./../interface'); const border = getBorderCharacters('void'); const dayMs = 1000 * 60 * 60 * 24; const defaultInvoicesLimit = 100; const earnedTokens = tokens => formatTokens({tokens, none: '-'}).display; const earnedViaInvoices = 'Invoiced'; const earnedViaRouting = 'Routing'; const formatReport = (from, n) => `💰 Earned on ${from}\n\n\`\`\`${n}\`\`\``; const formatReports = reports => reports.join('\n'); const header = ['', 'Day', 'Week']; const {isArray} = Array; const limit = 999999; const notFound = 404; const {now} = Date; const sumOf = arr => arr.reduce((sum, n) => sum + n, BigInt(Number())); const tokFromMtok = mtok => Number(BigInt(mtok) / BigInt(1e3)); const weekMs = 1000 * 60 * 60 * 24 * 7; /** Check node earnings Syntax of command: /earnings { from: <Command From User Id Number> id: <Connected User Id Number> nodes: [{ from: <From Name String> lnd: <Authenticated LND API Object> public_key: <Public Key Hex String> }] reply: <Reply Function> working: <Reply Bot is Working Function> } */ module.exports = ({from, id, nodes, reply, working}, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!from) { return cbk([400, 'ExpectedFromUserIdNumberForEarningsCommand']); } if (!reply) { return cbk([400, 'ExpectedReplyFunctionForEarningsCommand']); } if (!working) { return cbk([400, 'ExpectedWorkingFunctionForEarningsCommand']); } return cbk(); }, // Authenticate the command caller is authorized to this command checkAccess: ['validate', ({}, cbk) => checkAccess({from, id}, cbk)], // Get invoices getInvoices: ['checkAccess', ({}, cbk) => { working(); return asyncMap(nodes, (node, cbk) => { const after = new Date(now() - weekMs).toISOString(); const dayStart = new Date(now() - dayMs).toISOString(); const invoices = []; let token; // Pull invoices until they are older than the start date return asyncUntil( cbk => cbk(null, token === false), cbk => { return getInvoices({ token, limit: !token ? defaultInvoicesLimit : undefined, lnd: node.lnd, }, (err, res) => { if (!!err) { return cbk(err); } token = res.next || false; // Stop paging when there is an invoice older than the start if (!!res.invoices.find(n => n.created_at < after)) { token = false; } // Collect invoices that confirmed after the start res.invoices .filter(n => !!n.confirmed_at) .filter(n => n.confirmed_at >= after) .forEach(n => invoices.push(n)); return cbk(); }); }, err => { if (!!err) { return cbk(err); } // Filter out invoices that are actually self-payments return asyncFilter(invoices, (invoice, cbk) => { return asyncDetect(nodes, (node, cbk) => { return getPayment({ id: invoice.id, lnd: node.lnd, }, (err, res) => { if (isArray(err) && err.shift() === notFound) { return cbk(null, false); } if (!!err) { return cbk(err); } return cbk(null, !!res.payment); }); }, (err, payment) => { if (!!err) { return cbk(err); } // No found payment on any node means it was externally paid return cbk(null, !payment); }); }, (err, received) => { if (!!err) { return cbk(err); } const day = received.filter(n => n.confirmed_at >= dayStart); return cbk(null, { day: sumOf(day.map(n => BigInt(n.received_mtokens))), public_key: node.public_key, week: sumOf(received.map(n => BigInt(n.received_mtokens))), }); }); } ); }, cbk); }], // Get forwards getForwards: ['checkAccess', ({}, cbk) => { const after = new Date(now() - weekMs).toISOString(); const before = new Date().toISOString(); const dayStart = new Date(now() - dayMs).toISOString(); return asyncMap(nodes, (node, cbk) => { const {lnd} = node; return getForwards({after, before, limit, lnd}, (err, res) => { if (!!err) { return cbk(err); } const day = res.forwards.filter(n => n.created_at >= dayStart); return cbk(null, { day: sumOf(day.map(n => BigInt(n.fee_mtokens))), public_key: node.public_key, week: sumOf(res.forwards.map(n => BigInt(n.fee_mtokens))), }); }); }, cbk); }], // Determine the reply to send to Telegram response: [ 'getForwards', 'getInvoices', ({getForwards, getInvoices}, cbk) => { const reports = getForwards.map(node => { const key = node.public_key; const {from} = nodes.find(n => n.public_key === key); const got = getInvoices.find(n => n.public_key === key); // Exit early when there are no earnings if (!got.week && !node.week) { return formatReport(from, '- No earnings in the past week'); } const rows = [ [ earnedViaRouting, earnedTokens(tokFromMtok(node.day)), earnedTokens(tokFromMtok(node.week)), ], [ earnedViaInvoices, earnedTokens(tokFromMtok(got.day)), earnedTokens(tokFromMtok(got.week)), ], ]; const chart = renderTable([header].concat(rows), { border, singleLine: true, }); return formatReport(from, chart); }); return cbk(null, formatReports(reports)); }], // Send response to telegram reply: ['response', ({response}, cbk) => { reply(response); return cbk(); }], }, returnResult({reject, resolve}, cbk)); }); };