add cleartext export for credentials

This commit is contained in:
Alex Bosworth 2019-12-07 20:13:28 -08:00
parent 583f2523cc
commit a82fa3b3bc
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
29 changed files with 926 additions and 349 deletions

View file

@ -1,5 +1,9 @@
# Versions
## Version 5.6.2
- `credentials`: add `--cleartext` option to output node access credentials
## Version 5.6.1
- `increase-outbound-liquidity`: add `fee-rate` option to specify chain fee rate

View file

@ -1,4 +1,5 @@
const {floor} = Math;
const {isArray} = Array;
const {random} = Math;
/** Shuffle array
@ -13,15 +14,21 @@ const {random} = Math;
}
*/
module.exports = ({array}) => {
const shuffle = array.slice();
if (!!shuffle.length) {
for (let i = shuffle.length - 1; !!i; i--) {
const j = floor(random() * (i + 1));
[shuffle[i], shuffle[j]] = [shuffle[j], shuffle[i]];
}
if (!isArray(array)) {
throw new Error('ExpectedArrayToShuffle');
}
return {shuffled: shuffle};
if (!array.length) {
return {shuffled: []};
}
const shuffled = array.slice();
for (let i = shuffled.length - 1; !!i; i--) {
const j = floor(random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return {shuffled};
};

View file

@ -19,7 +19,6 @@ const none = 0;
[is_offchain_only]: <Get Only Channels Tokens Bool>
[is_onchain_only]: <Get Only Chain Tokens Bool>
lnd: <Authenticated LND gRPC API Object>
[node]: <Node Name String>
}
@returns via cbk

View file

@ -5,7 +5,6 @@ const {percentile} = require('stats-lite');
const {returnResult} = require('asyncjs-util');
const balanceFromTokens = require('./balance_from_tokens');
const {lndCredentials} = require('./../lnd');
const {round} = Math;
const topPercentile = 0.9;
@ -17,7 +16,7 @@ const topPercentile = 0.9;
[below]: <Tokens Below Tokens Number>
[is_outbound]: <Return Outbound Liquidity Bool>
[is_top]: <Return Top Liquidity Bool>
[node]: <Node Name String>
lnd: <Authenticated LND gRPC API Object>
[with]: <Liquidity With Specific Node Public Key Hex String>
}
@ -27,57 +26,54 @@ const topPercentile = 0.9;
}
*/
module.exports = (args, cbk) => {
return asyncAuto({
// Credentials
credentials: cbk => lndCredentials({node: args.node}, cbk),
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.lnd) {
return cbk([400, 'ExpectedLndToGetLiquidity']);
}
// Lnd
lnd: ['credentials', ({credentials}, cbk) => {
return cbk(null, authenticatedLndGrpc({
cert: credentials.cert,
macaroon: credentials.macaroon,
socket: credentials.socket,
}).lnd);
}],
return cbk();
},
// Get the channels
getChannels: ['lnd', ({lnd}, cbk) => getChannels({lnd}, cbk)],
// Get the channels
getChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.lnd}, cbk);
}],
// List of tokens to sum
tokens: ['getChannels', ({getChannels}, cbk) => {
const activeChannels = getChannels.channels
.filter(n => !!n.is_active)
.filter(n => !args.with || n.partner_public_key === args.with);
// List of tokens to sum
tokens: ['getChannels', ({getChannels}, cbk) => {
const activeChannels = getChannels.channels
.filter(n => !!n.is_active)
.filter(n => !args.with || n.partner_public_key === args.with);
const balanceType = !!args.is_outbound ? 'local' : 'remote';
const balanceType = !!args.is_outbound ? 'local' : 'remote';
const tokens = activeChannels.map(n => n[`${balanceType}_balance`]);
const tokens = activeChannels.map(n => n[`${balanceType}_balance`]);
if (!!args.is_top) {
return cbk(null, [round(percentile(tokens, topPercentile))]);
}
if (!!args.is_top) {
return cbk(null, [round(percentile(tokens, topPercentile))]);
}
return cbk(null, tokens);
}],
return cbk(null, tokens);
}],
// Total balances
total: ['tokens', ({tokens}, cbk) => {
const {above} = args;
const {below} = args;
// Total balances
total: ['tokens', ({tokens}, cbk) => {
const {above} = args;
const {below} = args;
try {
const balance = balanceFromTokens({above, below, tokens});
return cbk(null, balance);
} catch (err) {
return cbk([500, 'FailedToCalculateLiquidityBalance', err]);
}
}],
}],
// Liquidity
liquidity: ['total', ({total}, cbk) => {
return cbk(null, {balance: total});
}],
},
returnResult({of: 'liquidity'}, cbk));
// Liquidity
liquidity: ['total', ({total}, cbk) => {
return cbk(null, {balance: total});
}],
},
returnResult({reject, resolve, of: 'liquidity'}, cbk));
});
};

93
bos
View file

