UNPKG

ln-telegram

Version:
289 lines (240 loc) 8.99 kB
const asyncAuto = require('async/auto'); const asyncDetect = require('async/detect'); const asyncMap = require('async/map'); const {balancedOpenRequest} = require('paid-services'); const {getChannel} = require('ln-service'); const {getNodeAlias} = require('ln-sync'); const {returnResult} = require('asyncjs-util'); const {subscribeToPastPayment} = require('ln-service'); const getBalancedOpenMessage = require('./get_balanced_open_message'); const getRebalanceMessage = require('./get_rebalance_message'); const getReceivedMessage = require('./get_received_message'); const {icons} = require('./../interface'); const escape = text => text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\\$&'); const {isArray} = Array; const minQuizLength = 2; const maxQuizLength = 10; const randomIndex = n => Math.floor(Math.random() * n); const sendOptions = {parse_mode: 'MarkdownV2'}; const uniq = arr => Array.from(new Set(arr)); /** Post settled invoices { from: <Invoice From Node String> id: <Connected User Id Number> invoice: { description: <Invoice Description String> id: <Invoice Preimage Hash Hex String> is_confirmed: <Invoice is Settled Bool> payments: [{ [confirmed_at]: <Payment Settled At ISO 8601 Date String> created_at: <Payment Held Since ISO 860 Date String> created_height: <Payment Held Since Block Height Number> in_channel: <Incoming Payment Through Channel Id String> is_canceled: <Payment is Canceled Bool> is_confirmed: <Payment is Confirmed Bool> is_held: <Payment is Held Bool> messages: [{ type: <Message Type Number String> value: <Raw Value Hex String> }] mtokens: <Incoming Payment Millitokens String> [pending_index]: <Pending Payment Channel HTLC Index Number> tokens: <Payment Tokens Number> [total_mtokens]: <Total Payment Millitokens String> }] received: <Received Tokens Number> } key: <Node Public Key Id Hex String> lnd: <Authenticated LND API Object> [min_rebalance_tokens]: <Minimum Rebalance Tokens To Notify Number> nodes: [{ from: <From Node String> lnd: <Authenticated LND API Object> public_key: <Node Identity Public Key Hex String> }] quiz: ({answers: [<String>], correct: <Number>, question: <String>}) => {} send: <Send Message Function> (id, message, options) => {} } @returns via cbk or Promise */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.from) { return cbk([400, 'ExpectedFromNameToPostSettledInvoice']); } if (!args.id) { return cbk([400, 'ExpectedUserIdNumberToPostSettledInvoice']); } if (!args.invoice) { return cbk([400, 'ExpectedInvoiceToPostSettledInvoice']); } if (!args.key) { return cbk([400, 'ExpectedNodeIdentityKeyToPostSettledInvoice']); } if (!args.lnd) { return cbk([400, 'ExpectedLndObjectToPostSettledInvoice']); } if (!isArray(args.nodes)) { return cbk([400, 'ExpectedArrayOfNodesToPostSettledInvoice']); } if (!args.quiz) { return cbk([400, 'ExpectedSendQuizFunctionToPostSettledInvoice']); } if (!args.send) { return cbk([400, 'ExpectedSendFunctionToPostSettledInvoice']); } return cbk(); }, // Parse balanced open request details if present balancedOpen: ['validate', ({}, cbk) => { // A proposal will be a push payment if (!args.invoice.is_confirmed) { return cbk(); } const {proposal} = balancedOpenRequest({ confirmed_at: args.invoice.confirmed_at, is_push: args.invoice.is_push, payments: args.invoice.payments, received_mtokens: args.invoice.received_mtokens, }); return cbk(null, proposal); }], // Get the node aliases that forwarded this getNodes: ['validate', ({}, cbk) => { const inChannels = uniq(args.invoice.payments.map(n => n.in_channel)); return asyncMap(inChannels, (id, cbk) => { return getChannel({id, lnd: args.lnd}, (err, res) => { if (!!err) { return cbk(null, {id, alias: id}); } const peer = res.policies.find(n => n.public_key !== args.key); return getNodeAlias({id: peer.public_key, lnd: args.lnd}, cbk); }); }, cbk); }], // Find associated payment getPayment: ['validate', ({}, cbk) => { // Exit early when the invoice has yet to be confirmed if (!args.invoice.is_confirmed) { return cbk(); } const sub = subscribeToPastPayment({ id: args.invoice.id, lnd: args.lnd, }); sub.once('confirmed', payment => cbk(null, {payment})); sub.once('error', () => cbk()); sub.once('failed', () => cbk()); return; }], // Find associated transfer getTransfer: ['validate', ({}, cbk) => { // Exit early when the invoice has yet to be confirmed if (!args.invoice.is_confirmed) { return cbk(); } const otherNodes = args.nodes.filter(n => n.public_key !== args.key); return asyncDetect(otherNodes, ({lnd}, cbk) => { const sub = subscribeToPastPayment({lnd, id: args.invoice.id}); sub.once('confirmed', payment => cbk(null, true)); sub.once('error', () => cbk(null, false)); sub.once('failed', () => cbk(null, false)); }, cbk); }], // Details for message details: [ 'balancedOpen', 'getNodes', 'getPayment', 'getTransfer', ({balancedOpen, getNodes, getPayment, getTransfer}, cbk) => { // Exit early when the invoice has yet to be confirmed if (!args.invoice.is_confirmed) { return cbk(); } // Exit early when this is a node to node transfer if (!!getTransfer) { return cbk(); } // Exit early when this is a balanced open if (!!balancedOpen) { return getBalancedOpenMessage({ capacity: balancedOpen.capacity, from: balancedOpen.partner_public_key, lnd: args.lnd, rate: balancedOpen.fee_rate, }, cbk); } const isRebalance = !!getPayment; // Exit early with no message when the rebalance amount is too small if (isRebalance && args.invoice.received < args.min_rebalance_tokens) { return cbk(); } // Exit early when the received invoice is for a rebalance (self-pay) if (!!isRebalance) { return getRebalanceMessage({ fee_mtokens: getPayment.payment.fee_mtokens, hops: getPayment.payment.hops, lnd: args.lnd, payments: args.invoice.payments, received_mtokens: args.invoice.received_mtokens, }, cbk); } return getReceivedMessage({ description: args.invoice.description, lnd: args.lnd, payments: args.invoice.payments, received: args.invoice.received, via: getNodes, }, cbk); }], // Post invoice post: ['details', 'getPayment', async ({details, getPayment}) => { // Exit early when there is nothing to post if (!details) { return; } // Determine if node qualifier is necessary const isMultiNode = args.nodes.length > [args.key].length; const receivedOnNode = isMultiNode ? escape(` - ${args.from}`) : ''; const message = `${details.icon} ${details.message}${receivedOnNode}`; return await args.send(args.id, message, sendOptions); }], // Post quiz quiz: ['details', 'post', async ({details, post}) => { // Exit early when there is no quiz if (!details || !details.quiz || details.quiz.length < minQuizLength) { return; } // Exit early when the quiz has too many answers if (details.quiz.length > maxQuizLength) { return; } const [answer] = details.quiz; const correct = randomIndex(details.quiz.length); const replace = details.quiz[correct]; // Randomize the position of the correct answer const answers = details.quiz.map((n, i) => { if (i === correct) { return answer; } if (!i) { return replace; } return n; }); return await args.quiz({answers, correct, question: details.title}); }], }, returnResult({reject, resolve}, cbk)); }); };