avoid invalid local channels when specifying inbound peer

This commit is contained in:
Alex Bosworth 2019-11-25 21:05:36 -08:00
parent 91fb7b6502
commit 774f601d01
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
13 changed files with 1146 additions and 929 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
package-lock.json binary

View file

@ -1,6 +1,6 @@
# Versions
## Version 5.5.0
## Version 5.5.1
- `peers`: Add `fee-days` option to show fees earned via a peer
- `peers`: Add `first_connected` attribute to peers list

44
bos
View file

@ -215,18 +215,22 @@ prog
.option('--no-color', 'Mute all colors')
.option('--node <node_name>', 'Node to get chain fees view from')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getChainFees({
blocks: options.blocks,
node: options.node,
},
returnObject({
logger,
reject,
resolve,
file: options.file,
write: writeFile,
}));
return new Promise(async (resolve, reject) => {
try {
return getChainFees({
blocks: options.blocks,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
},
returnObject({
logger,
reject,
resolve,
file: options.file,
write: writeFile,
}));
} catch (err) {
return reject(err);
}
});
})
@ -258,12 +262,16 @@ prog
.option('--no-color', 'Mute all colors')
.option('--node <node_name>', 'Get channel closes from saved node')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getChannelCloses({
limit: options.limit,
node: options.node,
},
returnObject({logger, reject, resolve}));
return new Promise(async (resolve, reject) => {
try {
return getChannelCloses({
limit: options.limit,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
},
returnObject({logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})

View file

@ -19,10 +19,10 @@ const start = 2;
{
[blocks]: <Block Count Number>
[node]: <Node Name String>
lnd: <Authenticated LND gRPC API Object>
}
@returns via cbk
@returns via cbk or Promise
{
current_block_hash: <Chain Tip Best Block Hash Hex String>
fee_by_block_target: {
@ -30,63 +30,64 @@ const start = 2;
}
}
*/
module.exports = ({blocks, node}, cbk) => {
return asyncAuto({
// Authenticated lnd
getLnd: cbk => authenticatedLnd({node}, cbk),
module.exports = ({blocks, lnd}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedLndToGetChainFees']);
}
// Get wallet info
getInfo: ['getLnd', ({getLnd}, cbk) => {
return getWalletInfo({lnd: getLnd.lnd}, cbk);
}],
// Get the fees
getFees: ['getLnd', ({getLnd}, cbk) => {
const blockCount = blocks || defaultBlockCount;
return asyncTimesSeries(blockCount - iteration, (i, cbk) => {
return getChainFeeRate({
confirmation_target: start + i,
lnd: getLnd.lnd,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
// Exit with error when there is an invalid fee rate response
if (!res.tokens_per_vbyte || res.tokens_per_vbyte < minFeeRate) {
return cbk([503, 'UnexpectedChainFeeRateInGetFeesResponse']);
}
return cbk(null, {rate: res.tokens_per_vbyte, target: start + i});
});
return cbk();
},
cbk);
}],
// Collapse chain fees into steps
chainFees: ['getFees', 'getInfo', ({getFees, getInfo}, cbk) => {
let cursor = {};
const feeByBlockTarget = {};
// Get the fees
getFees: ['validate', ({}, cbk) => {
const blockCount = blocks || defaultBlockCount;
getFees
.filter(fee => {
const isNewFee = cursor.rate !== fee.rate;
return asyncTimesSeries(blockCount - iteration, (i, cbk) => {
return getChainFeeRate({
lnd,
confirmation_target: start + i,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
cursor = isNewFee ? fee : cursor;
return cbk(null, {rate: res.tokens_per_vbyte, target: start + i});
});
},
cbk);
}],
return isNewFee;
})
.forEach(fee => {
return feeByBlockTarget[fee.target+''] = ceil(fee.rate * bytesPerKb);
// Get wallet info
getInfo: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
// Collapse chain fees into steps
chainFees: ['getFees', 'getInfo', ({getFees, getInfo}, cbk) => {
let cursor = {};
const feeByBlockTarget = {};
getFees
.filter(fee => {
const isNewFee = cursor.rate !== fee.rate;
cursor = isNewFee ? fee : cursor;
return isNewFee;
})
.forEach(({target, rate}) => {
return feeByBlockTarget[target+''] = ceil(rate * bytesPerKb);
});
return cbk(null, {
current_block_hash: getInfo.current_block_hash,
fee_by_block_target: feeByBlockTarget,
});
return cbk(null, {
current_block_hash: getInfo.current_block_hash,
fee_by_block_target: feeByBlockTarget,
});
}],
},
returnResult({of :'chainFees'}, cbk));
}],
},
returnResult({reject, resolve, of :'chainFees'}, cbk));
});
};

View file

@ -2,6 +2,7 @@ const asyncAuto = require('async/auto');
const asyncMapSeries = require('async/mapSeries');
const {authenticatedLndGrpc} = require('ln-service');
const {getClosedChannels} = require('ln-service');
const {getNode} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {take} = require('lodash');
@ -16,7 +17,7 @@ const defaultLimit = 20;
{
[limit]: <Limit Number>
[node]: <Node Name String>
lnd: <Authenticated LND gRPC API Object>
}
@returns via cbk
@ -39,72 +40,89 @@ const defaultLimit = 20;
}]
}
*/
module.exports = (args, cbk) => {
return asyncAuto({
// Get lnd
getLnd: cbk => authenticatedLnd({node: args.node}, cbk),
// Lnd
lnd: ['getLnd', ({getLnd}, cbk) => cbk(null, getLnd.lnd)],
// Get closed channels
getClosed: ['lnd', ({lnd}, cbk) => getClosedChannels({lnd}, cbk)],
// Get the current height
getHeight: ['lnd', ({lnd}, cbk) => getWalletInfo({lnd}, cbk)],
// Get the network
getNetwork: ['lnd', ({lnd}, cbk) => getNetwork({lnd}, cbk)],
// Get spends
getSpends: [
'getClosed',
'getHeight',
'getNetwork',
({getClosed, getHeight, getNetwork}, cbk) =>
{
const closedChannels = getClosed.channels
.reverse()
.filter(channel => !channel.is_funding_cancel);
const limit = args.limit || defaultLimit;
return asyncMapSeries(take(closedChannels, limit), (channel, cbk) => {
return getChannelResolution({
close_transaction_id: channel.close_transaction_id,
is_cooperative_close: channel.is_cooperative_close,
network: getNetwork.network,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
const currentHeight = getHeight.current_block_height;
return cbk(null, {
blocks_since_close: currentHeight - channel.close_confirm_height,
capacity: channel.capacity,
close_transaction_id: channel.close_transaction_id,
is_breach_close: channel.is_breach_close || undefined,
is_cooperative_close: channel.is_cooperative_close || undefined,
is_local_force_close: channel.is_local_force_close || undefined,
is_remote_force_close: channel.is_remote_force_close || undefined,
output_resolutions: res.resolutions || undefined,
partner_public_key: channel.partner_public_key,
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
});
});
},
(err, closes) => {
if (!!err) {
return cbk(err);
module.exports = ({limit, lnd}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedLndToGetChannelCloses']);
}
return cbk(null, {closes: closes.reverse()});
});
}],
},
returnResult({of :'getSpends'}, cbk));
return cbk();
},
// Get closed channels
getClosed: ['validate', ({}, cbk) => getClosedChannels({lnd}, cbk)],
// Get the current height
getHeight: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
// Get the network
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
// Get spends
getSpends: [
'getClosed',
'getHeight',
'getNetwork',
({getClosed, getHeight, getNetwork}, cbk) =>
{
const closedChannels = getClosed.channels
.reverse()
.filter(channel => !channel.is_funding_cancel);
const num = limit || defaultLimit;
return asyncMapSeries(take(closedChannels, num), (channel, cbk) => {
return getChannelResolution({
close_transaction_id: channel.close_transaction_id,
is_cooperative_close: channel.is_cooperative_close,
network: getNetwork.network,
},
async (err, res) => {
if (!!err) {
return cbk(err);
}
let alias;
const currentHeight = getHeight.current_block_height;
const isRemoteForceClose = channel.is_remote_force_close;
try {
const node = await getNode({
lnd,
is_omitting_channels: true,
public_key: channel.partner_public_key,
});
alias = node.alias;
} catch (err) {}
return cbk(null, {
alias: alias || undefined,
blocks_since_close: currentHeight - channel.close_confirm_height,
capacity: channel.capacity,
close_transaction_id: channel.close_transaction_id,
is_breach_close: channel.is_breach_close || undefined,
is_cooperative_close: channel.is_cooperative_close || undefined,
is_local_force_close: channel.is_local_force_close || undefined,
is_remote_force_close: isRemoteForceClose || undefined,
output_resolutions: res.resolutions || undefined,
partner_public_key: channel.partner_public_key,
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
});
});
},
cbk);
}],
// Channel closes
closes: ['getSpends', ({getSpends}, cbk) => {
return cbk(null, {closes: getSpends.slice().reverse()});
}],
},
returnResult({reject, resolve, of :'closes'}, cbk));
});
};