@ -57,6 +57,7 @@ const {version} = require('./package');
const {exit} = process;
const flatten = arr => [].concat(...arr);
const {FLOAT} = prog;
const {INT} = prog;
const {isArray} = Array;
const {keys} = Object;
@ -281,12 +282,14 @@ prog
// Export LND credentials from the standard
.command('credentials', 'Export local credentials')
.help('Output encrypted remote access credentials. Use with "nodes --add"')
.option('--cleartext', 'Output remote access credentials without encryption')
.option('--node <node_name>', 'Get credentials for a saved node')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getCredentials({
logger,
ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
is_cleartext: options.cleartext,
node: options.node,
},
returnObject({logger, reject, resolve}));
@ -324,13 +327,17 @@ prog
.option('--no-color', 'Mute all colors')
.option('--node <node_name>', 'Node to find record on')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return findRecord({
node: options.node,
query: args.query,
},
returnObject({logger, reject, resolve}));
})
try {
return new Promise(async (resolve, reject) => {
return findRecord({
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
query: args.query,
},
returnObject({logger, reject, resolve}));
});
} catch (err) {
return reject(err);
}
})
// Get forwards
@ -387,14 +394,18 @@ prog
.argument('<amount>', 'Tokens to give', INT)
.option('--node <node_name>', 'Source node to use to pay gift')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return sendGift({
node: options.node,
to: args.target,
tokens: args.amount,
},
returnNumber({logger, reject, resolve, number: 'gave_tokens'}));
});
try {
return new Promise(async (resolve, reject) => {
return sendGift({
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
to: args.target,
tokens: args.amount,
},
returnNumber({logger, reject, resolve, number: 'gave_tokens'}));
});
} catch (err) {
return reject(err);
}
})
// Get inbound liquidity information: available inbound off-chain tokens
@ -405,16 +416,20 @@ prog
.option('--top', 'Top percentile inbound liquidity in an individual channel')
.option('--with', 'Liquidity with a specific node')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getLiquidity({
above: options.above || undefined,
below: options.below || undefined,
is_top: options.top || undefined,
node: options.node || undefined,
with: options.with || undefined,
},
returnNumber({logger, reject, resolve, number: 'balance'}));
});
try {
return new Promise(async (resolve, reject) => {
return getLiquidity({
above: options.above || undefined,
below: options.below || undefined,
is_top: options.top || undefined,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
with: options.with || undefined,
},
returnNumber({logger, reject, resolve, number: 'balance'}));
});
} catch (err) {
return reject(err);
}
})
// Increase inbound liquidity
@ -465,7 +480,7 @@ prog
.help('Open a new channel to add more off-chain liquidity')
.option('--amount <amount>', 'Amount to assign to new channel capacity', INT)
.option('--dryrun', 'Avoid actually opening a channel')
.option('--fee-rate <fee_rate>', 'Use specific fee rate (per vbyte)', INT)
.option('--fee-rate <fee_rate>', 'Use specific fee rate (per vbyte)', FLOAT)
.option('--node <node_name>', 'Increase outbound liquidity on saved node')
.option('--with <peer_public_key>', 'Select a specific peer to open with')
.action((args, options, logger) => {
@ -567,17 +582,21 @@ prog
.option('--top', 'Top percentile inbound liquidity in an individual channel')
.option('--with', 'Liquidity with a specific node')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getLiquidity({
above: options.above || undefined,
below: options.below || undefined,
is_outbound: true,
is_top: options.top || undefined,
node: options.node || undefined,
with: options.with || undefined,
},
returnNumber({logger, reject, resolve, number: 'balance'}));
});
try {
return new Promise(async (resolve, reject) => {
return getLiquidity({
above: options.above || undefined,
below: options.below || undefined,
is_outbound: true,
is_top: options.top || undefined,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
with: options.with || undefined,
},
returnNumber({logger, reject, resolve, number: 'balance'}));
});
} catch (err) {
return reject(err);
}
})
// Pay a payment request, probing first

View file

@ -9,7 +9,7 @@ const {take} = require('lodash');
const {authenticatedLnd} = require('./../lnd');
const getChannelResolution = require('./get_channel_resolution');
const {getNetwork} = require('./../network');
const getNetwork = require('./../network/get_network');
const defaultLimit = 20;

View file

