UNPKG

balanceofsatoshis

Version:
304 lines (264 loc) 9.5 kB
const asyncAuto = require('async/auto'); const asyncMap = require('async/map'); const asyncWhilst = require('async/whilst'); const {getChannel} = require('ln-service'); const {getHeight} = require('ln-service'); const {getWalletVersion} = require('ln-service'); const {parsePaymentRequest} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {channelsFromHints} = require('./../routing'); const {executeProbe} = require('./../network'); const {multiPathPayment} = require('./../network'); const {multiPathProbe} = require('./../network'); const defaultMaxPaths = 7; const flatten = arr => [].concat(...arr); const {isArray} = Array; const legacyMax = 4294967; const {max} = Math; const minShardTokens = 1e5; const normalMax = 16777215; const sumOf = arr => arr.reduce((sum, n) => sum + n, Number()); const tokensAsMtokens = tokens => (BigInt(tokens) * BigInt(1e3)).toString(); const tooLargeError = 'PaymentTooLargeToFindRoute'; const uniq = arr => Array.from(new Set(arr)); /** Get routes for funding a swap { cltv_delta: <CLTV Delta Number> destination: <Destination Public Key Hex String> [features]: [{ bit: <Feature Bit Number> }] ignore: [{ from_public_key: <From Public Key Hex String> [to_public_key]: <To Public Key Hex String> }] lnd: <Authenticated LND API Object> logger: <Winston Logger Object> max_fee: <Maximum Fee Tokens Number> [max_paths]: <Maximum Paths To Use for Multi-Path Number> [out_through]: <Out Through Peer With Public Key Hex String> [outgoing_channel]: <Outgoing Standard Format Channel Id String> [payment]: <Payment Identifier Hex String> request: <Payment Request String> routes: [[{ [base_fee_mtokens]: <Base Routing Fee In Millitokens String> [channel]: <Standard Format Channel Id String> [cltv_delta]: <CLTV Blocks Delta Number> [fee_rate]: <Fee Rate In Millitokens Per Million Number> public_key: <Forward Edge Public Key Hex String> }]] tokens: <Tokens To Send For Funding Number> } @returns via cbk or Promise { fee: <Total Fee Tokens Number> routes: [{ fee: <Total Fee Tokens To Pay Number> fee_mtokens: <Total Fee Millitokens To Pay String> hops: [{ channel: <Standard Format Channel Id String> channel_capacity: <Channel Capacity Tokens Number> fee: <Fee Number> fee_mtokens: <Fee Millitokens String> forward: <Forward Tokens Number> forward_mtokens: <Forward Millitokens String> [public_key]: <Public Key Hex String> timeout: <Timeout Block Height Number> }] [messages]: [{ type: <Message Type Number String> value: <Message Raw Value Hex Encoded String> }] mtokens: <Total Millitokens To Pay String> [payment]: <Payment Identifier Hex String> timeout: <Expiration Block Height Number> tokens: <Total Tokens To Pay Number> [total_mtokens]: <Total Millitokens String> }] } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.cltv_delta) { return cbk([400, 'ExpectedCltvDeltaToGetRoutesForFunding']); } if (!args.destination) { return cbk([400, 'ExpectedDestinationToGetRoutesForFunding']); } if (!args.lnd) { return cbk([400, 'ExpectedLndToGetRoutesForFunding']); } if (!args.logger) { return cbk([400, 'ExpectedLoggerToGetRoutesForFunding']); } if (!args.request) { return cbk([400, 'ExpectedFundingRequestToGetRoutesForFunding']); } if (!args.tokens) { return cbk([400, 'ExpectedTokensToGetRoutesForFunding']); } return cbk(); }, // Determine if the backing wallet is capable of multi-path getMultiSupport: ['validate', ({}, cbk) => { return getWalletVersion({lnd: args.lnd}, (err, res) => { return cbk(null, {is_multi_supported: !err}); }); }], // Get single path getSinglePath: ['validate', ({}, cbk) => { return executeProbe({ cltv_delta: args.cltv_delta, destination: args.destination, features: args.features, ignore: args.ignore, lnd: args.lnd, logger: args.logger, max_fee: args.max_fee, outgoing_channel: args.outgoing_channel, payment: args.payment, routes: args.routes, tokens: args.tokens, total_mtokens: tokensAsMtokens(args.tokens), }, (err, res) => { // Exit with no route when the amount is too big for a single path if (!!isArray(err) && err.includes(tooLargeError)) { return cbk(null, {is_size_limited: true}); } if (!!err) { return cbk([503, 'UnexpectedErrorFindingRouteToFundSwap', {err}]); } return cbk(null, res); }); }], // Get multiple limits getMultiLimits: [ 'getMultiSupport', 'getSinglePath', ({getMultiSupport, getSinglePath}, cbk) => { // Exit early when multi-path is not supported by the backing LND if (!getMultiSupport.is_multi_supported) { return cbk(); } // Exit early when a specific peer is set to swap out through if (!!args.out_through) { return cbk(); } // Exit early when the path limit is a single path if (args.max_paths === [getSinglePath].length) { return cbk(); } let error; const probes = []; return asyncWhilst( cbk => { if ((args.max_paths || defaultMaxPaths) === probes.length) { return cbk(null, false); } return cbk(null, !error); }, cbk => { return multiPathProbe({ destination: args.destination, find_max: !getSinglePath.is_size_limited ? normalMax : legacyMax, ignore: args.ignore, in_through: args.in_through, lnd: args.lnd, logger: args.logger, probes: probes.filter(n => !!n), request: args.request, routes: args.routes, tokens: minShardTokens, }, (err, res) => { if (!!err) { return cbk(err); } if (!!res.error) { error = res.error; } else { probes.push(res.probe); } return cbk(); }); }, err => { if (!!err) { return cbk(err); } const complete = probes.filter(n => !!n); if (!complete.length) { return cbk(error); } const latencyMs = complete .map(n => n.latency_ms) .filter(n => !!n) .reduce((sum, n) => sum + n, Number()); const max = complete .map(n => n.route_maximum) .reduce((sum, n) => sum + n, Number()); return cbk(null, { latency_ms: latencyMs, probes: complete.map(probe => { return { channels: probe.success, liquidity: probe.route_maximum, relays: probe.relays, }; }), routes_max: max, }); } ); }], // Translate multiple limits into multiple routes getMultiPaths: ['getMultiLimits', async ({getMultiLimits}) => { if (!getMultiLimits || getMultiLimits.routes_max < args.tokens) { return; } const hintChans = channelsFromHints({request: args.request}).channels; const ids = flatten(getMultiLimits.probes.map(n => n.channels)); const channels = await asyncMap(uniq(ids), async id => { // Exit early when channel is known from hints if (!!hintChans.find(n => n.id === id)) { return hintChans.find(n => n.id === id); } return await getChannel({id, lnd: args.lnd}); }); const {routes} = multiPathPayment({ channels, cltv_delta: args.cltv_delta, destination: args.destination, height: (await getHeight({lnd: args.lnd})).current_block_height, max: getMultiLimits.routes_max, mtokens: tokensAsMtokens(args.tokens), payment: args.payment, probes: getMultiLimits.probes, }); return routes; }], // Result of pathfinding result: [ 'getMultiPaths', 'getSinglePath', ({getMultiPaths, getSinglePath}, cbk) => { const paths = getMultiPaths || [getSinglePath.route].filter(n => !!n); if (!paths.length) { return cbk([503, 'PathfindingFailedToFullyFundSwapOffchain']); } const fee = sumOf(paths.map(n => n.fee)); if (fee > args.max_fee) { return cbk([503, 'MaxFeeSettingExceeded', {needed_max_fee: fee}]); } return cbk(null, {fee: sumOf(paths.map(n => n.fee)), routes: paths}); }], }, returnResult({reject, resolve, of: 'result'}, cbk)); }); };