UNPKG

balanceofsatoshis

Version:
375 lines (306 loc) 12.5 kB
const {createHash} = require('crypto'); const {randomBytes} = require('crypto'); const asyncAuto = require('async/auto'); const asyncFilterLimit = require('async/filterLimit'); const asyncMapSeries = require('async/mapSeries'); const asyncTimeout = require('async/timeout'); const {createInvoice} = require('ln-service'); const {getChannels} = require('ln-service'); const {getNetworkGraph} = require('ln-service'); const {getRouteToDestination} = require('ln-service'); const {getWalletInfo} = require('ln-service'); const {payViaRoutes} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {subscribeToProbeForRoute} = require('ln-service'); const getNodesGraph = require('./get_nodes_graph'); const {getTags} = require('./../tags'); const {isMatchingFilters} = require('./../display'); const {shuffle} = require('./../arrays'); const {ceil} = Math; const cltvDelay = 144; const createSecret = () => randomBytes(32).toString('hex'); const defaultFilter = ['channels_count > 9']; const defaultMsg = (alias, key) => `Check out my node! ${alias} ${key}`; const directPeersDistance = 0; const featureKeysendBit = 55; const filterLimit = 10; const flatten = arr => [].concat(...arr); const hashOf = n => createHash('sha256').update(n).digest().toString('hex'); const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const invoiceDescription = n => `👀 ${n}`; const invoiceExpiration = () => new Date(Date.now() + 1000 * 60 * 60 * 24 * 5); const invoiceTokens = 1; const {isArray} = Array; const keySendValueType = '5482373484'; const maxFeeTokens = 10; const messageWithReply = (msg, req) => `${msg} (Mark seen: ${req})`; const minChannelCount = 10; const mtokPerToken = 1e3; const pathTimeoutMs = 1000 * 45; const payTimeoutMs = 1000 * 60; const probeTimeoutMs = 1000 * 60 * 2; const routeDistance = route => route.hops.length - 1; const sendTokens = 10; const sumOf = arr => arr.reduce((sum, n) => sum + n, Number()); const textMessageType = '34349334'; const uniq = arr => Array.from(new Set(arr)); const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex'); /** Advertise to nodes that accept KeySend { filters: [<Node Condition Filter String>] fs: { getFile: <Read File Contents Function> (path, cbk) => {} } [is_dry_run]: <Avoid Sending Advertisements Bool> lnd: <Authenticated LND API Object> logger: <Winston Logger Object> [message]: <Message To Send String> [max_hops]: <Maxmimum Relaying Nodes Number> [min_hops]: <Minimum Relaying Nodes Number> tags: [<Tag Name String>] } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!isArray(args.filters)) { return cbk([400, 'ExpectedArrayOfFiltersToAdvertise']); } if (!args.fs) { return cbk([400, 'ExpectedFileSystemMethodsToAdvertise']); } if (!args.lnd) { return cbk([400, 'ExpectedAuthenticatedLndToAdvertise']); } if (!args.logger) { return cbk([400, 'ExpectedWinstonLoggerToAdvertise']); } if (!isArray(args.tags)) { return cbk([400, 'ExpectedArrayOfTagsToAdvertiseTo']); } return cbk(); }, // Get channels to substitute for graph when ads are for peers only getChannels: ['validate', ({}, cbk) => { // Exit early when advertising to more than direct peers if (args.max_hops !== 0) { return cbk(); } return getChannels({lnd: args.lnd}, cbk); }], // Get the local identity to compose the ad message getIdentity: ['validate', ({}, cbk) => { return getWalletInfo({lnd: args.lnd}, cbk); }], // Get the list of tags and filter them. getTags: ['getIdentity', ({getIdentity}, cbk) => { // Exit early when there are no tags specified to advertise to if (!args.tags.length) { return cbk(); } return getTags({fs: args.fs}, (err, res) => { if (!!err) { return cbk(err); } // Look for a referenced tag that doesn't match a present tag const unknown = args.tags.find(tagName => { return !res.tags.find(t => t.alias === tagName); }); // Make sure the tag is present if (!!unknown) { return cbk([404, 'FailedToFindTagWithSpecifiedName', {unknown}]); } // Collect all the node identity keys const matches = args.tags.map(tagName => { const tag = res.tags.find(t => t.alias === tagName); return tag.nodes.filter(n => n !== getIdentity.public_key); }); // Merge all tagged nodes and remove duplicates return cbk(null, uniq(flatten(matches))); }); }], // Get the graph to use to find candidates to send ads to getGraph: ['getChannels', 'getTags', ({getChannels, getTags}, cbk) => { // Exit early when when advertising to tags. if (!!args.tags.length) { return getNodesGraph({lnd: args.lnd, nodes: getTags}, cbk); } // Exit early when using the entire graph if (args.max_hops !== directPeersDistance) { args.logger.info({sending_to_all_graph_nodes: true}); return getNetworkGraph({lnd: args.lnd}, cbk); } // Collect all ids of channel peers const nodes = getChannels.channels.map(n => n.partner_public_key); return getNodesGraph({lnd: args.lnd, nodes: uniq(nodes)}, cbk); }], // Derive the message to send message: ['getIdentity', ({getIdentity}, cbk) => { const alias = getIdentity.alias || String(); const identity = getIdentity.public_key; const message = args.message || defaultMsg(alias, identity); args.logger.info({sending_message: message}); return cbk(null, message); }], // Filter out nodes where there is no path nodes: [ 'getGraph', 'getIdentity', 'message', ({getGraph, getIdentity, message}, cbk) => { const {shuffled} = shuffle({array: getGraph.nodes}); // Only consider 3rd party nodes that have known features const filteredNodes = shuffled .filter(n => n.public_key !== getIdentity.public_key) .filter(n => !!n.features.length) .filter(n => n.features.map(n => n.bit).includes(featureKeysendBit)) .map(node => { const channels = getGraph.channels.filter(channel => { const {policies} = channel; return policies.find(n => n.public_key === node.public_key); }); const filters = args.filters.length ? args.filters : defaultFilter; const variables = { capacity: sumOf(channels.map(n => n.capacity)), channels_count: channels.length, }; const isMatching = isMatchingFilters({filters, variables}); // Exit early when there is a failure with a filter if (isMatching.failure) { return {failure: isMatching.failure}; } return !!isMatching.is_matching ? {node} : {}; }); const failure = filteredNodes.find(n => !!n.failure); if (!!filteredNodes.find(n => !!n.failure)) { return cbk([400, 'ExpectedValidFiltersForNodes', {failure}]); } const nodes = filteredNodes.map(n => n.node).filter(n => !!n); args.logger.info({potential_nodes: nodes.length}); return asyncFilterLimit(nodes, filterLimit, (node, cbk) => { return getRouteToDestination({ destination: node.public_key, lnd: args.lnd, max_fee: maxFeeTokens, messages: [ {type: keySendValueType, value: createSecret()}, {type: textMessageType, value: utf8AsHex(message)}, ], tokens: sendTokens, }, (err, res) => { // Exit early when there is a problem getting a route if (!!err || !res.route) { return cbk(null, false); } const relaysCount = routeDistance(res.route); // Exit early when there are too many relaying nodes if (args.max_hops !== undefined && relaysCount > args.max_hops) { return cbk(null, false); } // Exit early when there are too few relaying nodes if (args.min_hops !== undefined && relaysCount < args.min_hops) { return cbk(null, false); } // There exists a route to the destination within range constraints return cbk(null, true); }); }, cbk); }], // Send message to nodes send: ['message', 'nodes', ({message, nodes}, cbk) => { const maxSpendPerNode = sendTokens + maxFeeTokens; const sent = []; args.logger.info({routable_nodes: nodes.length}); return asyncMapSeries(nodes, (node, cbk) => { const paidTokens = ceil(Number(sent.reduce((sum, n) => { return sum + BigInt(n.mtokens) + BigInt(n.fee_mtokens); }, BigInt(Number()))) / mtokPerToken); // Check that the potential next ad would not go over budget if (!!args.budget && paidTokens + maxSpendPerNode > args.budget) { return cbk([400, 'AdvertisingBudgetExhausted']); } const probe = subscribeToProbeForRoute({ cltv_delay: cltvDelay, destination: node.public_key, lnd: args.lnd, max_fee: maxFeeTokens, messages: [ {type: keySendValueType, value: createSecret()}, {type: textMessageType, value: utf8AsHex(message)}, ], path_timeout_ms: pathTimeoutMs, probe_timeout_ms: probeTimeoutMs, tokens: sendTokens, }); let isFinished = false; const timeout = setTimeout(() => { isFinished = true; probe.removeAllListeners(); return cbk(); }, payTimeoutMs); probe.on('end', () => { if (isFinished) { return; } clearTimeout(timeout); return cbk(); }); probe.on('error', () => {}); probe.on('probe_success', async ({route}) => { try { // Create a "mark seen" invoice const invoice = await createInvoice({ description: invoiceDescription(node.public_key), expires_at: invoiceExpiration(), lnd: args.lnd, tokens: invoiceTokens, }); // Add a reply payment request to the message const finalMessage = messageWithReply(message, invoice.request); // Generate the preimage const secret = createSecret(); // Encode the preimage and the advertising message in messages route.messages = [ {type: keySendValueType, value: secret}, {type: textMessageType, value: utf8AsHex(finalMessage)}, ]; // Exit early when this is a dry run if (!!args.is_dry_run) { return args.logger.info({skipping_due_to_dry_run: node.alias}); } // Send the payment const paid = await payViaRoutes({ id: hashOf(hexAsBuffer(secret)), lnd: args.lnd, routes: [route], }); sent.push(paid); args.logger.info({ sent_to: `${node.alias || String()} ${node.public_key}`, paid: paid.fee + paid.tokens, total: { ads: sent.length, paid: ceil(Number(sent.reduce((sum, n) => { return sum + BigInt(n.mtokens) + BigInt(n.fee_mtokens); }, BigInt(Number()))) / mtokPerToken), }, }); } catch (err) { } }); return; }, cbk); }], }, returnResult({reject, resolve}, cbk)); }); };