@ -4,13 +4,13 @@ const {returnResult} = require('asyncjs-util');
const interval = n => 50 * Math.pow(2, n);
const isNumber = n => !isNaN(n);
const times = 10;
/** Get mempool size
{
network: <Network Name String>
request: <Request Function>
[retries]: <Retries Count Number>
}
@returns via cbk or Promise
@ -18,7 +18,7 @@ const times = 10;
[vbytes]: <Size of Mempool Virtual Bytes Number>
}
*/
module.exports = ({network, request}, cbk) => {
module.exports = ({network, request, retries}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
@ -54,7 +54,7 @@ module.exports = ({network, request}, cbk) => {
return cbk(null, {});
}
return asyncRetry({interval, times}, cbk => {
return asyncRetry({interval, times: retries}, cbk => {
return request({
json: true,
url: `${api}/api/mempool`,

View file

@ -6,8 +6,6 @@ const {getPayment} = require('ln-service');
const moment = require('moment');
const {returnResult} = require('asyncjs-util');
const authenticatedLnd = require('./authenticated_lnd');
const asBigUnit = tokens => (tokens / 1e8).toFixed(8);
const standardIdHexLength = Buffer.alloc(32).toString('hex').length;
@ -16,7 +14,7 @@ const standardIdHexLength = Buffer.alloc(32).toString('hex').length;
Try to find a record by id
{
[node]: <Node Name String>
lnd: <Authenticated LND gRPC API Object>
[query]: <Query String>
}
@ -27,98 +25,104 @@ const standardIdHexLength = Buffer.alloc(32).toString('hex').length;
}
}
*/
module.exports = ({node, query}, cbk) => {
return asyncAuto({
// Lnd
getLnd: cbk => authenticatedLnd({node}, cbk),
module.exports = ({lnd, query}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedLndObjectToFindRecord']);
}
// Get graph
getGraph: ['getLnd', ({getLnd}, cbk) => {
return getNetworkGraph({lnd: getLnd.lnd}, cbk);
}],
return cbk();
},
// Payment
getPayment: ['getLnd', ({getLnd}, cbk) => {
if (query.length !== standardIdHexLength) {
return cbk(null, {});
}
// Get graph
getGraph: ['validate', ({}, cbk) => getNetworkGraph({lnd}, cbk)],
return getPayment({id: query, lnd: getLnd.lnd}, cbk);
}],
// Payment
getPayment: ['validate', ({}, cbk) => {
if (query.length !== standardIdHexLength) {
return cbk(null, {});
}
// Records
records: ['getGraph', 'getPayment', ({getGraph, getPayment}, cbk) => {
const nodes = getGraph.nodes
.filter(node => {
if (node.alias.toLowerCase().includes(query.toLowerCase())) {
return true;
}
return getPayment({lnd, id: query}, cbk);
}],
return node.public_key === query;
})
.map(node => {
return {
alias: node.alias,
capacity: asBigUnit(getGraph.channels.reduce(
(sum, {capacity, policies}) => {
if (!policies.find(n => n.public_key === node.public_key)) {
return sum;
}
return sum + capacity;
},
0
)),
public_key: node.public_key,
updated: moment(node.updated_at).fromNow(),
urls: node.sockets.map(socket => `${node.public_key}@${socket}`),
}
})
.filter(node => node.capacity !== asBigUnit(0));
const channels = getGraph.channels
.filter(channel => {
try {
if (channel.id === chanFormat({number: query}).channel) {
// Records
records: ['getGraph', 'getPayment', ({getGraph, getPayment}, cbk) => {
const nodes = getGraph.nodes
.filter(node => {
if (node.alias.toLowerCase().includes(query.toLowerCase())) {
return true;
}
} catch (err) {}
return channel.id === query;
})
.map(channel => {
return {
capacity: channel.capacity,
id: channel.id,
policies: channel.policies.map(policy => {
const node = getGraph.nodes
.find(n => n.public_key === policy.public_key);
return node.public_key === query;
})
.map(node => {
return {
alias: node.alias,
capacity: asBigUnit(getGraph.channels.reduce(
(sum, {capacity, policies}) => {
if (!policies.find(n => n.public_key === node.public_key)) {
return sum;
}
return {
alias: !node ? undefined : node.alias,
base_fee_mtokens: policy.base_fee_mtokens,
cltv_delta: policy.cltv_delta,
fee_rate: policy.fee_rate,
is_disabled: policy.is_disabled,
max_htlc_mtokens: policy.max_htlc_mtokens,
min_htlc_mtokens: policy.min_htlc_mtokens,
public_key: policy.public_key,
};
}),
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
updated_at: channel.updated_at,
};
return sum + capacity;
},
0
)),
public_key: node.public_key,
updated: moment(node.updated_at).fromNow(),
urls: node.sockets.map(socket => `${node.public_key}@${socket}`),
}
})
.filter(node => node.capacity !== asBigUnit(0));
const channels = getGraph.channels
.filter(channel => {
try {
if (channel.id === chanFormat({number: query}).channel) {
return true;
}
} catch (err) {}
return channel.id === query;
})
.map(channel => {
return {
capacity: channel.capacity,
id: channel.id,
policies: channel.policies.map(policy => {
const node = getGraph.nodes
.find(n => n.public_key === policy.public_key);
return {
alias: !node ? undefined : node.alias,
base_fee_mtokens: policy.base_fee_mtokens,
cltv_delta: policy.cltv_delta,
fee_rate: policy.fee_rate,
is_disabled: policy.is_disabled,
max_htlc_mtokens: policy.max_htlc_mtokens,
min_htlc_mtokens: policy.min_htlc_mtokens,
public_key: policy.public_key,
};
}),
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
updated_at: channel.updated_at,
};
});
return cbk(null, {
channels: !!channels.length ? channels : undefined,
nodes: !!nodes.length ? nodes : undefined,
payment: getPayment.payment || undefined,
payment_failed: getPayment.failed || undefined,
payment_pending: getPayment.is_pending || undefined,
});
return cbk(null, {
channels: !!channels.length ? channels : undefined,
nodes: !!nodes.length ? nodes : undefined,
payment: getPayment.payment || undefined,
payment_failed: getPayment.failed || undefined,
payment_pending: getPayment.is_pending || undefined,
});
}],
},
returnResult({of :'records'}, cbk));
}],
},
returnResult({reject, resolve, of :'records'}, cbk));
});
};

View file

@ -9,25 +9,31 @@ const {pemAsDer} = require('./../encryption');
{
ask: <Inquirer Function> ({message, name, type}, cbk) => {}
is_cleartext: <Export Clear Credential Components Bool>
logger: <Winston Logger Object> ({info}) => ()
[node]: <Node Name String>
}
@returns via cbk or Promise
{
credentials: <Encrypted Node Credentials CBOR Hex String>
[cleartext]: {
cert: <TLS Cert File Base64 Encoded String>
macaroon: <Macaroon Authentication File Base64 Encoded String>
socket: <External Host and Port String>
}
[credentials]: <Encrypted Node Credentials CBOR Hex String>
}
*/
module.exports = ({ask, logger, node}, cbk) => {
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!ask) {
if (!args.ask) {
return cbk([400, 'ExpectedPromptFunctionToGetCredentials']);
}
if (!logger) {
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToGetCredentials']);
}
@ -36,22 +42,43 @@ module.exports = ({ask, logger, node}, cbk) => {
// Ask for the transfer key
key: ['validate', ({}, cbk) => {
if (!!args.is_cleartext) {
return cbk();
}
const enterTransferKey = {
message: 'Enter a transfer public key:',
name: 'key',
type: 'input',
};
return ask(enterTransferKey, ({key}) => cbk(null, key));
return args.ask(enterTransferKey, ({key}) => cbk(null, key));
}],
// Get credentials encrypted to transfer key
getCredentials: ['key', ({key}, cbk) => {
return lndCredentials({key, logger, node}, cbk);
if (!args.is_cleartext && !key) {
return cbk([400, 'ExpectedCredentialsTransferKeyFromNodesAdd']);
}
return lndCredentials({
key,
logger: args.logger,
node: args.node,
},
cbk);
}],
// Packaged credentials
credentials: ['getCredentials', ({getCredentials}, cbk) => {
if (!!args.is_cleartext) {
return cbk(null, {
cert: getCredentials.cert,
macaroon: getCredentials.macaroon,
socket: getCredentials.external_socket || getCredentials.socket,
});
}
const encryptedMacaroon = getCredentials.encrypted_macaroon;
const externalSocket = getCredentials.external_socket;

View file

@ -0,0 +1,35 @@
/** Map a gift route error to a callback error
{
err: {
message: <Error Message String>
}
}
@returns
[
<Callback Error Code Number>
<Callback Error Message String>
[Callback Error Context Object>]
]
*/
module.exports = ({err}) => {
const {message} = err;
switch (message) {
case 'NoActiveChannelWithSpecifiedPeer':
return [400, 'SendingGiftRequiresActiveChannelWithPeer'];
case 'NoActiveChannelWithSufficientLocalBalance':
return [400, 'SendingGiftRequiresChanWithSufficientBalance'];
case 'NoActiveChannelWithSufficientRemoteBalance':
return [400, 'SendingGiftRequiresChanWithSomeRemoteBalance'];
case 'NoDirectChannelWithSpecifiedPeer':
return [400, 'SendingGiftRequiresDirectChannelWithPeer'];
default:
return [500, 'UnexpectedErrorDeterminingChanForGift', {err}];
}
};

View file

@ -17,10 +17,12 @@ const getScoredNodes = require('./get_scored_nodes');
const peersWithActivity = require('./peers_with_activity');
const {shuffle} = require('./../arrays');
const asBigTok = tokens => (tokens / 1e8).toFixed(8);
const channelTokens = 5e6;
const days = 90;
const fastConf = 6;
const {floor} = Math;
const getMempoolRetries = 10;
const maxMempoolSize = 2e6;
const minOutbound = 4294967;
const minForwarded = 1e5;
@ -121,6 +123,7 @@ module.exports = (args, cbk) => {
return getMempoolSize({
network: getNetwork.network,
request: args.request,
retries: getMempoolRetries,
},
cbk);
}],
@ -247,9 +250,9 @@ module.exports = (args, cbk) => {
const node = {
alias: !!res && !!res.alias ? res.alias : undefined,
forwarded: candidate.forwarded,
inbound: candidate.inbound,
outbound: candidate.outbound,
past_forwarded: asBigTok(candidate.forwarded),
current_inbound: asBigTok(candidate.inbound),
current_outbound: asBigTok(candidate.outbound),
public_key: candidate.public_key,
socket: !!socket ? socket.socket : undefined,
};
@ -272,6 +275,7 @@ module.exports = (args, cbk) => {
opening_with: node,
chain_fee_tokens_per_vbyte: getNormalFee.tokens_per_vbyte,
is_dry_run: true,
new_channel_size: asBigTok(args.tokens || channelTokens),
});
return cbk(null, true);
@ -305,6 +309,7 @@ module.exports = (args, cbk) => {
opening_with: node,
chain_fee_tokens_per_vbyte: getNormalFee.tokens_per_vbyte,
transaction_id: res.transaction_id,
new_channel_size: asBigTok(args.tokens || channelTokens),
});
return cbk(null, true);

View file

@ -4,159 +4,127 @@ const {getChannel} = require('ln-service');
const {getChannels} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {payViaRoutes} = require('ln-service');
const {routeFromChannels} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {authenticatedLnd} = require('./../lnd');
const {channelForGift} = require('./../routing');
const giftCallbackError = require('./gift_callback_error');
const {giftRoute} = require('./../routing');
const {sortBy} = require('./../arrays');
const {floor} = Math;
const minFeeRate = 0;
const minReceivableMtokens = BigInt(1000);
const mtokPerTok = BigInt(1000);
const reserveRatio = 0.01;
/** Send a gift of some tokens to a peer.
{
[node]: <From Node Name String>
to: <To Node Public Key hex string>
lnd: <Authenticated LND gRPC API Object>
to: <To Node Public Key Hex string>
tokens: <Tokens to Gift Number>
}
@returns via cbk
@returns via cbk or Promise
{
gave_tokens: <Gave Tokens Number>
}
*/
module.exports = ({node, to, tokens}, cbk) => {
return asyncAuto({
// Credentials
getLnd: cbk => authenticatedLnd({node}, cbk),
// Check arguments
validate: cbk => {
if (!to) {
return cbk([400, 'ExpectedPeerToSendGiftTo']);
}
if (!tokens) {
return cbk([400, 'ExpectedTokensToGiftToPeer']);
}
return cbk();
},
// Lnd
lnd: ['getLnd', ({getLnd}, cbk) => cbk(null, getLnd.lnd)],
// Get channels
getChannels: ['lnd', ({lnd}, cbk) => getChannels({lnd}, cbk)],
// Peer channel
peerChannel: ['getChannels', ({getChannels}, cbk) => {
try {
const {channels} = getChannels;
return cbk(null, channelForGift({channels, to, tokens}).id);
} catch (err) {
const {message} = err;
switch (message) {
case 'NoActiveChannelWithSpecifiedPeer':
return cbk([400, 'SendingGiftRequiresActiveChannelWithPeer']);
case 'NoActiveChannelWithSufficientLocalBalance':
return cbk([400, 'SendingGiftRequiresChannelWithSufficientBalance']);
case 'NoActiveChannelWithSufficientRemoteBalance':
return cbk([400, 'SendingGiftRequiresChannelWithSomeRemoteBalance']);
case 'NoDirectChannelWithSpecifiedPeer':
return cbk([400, 'SendingGiftRequiresDirectChannelWithPeer']);
default:
return cbk([500, 'UnexpectedErrorDeterminingChannelForGift', {err}]);
module.exports = ({lnd, to, tokens}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedLndToSendGiftWith']);
}
}
}],
// Get channel policy info
getChannel: ['lnd', 'peerChannel', ({lnd, peerChannel}, cbk) => {
return getChannel({lnd, id: peerChannel}, cbk);
}],
// Create invoice
createInvoice: ['getChannel', 'lnd', ({lnd}, cbk) => {
return createInvoice({lnd}, cbk);
}],
// Get wallet
getWallet: ['getChannel', 'lnd', ({lnd}, cbk) => {
return getWalletInfo({lnd}, cbk);
}],
// Route
route: [
'createInvoice',
'getChannel',
'getWallet',
({createInvoice, getChannel, getWallet}, cbk) =>
{
try {
const {route} = giftRoute({
tokens,
channel: getChannel,
destination: getWallet.public_key,
height: getWallet.current_block_height,
});
return cbk(null, route);
} catch (err) {
const {message} = err;
switch (message) {
case 'GiftAmountTooLowToSend':
case 'OwnPolicyTooLowToCompleteForward':
case 'PeerPolicyTooLowToCompleteForward':
return cbk([400, 'AmountTooLowToCompleteGiftSend']);
default:
return cbk([500, 'FailedToConstructGiftRoute', {err}]);
if (!to) {
return cbk([400, 'ExpectedPeerToSendGiftTo']);
}
}
}],
// Send the gift
pay: [
'createInvoice',
'lnd',
'route',
({createInvoice, lnd, route}, cbk) =>
{
const {id} = createInvoice;
if (!tokens) {
return cbk([400, 'ExpectedTokensToGiftToPeer']);
}
return payViaRoutes({id, lnd, routes: [route]}, (err, res) => {
if (!!err) {
const [errCode, errMessage] = err;
return cbk();
},
switch (errMessage) {
case 'RejectedUnacceptableFee':
return cbk([400, 'GiftTokensAmountTooLowToSend']);
// Get channels
getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
// Peer channel
peerChannel: ['getChannels', ({getChannels}, cbk) => {
try {
const {channels} = getChannels;
const {id} = channelForGift({channels, to, tokens});
return cbk(null, id);
} catch (err) {
return cbk(giftCallbackError({err}));
}
}],
// Get channel policy info
getChannel: ['peerChannel', ({peerChannel}, cbk) => {
return getChannel({lnd, id: peerChannel}, cbk);
}],
// Create invoice
createInvoice: ['getChannel', ({l}, cbk) => createInvoice({lnd}, cbk)],
// Get wallet
getWallet: ['getChannel', ({}, cbk) => getWalletInfo({lnd}, cbk)],
// Route
route: [
'createInvoice',
'getChannel',
'getWallet',
({createInvoice, getChannel, getWallet}, cbk) =>
{
try {
const {route} = giftRoute({
tokens,
channel: getChannel,
destination: getWallet.public_key,
height: getWallet.current_block_height,
});
return cbk(null, route);
} catch (err) {
const {message} = err;
switch (message) {
case 'GiftAmountTooLowToSend':
case 'OwnPolicyTooLowToCompleteForward':
case 'PeerPolicyTooLowToCompleteForward':
return cbk([400, 'AmountTooLowToCompleteGiftSend']);
default:
return cbk([503, 'UnexpectedErrorSendingGiftTokens', {err}]);
return cbk([500, 'FailedToConstructGiftRoute', {err}]);
}
}
}],
return cbk(null, {gave_tokens: res.fee});
});
}],
// Send the gift
pay: ['createInvoice', 'route', ({createInvoice, route}, cbk) => {
const {id} = createInvoice;
// Done paying
paid: ['pay', ({pay}, cbk) => cbk(null, {gave_tokens: pay.gave_tokens})],
},
returnResult({of: 'paid'}, cbk));
return payViaRoutes({id, lnd, routes: [route]}, (err, res) => {
if (!!err) {
const [errCode, errMessage] = err;
switch (errMessage) {
case 'RejectedUnacceptableFee':
return cbk([400, 'GiftTokensAmountTooLowToSend']);
default:
return cbk([503, 'UnexpectedErrorSendingGiftTokens', {err}]);
}
}
return cbk(null, {gave_tokens: res.fee});
});
}],
// Done paying
paid: ['pay', ({pay}, cbk) => cbk(null, {gave_tokens: pay.gave_tokens})],
},
returnResult({reject, resolve, of: 'paid'}, cbk));
});
};

8
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "balanceofsatoshis",
"version": "5.6.1",
"version": "5.6.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3333,9 +3333,9 @@
}
},
"ln-service": {
"version": "47.5.4",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-47.5.4.tgz",
"integrity": "sha512-y8R+3gkEVG+RUXbPuWSU6C8+QZgT2Pvcs/xUAqIOc/bzTrlPz8S2kIy7K9JAXGRMBMqg7AmRC/InWTF/aOqAOg==",
"version": "47.5.5",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-47.5.5.tgz",
"integrity": "sha512-/x3nogCbOL5i0FZQc03BOs0xT/X8dJ+EDmBtWfI9Qt1yoZ1+174dASoZuaUSwiTIv6wnWKNyTDa05mMu6SlW/w==",
"requires": {
"@datastructures-js/priority-queue": "1.0.2",
"@grpc/proto-loader": "0.5.3",

View file

@ -26,7 +26,7 @@
"ini": "1.3.5",
"inquirer": "7.0.0",
"ln-accounting": "3.1.6",
"ln-service": "47.5.4",
"ln-service": "47.5.5",
"moment": "2.24.0",
"qrcode-terminal": "0.12.0",
"request": "2.88.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.6.1"
"version": "5.6.2"
}

View file

@ -9,6 +9,7 @@ const isRoutePayable = require('./is_route_payable');
const accuracy = 1000;
const {isArray} = Array;
const from = 0;
const slowPaymentMs = 1000 * 30;
const to = tokens => tokens + Math.round(Math.random() * 1000);
/** Find max routable
@ -91,7 +92,14 @@ module.exports = ({cltv, hops, lnd, logger, max}, cbk) => {
logger.info({evaluating_amount: cursor});
const slowWarning = setTimeout(() => {
logger.info({slow_path_timeout_in: '30 seconds'});
},
slowPaymentMs);
return isRoutePayable({channels, cltv, lnd, tokens}, (err, res) => {
clearTimeout(slowWarning);
if (!!err) {
return cbk(err);
}

View file

@ -1,4 +1,6 @@
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const {getChannel} = require('ln-service');
const {getChannels} = require('ln-service');
const {getNode} = require('ln-service');
const {getWalletInfo} = require('ln-service');
@ -62,15 +64,32 @@ module.exports = ({destination, lnd, through, tokens}, cbk) => {
return getNode({lnd, public_key: destination}, cbk);
}],
// Local channels
localChannels: ['getChannels', ({getChannels}, cbk) => {
const localChannels = getChannels.channels.filter(n => {
return n.partner_public_key === through && !!n.is_private;
});
return asyncMap(localChannels, (channel, cbk) => {
return getChannel({lnd, id: channel.id}, cbk);
},
cbk);
}],
// Connecting path
path: [
'getChannels',
'getInfo',
'getNode',
({getChannels, getInfo, getNode}, cbk) =>
'localChannels',
({getChannels, getInfo, getNode, localChannels}, cbk) =>
{
const connectingChannels = getNode.channels
.filter(chan => !!chan.policies.find(n => n.public_key === through));
const channels = [].concat(getNode.channels).concat(localChannels);
const connectingChannels = channels.filter(chan => {
// Channel has a policy that matches the key of the through key
return !!chan.policies.find(n => n.public_key === through);
});
if (!connectingChannels.length) {
return cbk([400, 'NoConnectingChannelToPayIn']);

View file

@ -1,4 +1,5 @@
const asyncAuto = require('async/auto');
const asyncTimeout = require('async/timeout');
const {getWalletInfo} = require('ln-service');
const {payViaRoutes} = require('ln-service');
const {returnResult} = require('asyncjs-util');
@ -8,6 +9,7 @@ const invalidPaymentMessage = 'UnknownPaymentHash';
const {isArray} = Array;
const mtokensFromTokens = tokens => (BigInt(tokens) * BigInt(1e3)).toString();
const pathfindingTimeoutMs = 1000 * 60;
const payWithTimeout = asyncTimeout(payViaRoutes, 1000 * 60);
/** Find out if route is payable
@ -76,12 +78,16 @@ module.exports = ({channels, cltv, lnd, tokens}, cbk) => {
// Attempt the route
attempt: ['route', ({route}, cbk) => {
return payViaRoutes({
return payWithTimeout({
lnd,
pathfinding_timeout: pathfindingTimeoutMs,
routes: [route],
},
err => {
if (!!err && !isArray(err)) {
return cbk(null, {is_payable: false});
}
const [, code] = err;
return cbk(null, {is_payable: code === invalidPaymentMessage});

View file

@ -215,7 +215,7 @@ module.exports = (args, cbk) => {
return probeDestination({
destination: getPublicKey.public_key,
find_max: 5e6,
find_max: 4294967,
ignore: [{from_public_key: getPublicKey.public_key}].concat(avoid),
in_through: getInbound.public_key,
logger: args.logger,

View file

@ -68,21 +68,6 @@ module.exports = (args, cbk) => {
return cbk();
},
// Get channels
getLiquidity: ['validate', ({}, cbk) => {
// Exit early when recovering from an existing swap
if (!!args.recovery) {
return cbk();
}
return getLiquidity({
above: args.tokens,
is_top: true,
node: args.node,
},
cbk);
}],
// Get authenticated lnd connection
getLnd: ['validate', ({}, cbk) => {
return authenticatedLnd({logger: args.logger, node: args.node}, cbk);
@ -93,6 +78,21 @@ module.exports = (args, cbk) => {
return getWalletInfo({lnd: getLnd.lnd}, cbk);
}],
// Get channels
getLiquidity: ['getLnd', ({getLnd}, cbk) => {
// Exit early when recovering from an existing swap
if (!!args.recovery) {
return cbk();
}
return getLiquidity({
above: args.tokens,
is_top: true,
lnd: getLnd.lnd,
},
cbk);
}],
// Get network
getNetwork: ['getLnd', ({getLnd}, cbk) => {
return getNetwork({lnd: getLnd.lnd}, cbk)

View file

@ -830,6 +830,21 @@ module.exports = (args, cbk) => {
return cbk();
}],
// Register deposit height
depositHeight: ['findDeposit', ({findDeposit}, cbk) => {
if (!!args.recovery) {
return cbk();
}
return getWalletInfo({lnd: args.lnd}, (err, res) => {
if (!!err) {
return cbk(err);
}
return cbk(null, res.current_block_height);
});
}],
// Claim details
claim: [
'findDeposit',
@ -850,6 +865,7 @@ module.exports = (args, cbk) => {
rawRecovery: [
'claim',
'createAddress',
'depositHeight',
'initiateSwap',
'network',
'recover',
@ -857,6 +873,7 @@ module.exports = (args, cbk) => {
({
claim,
createAddress,
depositHeight,
initiateSwap,
network,
recover,
@ -888,7 +905,7 @@ module.exports = (args, cbk) => {
min_fee_rate: minFeeRate,
private_key: claim.private_key,
secret: claim.secret,
start_height: initiateSwap.start_height || startHeight,
start_height: initiateSwap.start_height || depositHeight,
sweep_address: createAddress.address,
transaction_id: claim.transaction_id,
transaction_vout: claim.transaction_vout,
@ -918,6 +935,7 @@ module.exports = (args, cbk) => {
sweep: [
'claim',
'createAddress',
'depositHeight',
'initiateSwap',
'network',
'rawRecovery',
@ -926,6 +944,7 @@ module.exports = (args, cbk) => {
({
claim,
createAddress,
depositHeight,
initiateSwap,
network,
recover,
@ -959,7 +978,7 @@ module.exports = (args, cbk) => {
max_fee_multiplier: maxFeeMultiplier,
private_key: claim.private_key,
secret: claim.secret,
start_height: startHeight,
start_height: depositHeight,
sweep_address: createAddress.address,
transaction_id: claim.transaction_id,
transaction_vout: claim.transaction_vout,

View file

@ -0,0 +1,41 @@
const {test} = require('tap');
const {shuffle} = require('./../../arrays');
const tests = [
{
args: {},
description: 'An array is required',
error: 'ExpectedArrayToShuffle',
},
{
args: {array: []},
description: 'An empty array returns an empty array',
expected: {shuffled: ''},
},
{
args: {array: [1, 2, 3]},
description: 'An array is shuffled as expected',
expected: {shuffled: '3,1,2'},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, ({deepIs, end, equal, throws}) => {
if (!!error) {
throws(() => shuffle(args), new Error(error), 'Got expected error');
} else if (!expected.shuffled) {
equal(shuffle(args).shuffled.join(''), '', 'Empty array is returned');
} else {
let shuffled = [];
while (shuffled.join(',') !== expected.shuffled) {
shuffled = shuffle(args).shuffled;
}
equal(shuffled.join(','), expected.shuffled, 'Array is shuffled');
}
return end();
});
});

View file

@ -0,0 +1,115 @@
const {test} = require('tap');
const {getLiquidity} = require('./../../balances');
const makeChannels = () => {
return [
{
active: true,
capacity: '1',
chan_id: '1',
channel_point: '00:1',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
local_balance: 1,
local_chan_reserve_sat: 1,
num_updates: 1,
pending_htlcs: [],
private: false,
remote_balance: 1,
remote_chan_reserve_sat: 1,
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
},
{
active: true,
capacity: '1',
chan_id: '1',
channel_point: '00:1',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
local_balance: 1,
local_chan_reserve_sat: 1,
num_updates: 1,
pending_htlcs: [],
private: false,
remote_balance: 1,
remote_chan_reserve_sat: 1,
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
},
{
active: true,
capacity: '1',
chan_id: '1',
channel_point: '00:1',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
local_balance: 1,
local_chan_reserve_sat: 1,
num_updates: 1,
pending_htlcs: [],
private: false,
remote_balance: 1,
remote_chan_reserve_sat: 1,
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
},
];
};
const tests = [
{
args: {},
description: 'LND is required',
error: [400, 'ExpectedLndToGetLiquidity'],
},
{
args: {
is_top: true,
lnd: {
default: {
listChannels: ({}, cbk) => cbk(null, {channels: makeChannels()}),
},
},
},
description: 'Liquidity is returned',
expected: {balance: 1},
},
{
args: {
is_outbound: true,
lnd: {
default: {
listChannels: ({}, cbk) => cbk(null, {channels: makeChannels()}),
},
},
with: 'b',
},
description: 'Liquidity is returned',
expected: {balance: 3},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({end, equal, rejects}) => {
if (!!error) {
rejects(getLiquidity(args), error, 'Got expected error');
} else {
const {balance} = await getLiquidity(args);
equal(balance, expected.balance, 'Balance is calculated');
}
return end();
});
});

View file

@ -0,0 +1,67 @@
const {test} = require('tap');
const {getMempoolSize} = require('./../../chain');
const tests = [
{
args: {},
description: 'Network name is required',
error: [400, 'ExpectedNetworkNameToGetMempoolSize'],
},
{
args: {network: 'btc'},
description: 'Request function is required',
error: [400, 'ExpectedRequestMethodToGetMempoolSize'],
},
{
args: {network: 'btc', request: ({}, cbk) => cbk('err'), retries: 1},
description: 'Request errors are passed back',
error: [503, 'FailedToGetMempoolSizeInfo', {err: 'err'}],
},
{
args: {network: 'btc', request: ({}, cbk) => cbk(), retries: 1},
description: 'Mempool information is expected',
error: [503, 'ExpectedMempoolInfoInResponse'],
},
{
args: {
network: 'btc',
request: ({}, cbk) => cbk(null, null, {}),
retries: 1,
},
description: 'Mempool response vbytes are expected',
error: [503, 'ExpectedMempoolVirtualByteSize'],
},
{
args: {
network: 'btctestnet',
request: ({}, cbk) => cbk(null, null, {vsize: 1}),
retries: 1,
},
description: 'VBytes are returned',
expected: {vbytes: 1},
},
{
args: {
network: 'network',
request: ({}, cbk) => cbk(),
retries: 1,
},
description: 'No vbytes are returned',
expected: {},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({end, equal, rejects}) => {
if (!!error) {
rejects(getMempoolSize(args), error, 'Got expected error');
} else {
const {vbytes} = await getMempoolSize(args);
equal(vbytes, expected.vbytes, 'Got expected vbytes');
}
return end();
});
});

25
test/fixtures/chan_info_response.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
"capacity": "1",
"chan_point": "1:1",
"channel_id": "1",
"node1_policy": {
"disabled": false,
"fee_base_msat": "1",
"fee_rate_milli_msat": "1",
"last_update": 1,
"max_htlc_msat": "1",
"min_htlc": "1",
"time_lock_delta": 1
},
"node1_pub": "a",
"node2_policy": {
"disabled": false,
"fee_base_msat": "1",
"fee_rate_milli_msat": "1",
"last_update": 1,
"max_htlc_msat": "1",
"min_htlc": "1",
"time_lock_delta": 1
},
"node2_pub": "b"
}

15
test/fixtures/get_info_response.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"alias": "",
"best_header_timestamp": 1,
"block_hash": "00",
"block_height": 1,
"chains": [{"chain": "bitcoin", "network": "mainnet"}],
"color": "#000000",
"identity_pubkey": "00",
"num_active_channels": 0,
"num_peers": 0,
"num_pending_channels": 0,
"synced_to_chain": false,
"uris": [],
"version": ""
}

4
test/fixtures/index.js vendored Normal file
View file

@ -0,0 +1,4 @@
const chanInfoResponse = require('./chan_info_response');
const getInfoResponse = require('./get_info_response');
module.exports = {chanInfoResponse, getInfoResponse};

View file

@ -0,0 +1,47 @@
const {test} = require('tap');
const giftCallbackError = require('./../../network/gift_callback_error');
const tests = [
{
args: {err: {message: 'NoActiveChannelWithSpecifiedPeer'}},
description: 'No active channel returns a user error',
expected: {err: [400, 'SendingGiftRequiresActiveChannelWithPeer']},
},
{
args: {err: {message: 'NoActiveChannelWithSufficientLocalBalance'}},
description: 'No active channel with balance returns a user error',
expected: {err: [400, 'SendingGiftRequiresChanWithSufficientBalance']},
},
{
args: {err: {message: 'NoActiveChannelWithSufficientRemoteBalance'}},
description: 'No channel with remote balance returns a user error',
expected: {err: [400, 'SendingGiftRequiresChanWithSomeRemoteBalance']},
},
{
args: {err: {message: 'NoDirectChannelWithSpecifiedPeer'}},
description: 'No direct channel with peer returns a user error',
expected: {err: [400, 'SendingGiftRequiresDirectChannelWithPeer']},
},
{
args: {err: {message: 'message'}},
description: 'An unanticipated error returns a non-user error',
expected: {
err: [
500,
'UnexpectedErrorDeterminingChanForGift',
{err: {message: 'message'}},
],
},
},
];
tests.forEach(({args, description, expected}) => {
return test(description, ({deepIs, end}) => {
const err = giftCallbackError(args);
deepIs(err, expected.err, 'Got expected error');
return end();
});
});

View file

@ -0,0 +1,145 @@
const {test} = require('tap');
const {chanInfoResponse} = require('./../fixtures');
const {getInfoResponse} = require('./../fixtures');
const {sendGift} = require('./../../network');
const tests = [
{
args: {},
description: 'LND is required to send a gift',
error: [400, 'ExpectedLndToSendGiftWith'],
},
{
args: {lnd: {}},
description: 'Peer public key is required to send a gift',
error: [400, 'ExpectedPeerToSendGiftTo'],
},
{
args: {lnd: {}, to: 'b'},
description: 'Tokens are required to send a gift',
error: [400, 'ExpectedTokensToGiftToPeer'],
},
{
args: {
lnd: {
default: {
listChannels: ({}, cbk) => cbk(null, {channels: []})
},
},
to: 'b',
tokens: 1,
},
description: 'A channel is required to send a gift',
error: [400, 'SendingGiftRequiresDirectChannelWithPeer'],
},
{
args: {
lnd: {
default: {
addInvoice: ({}, cbk) => cbk(null, {
payment_request: 'payment_request',
r_hash: Buffer.alloc(32),
}),
getChanInfo: (args, cbk) => cbk(null, chanInfoResponse),
getInfo: ({}, cbk) => cbk(null, getInfoResponse),
listChannels: ({}, cbk) => cbk(null, {
channels: [{
active: true,
capacity: 1,
chan_id: 1,
channel_point: '1:0',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
local_balance: 1,
local_chan_reserve_sat: '1',
num_updates: 1,
pending_htlcs: [],
private: true,
remote_balance: 1,
remote_chan_reserve_sat: '1',
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
}],
}),
},
},
to: 'b',
tokens: 1,
},
description: 'A channel with balance is required to send a gift',
error: [400, 'SendingGiftRequiresChanWithSufficientBalance'],
},
{
args: {
lnd: {
default: {
addInvoice: ({}, cbk) => cbk(null, {
payment_request: 'payment_request',
r_hash: Buffer.alloc(32),
}),
getChanInfo: (args, cbk) => cbk(null, chanInfoResponse),
getInfo: ({}, cbk) => cbk(null, getInfoResponse),
listChannels: ({}, cbk) => cbk(null, {
channels: [{
active: true,
capacity: 1,
chan_id: 1,
channel_point: '1:0',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
local_balance: 100,
local_chan_reserve_sat: '1',
num_updates: 1,
pending_htlcs: [],
private: true,
remote_balance: 1,
remote_chan_reserve_sat: '1',
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
}],
}),
lookupInvoice: ({}, cbk) => cbk(null, {
creation_date: 1,
description_hash: '',
expiry: 1,
htlcs: [],
memo: '',
payment_request: 'payment_request',
r_preimage: Buffer.alloc(32),
settled: false,
value: '1',
}),
},
},
to: 'b',
tokens: 1,
},
description: 'A channel with balance is required to send a gift',
error: [
500,
'FailedToConstructGiftRoute',
{err: new Error('ExpectedDestinationPolicyToCalculateGiftRoute')},
],
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({deepIs, end, equal, rejects}) => {
if (!!error) {
rejects(sendGift(args), error, 'Got expected error');
} else {
const sent = await sendGift(args);
equal(sent.gave_tokens, expected.gave_tokens, 'Sent expected tokens');
}
return end();
});
});

View file

@ -45,16 +45,18 @@ module.exports = ({node, style}, cbk) => {
// Get authenticated lnd connection
getLnd: cbk => authenticatedLnd({node}, cbk),
// Get forwards
getForwards: cbk => getForwards({node}, cbk),
// Get exchange rate
getRate: cbk => getExchangeRates({symbols: ['USD']}, cbk),
// Get balance
getBalance: ['getLnd', ({getLnd}, cbk) => {
return getBalance({node, lnd: getLnd.lnd}, cbk);
}],
// Get exchange rate
getRate: cbk => getExchangeRates({symbols: ['USD']}, cbk),
// Get forwards
getForwards: ['getLnd', ({getLnd}, cbk) => {
return getForwards({lnd: getLnd.lnd}, cbk);
}],
// Get autopilot status
getAutopilot: ['getLnd', ({getLnd}, cbk) => {