UNPKG

balanceofsatoshis

Version:
440 lines (373 loc) 12.9 kB
const asyncAuto = require('async/auto'); const asyncMap = require('async/map'); const asyncWhilst = require('async/whilst'); const {findKey} = require('ln-sync'); const {formatTokens} = require('ln-sync'); const {getChannel} = require('ln-service'); const {getChannels} = require('ln-service'); const {getIdentity} = require('ln-service'); const {getNode} = require('ln-service'); const {getSyntheticOutIgnores} = require('probing'); const {getWalletVersion} = require('ln-service'); const {parseAmount} = require('ln-accounting'); const {parsePaymentRequest} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {subscribeToMultiPathProbe} = require('probing'); const {describeRoute} = require('./../display'); const {describeRoutingFailure} = require('./../display'); const {getIcons} = require('./../display'); const {getIgnores} = require('./../routing'); const {getTags} = require('./../tags'); const probeDestination = require('./probe_destination'); const defaultFinalCltvDelta = 144; const defaultMaxPaths = 5; const effectiveFeeRate = (n, m) => Number(BigInt(1e6) * BigInt(n) / BigInt(m)); const flatten = arr => [].concat(...arr); const {isArray} = Array; const pathTimeoutMs = 1000 * 60 * 90; const singlePath = 1; const uniq = arr => Array.from(new Set(arr)); const unsupported = 501; /** Probe a destination, looking for multiple non-overlapping paths { avoid: [<Avoid Forwarding Through String>] [destination]: <Destination Public Key Hex String> [find_max]: <Find Maximum Payable On Probed Routes Below Tokens Number> [fs]: { getFile: <Read File Contents Function> (path, cbk) => {} } [in_through]: <Pay In Through Public Key Hex String> [is_strict_max_fee]: <Avoid Probing Too-High Fee Routes Bool> lnd: <Authenticated LND API Object> logger: <Winston Logger Object> max_fee: <Max Fee Tokens Number> [max_paths]: <Maximum Probe Paths Number> out: [<Out Through Peer With Public Key Hex String>] [request]: <BOLT 11 Encoded Payment Request String> [timeout_minutes]: <Stop Searching For Routes After N Minutes Number> [tokens]: <Tokens Amount String> } @returns via cbk or Promise { [fee]: <Total Fee Tokens To Destination Number> [latency_ms]: <Latency Milliseconds Number> [relays]: [[<Relaying Public Key Hex String>]] [routes_maximum]: <Maximum Sendable Tokens on Paths Number> } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!isArray(args.avoid)) { return cbk([400, 'ExpectedAvoidArrayToProbe']); } if (!args.lnd) { return cbk([400, 'ExpectedLndApiObjectToProbe']); } if (!args.logger) { return cbk([400, 'ExpectedLoggerObjectToStartProbe']); } if (args.max_fee === undefined) { return cbk([400, 'ExpectedMaxFeeToleranceToProbeDestination']); } if (!isArray(args.out)) { return cbk([400, 'ExpectedArrayOfOutPeersToStartProbe']); } if (!!args.request) { try { parsePaymentRequest({request: args.request}); } catch (err) { return cbk([400, 'ExpectedValidPaymentRequestToProbe', {err}]); } } return cbk(); }, // Decode payment request decodeRequest: ['validate', ({}, cbk) => { // Exit early when there is no request to decode if (!args.request) { return cbk(null, {}); } const decoded = parsePaymentRequest({request: args.request}); return cbk(null, { cltv_delta: decoded.cltv_delta, destination: decoded.destination, features: decoded.features, routes: decoded.routes, }); }], // Get channels for figuring out avoid flags getChannels: ['validate', ({}, cbk) => { // Exit early when there are no avoids if (!args.avoid.length) { return cbk(); } return getChannels({lnd: args.lnd}, cbk); }], // Get node icons getIcons: ['validate', ({}, cbk) => { if (!args.fs || !args.find_max) { return cbk(); } return getIcons({fs: args.fs}, cbk); }], // Get the node public key getIdentity: ['validate', ({}, cbk) => { // Exit early when there are no avoids if (!args.avoid.length) { return cbk(); } return getIdentity({lnd: args.lnd}, cbk); }], // Find public keys to pay out through getOuts: ['validate', ({}, cbk) => { return asyncMap(args.out, (query, cbk) => { return findKey({query, lnd: args.lnd}, cbk); }, cbk); }], // Get tags for figuring out avoid flags getTags: ['validate', ({}, cbk) => { // Exit early when there are no avoids if (!args.avoid.length) { return cbk(); } return getTags({fs: args.fs}, cbk); }], // Determine if this wallet is a legacy isLegacy: ['validate', ({}, cbk) => { return getWalletVersion({lnd: args.lnd}, err => { if (!!err && err.slice().shift === unsupported) { return cbk(null, true); } if (!!err) { return cbk(err); } return cbk(null, false); }); }], // Parse amount to probe tokens: ['validate', ({}, cbk) => { // Exit early when no tokens are specified if (!args.tokens) { return cbk(); } try { return cbk(null, parseAmount({amount: args.tokens}).tokens); } catch (err) { return cbk([400, err.message]); } }], // Get base ignores getBaseIgnores: [ 'getChannels', 'getIdentity', 'getTags', ({getChannels, getIdentity, getTags}, cbk) => { // Exit early when there are no avoids if (!args.avoid.length) { return cbk(null, {ignore: []}); } const [out] = args.out || []; return getIgnores({ avoid: args.avoid, channels: getChannels.channels, in_through: args.in_through, lnd: args.lnd, logger: args.logger, out_through: out, public_key: getIdentity.public_key, tags: getTags.tags, }, cbk); }], // Get synthetic ignores to approximate out getIgnores: [ 'getBaseIgnores', 'getOuts', ({getBaseIgnores, getOuts}, cbk) => { // Exit early when not doing a multi-path if (!args.find_max && args.max_paths === singlePath) { return cbk(); } // Exit early when there is no outbound restriction if (!getOuts.length) { return cbk(null, {ignore: getBaseIgnores.ignore}); } return getSyntheticOutIgnores({ ignore: getBaseIgnores.ignore, lnd: args.lnd, out: getOuts.map(n => n.public_key), }, cbk); }], // Probe just through a single path singleProbe: [ 'getBaseIgnores', 'getOuts', 'tokens', ({getBaseIgnores, getOuts, tokens}, cbk) => { // Exit early when not finding max if (!!args.find_max || args.max_paths !== singlePath) { return cbk(); } // Exit early when probing on a single path if (getOuts.length > singlePath) { return cbk([501, 'MultipleOutPeersNotSupportedWithSinglePath']); } const [outThrough] = getOuts.map(n => n.public_key); return probeDestination({ tokens, destination: args.destination, fs: args.fs, ignore: getBaseIgnores.ignore, in_through: args.in_through, is_strict_max_fee: args.is_strict_max_fee, lnd: args.lnd, logger: args.logger, max_fee: args.max_fee, out_through: outThrough, request: args.request, }, cbk); }], // Get probe destination name getDestination: ['decodeRequest', ({decodeRequest}, cbk) => { const publicKey = decodeRequest.destination || args.destination; return getNode({ is_omitting_channels: true, lnd: args.lnd, public_key: publicKey, }, (err, res) => { if (!!err) { return cbk(null, publicKey); } return cbk(null, `${res.alias} ${publicKey}`.trim()); }); }], // Probe iteratively through multiple paths multiProbe: [ 'decodeRequest', 'getDestination', 'getIcons', 'getIgnores', 'getOuts', 'isLegacy', ({ decodeRequest, getDestination, getIcons, getIgnores, isLegacy, }, cbk) => { // Exit early when not doing a multi-path if (!args.find_max && args.max_paths === singlePath) { return cbk(); } if (!!args.is_strict_max_fee) { return cbk([501, 'StrictMaxFeeNotSupportedWithMultiPathProbes']); } // Exit with error when the backing LND is below 0.10.0 if (!!isLegacy) { return cbk([501, 'BackingLndDoesNotSupportMultiPathPayments']); } const paths = []; args.logger.info({probing: getDestination}); const sub = subscribeToMultiPathProbe({ cltv_delta: decodeRequest.cltv_delta || defaultFinalCltvDelta, destination: decodeRequest.destination || args.destination, features: decodeRequest.features, ignore: getIgnores.ignore, incoming_peer: args.in_through, lnd: args.lnd, max_fee: args.max_fee, max_paths: args.max_paths, path_timeout_ms: pathTimeoutMs, routes: decodeRequest.routes, }); sub.on('error', err => cbk(err)); sub.on('evaluating', ({tokens}) => { return args.logger.info({evaluating: tokens}); }); sub.on('failure', () => { return cbk([503, 'FailedToFindAnyPathsToDestination']); }); sub.on('path', path => { paths.push(path); const liquidity = paths.reduce((m, n) => m + n.liquidity, Number()); // Exit early when there is only one path if (args.max_paths === singlePath) { return; } return args.logger.info({ found_liquidity: formatTokens({tokens: liquidity}).display, found_paths: paths.length, }); }); sub.on('probing', async ({route}) => { const {description} = await describeRoute({ route, lnd: args.lnd, tagged: !!getIcons ? getIcons.nodes : undefined, }); return args.logger.info({probing: description}); }); sub.on('routing_failure', async failure => { const {description} = await describeRoutingFailure({ index: failure.index, lnd: args.lnd, reason: failure.reason, route: failure.route, tagged: !!getIcons ? getIcons.nodes : undefined, }); return args.logger.info({failure: description}); }); sub.on('success', ({paths}) => { const liquidity = paths.reduce((m, n) => m + n.liquidity, Number()); const fees = paths.reduce((m, n) => m + n.fee, Number()); const numPaths = paths.filter(n => !!n).length; const target = !args.find_max ? decodeRequest.tokens : undefined; args.logger.info({ target_amount: !!target ? formatTokens({tokens: target}) : target, total_liquidity: formatTokens({tokens: liquidity}).display, total_fee: formatTokens({tokens: fees}).display, total_fee_rate: String(effectiveFeeRate(fees, liquidity)), total_paths: args.max_paths !== singlePath ? numPaths : undefined, }); return cbk(); }); return; }], // Results of probe probe: [ 'multiProbe', 'singleProbe', ({multiProbe, singleProbe}, cbk) => { return cbk(null, multiProbe || singleProbe); }], // Check the fee checkFee: ['probe', ({probe}, cbk) => { if (!probe || probe.fee === undefined) { return cbk(); } if (probe.fee > args.max_fee) { return cbk([503, 'FailedToFindPathUnderMaxFee', { max_fee: args.max_fee, needed_fee: probe.fee, }]); } return cbk(); }], }, returnResult({reject, resolve, of: 'probe'}, cbk)); }); };