UNPKG

balanceofsatoshis

Version:
516 lines (439 loc) 16.3 kB
const {createHash} = require('crypto'); const {randomBytes} = require('crypto'); const asyncAuto = require('async/auto'); const {decodePaymentRequest} = require('ln-service'); const {getChannels} = require('ln-service'); const {getIdentity} = require('ln-service'); const {getNode} = require('ln-service'); const {getPeers} = require('ln-service'); const moment = require('moment'); const {parsePaymentRequest} = require('ln-service'); const {payViaRoutes} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {signBytes} = require('ln-service'); const {subscribeToFindMaxPayable} = require('probing'); const executeProbe = require('./execute_probe'); const {getIcons} = require('./../display'); const {sortBy} = require('./../arrays'); const bufFromHex = hex => Buffer.from(hex, 'hex'); const channelReserve = n => n.local_reserve || Math.floor(n.capacity * 0.01); const cltvBuffer = 3; const dateBytesLength = 8; const datePrecisionLength = 6; const dateType = '34349343'; const defaultCltvDelta = 144; const defaultMaxFee = 1337; const defaultTokens = 1; const featureTypeChannelType = 45; const featureTypeTrustedFunding = 51; const feeBuffer = fees => fees * 2; const {floor} = Math; const fromKeyType = '34349339'; const htlcOutputSize = 43; const keySendPreimageType = '5482373484'; const makeNonce = () => randomBytes(32).toString('hex'); const {max} = Math; const messageType = '34349334'; const {min} = Math; const nodeKeyFamily = 6; const preimageByteLength = 32; const {now} = Date; const rate = n => n.commit_transaction_fee / (n.commit_transaction_weight / 4); const signatureType = '34349337'; const tokAsMtok = tokens => (BigInt(tokens || 0) * BigInt(1e3)).toString(); /** Determine if a destination can be paid by probing it { [destination]: <Destination Public Key Hex String> [find_max]: <Find Maximum Payable On Probed Route Below Tokens Number> [fs]: { getFile: <Read File Contents Function> (path, cbk) => {} } [ignore]: [{ from_public_key: <Avoid Node With Public Key Hex String> [to_public_key]: <Avoid Routing To Node With Public Key Hex String> }] [in_through]: <Pay In Through Public Key Hex String> [is_omitting_message_from]: <Omit Message From Fields Bool> [is_push]: <Is Push Payment Bool> [is_real_payment]: <Pay the Request after Probing Bool> // default: false [is_strict_max_fee]: <Avoid Probing Too-High Fee Routes Bool> lnd: <Authenticated LND gRPC API Object> logger: <Winston Logger Object> [max_fee]: <Maximum Fee Tokens Number> [max_fee_mtokens]: <Maximum Fee Millitokens Number> [message]: <Message String> [messages]: [{ type: <Additional Message To Final Destination Type Number String> value: <Message To Final Destination Raw Value Hex Encoded String> }] [out_through]: <Out Through Peer With Public Key Hex String> [request]: <Payment Request String> [timeout_minutes]: <Stop Searching For Route After N Minutes Number> [tokens]: <Tokens Number> } @returns via cbk { [fee]: <Fee Tokens To Destination Number> [id]: <Payment Hash Hex String> [latency_ms]: <Latency Milliseconds Number> [route_maximum]: <Maximum Sendable Tokens On Successful Probe Path Number> [paid]: <Paid Tokens Number> [preimage]: <Payment HTLC Preimage Hex String> [relays]: [<Relaying Node Public Key Hex String] [success]: [<Standard Format Channel Id String>] } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.lnd) { return cbk([400, 'ExpectedLndToProbeDestination']); } if (!args.logger) { return cbk([400, "ExpectedLoggerToProbeDestination"]); } return cbk(); }, // Get channels to determine an outgoing channel id restriction getChannels: ['validate', ({}, cbk) => { // Exit early when there is no need to add an outgoing channel id if (!args.out_through) { return cbk(); } return getChannels({lnd: args.lnd}, cbk); }], // Get node icons getIcons: ['validate', ({}, cbk) => { if (!args.fs) { return cbk(); } return getIcons({fs: args.fs}, cbk); }], // Get identity key getIdentity: ['validate', ({}, cbk) => { return getIdentity({lnd: args.lnd}, cbk); }], // Destination to pay to: ['validate', ({}, cbk) => { // Exit early when sending a push payment if (!!args.is_push) { const secret = randomBytes(preimageByteLength); return cbk(null, { secret, destination: args.destination, id: createHash('sha256').update(secret).digest().toString('hex'), mtokens: !args.tokens ? '0': tokAsMtok(args.tokens), }); } // Exit early when probing a destination if (!args.request && !!args.destination) { return cbk(null, { destination: args.destination, mtokens: !args.tokens ? '0': tokAsMtok(args.tokens), payment: makeNonce(), routes: [], }); } if (!args.request) { return cbk([400, 'PayRequestOrDestinationRequiredToInitiateProbe']); } try { const details = parsePaymentRequest({request: args.request}); if (details.is_expired) { return cbk([400, 'InvoiceIsExpired']); } // Exit early when tokens are specified for a request if (!!args.tokens) { return cbk(null, { cltv_delta: details.cltv_delta, destination: details.destination, features: details.features, id: details.id, mtokens: tokAsMtok(args.tokens), payment: details.payment, routes: details.routes, tokens: args.tokens, }); } args.logger.info({ description: details.description || undefined, destination: details.destination, expires: moment(details.expires_at).fromNow(), id: details.id, tokens: details.tokens, }); return cbk(null, { cltv_delta: details.cltv_delta, destination: details.destination, features: details.features, id: details.id, mtokens: details.mtokens || '0', payment: details.payment, routes: details.routes, tokens: details.tokens, }); } catch (err) { return cbk([400, 'FailedToDecodePaymentRequest', {err}]); } }], // Tokens tokens: ['to', ({to}, cbk) => { return cbk(null, args.tokens || to.tokens || defaultTokens); }], // Lookup node destination details getDestinationNode: ['to', ({to}, cbk) => { return getNode({ is_omitting_channels: true, lnd: args.lnd, public_key: to.destination, }, (err, res) => { // Suppress errors when the node is not found if (!!err) { return cbk(null, {alias: String()}); } return cbk(null, res); }); }], // For destinations that are peers, get features directly getPeerFeatures: [ 'getDestinationNode', 'to', ({getDestinationNode, to}, cbk) => { // Exit early when features are specified or known already if (!!to.features || !!(getDestinationNode.features || []).length) { return cbk(); } return getPeers({lnd: args.lnd}, (err, res) => { if (!!err) { return cbk(err); } const peer = res.peers.find(n => n.public_key === to.destination); // Exit early when there are no peer features if (!peer || !peers.features.length) { return cbk(); } return cbk(null, peer.features); }); }], // Get the features of the node to probe getFeatures: [ 'getDestinationNode', 'getIdentity', 'getPeerFeatures', 'to', ({getDestinationNode, getIdentity, getPeerFeatures, to}, cbk) => { // Exit early when features are directly known if (!!to.features) { return cbk(null, {features: to.features}); } // Use found features from getting the destination node or the peer const nodeFeatures = getDestinationNode.features || getPeerFeatures; // Only incorporate features that are important to finding routes const features = (nodeFeatures || []) .filter(n => !!n.is_known) .filter(n => n.bit !== featureTypeChannelType) .filter(n => n.bit !== featureTypeTrustedFunding) .filter(n => !!n.type); return cbk(null, {features}); }], // Determine messages to attach messages: [ 'getFeatures', 'getIdentity', 'to', async ({getFeatures, getIdentity, to}, cbk) => { // Exit early when there are no messages if (!args.message && !args.messages && !args.is_push) { return; } const date = Buffer.alloc(dateBytesLength); const messages = [].concat(args.messages || []); date.writeUIntBE(now(), Number(), datePrecisionLength); // Add message if (!!args.message) { messages.push({ type: messageType, value: Buffer.from(args.message).toString('hex'), }); } // Add message from fields if (!!args.message && !args.is_omitting_message_from) { messages.push({type: dateType, value: date.toString('hex')}); messages.push({type: fromKeyType, value: getIdentity.public_key}); const preimage = Buffer.concat([ bufFromHex(getIdentity.public_key), bufFromHex(to.destination), date, Buffer.from(args.message), ]); const {signature} = await signBytes({ preimage: preimage.toString('hex'), key_family: nodeKeyFamily, key_index: Number(), lnd: args.lnd, }); messages.push({type: signatureType, value: signature}); } if (!!args.is_push) { messages.push({type: keySendPreimageType, value: to.secret}); } return messages; }], // Outgoing channel id outId: ['getChannels', 'to', ({getChannels, to}, cbk) => { if (!getChannels) { return cbk(); } const {channels} = getChannels; const outPeer = args.out_through; const tokens = args.tokens || to.tokens || defaultTokens; const withPeer = channels .filter(n => !!n.is_active) .filter(n => n.partner_public_key === outPeer); if (!withPeer.length) { return cbk([404, 'NoActiveChannelWithOutgoingPeer']); } const withBalance = withPeer.filter(n => { const fees = n.commit_transaction_fee + (htlcOutputSize * rate(n)); const reserve = channelReserve(n); return n.local_balance - tokens > reserve + feeBuffer(fees); }); if (!withBalance.length) { return cbk([404, 'NoOutboundPeerWithSufficientBalance']); } const attribute = 'local_balance'; const [channel] = sortBy({attribute, array: withBalance}).sorted; const bufferFees = htlcOutputSize * rate(channel); const reserve = channelReserve(channel); const fees = channel.commit_transaction_fee + bufferFees; const spendable = channel.local_balance - reserve; const maxPayable = floor(spendable - feeBuffer(fees)); return cbk(null, {id: channel.id, max: maxPayable || args.find_max}); }], // Log sending towards destination checkPath: [ 'getDestinationNode', 'getIdentity', 'to', ({getDestinationNode, getIdentity, to}, cbk) => { const sendingTo = `${getDestinationNode.alias} ${to.destination}`; if (to.destination === getIdentity.public_key) { args.logger.info({circular_rebalance_for: sendingTo}); } else { args.logger.info({checking_for_path_to: sendingTo}); } return cbk(); }], // Probe towards destination probe: [ 'getFeatures', 'getIcons', 'getIdentity', 'messages', 'outId', 'to', ({getFeatures, getIcons, messages, outId, to}, cbk) => { return executeProbe({ messages, cltv_delta: (to.cltv_delta || defaultCltvDelta) + cltvBuffer, destination: to.destination, features: getFeatures.features, ignore: args.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, max_fee_mtokens: args.max_fee_mtokens, mtokens: !BigInt(to.mtokens) ? tokAsMtok(defaultTokens) : to.mtokens, outgoing_channel: !!outId ? outId.id : undefined, payment: to.payment, routes: to.routes, tagged: !!getIcons ? getIcons.nodes : undefined, timeout_minutes: args.timeout_minutes || undefined, total_mtokens: !!to.payment ? to.mtokens : undefined, }, cbk); }], // Get maximum value of the successful route getMax: ['outId', 'probe', 'to', ({outId, probe, to}, cbk) => { if (!args.find_max || !probe.route) { return cbk(null, {}); } const sub = subscribeToFindMaxPayable({ cltv: (to.cltv_delta || defaultCltvDelta) + cltvBuffer, hops: probe.route.hops, lnd: args.lnd, max: args.find_max, request: args.request, }); sub.on('evaluating', ({tokens}) => { return args.logger.info({evaluating_amount: tokens}); }); sub.once('error', err => cbk(err)); // Did not find any higher value routes sub.once('failure', () => { return cbk(null, {maximum: min(args.find_max, probe.route.tokens)}); }); // Found a successful high value route sub.once('success', ({maximum}) => { return cbk(null, { maximum: min(outId.max, max(maximum, probe.route.tokens)), }); }); return; }], // If there is a successful route, pay it pay: ['probe', 'to', ({probe, to}, cbk) => { if (!args.is_real_payment) { return cbk(); } if (!probe.route) { return cbk(); } if (args.max_fee !== undefined && probe.route.fee > args.max_fee) { return cbk([400, 'MaxFeeTooLow', {required_fee: probe.route.fee}]); } args.logger.info({ paying: probe.route.hops.map(({channel}) => channel), }); return payViaRoutes({ id: to.id, lnd: args.lnd, routes: [probe.route], }, cbk); }], // Outcome of probes and payment outcome: ['getMax', 'pay', 'probe', ({getMax, pay, probe}, cbk) => { if (!probe.route) { return cbk(null, { attempted_paths: probe.attempted_paths, is_failed: true, }); } const {route} = probe; return cbk(null, { fee: !route ? undefined : route.fee, id: !pay ? undefined : pay.id, latency_ms: !route ? undefined : probe.latency_ms, route_maximum: getMax.maximum, paid: !pay ? undefined : pay.tokens, preimage: !pay ? undefined : pay.secret, probed: !!pay ? undefined : route.tokens - route.fee, relays: !route ? undefined : route.hops.map(n => n.public_key), success: !route ? undefined : route.hops.map(({channel}) => channel), }); }], }, returnResult({reject, resolve, of: 'outcome'}, cbk)); }); };