mirror of
https://github.com/alexbosworth/balanceofsatoshis.git
synced 2026-08-13 12:33:37 +02:00
390 lines
12 KiB
JavaScript
390 lines
12 KiB
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));
|
|
});
|
|
};
|