UNPKG

balanceofsatoshis

Version:
552 lines (465 loc) 18.2 kB
const asyncAuto = require('async/auto'); const asyncMapSeries = require('async/mapSeries'); const {closeChannel} = require('ln-service'); const {decodeChanId} = require('bolt07'); const {getChainFeeRate} = require('ln-service'); const {getChainTransactions} = require('ln-service'); const {getChannels} = require('ln-service'); const {getHeight} = require('ln-service'); const {getNetwork} = require('ln-sync'); const {getPendingChannels} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {Transaction} = require('bitcoinjs-lib'); const {getMempoolSize} = require('./../chain'); const getPeers = require('./get_peers'); const arrayWithEntries = arr => !!arr.length ? arr : undefined; const asOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`; const asRate = n => n.toFixed(2); const defaultDays = 365 * 2; const estimateDisk = n => Math.round(n * 500 / 1e6 * 10) / 10; const fastConf = 6; const {floor} = Math; const {fromHex} = Transaction; const getMempoolRetries = 10; const iconDisabled = channel => !channel.is_active ? '💀 ' : ''; const iconPending = channel => channel.pending_payments.length ? '💸 ' : '' const {isArray} = Array; const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n); const maxFeeRate = (chan, rate) => !!chan.is_active ? rate * 100 : undefined; const maxInputs = 1; const maxMempoolSize = 2e6; const regularConf = 72; const slowConf = 144; const sumOf = arr => arr.reduce((sum, n) => sum + n, Number()); const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8); /** Close out channels with a peer and disconnect them { addresses: [<Close Out Funds to On-Chain Address String>] ask: <Ask Function> [chain_fee_rate]: <Chain Fee Per VByte Number> fs: { getFile: <Read File Contents Function> (path, cbk) => {} } [idle_days]: <No Activity From Peer For Days Number> [is_active]: <Peer Is Actively Connected Bool> [is_dry_run]: <Avoid Actually Closing Channel Bool> [is_forced]: <Force Close When Cooperative Close Is Impossible Bool> [is_offline]: <Peer Is Disconnected Bool> [is_private]: <Peer is Privately Connected Bool> [is_public]: <Peer is Publicly Connected Bool> [is_selecting_channels]: <Interactively Select Channels to Remove Bool> lnd: <Authenticated LND API Object> logger: <Winston Logger Object> [omit]: [<Avoid Peer With Public Key String>] outpoints: [<Only Remove Specific Channel Funding Outpoint String>] [public_key]: <Public Key Hex String> request: <Request Function> } @returns via cbk or Promise */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!isArray(args.addresses)) { return cbk([400, 'ExpectedArrayOfAddressesToRemovePeer']); } if (!!args.addresses.filter(isPublicKey).length) { return cbk([400, 'UnexpectedPublicKeyInCooperativeCloseAddresses']); } if (!args.ask) { return cbk([400, 'ExpectedAskFunctionToRemovePeer']); } if (!args.fs) { return cbk([400, 'ExpectedFsMethodsToRemovePeer']); } if (!!args.is_selecting_channels && !args.public_key) { return cbk([400, 'ExpectedPeerToRemoveWhenSelectingChannels']); } if (!args.lnd) { return cbk([400, 'LndIsRequiredToRemovePeer']); } if (!args.logger) { return cbk([400, 'LoggerIsRequiredToRemovePeer']); } if (!isArray(args.outpoints)) { return cbk([400, 'ExpectedSpecificOutpointsToRemoveFromPeer']); } if (!!args.outpoints.length && !args.public_key) { return cbk([400, 'ExpectedPeerToRemoteWhenOutpointsSpecified']); } if (!!args.public_key && !isPublicKey(args.public_key)) { return cbk([400, 'ExpectedPublicKeyOfPeerToRemove']); } if (!args.request) { return cbk([400, 'RequestIsRequiredToRemovePeer']); } return cbk(); }, // Get channels getChannels: ['validate', ({}, cbk) => { return getChannels({lnd: args.lnd}, cbk); }], // Get fast fee rate getFastFee: ['validate', ({}, cbk) => { return getChainFeeRate({ confirmation_target: fastConf, lnd: args.lnd, }, cbk); }], // Get the chain height to optimize chain txs lookups for chain fees getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)], // Get network getNetwork: ['validate', ({}, cbk) => { return getNetwork({lnd: args.lnd}, cbk); }], // Get normal fee rate getNormalFee: ['validate', ({}, cbk) => { return getChainFeeRate({ confirmation_target: regularConf, lnd: args.lnd, }, cbk); }], // Get peers getPeers: ['validate', ({}, cbk) => { return getPeers({ earnings_days: args.idle_days || defaultDays, filters: args.filters, fs: args.fs, idle_days: args.idle_days || Number(), is_active: args.is_active, is_offline: args.is_offline, is_private: args.is_private, is_public: args.is_public, is_showing_last_received: true, lnd: args.lnd, omit: args.omit || [], sort_by: 'last_activity', }, cbk); }], // Get low fee rate getSlowFee: ['validate', ({}, cbk) => { return getChainFeeRate({ confirmation_target: slowConf, lnd: args.lnd, }, cbk); }], // Determine outpoints to use outpoints: ['getChannels', ({getChannels}, cbk) => { // Exit early when a peer is not specified if (!args.public_key) { return cbk(null, []); } const channelsWithPeer = getChannels.channels .filter(channel => { // Ignore channels that are not the specified public key if (channel.partner_public_key !== args.public_key) { return false; } //Return channels with the peer return true; }) .sort((a, b) => { const heightA = decodeChanId({channel: a.id}).block_height; const heightB = decodeChanId({channel: b.id}).block_height; // Sort channels by oldest to newest return heightA - heightB; }); // Exit early when no channels are available if (!channelsWithPeer.length) { return cbk([404, 'NoChannelsToCloseWithSpecifiedPeer']); } // Collect any outpoints that are unable to be cooperatively closed const blocked = channelsWithPeer .filter(channel => { // Channels that are inactive or have HTLCs cannot be coop-closed return !channel.is_active || !!channel.pending_payments.length; }) .map(channel => asOutpoint(channel)); // Find a directly referenced outpoint that is in the blocked list const blockedOutpoint = args.outpoints.find(n => blocked.includes(n)); // Make sure we aren't trying to coop close a channel that can't be if (!args.is_forced && !!blockedOutpoint) { return cbk([400, 'CannotCoopClose', {outpoint: blockedOutpoint}]); } // Exit early if not selecting a channel if (!args.is_selecting_channels) { return cbk(null, args.outpoints); } // Interactively select outpoints to close return args.ask({ choices: channelsWithPeer.map(channel => { // In closing, channels are identified by their funding outpoint const value = asOutpoint(channel); // Channels that are inactive or have HTLCs cannot be coop-closed const isBlocked = blocked.includes(value); const disk = `Est disk mb: ${estimateDisk(channel.past_states)}`; const icon = iconDisabled(channel) || iconPending(channel); const {id} = channel; const inbound = `in: ${tokensAsBigUnit(channel.remote_balance)}`; const outbound = `out: ${tokensAsBigUnit(channel.local_balance)}`; return { value, checked: args.outpoints.includes(value), disabled: !args.is_forced ? isBlocked : false, name: `${icon}${id}: ${inbound} | ${outbound}. ${disk}.`, }; }), loop: false, message: `Channels to ${!!args.is_forced ? 'force ' : ''}close?`, name: 'outpoints', type: 'checkbox', validate: input => !!input.length, }, ({outpoints}) => cbk(null, outpoints)); }], // Check channels for peer to make sure that they can be cleanly closed checkChannels: [ 'getChannels', 'outpoints', ({getChannels, outpoints}, cbk) => { // Exit early when a peer is not specified or force closing is OK if (!args.public_key || !!args.is_forced) { return cbk(); } const selectedChannels = getChannels.channels.filter(channel => { // Ignore channels that are not the specified public key if (channel.partner_public_key !== args.public_key) { return false; } // Exit early when there are no outpoints, consider all peer channels if (!outpoints.length) { return true; } // Only include selected channels return outpoints.includes(asOutpoint(channel)); }); const costToClose = selectedChannels .filter(n => n.is_partner_initiated === false) .map(n => n.commit_transaction_fee) .reduce((sum, n) => sum + n, Number()); const [cannotCoopClose] = selectedChannels.filter(channel => { // Inactive channels cannot be cooperatively closed if (!channel.is_active) { return true; } // Channels with pending payments cannot be cooperatively closed if (!!channel.pending_payments.length) { return true; } // Channel with the peer can be cooperatively closed return false; }); // Exit with error when there is a channel that cannot be coop closed if (!!cannotCoopClose) { return cbk([400, 'CannotCurrentlyCooperativelyCloseWithPeer', { is_active: cannotCoopClose.is_active, pending: arrayWithEntries(cannotCoopClose.pending_payments), cost_to_force_close: costToClose, }]); } return cbk(); }], // Get mempool size getMempool: ['getNetwork', ({getNetwork}, cbk) => { // Exit early when the chain fee is specified directly if (!!args.chain_fee_rate) { return cbk(); } return getMempoolSize({ network: getNetwork.network, request: args.request, retries: getMempoolRetries, }, cbk); }], // Check if the chain fee rate is high checkChainFees: [ 'getFastFee', 'getMempool', 'getNormalFee', 'getSlowFee', ({getFastFee, getMempool, getNormalFee, getSlowFee}, cbk) => { // Exit early when force closing or closing with a set fee rate if (!!args.is_forced || !!args.chain_fee_rate) { return cbk(); } const fastFee = getFastFee.tokens_per_vbyte; const feeRate = getNormalFee.tokens_per_vbyte; const slowFee = getSlowFee.tokens_per_vbyte; const estimateRatio = fastFee / slowFee; const vbytesRatio = (getMempool.vbytes || Number()) / maxMempoolSize; if (!!floor(estimateRatio) && !!floor(vbytesRatio)) { return cbk([503, 'FeeRateIsHighNow', {needed_fee_rate: feeRate}]); } return cbk(); }], // Select a peer selectPeer: [ 'checkChainFees', 'getChannels', 'getPeers', ({getChannels, getPeers}, cbk) => { const [peer] = getPeers.peers .filter(peer => { // Exit early when any peer is eligible if (!args.public_key) { return true; } return peer.public_key === args.public_key; }) .filter(peer => { // Exit early when force closes are allowed if (!args.is_forced) { return true; } const channels = getChannels.channels.filter(channel => { return channel.partner_public_key === peer.public_key; }); // Exit early when a channel has a payment in flight if (!!channels.find(n => !!n.pending_payments.length)) { return false; } // Exit early when a channel is offline if (!!channels.find(n => !n.is_active)) { return false; } return true; }); if (!peer && !!args.public_key) { return cbk(null, {public_key: args.public_key}); } return cbk(null, peer); }], // Determine which channels need to be closed and close them channelsToClose: [ 'checkChannels', 'getChannels', 'getNormalFee', 'outpoints', 'selectPeer', ({getChannels, getNormalFee, outpoints, selectPeer}, cbk) => { // Exit early when there is no peer to close out with if (!selectPeer) { return cbk([400, 'NoPeerFoundToRemove']); } const feeRate = args.chain_fee_rate || getNormalFee.tokens_per_vbyte; const toClose = getChannels.channels .filter(chan => chan.partner_public_key === selectPeer.public_key) .filter(chan => { // When no outpoints are specified, all channels should be closed if (!outpoints.length) { return true; } return !!outpoints.includes(asOutpoint(chan)); }) .map((channel, index) => ({channel, index})); // Exit early when there are no channels to close if (!toClose.length) { return cbk([400, 'NoChannelsToCloseWithPeer']); } args.logger.info({ close_with_peer: selectPeer, channels_to_close: toClose.map(n => n.channel.id), fee_rate: !args.is_forced ? feeRate : undefined, }); // Exit early when not closing any channels if (!!args.is_dry_run) { args.logger.info({is_dry_run: true}); return cbk(null, []); } const [defaultAddress] = args.addresses; return asyncMapSeries(toClose, ({channel, index}, cbk) => { const address = args.addresses[index] || defaultAddress; const isLocked = !!channel.cooperative_close_address; return closeChannel({ address: !isLocked && !!address ? address : undefined, is_force_close: !channel.is_active, lnd: args.lnd, max_tokens_per_vbyte: maxFeeRate(channel, feeRate), tokens_per_vbyte: !!channel.is_active ? feeRate : undefined, transaction_id: channel.transaction_id, transaction_vout: channel.transaction_vout, }, (err, res) => { if (!!err) { return cbk([503, 'UnexpectedErrorClosingChannel', {err}]); } args.logger.info({ close_transaction_id: res.transaction_id, close_transaction_vout: res.transaction_vout, }); return cbk(null, { funding_outpoint: asOutpoint(channel), closing_outpoint: asOutpoint(res), closing_tx_id: res.transaction_id, }); }); }, cbk); }], // Get pending chain transactions to relate funding outpoints getPending: ['channelsToClose', ({}, cbk) => { return getPendingChannels({lnd: args.lnd}, cbk); }], // Get unconfirmed chain transactions to derive final transaction fees getTransactions: ['channelsToClose', 'getHeight', ({getHeight}, cbk) => { return getChainTransactions({ after: getHeight.current_block_height, lnd: args.lnd, }, cbk); }], // Derive the chain transaction fees fees: [ 'channelsToClose', 'getPending', 'getTransactions', ({channelsToClose, getPending, getTransactions}, cbk) => { // Map pending channels to chain transaction fees being paid const pending = getPending.pending_channels.map(channel => { const outpoint = channelsToClose.find(outpoints => { return outpoints.funding_outpoint === asOutpoint(channel); }); // Exit early when this pending channel isn't part of the close set if (!outpoint) { return; } const matching = getTransactions.transactions.find(tx => { return tx.id === outpoint.closing_tx_id; }); // Exit early when there is no matching tx for the close tx if (!matching || matching.inputs > maxInputs) { return; } // Exit early when there is no raw transaction if (!matching.transaction) { return; } const tx = fromHex(matching.transaction); const fee = channel.capacity - sumOf(tx.outs.map(n => n.value)); return { close_transaction_id: matching.id, peer_pays_closing_fee: channel.is_partner_initiated === true, transaction_fee: tokensAsBigUnit(fee), transaction_fee_rate: asRate(fee / tx.virtualSize()), }; }); // Log out the pending channel fees pending.filter(n => !!n).forEach(n => args.logger.info(n)); return cbk(); }], }, returnResult({reject, resolve}, cbk)); }); };