UNPKG

balanceofsatoshis

Version:
482 lines (405 loc) 15.4 kB
const asyncAuto = require('async/auto'); const asyncEach = require('async/each'); const asyncMap = require('async/map'); const asyncRetry = require('async/retry'); const {findKey} = require('ln-sync'); const {getChannel} = require('ln-service'); const {getChannels} = require('ln-service'); const {getFeeRates} = require('ln-service'); const {getIdentity} = require('ln-service'); const {getNode} = require('ln-service'); const {getNodeAlias} = require('ln-sync'); const {getPendingChannels} = require('ln-service'); const {gray} = require('colorette'); const {green} = require('colorette'); const moment = require('moment'); const {returnResult} = require('asyncjs-util'); const {updateChannelFee} = require('ln-sync'); const {chartAliasForPeer} = require('./../display'); const {formatFeeRate} = require('./../display'); const {getIcons} = require('./../display'); const parseFeeRateFormula = require('./parse_fee_rate_formula'); const asRate = rate => formatFeeRate({rate}).display; const asTxOut = n => `${n.transaction_id}:${n.transaction_vout}`; const {ceil} = Math; const flatten = arr => [].concat(...arr); const interval = 1000 * 60 * 2; const isAllUndefined = arr => arr.findIndex(n => n !== undefined) === -1; const {isArray} = Array; const isNumber = n => !!n && !isNaN(n); const {max} = Math; const {min} = Math; const minCltvDelta = 18; const nodeMatch = /\bFEE_RATE_OF_[0-9A-F]{66}\b/gim; const noFee = gray('Unknown Rate'); const present = (arg, existing) => arg !== undefined ? arg : existing; const pubKeyForNodeMatch = n => n.substring(12).toLowerCase(); const shortKey = key => key.substring(0, 20); const sumOf = arr => arr.reduce((sum, n) => sum + n, 0); const times = 360; const uniq = arr => Array.from(new Set(arr)); /** View and adjust routing fees { [cltv_delta]: <Set CLTV Delta Number> [fee_rate]: <Fee Rate String> fs: { getFile: <Read File Contents Function> (path, cbk) => {} } [inbound_rate_discount]: <Discount Fee Rate Number> lnd: <Authenticated LND API Object> logger: <Winstone Logger Object> to: [<Adjust Routing Fee To Peer Alias or Public Key or Tag String>] } @returns via cbk or Promise { rows: [[<Table Cell String>]] } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.fs) { return cbk([400, 'ExpectedFsMethodsToAdjustFeeRates']); } if (!args.lnd) { return cbk([400, 'ExpectedLndToAdjustFeeRates']); } if (!args.logger) { return cbk([400, 'ExpectedLoggerToAdjustFeeRates']); } if (!isArray(args.to)) { return cbk([400, 'ExpectedArrayOfPeersToAdjustFeesTowards']); } if (args.cltv_delta !== undefined && !args.to.length) { return cbk([400, 'SettingGlobalCltvDeltaNotSupported']); } if (args.cltv_delta !== undefined && args.cltv_delta < minCltvDelta) { return cbk([400, 'SettingLowCltvDeltaIsNotSupported']); } if (args.fee_rate !== undefined && !args.to.length) { return cbk([400, 'SettingGlobalFeeRateNotSupported']); } if (args.inbound_rate_discount !== undefined && !args.to.length) { return cbk([400, 'SettingGlobalInboundRateDiscountNotSupported']); } if (!!args.inbound_rate_discount) { if (!isNumber(args.inbound_rate_discount)) { return cbk([400, 'ExpectedNumericRateToSetInboundRateDiscount']); } } return cbk(); }, // Get the channels getChannels: ['validate', ({}, cbk) => { return getChannels({lnd: args.lnd}, cbk); }], // Get node icons getIcons: ['validate', ({}, cbk) => getIcons({fs: args.fs}, cbk)], // Get the pending channels getPending: ['validate', ({}, cbk) => { return getPendingChannels({lnd: args.lnd}, cbk); }], // Get the wallet public key getPublicKey: ['validate', ({}, cbk) => { return getIdentity({lnd: args.lnd}, cbk); }], // Get the current fee rates getFeeRates: ['validate', ({}, cbk) => { return getFeeRates({lnd: args.lnd}, cbk); }], // Get the aliases of the channel partners getAliases: ['getChannels', ({getChannels}, cbk) => { const ids = uniq(getChannels.channels.map(n => n.partner_public_key)); return asyncMap(ids, (id, cbk) => { return getNodeAlias({id, lnd: args.lnd}, cbk); }, cbk); }], // Get the peers to assign fee rates towards getPeers: ['getChannels', 'getIcons', ({getChannels, getIcons}, cbk) => { const {channels} = getChannels; return asyncMap(args.to, (query, cbk) => { const nodes = getIcons.nodes.filter(n => n.aliases.includes(query)); // Exit early when there is a tag match if (!!nodes.length) { return cbk(null, nodes.map(n => ({public_key: n.public_key}))); } return findKey({channels, query, lnd: args.lnd}, cbk); }, (err, res) => { if (!!err) { return cbk(err); } return cbk(null, flatten(res)); }); }], // Get referenced other node rates getNodeRates: ['getPeers', ({getPeers}, cbk) => { // Exit early when not referencing another node's fee rate if (!args.fee_rate || !args.fee_rate.match(nodeMatch)) { return cbk(null, []); } const [peer, otherPeer] = getPeers; // Exit with error when multiple to peers are specified if (!!otherPeer) { return cbk([400, 'MultipleToNotSupportedWhenReferencingNodeRate']); } const nodeIds = args.fee_rate.match(nodeMatch).map(pubKeyForNodeMatch); return getNode({ lnd: args.lnd, public_key: peer.public_key, }, (err, res) => { if (!!err) { return cbk(err); } // Map referenced node ids to their fee rates const feeRates = nodeIds.map(key => { // Relevant forwarding policies const policies = res.channels .filter(n => !!n.policies.find(n => n.public_key === key)) .map(({policies}) => policies.find(n => n.public_key === key)) .filter(n => !!n.updated_at); // Exit early when there is no defined policy for an edge if (!policies.length) { return; } return { key: `FEE_RATE_OF_${key.toUpperCase()}`, rate: max(...policies.map(n => n.fee_rate)), }; }); const rates = feeRates.filter(n => !!n); if (!rates.length) { return cbk([400, 'NoNodeRatesFoundToUpdateFeeRatesFromNode']); } return cbk(null, rates); }); }], // Get the policies of all channels getPolicies: ['getChannels', ({getChannels}, cbk) => { return asyncMap(getChannels.channels, (channel, cbk) => { return getChannel({id: channel.id, lnd: args.lnd}, (err, res) => { if (isArray(err) && err.slice().shift() === 404) { return cbk(); } if (!!err) { return cbk(err); } // Exit early when the channel policies are not defined if (!!res.policies.find(n => n.cltv_delta === undefined)) { return cbk(); } return cbk(null, res); }); }, cbk); }], // Figure out updated fee rates of the specified channels for adjustments feeUpdates: [ 'getChannels', 'getFeeRates', 'getNodeRates', 'getPeers', 'getPending', 'getPolicies', 'getPublicKey', ({ getChannels, getFeeRates, getNodeRates, getPeers, getPending, getPolicies, getPublicKey, }, cbk) => { const adjustments = [ args.cltv_delta, args.fee_rate, args.inbound_rate_discount, ]; // Exit early when not updating any policy values if (isAllUndefined(adjustments)) { return cbk(); } const ownKey = getPublicKey.public_key; const peerKeys = getPeers.map(n => n.public_key).filter(n => !!n); return asyncMap(peerKeys, (key, cbk) => { const channels = [] .concat(getChannels.channels) .concat(getPending.pending_channels.filter(n => !!n.is_opening)) .filter(channel => channel.partner_public_key === key); const peerPolicies = getPolicies .filter(n => !!n) .filter(n => channels.find(chan => asTxOut(chan) === asTxOut(n))) .map(n => n.policies.find(p => p.public_key !== ownKey)) .filter(n => !!n); const inboundFeeRate = max(...peerPolicies.map(n => n.fee_rate)); const feeRates = getFeeRates.channels.filter(rate => { return channels.find(n => asTxOut(n) === asTxOut(rate)); }); const currentPolicies = getPolicies .filter(n => !!n) .filter(n => channels.find(chan => asTxOut(chan) === asTxOut(n))) .map(n => n.policies.find(p => p.public_key === ownKey)) .filter(n => !!n); const baseFeeMillitokens = feeRates .map(n => BigInt(n.base_fee_mtokens)) .reduce((sum, fee) => fee > sum ? fee : sum, BigInt(Number())); const {failure, rate} = parseFeeRateFormula({ fee_rate: args.fee_rate, inbound_fee_rate: inboundFeeRate, inbound_liquidity: sumOf(channels.map(n => n.remote_balance)), outbound_liquidity: sumOf(channels.map(n => n.local_balance)), node_rates: getNodeRates, }); if (!!failure) { return cbk([400, failure]); } return cbk(null, channels.map(channel => { // Exit early when there is no known policy if (!currentPolicies.length) { return { cltv_delta: args.cltv_delta, fee_rate: rate, inbound_rate_discount: args.inbound_rate_discount, transaction_id: channel.transaction_id, transaction_vout: channel.transaction_vout, }; } const discount = args.inbound_rate_discount; const inbounds = currentPolicies.map(n => n.inbound_rate_discount); // Only the highest CLTV delta across all peer channels applies const cltvDelta = max(...currentPolicies.map(n => n.cltv_delta)); // Only the highest fee rate across all peer channels applies const maxFeeRate = max(...currentPolicies.map(n => n.fee_rate)); // Only the lowest discount rate across all peer channels applies const minInboundDiscountRate = min(...inbounds); return { base_fee_mtokens: baseFeeMillitokens.toString(), cltv_delta: args.cltv_delta || cltvDelta, fee_rate: rate !== undefined ? rate : maxFeeRate, inbound_rate_discount: present(discount, minInboundDiscountRate), transaction_id: channel.transaction_id, transaction_vout: channel.transaction_vout, }; })); }, cbk); }], // Execute fee updates updateFees: [ 'feeUpdates', 'getPublicKey', ({feeUpdates, getPublicKey}, cbk) => { if (!feeUpdates) { return cbk(); } return asyncEach(flatten(feeUpdates), (update, cbk) => { return asyncRetry({interval, times}, cbk => { return updateChannelFee({ base_fee_mtokens: update.base_fee_mtokens, cltv_delta: update.cltv_delta, fee_rate: ceil(update.fee_rate), from: getPublicKey.public_key, inbound_rate_discount: ceil(update.inbound_rate_discount), lnd: args.lnd, transaction_id: update.transaction_id, transaction_vout: update.transaction_vout, }, err => { if (!!err) { args.logger.error(err); args.logger.info({ next_retry: moment().add(interval, 'ms').calendar(), }); return cbk(err); } return cbk(); }); }, cbk); }, cbk); }], // Get final fee rates getRates: ['updateFees', ({}, cbk) => { return getFeeRates({lnd: args.lnd}, cbk); }], // Get fee rundown fees: [ 'getAliases', 'getChannels', 'getIcons', 'getPeers', 'getPolicies', 'getRates', ({ getAliases, getChannels, getIcons, getPeers, getPolicies, getRates, }, cbk) => { const peersWithFees = getAliases.map(({alias, id}) => { const channels = getChannels.channels.filter(channel => { return channel.partner_public_key === id; }); const peerRates = getRates.channels.filter(channel => { return !!channels.find(rate => { if (channel.transaction_id !== rate.transaction_id) { return false; } return channel.transaction_vout === rate.transaction_vout; }); }); const discounts = peerRates.map(n => n.inbound_rate_discount); const rate = max(...peerRates.map(n => n.fee_rate)); const inboundRateDiscount = min(...discounts); const nodeIcons = getIcons.nodes.find(n => n.public_key === id); const {display} = chartAliasForPeer({ alias, icons: !!nodeIcons ? nodeIcons.icons : undefined, is_inactive: channels.find(n => !n.is_active), public_key: id, }); return { alias: display, discount: !peerRates.length ? noFee : asRate(inboundRateDiscount), id: id, out_fee: !peerRates.length ? noFee : formatFeeRate({rate}).display, }; }); const rows = [] .concat([['Peer', 'Out Fee', 'Inbound Discount', 'Public Key']]) .concat(peersWithFees .filter(peer => { if (!args.to.length) { return true; } return getPeers.find(n => n.public_key === peer.id); }) .map(peer => { const isChange = getPeers.find(n => n.public_key === peer.id); return [ isChange ? green(peer.alias) : peer.alias, isChange ? green(peer.out_fee) : peer.out_fee, isChange ? green(peer.discount) : peer.discount, isChange ? green(peer.id) : peer.id, ]; }) ); return cbk(null, {rows}); }], }, returnResult({reject, resolve, of: 'fees'}, cbk)); }); };