balanceofsatoshis
Version:
Lightning balance CLI
391 lines (332 loc) • 12.1 kB
JavaScript
const {addPeer} = require('ln-service');
const asyncAuto = require('async/auto');
const asyncDetectSeries = require('async/detectSeries');
const asyncReflect = require('async/reflect');
const asyncTimeout = require('async/timeout');
const {getChainFeeRate} = require('ln-service');
const {getChannels} = require('ln-service');
const {getClosedChannels} = require('ln-service');
const {getIdentity} = require('ln-service');
const {getNetwork} = require('ln-sync');
const {getNode} = require('ln-service');
const {getPeers} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {getSeedNodes} = require('ln-sync');
const {openChannel} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const adjustFees = require('./../routing/adjust_fees');
const connectToPeer = require('./../peers/connect_to_peer');
const {getMempoolSize} = require('./../chain');
const {getPastForwards} = require('./../routing');
const peersWithActivity = require('./peers_with_activity');
const {shuffle} = require('./../arrays');
const asBigTok = tokens => (tokens / 1e8).toFixed(8);
const channelTokens = 5e6;
const connectTimeout = 1000 * 30;
const days = 90;
const defaultDescription = 'bos increase-outbound-liquidity';
const fastConf = 6;
const {floor} = Math;
const getMempoolRetries = 10;
const maxMempoolSize = 2e6;
const minOutbound = 4294967;
const minForwarded = 1e5;
const numericFeeRate = n => !!n && /^\d+$/.test(n) ? Number(n) : undefined;
const regularConf = 72;
const slowConf = 144;
/** Open up a new channel
{
[chain_fee_rate]: <Chain Fee Tokens Per VByte to Pay Number>
[is_dry_run]: <Avoid Actually Opening a New Channel Bool>
[is_private]: <Mark Channel as Private Booll>
lnd: <Authenticated LND gRPC API Object>
logger: <Winston Logger Object>
[peer]: <Peer Public Key Hex String>
request: <Request Function>
[set_fee_rate]: <Fee Rate String>
[tokens]: <Tokens for New Channel Number>
}
@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.lnd) {
return cbk([400, 'ExpectedLndObjectToOpenNewChannel']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerObjectToOpenNewChannel']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToOpenNewChannel']);
}
if (args.tokens === 0) {
return cbk([400, 'ExpectedTokensValueToOpenNewChannel']);
}
return cbk();
},
// Get channels
getChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.lnd}, cbk);
}],
// Get closed channels
getClosed: ['validate', ({}, cbk) => {
return getClosedChannels({lnd: args.lnd}, cbk);
}],
// Get fast fee rate
getFastFee: ['validate', ({}, cbk) => {
return getChainFeeRate({
confirmation_target: fastConf,
lnd: args.lnd,
},
cbk);
}],
// Get forwards
getForwards: ['validate', ({}, cbk) => {
return getPastForwards({days, 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 connected peers
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
// Get pending channels
getPending: ['validate', ({}, cbk) => {
return getPendingChannels({lnd: args.lnd}, cbk);
}],
// Get low fee rate
getSlowFee: ['validate', ({}, cbk) => {
return getChainFeeRate({
confirmation_target: slowConf,
lnd: args.lnd,
},
cbk);
}],
// Get wallet identity
getWallet: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
// Get mempool size
getMempool: ['getNetwork', ({getNetwork}, cbk) => {
return getMempoolSize({
network: getNetwork.network,
request: args.request,
retries: getMempoolRetries,
},
cbk);
}],
// Get seed nodes
getSeed: ['getNetwork', asyncReflect(({getNetwork}, cbk) => {
// Exit early when a peer is specified
if (!!args.peer) {
return cbk(null, {nodes: []});
}
return getSeedNodes({
network: getNetwork.network,
request: args.request,
},
cbk);
})],
// Candidate peers
candidates: [
'getChannels',
'getClosed',
'getPending',
'getForwards',
'getSeed',
({getChannels, getClosed, getForwards, getPending, getSeed}, cbk) =>
{
const allChannels = []
.concat(getChannels.channels)
.concat(getPending.pending_channels.filter(n => !!n.is_opening));
const {peers} = peersWithActivity({
additions: [].concat(args.peer).filter(n => !!n),
channels: allChannels,
forwards: getForwards.forwards,
terminated: getClosed.channels,
});
// Exit early when a peer is specified
if (!!args.peer) {
return cbk(null, peers.filter(n => n.public_key === args.peer));
}
const depletedPeers = peers
.filter(n => n.outbound < minOutbound) // Depleted
.filter(n => n.forwarded > minForwarded); // Previous forwards
const seeded = !!getSeed.value ? getSeed.value.nodes : [];
const scorePeers = peersWithActivity({
additions: seeded.map(n => n.public_key),
channels: allChannels,
forwards: getForwards.forwards,
terminated: getClosed.channels,
});
const untriedPeers = scorePeers.peers
.filter(peer => !peer.outbound)
.filter(peer => {
const previous = getClosed.channels.find(n => {
return peer.public_key === n.partner_public_key;
});
return !previous;
});
return cbk(null, [].concat(depletedPeers).concat(untriedPeers));
}],
// Check if the chain fee rate is high
checkChainFees: [
'getFastFee',
'getMempool',
'getNormalFee',
'getSlowFee',
({getFastFee, getMempool, getNormalFee, getSlowFee}, cbk) =>
{
if (!!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 and open a channel
openChannel: [
'candidates',
'checkChainFees',
'getNormalFee',
'getPeers',
'getWallet',
({candidates, getNormalFee, getPeers, getWallet}, cbk) =>
{
if (!candidates.length) {
return cbk([404, 'NoObviousCandidateForNewChannel']);
}
const hasPeer = !!getPeers.peers.find(n => n.public_key === args.peer);
// Find peer that can be connected to
return asyncDetectSeries(
shuffle({array: candidates}).shuffled,
(candidate, cbk) => {
// Exit early when the candidate is self
if (candidate.public_key === getWallet.public_key) {
return cbk(null, false);
}
return getNode({
is_omitting_channels: true,
lnd: args.lnd,
public_key: candidate.public_key,
},
(err, res) => {
// Ignore errors when node is unknown
const sockets = !!res ? res.sockets : [];
// Exit early when there is no socket to connect to
if (!sockets.length && !hasPeer) {
return cbk(null, false);
}
const node = {
alias: !!res && !!res.alias ? res.alias : undefined,
past_forwarded: asBigTok(candidate.forwarded),
current_inbound: asBigTok(candidate.inbound),
current_outbound: asBigTok(candidate.outbound),
public_key: candidate.public_key,
};
args.logger.info({
evaluating: `${node.alias || String()} ${node.public_key}`,
});
return connectToPeer({
id: node.public_key,
lnd: args.lnd,
logger: args.logger,
sockets: sockets.map(n => n.socket),
},
err => {
if (!!err && !hasPeer) {
return cbk(null, false);
}
const normalFee = getNormalFee.tokens_per_vbyte;
const feeRate = args.chain_fee_rate || normalFee;
// Exit early when this is a dry run
if (!!args.is_dry_run) {
args.logger.info({
opening_with: node,
chain_fee_tokens_per_vbyte: feeRate,
is_dry_run: true,
new_channel_size: asBigTok(args.tokens || channelTokens),
});
return cbk(null, true);
}
return openChannel({
chain_fee_tokens_per_vbyte: feeRate,
description: defaultDescription,
fee_rate: numericFeeRate(args.set_fee_rate),
is_private: args.is_private,
lnd: args.lnd,
local_tokens: args.tokens || channelTokens,
partner_public_key: node.public_key,
},
(err, res) => {
const [, code] = err || [];
// Exit early when there is not enough balance
if (code === 'InsufficientFundsToCreateChannel') {
return cbk(err);
}
// Exit early when there is only one candidate
if (!!err && !!args.peer) {
return cbk(err);
}
// Channel open failure, try a different peer
if (!!err) {
return cbk(null, false);
}
args.logger.info({
opening_with: node,
chain_fee_tokens_per_vbyte: feeRate,
transaction_id: res.transaction_id,
new_channel_size: asBigTok(args.tokens || channelTokens),
is_private: args.is_private || undefined,
});
return cbk(null, true);
});
});
});
},
(err, selected) => {
if (!!err) {
return cbk(err);
}
if (!selected) {
return cbk([400, 'FailedToConnectToAnyCandidatePeer']);
}
return cbk(null, selected);
},
);
}],
// Set fee rate
setFeeRate: ['openChannel', ({openChannel}, cbk) => {
// Exit early when not specifying fee rates
if (!args.set_fee_rate) {
return cbk();
}
return adjustFees({
cltv_delta: undefined,
fee_rate: args.set_fee_rate,
fs: args.fs,
lnd: args.lnd,
logger: args.logger,
to: [openChannel.public_key],
},
cbk);
}],
},
returnResult({reject, resolve}, cbk));
});
};