View file

@ -138,7 +138,7 @@ module.exports = (args, cbk) => {
});
if (!withBalance.length) {
return cbk([404, 'NoChannelWithSufficientBalance']);
return cbk([404, 'NoOutboundPeerWithSufficientBalance']);
}
const attribute = 'local_balance';

1064
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@
"ini": "1.3.5",
"inquirer": "7.0.0",
"ln-accounting": "3.1.6",
"ln-service": "47.5.0",
"ln-service": "47.5.1",
"moment": "2.24.0",
"qrcode-terminal": "0.12.0",
"request": "2.88.0",
@ -38,7 +38,7 @@
},
"description": "Lightning balance CLI",
"devDependencies": {
"tap": "14.10.1"
"tap": "14.10.2"
},
"engines": {
"node": ">=10.12.0"
@ -61,5 +61,5 @@
"scripts": {
"test": "tap test/arrays/*.js test/balances/*.js test/chain/*.js test/encryption/*.js test/fiat/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/responses/*.js test/routing/*.js test/swaps/*.js test/telegram/*.js"
},
"version": "5.5.0"
"version": "5.5.1"
}

View file

@ -1,5 +1,7 @@
const asyncAuto = require('async/auto');
const {getChannels} = require('ln-service');
const {getNode} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const tokensAsMtokens = tokens => BigInt(tokens) * BigInt(1e3);
@ -49,13 +51,24 @@ module.exports = ({destination, lnd, through, tokens}, cbk) => {
return cbk();
},
// Get channels to validate the inbound channel exists
getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
// Get local node info to check if this is a local inbound channel
getInfo: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
// Get node
getNode: ['validate', ({}, cbk) => {
return getNode({lnd, public_key: destination}, cbk);
}],
// Connecting path
path: ['getNode', ({getNode}, cbk) => {
path: [
'getChannels',
'getInfo',
'getNode',
({getChannels, getInfo, getNode}, cbk) =>
{
const connectingChannels = getNode.channels
.filter(chan => !!chan.policies.find(n => n.public_key === through));
@ -63,6 +76,8 @@ module.exports = ({destination, lnd, through, tokens}, cbk) => {
return cbk([400, 'NoConnectingChannelToPayIn']);
}
const publicKey = getInfo.public_key;
const [channel] = connectingChannels.filter(chan => {
const policy = chan.policies.find(n => n.public_key === through);
@ -74,6 +89,17 @@ module.exports = ({destination, lnd, through, tokens}, cbk) => {
return false;
}
const isLocal = chan.policies.find(n => n.public_key === publicKey);
const localChannel = getChannels.channels.find(({id}) => {
return id === chan.id;
});
// Exit early when this is a local channel but doesn't exist
if (!!isLocal && !localChannel) {
return false;
}
return BigInt(policy.max_htlc_mtokens) > tokensAsMtokens(tokens);
});

View file

@ -26,6 +26,7 @@ const mtokensPerToken = BigInt(1e3);
const notFoundIndex = -1;
const rateDivisor = 1e6;
const sample = a => !!a.length ? a[Math.floor(Math.random()*a.length)] : null;
const tokAsBigTok = tokens => (tokens / 1e8).toFixed(8);
const topOf = arr => arr.slice(0, Math.ceil(arr.length / 2));
/** Rebalance funds between peers
@ -40,329 +41,344 @@ const topOf = arr => arr.slice(0, Math.ceil(arr.length / 2));
[node]: <Node Name String>
[out_through]: <Out through peer with Public Key Hex String>
}
@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToRebalance'])
}
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToRebalance'])
}
if (!!args.in_through && args.in_through === args.out_through) {
return cbk([400, 'ExpectedInPeerNotEqualToOutPeer']);
}
if (!!args.in_through && args.in_through === args.out_through) {
return cbk([400, 'ExpectedInPeerNotEqualToOutPeer']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToExecuteRebalance']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToExecuteRebalance']);
}
if (args.max_fee === 0) {
return cbk([400, 'ExpectedNonZeroMaxFeeForRebalance']);
}
if (args.max_fee === 0) {
return cbk([400, 'ExpectedNonZeroMaxFeeForRebalance']);
}
if (args.max_fee_rate === 0) {
return cbk([400, 'ExpectedNonZeroMaxFeeRateForRebalance']);
}
if (args.max_fee_rate === 0) {
return cbk([400, 'ExpectedNonZeroMaxFeeRateForRebalance']);
}
if (!!args.out_through && args.in_through === args.out_through) {
return cbk([400, 'ExpectedOutPeerNotEqualToInPeer']);
}
if (!!args.out_through && args.in_through === args.out_through) {
return cbk([400, 'ExpectedOutPeerNotEqualToInPeer']);
}
return cbk();
},
// Lnd by itself
lnd: ['validate', ({}, cbk) => cbk(null, args.lnd)],
// Get initial liquidity
getInitialLiquidity: ['lnd', ({lnd}, cbk) => getChannels({lnd}, cbk)],
// Get public key
getPublicKey: ['lnd', ({lnd}, cbk) => getWalletInfo({lnd}, cbk)],
// Get fee rates
getFees: ['getPublicKey', 'lnd', ({getPublicKey, lnd}, cbk) => {
return getNode({lnd, public_key: getPublicKey.public_key}, cbk);
}],
// Get outbound node details
getOutbound: [
'getInitialLiquidity',
'lnd',
({getInitialLiquidity, lnd}, cbk) =>
{
const ignore = args.avoid || [];
const active = getInitialLiquidity.channels
.filter(n => !!n.is_active)
.filter(n => ignore.indexOf(n.partner_public_key) === notFoundIndex);
const channels = active
.map(channel => {
const remote = active
.filter(n => n.partner_public_key === channel.partner_public_key)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
return {remote, partner_public_key: channel.partner_public_key};
})
.filter(n => n.remote < minRemoteBalance);
if (!args.out_through && !channels.length) {
return cbk([400, 'NoOutboundChannelNeedsARebalance']);
}
const {sorted} = sortBy({array: channels, attribute: 'remote'});
const key = args.out_through || sample(sorted).partner_public_key;
return getNode({
lnd,
is_omitting_channels: true,
public_key: key,
return cbk();
},
(err, res) => {
return cbk(null, {
alias: !!res && !!res.alias ? `${res.alias} ${key}` : key,
// Lnd by itself
lnd: ['validate', ({}, cbk) => cbk(null, args.lnd)],
// Get initial liquidity
getInitialLiquidity: ['lnd', ({lnd}, cbk) => getChannels({lnd}, cbk)],
// Get public key
getPublicKey: ['lnd', ({lnd}, cbk) => getWalletInfo({lnd}, cbk)],
// Get fee rates
getFees: ['getPublicKey', 'lnd', ({getPublicKey, lnd}, cbk) => {
return getNode({lnd, public_key: getPublicKey.public_key}, cbk);
}],
// Get outbound node details
getOutbound: [
'getInitialLiquidity',
'lnd',
({getInitialLiquidity, lnd}, cbk) =>
{
const ignore = args.avoid || [];
const active = getInitialLiquidity.channels
.filter(n => !!n.is_active)
.filter(n => ignore.indexOf(n.partner_public_key) === notFoundIndex);
const channels = active
.map(channel => {
const remote = active
.filter(n => n.partner_public_key === channel.partner_public_key)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
return {remote, partner_public_key: channel.partner_public_key};
})
.filter(n => n.remote < minRemoteBalance);
if (!args.out_through && !channels.length) {
return cbk([400, 'NoOutboundChannelNeedsARebalance']);
}
const {sorted} = sortBy({array: channels, attribute: 'remote'});
const key = args.out_through || sample(sorted).partner_public_key;
return getNode({
lnd,
is_omitting_channels: true,
public_key: key,
},
(err, res) => {
return cbk(null, {
alias: !!res && !!res.alias ? `${res.alias} ${key}` : key,
public_key: key,
});
});
});
}],
}],
// Get inbound node details
getInbound: [
'getFees',
'getInitialLiquidity',
'getOutbound',
'lnd',
({getFees, getInitialLiquidity, getOutbound, lnd}, cbk) =>
{
const ignore = args.avoid || [];
// Get inbound node details
getInbound: [
'getFees',
'getInitialLiquidity',
'getOutbound',
'lnd',
({getFees, getInitialLiquidity, getOutbound, lnd}, cbk) =>
{
const hasInThrough = !!args.in_through;
const ignore = args.avoid || [];
const activeChannels = getInitialLiquidity.channels
.filter(n => !!n.is_active)
.filter(n => n.partner_public_key !== getOutbound.public_key)
.filter(n => ignore.indexOf(n.partner_public_key) === notFoundIndex);
const activeChannels = getInitialLiquidity.channels
.filter(n => !!n.is_active)
.filter(n => n.partner_public_key !== getOutbound.public_key)
.filter(n => ignore.indexOf(n.partner_public_key) === notFoundIndex);
const channels = activeChannels
.filter(n => !!args.in_through || n.remote_balance > minInboundBalance)
.map(channel => {
const remote = activeChannels
.filter(n => n.partner_public_key === channel.partner_public_key)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
const channels = activeChannels
.filter(n => hasInThrough || n.remote_balance > minInboundBalance)
.map(channel => {
const remote = activeChannels
.filter(n => n.partner_public_key === channel.partner_public_key)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
return {
remote,
id: channel.id,
partner_public_key: channel.partner_public_key,
};
return {
remote,
id: channel.id,
partner_public_key: channel.partner_public_key,
};
});
if (!channels.length) {
return cbk([400, 'NoInboundChannelIsAvailableToReceiveRebalance']);
}
const array = channels.filter(chan => {
const peerKey = chan.partner_public_key;
const policies = getFees.channels.map(({policies}) => {
return policies.find(n => n.public_key === peerKey);
});
const feeRates = policies.filter(n => !!n).map(n => n.fee_rate);
const feeRate = max(...feeRates);
return feeRate < (args.max_fee_rate || defaultMaxFeeRate);
});
if (!channels.length) {
return cbk([400, 'NoInboundChannelIsAvailableToReceiveRebalance']);
}
if (!array.length) {
return cbk([400, 'NoLowFeeInboundChannelToReceiveRebalance']);
}
const array = channels.filter(chan => {
const policies = getFees.channels.map(({policies}) => {
return policies.find(n => n.public_key === chan.partner_public_key);
});
const {sorted} = sortBy({array, attribute: 'remote'});
const feeRate = max(...policies.filter(n => !!n).map(n => n.fee_rate));
const suggestedInbound = sample(topOf(sorted.slice().reverse()));
return feeRate < (args.max_fee_rate || defaultMaxFeeRate);
});
const key = args.in_through || suggestedInbound.partner_public_key;
if (!array.length) {
return cbk([400, 'NoLowFeeInboundChannelToReceiveRebalance']);
}
const {sorted} = sortBy({array, attribute: 'remote'});
const suggestedInbound = sample(topOf(sorted.slice().reverse()));
const key = args.in_through || suggestedInbound.partner_public_key;
return getNode({
lnd,
is_omitting_channels: true,
public_key: key,
},
(err, res) => {
return cbk(null, {
alias: !!res && !!res.alias ? `${res.alias} ${key}` : key,
return getNode({
lnd,
is_omitting_channels: true,
public_key: key,
},
(err, res) => {
return cbk(null, {
alias: !!res && !!res.alias ? `${res.alias} ${key}` : key,
public_key: key,
});
});
});
}],
}],
// Find a route to the destination
findRoute: [
'getInbound',
'getOutbound',
'getPublicKey',
({getInbound, getOutbound, getPublicKey}, cbk) =>
{
const avoid = (args.avoid || []).map(n => ({from_public_key: n}));
// Find a route to the destination
findRoute: [
'getInbound',
'getOutbound',
'getPublicKey',
({getInbound, getOutbound, getPublicKey}, cbk) =>
{
const avoid = (args.avoid || []).map(n => ({from_public_key: n}));
return probeDestination({
destination: getPublicKey.public_key,
find_max: 5e6,
ignore: [{from_public_key: getPublicKey.public_key}].concat(avoid),
in_through: getInbound.public_key,
logger: args.logger,
lnd: args.lnd,
max_fee: Math.floor(5e6 * 0.0025),
node: args.node,
out_through: getOutbound.public_key,
tokens: 10000,
},
cbk);
}],
return probeDestination({
destination: getPublicKey.public_key,
find_max: 5e6,
ignore: [{from_public_key: getPublicKey.public_key}].concat(avoid),
in_through: getInbound.public_key,
logger: args.logger,
lnd: args.lnd,
max_fee: Math.floor(5e6 * 0.0025),
node: args.node,
out_through: getOutbound.public_key,
tokens: 10000,
},
cbk);
}],
// Get channels for the rebalance route
channels: [
'findRoute',
'getInitialLiquidity',
'getPublicKey',
'lnd',
({findRoute, getPublicKey, lnd}, cbk) =>
{
if (!findRoute.success) {
return cbk([400, 'FailedToFindPathBetweenPeers']);
}
// Get channels for the rebalance route
channels: [
'findRoute',
'getInitialLiquidity',
'getPublicKey',
'lnd',
({findRoute, getPublicKey, lnd}, cbk) =>
{
if (!findRoute.success) {
return cbk([400, 'FailedToFindPathBetweenPeers']);
}
let from = getPublicKey.public_key;
let from = getPublicKey.public_key;
return asyncMapSeries(findRoute.success, (id, cbk) => {
return getChannel({id, lnd}, (err, channel) => {
if (!!err) {
return cbk(err);
return asyncMapSeries(findRoute.success, (id, cbk) => {
return getChannel({id, lnd}, (err, channel) => {
if (!!err) {
return cbk(err);
}
const {capacity} = channel;
const {policies} = channel;
const to = policies.find(n => n.public_key !== from).public_key;
// The next hop from will be this hop's to
from = to;
return cbk(null, {capacity, id, policies, destination: to});
});
},
cbk);
}],
// Create local invoice
invoice: ['channels', 'findRoute', 'lnd', ({findRoute, lnd}, cbk) => {
return createInvoice({
lnd,
cltv_delta: cltvDelta,
description: 'Rebalance',
tokens: min(maxRebalanceTokens, findRoute.route_maximum),
},
cbk);
}],
// Get the current height
getHeight: ['channels', 'lnd', ({lnd}, cbk) => {
return getWalletInfo({lnd}, cbk);
}],
// Calculate route for rebalance
routes: [
'channels',
'getHeight',
'getPublicKey',
'invoice',
({channels, getHeight, getPublicKey, invoice}, cbk) =>
{
try {
const {route} = routeFromChannels({
channels,
cltv_delta: cltvDelta,
destination: getPublicKey.public_key,
height: getHeight.current_block_height,
mtokens: (BigInt(invoice.tokens) * mtokensPerToken).toString(),
});
const maxFee = args.max_fee || defaultMaxFee;
const maxFeeRate = args.max_fee_rate || defaultMaxFeeRate;
// Exit early when a max fee is specified and exceeded
if (!!maxFee && route.fee > maxFee) {
return cbk([
400,
'RebalanceFeeTooHigh',
{needed_max_fee: route.fee},
]);
}
const {capacity} = channel;
const {policies} = channel;
const feeRate = ceil(route.fee / route.tokens * rateDivisor);
const to = policies.find(n => n.public_key !== from).public_key;
// Exit early when the max fee rate is specified and exceeded
if (!!maxFeeRate && feeRate > maxFeeRate) {
return cbk([
400,
'RebalanceFeeTooHigh',
{needed_max_fee_rate: feeRate},
]);
}
// The next hop from will be this hop's to
from = to;
return cbk(null, [route]);
} catch (err) {
return cbk([500, 'FailedToConstructRebalanceRoute', {err}]);
}
}],
return cbk(null, {capacity, id, policies, destination: to});
});
},
cbk);
}],
// Execute the rebalance
pay: ['invoice', 'lnd', 'routes', ({invoice, lnd, routes}, cbk) => {
return payViaRoutes({lnd, routes, id: invoice.id}, cbk);
}],
// Create local invoice
invoice: ['channels', 'findRoute', 'lnd', ({findRoute, lnd}, cbk) => {
return createInvoice({
lnd,
cltv_delta: cltvDelta,
description: 'Rebalance',
tokens: min(maxRebalanceTokens, findRoute.route_maximum),
},
cbk);
}],
// Final rebalancing
rebalance: [
'getInbound',
'getInitialLiquidity',
'getOutbound',
'pay',
({getInbound, getInitialLiquidity, getOutbound, pay}, cbk) =>
{
const inPeerInbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getInbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
// Get the current height
getHeight: ['channels', 'lnd', ({lnd}, cbk) => getWalletInfo({lnd}, cbk)],
const inPeerOutbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getInbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.local_balance, minTokens);
// Calculate route for rebalance
routes: [
'channels',
'getHeight',
'getPublicKey',
'invoice',
({channels, getHeight, getPublicKey, invoice}, cbk) =>
{
try {
const {route} = routeFromChannels({
channels,
cltv_delta: cltvDelta,
destination: getPublicKey.public_key,
height: getHeight.current_block_height,
mtokens: (BigInt(invoice.tokens) * mtokensPerToken).toString(),
const outPeerInbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getOutbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
const outPeerOutbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getOutbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.local_balance, minTokens);
args.logger.info({
rebalance: [
{
increased_inbound_on: getOutbound.alias,
liquidity_inbound: tokAsBigTok(outPeerInbound + pay.tokens),
liquidity_outbound: tokAsBigTok(outPeerOutbound - pay.tokens),
},
{
decreased_inbound_on: getInbound.alias,
liquidity_inbound: tokAsBigTok(inPeerInbound - pay.tokens),
liquidity_outbound: tokAsBigTok(inPeerOutbound + pay.tokens),
},
{
rebalanced: tokAsBigTok(pay.tokens),
rebalance_fees_spent: tokAsBigTok(pay.fee),
},
],
});
const maxFee = args.max_fee || defaultMaxFee;
const maxFeeRate = args.max_fee_rate || defaultMaxFeeRate;
// Exit early when a max fee is specified and exceeded
if (!!maxFee && route.fee > maxFee) {
return cbk([
400,
'RebalanceFeeTooHigh',
{needed_max_fee: route.fee},
]);
}
const feeRate = ceil(route.fee / route.tokens * rateDivisor);
// Exit early when the max fee rate is specified and exceeded
if (!!maxFeeRate && feeRate > maxFeeRate) {
return cbk([
400,
'RebalanceFeeTooHigh',
{needed_max_fee_rate: feeRate},
]);
}
return cbk(null, [route]);
} catch (err) {
return cbk([500, 'FailedToConstructRebalanceRoute', {err}]);
}
}],
// Execute the rebalance
pay: ['invoice', 'lnd', 'routes', ({invoice, lnd, routes}, cbk) => {
return payViaRoutes({lnd, routes, id: invoice.id}, cbk);
}],
// Final rebalancing
rebalance: [
'getInbound',
'getInitialLiquidity',
'getOutbound',
'pay',
({getInbound, getInitialLiquidity, getOutbound, pay}, cbk) =>
{
const inPeerInbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getInbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
const inPeerOutbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getInbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.local_balance, minTokens);
const outPeerInbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getOutbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.remote_balance, minTokens);
const outPeerOutbound = getInitialLiquidity.channels
.filter(n => n.partner_public_key === getOutbound.public_key)
.filter(n => !!n.is_active)
.reduce((sum, n) => sum + n.local_balance, minTokens);
return cbk(null, {
rebalanced_liquidity: [
{
spent_out: getOutbound.alias,
liquidity_inbound: outPeerInbound + pay.tokens,
liquidity_outbound: outPeerOutbound - pay.tokens,
},
{
received_in: getInbound.alias,
liquidity_inbound: inPeerInbound - pay.tokens,
liquidity_outbound: inPeerOutbound + pay.tokens,
},
],
rebalanced: pay.tokens,
rebalance_fee_paid: pay.fee,
});
}],
},
returnResult({of: 'rebalance'}, cbk));
return cbk();
}],
},
returnResult({reject, resolve}, cbk));
});
};

View file

@ -349,10 +349,12 @@ module.exports = (args, cbk) => {
const swapDelayMin = !args.is_fast ? slowDelayMinutes : fastDelayMinutes;
const fundAt = moment().add(swapDelayMin, 'minutes');
return createSwapOut({
network,
service,
fund_at: moment().add(swapDelayMin, 'minutes').toISOString(),
fund_at: fundAt.toISOString(),
tokens: args.tokens,
},
cbk);
@ -664,6 +666,8 @@ module.exports = (args, cbk) => {
return cbk();
}
args.logger.info({funding_swap: decodeFundingRequest.id});
return payViaRoutes({
id: decodeFundingRequest.id,
lnd: args.lnd,
@ -696,7 +700,14 @@ module.exports = (args, cbk) => {
return cbk();
}
args.logger.info({paying_execution_request: decodeExecutionRequest.id});
const swapDelayMin = !args.is_fast ? slowDelayMinutes : fastDelayMinutes;
const fundAt = moment().add(swapDelayMin, 'minutes');
args.logger.info({
paying_execution_request: decodeExecutionRequest.id,
estimated_swap_start_time: fundAt.calendar(),
});
const sub = subscribeToPayViaRequest({
lnd: args.lnd,

View file

@ -0,0 +1,57 @@
const {test} = require('tap');
const {getChainFees} = require('./../../chain');
const {getInfoResponse} = require('./../network/fixtures');
const getInfoRes = () => JSON.parse(JSON.stringify(getInfoResponse));
const tests = [
{
args: {},
description: 'LND is required',
error: [400, 'ExpectedLndToGetChainFees'],
},
{
args: {
lnd: {
default: {getInfo: ({}, cbk) => cbk(null, getInfoRes())},
wallet: {estimateFee: (args, cbk) => cbk('err')},
},
},
description: 'Errors from get chain fee are passed back',
error: [503, 'UnexpectedErrorGettingFeeFromLnd', {err: 'err'}],
},
{
args: {
lnd: {
default: {getInfo: ({}, cbk) => cbk(null, getInfoRes())},
wallet: {
estimateFee: (args, cbk) => {
return cbk(null, {
sat_per_kw: 1 + Math.round(1 / args.conf_target),
});
},
},
},
},
description: 'Fees are mapped to levels',
expected: {
current_block_hash: '00',
fee_by_block_target: {'2': 8, '3': 4},
},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({deepIs, end, rejects}) => {
if (!!error) {
rejects(getChainFees(args), error, 'Got expected error');
} else {
const fees = await getChainFees(args);
deepIs(fees, expected, 'Got expected fees rundown');
}
return end();
});
});

View file

@ -1,6 +1,9 @@
const {test} = require('tap');
const {getInboundPath} = require('./../../routing');
const {getInfoResponse} = require('./../network/fixtures');
const getInfoRes = () => JSON.parse(JSON.stringify(getInfoResponse));
const makeLnd = ({channels}) => {
return {
@ -8,6 +11,7 @@ const makeLnd = ({channels}) => {
getChanInfo: (args, cbk) => {
return cbk(null, channels.find(n => n.channel_id === args.chan_id));
},
getInfo: ({}, cbk) => cbk(null, getInfoRes()),
getNodeInfo: ({}, cbk) => {
return cbk(null, {
channels: channels || [],
@ -24,6 +28,7 @@ const makeLnd = ({channels}) => {
total_capacity: '0',
});
},
listChannels: ({}, cbk) => cbk(null, {channels: []}),
},
};
};