add locking and unlocking of saved node credentials

This commit is contained in:
Alex Bosworth 2019-10-17 17:13:26 +02:00
parent d7a50cdb51
commit 3abd378bba
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
21 changed files with 1105 additions and 323 deletions

View file

@ -1,5 +1,10 @@
# Versions
## Version 4.5.0
- `nodes`: Add `--lock` and `--unlock` methods to encrypt and decrypt saved node
credentials
## Version 4.4.0
- `nodes`: Add method to list out all saved nodes

View file

@ -77,7 +77,7 @@ bos liquidity-cost "inbound" "amount"
# See market price history
bos market
# See list of saved nodes
# View and adjust list of saved nodes
bos nodes
# Outputs the sum total of local channel liquidity

80
bos
View file

@ -8,6 +8,7 @@ const prog = require('caporal');
const {rateProviders} = require('ln-accounting');
const {accountingCategories} = require('./balances');
const {adjustSavedNodes} = require('./nodes');
const {authenticatedLnd} = require('./lnd');
const {exchanges} = require('./fiat');
const {findRecord} = require('./lnd');
@ -21,7 +22,6 @@ const {getLiquidity} = require('./balances');
const {getPeers} = require('./network');
const {getPriceChart} = require('./fiat');
const {getReport} = require('./wallets');
const {getSavedNodes} = require('./nodes');
const {getSwapCost} = require('./swaps');
const {getSwapService} = require('./swaps');
const {getUtxos} = require('./chain');
@ -66,16 +66,20 @@ prog
const table = !!options.csv ? null : 'rows';
return new Promise(async (resolve, reject) => {
return getAccountingReport({
category: args.category,
is_csv: !!options.csv,
lnd: (await authenticatedLnd({node: options.node})).lnd,
month: options.month,
node: options.node,
rate_provider: options.rateProvider,
year: options.year,
},
returnObject({logger, reject, resolve, table}));
try {
return getAccountingReport({
category: args.category,
is_csv: !!options.csv,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
month: options.month,
node: options.node,
rate_provider: options.rateProvider,
year: options.year,
},
returnObject({logger, reject, resolve, table}));
} catch (err) {
return reject(err);
}
});
})
@ -295,23 +299,28 @@ prog
.option('--show-raw-recovery', 'Show raw recovery transactions')
.option('--with <peer>', 'Public key of peer to increase liquidity from')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return swapOut({
logger,
avoid: flatten([options.avoid].filter(n => !!n)),
confs: options.confs,
is_raw_recovery_shown: options.showRawRecovery || undefined,
is_dry_run: options.dryrun || false,
max_fee: options.maxFee,
max_wait_blocks: Math.ceil((options.maxHours) * 60 / 10),
node: options.node || undefined,
out_address: options.address || undefined,
peer: options.with || undefined,
recovery: options.recovery,
timeout: 1000 * 60 * 60 * 10,
tokens: options.amount,
},
returnObject({exit, logger, reject, resolve}));
return new Promise(async (resolve, reject) => {
try {
return swapOut({
logger,
avoid: flatten([options.avoid].filter(n => !!n)),
confs: options.confs,
is_raw_recovery_shown: options.showRawRecovery || undefined,
is_dry_run: options.dryrun || false,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
max_fee: options.maxFee,
max_wait_blocks: Math.ceil((options.maxHours) * 60 / 10),
node: options.node || undefined,
out_address: options.address || undefined,
peer: options.with || undefined,
recovery: options.recovery,
timeout: 1000 * 60 * 60 * 10,
tokens: options.amount,
},
returnObject({exit, logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})
@ -354,16 +363,25 @@ prog
});
})
// List saved nodes
.command('nodes', 'Get the list of saved nodes')
// Saved nodes
.command('nodes', 'List and edit saved nodes')
.help('Locking and unlocking requires existing installation of GPG')
.argument('[node]', 'Specify a saved node')
.option('--lock <id>', 'Encrypt node authentication to GPG key>', REPEATABLE)
.option('--unlock', 'Remove encryption from auth macaroon')
.action((args, options, logger) => {
return new Promise((resolve, reject) => {
return getSavedNodes({
return adjustSavedNodes({
logger,
fs: {
writeFile,
getDirectoryFiles: readdir,
getFile: readFile,
getFileStatus: lstat,
},
is_unlocking: options.unlock || undefined,
lock_credentials_to: flatten([options.lock].filter(n => !!n)),
node: args.node || undefined,
},
returnObject({logger, reject, resolve}));
});

View file

@ -0,0 +1,58 @@
const {spawn} = require('child_process');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
/** Decrypt ciphertext that has been encrypted to GPG keys
{
cipher: <Encrypted Text String>
}
@returns via cbk or Promise
{
clear: <Clear Text String>
}
*/
module.exports = ({cipher}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!cipher) {
return cbk([400, 'ExpectedCiphertextToDecrypt']);
}
return cbk();
},
// Decrypt the ciphertext
decrypt: ['validate', ({}, cbk) => {
const datas = [];
const decrypt = spawn('gpg', ['-d']);
decrypt.stdin.setEncoding('utf-8');
decrypt.stdout.on('data', data => datas.push(data));
decrypt.stdout.on('error', err => cbk([503, 'DecryptionFail', {err}]));
decrypt.stdout.on('end', () => {
if (!datas.length) {
return cbk([503, 'FailedToDecrypt']);
}
return cbk(null, {
clear: Buffer.concat(datas).toString('utf8').trim(),
});
});
decrypt.stdin.write(`${cipher}`);
decrypt.stdin.end();
return;
}],
},
returnResult({reject, resolve, of: 'decrypt'}, cbk));
});
};

View file

@ -0,0 +1,68 @@
const {spawn} = require('child_process');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const flatten = arr => [].concat(...arr);
const {isArray} = Array;
/** Encrypt a string using a spawned GPG
{
plain: <Plain Clear Text String>
to: [<Encrypt To Recipient String>]
}
@returns via cbk or Promise
{
cipher: <Armored Encrypted Text String>
}
*/
module.exports = ({plain, to}, cbk) => {
return new Promise((reject, resolve) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!plain) {
return cbk([400, 'ExpectedPlainTextToEncrypt']);
}
if (!isArray(to) || !to.length) {
return cbk([400, 'ExpectedRecipientOfEncryptedData']);
}
return cbk();
},
// Encrypt plain text
encrypt: ['validate', ({}, cbk) => {
const datas = [];
const recipients = to
.map(n => n.replace(/\s/g, ''))
.map(n => (['--recipient', n]));
const encrypt = spawn('gpg', ['-ea'].concat(flatten(recipients)));
encrypt.stdin.setEncoding('utf-8');
encrypt.stdout.on('data', data => datas.push(data));
encrypt.stdout.on('end', () => {
return cbk(null, {
cipher: Buffer.concat(datas).toString('utf8').trim(),
});
});
encrypt.stdout.on('error', err => cbk([503, 'EncryptingErr', {err}]));
encrypt.stdin.write(`${plain}\n`);
encrypt.stdin.end();
return;
}],
},
returnResult({reject, resolve, of: 'encrypt'}, cbk));
});
};

View file

@ -1,3 +1,5 @@
const decryptCiphertext = require('./decrypt_ciphertext');
const decryptPayload = require('./decrypt_payload');
const encryptToPublicKeys = require('./encrypt_to_public_keys');
module.exports = {decryptPayload};
module.exports = {decryptCiphertext, decryptPayload, encryptToPublicKeys};

View file

@ -7,6 +7,7 @@ const lndCredentials = require('./lnd_credentials');
/** Authenticated LND
{
[logger]: <Winston Logger Object>
[node]: <Node Name String>
}
@ -15,11 +16,11 @@ const lndCredentials = require('./lnd_credentials');
lnd: <Authenticated LND gRPC API Object>
}
*/
module.exports = ({node}, cbk) => {
module.exports = ({logger, node}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Credentials
credentials: cbk => lndCredentials({node}, cbk),
credentials: cbk => lndCredentials({logger, node}, cbk),
// Lnd
lnd: ['credentials', ({credentials}, cbk) => {

View file

@ -7,6 +7,8 @@ const asyncDetectSeries = require('async/detectSeries');
const {flatten} = require('lodash');
const {returnResult} = require('asyncjs-util');
const {decryptCiphertext} = require('./../encryption');
const {getSavedCredentials} = require('./../nodes');
const lndDirectory = require('./lnd_directory');
const base64 = 'base64';
@ -23,6 +25,7 @@ const socket = 'localhost:10009';
/** Lnd credentials
{
[logger]: <Winston Logger Object>
[node]: <Node Name String> // Defaults to default local mainnet node creds
}
@ -33,7 +36,7 @@ const socket = 'localhost:10009';
socket: <Socket String>
}
*/
module.exports = ({node}, cbk) => {
module.exports = ({logger, node}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Get the default cert
@ -100,43 +103,54 @@ module.exports = ({node}, cbk) => {
return cbk();
}
const path = [homedir(), home, node, credsFile];
return readFile(join(...path), (err, creds) => {
if (!!err) {
return cbk([503, 'FailedToGetNodeCredentials', err]);
}
try {
parse(creds);
} catch (err) {
return cbk([503, 'FailedToParseNodeCredentials', err]);
}
const {cert, macaroon, socket} = parse(creds);
if (!cert) {
return cbk([503, 'FailedToFindCertInCredentials']);
}
if (!macaroon) {
return cbk([503, 'FailedToFindMacaroonInCredentials']);
}
if (!socket) {
return cbk([503, 'FailedToFindSocketInCredentials']);
}
return cbk(null, {cert, macaroon, socket});
});
return getSavedCredentials({node, fs: {getFile: readFile}}, cbk);
},
// Node credentials
nodeCredentials: ['getNodeCredentials', ({getNodeCredentials}, cbk) => {
if (!node) {
return cbk();
}
if (!getNodeCredentials.credentials) {
return cbk([400, 'CredentialsForSpecifiedNodeNotFound']);
}
const {credentials} = getNodeCredentials;
if (!credentials.encrypted_macaroon) {
return cbk(null, {
cert: credentials.cert,
macaroon: credentials.macaroon,
socket: credentials.socket,
});
}
const cipher = credentials.encrypted_macaroon;
if (!!logger) {
logger.info({decrypt_credentials_for: node});
}
return decryptCiphertext({cipher}, (err, res) => {
if (!!err) {
return cbk(err);
}
return cbk(null, {
cert: credentials.cert,
macaroon: res.clear,
socket: credentials.socket,
});
});
}],
// Credentials to use
credentials: [
'getCert',
'getMacaroon',
'getNodeCredentials',
({getCert, getMacaroon, getNodeCredentials}) =>
'nodeCredentials',
({getCert, getMacaroon, nodeCredentials}) =>
{
// Exit early with the default credentials when no node is specified
if (!node) {
@ -144,9 +158,9 @@ module.exports = ({node}, cbk) => {
}
return cbk(null, {
cert: getNodeCredentials.cert,
macaroon: getNodeCredentials.macaroon,
socket: getNodeCredentials.socket,
cert: nodeCredentials.cert,
macaroon: nodeCredentials.macaroon,
socket: nodeCredentials.socket,
});
}],
},

View file

@ -142,8 +142,6 @@ module.exports = ({node, to, tokens}, cbk) => {
if (!!err) {
const [errCode, errMessage] = err;
console.log("ERR", err);
switch (errMessage) {
case 'RejectedUnacceptableFee':
return cbk([400, 'GiftTokensAmountTooLowToSend']);

131
nodes/adjust_saved_nodes.js Normal file
View file

@ -0,0 +1,131 @@
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const decryptSavedMacaroons = require('./decrypt_saved_macaroons');
const encryptSavedMacaroons = require('./encrypt_saved_macaroons');
const getSavedCredentials = require('./get_saved_credentials');
const getSavedNodes = require('./get_saved_nodes');
const {isArray} = Array;
/** Adjust or view the set of saved nodes
{
fs: {
getDirectoryFiles: <Read Directory Contents Function> (path, cbk) => {}
getFile: <Read File Contents Function> (path, cbk) => {}
getFileStatus: <File Status Function> (path, cbk) => {}
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
}
[is_unlocking]: <Change Credentials To Decrypted Copy Bool>
lock_credentials_to: [<Encrypt Macaroon to GPG Key With Id String>]
logger: <Winston Logger Object>
[node]: <Node Name String>
}
@returns via cbk or Promise
{
nodes: [{
[is_online]: <Node is Online Bool>
[encrypted_to]: [<Encrypted To GPG Id String>]
node_name: <Node Name String>
}]
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.fs) {
return cbk([400, 'ExpectedFilesystemMethodsToAdjustSavedNodes']);
}
if (!isArray(args.lock_credentials_to)) {
return cbk([400, 'ExpectedArrayOfLockingCredentialGpgIds']);
}
if (!!args.is_unlocking && args.is_unlocking !== true) {
return cbk([400, 'UnexpectedArgumentForUnlocking']);
}
if (!!args.is_unlocking && !!args.lock_credentials_to.length) {
return cbk([400, 'CannotBothUnlockAndLockNodeCredentials']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerFunctionForSavedNodes']);
}
return cbk();
},
// Check specified node exists
checkNode: ['validate', ({}, cbk) => {
if (!args.node) {
return cbk();
}
return getSavedCredentials({
fs: args.fs,
node: args.node,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
if (!res.credentials) {
return cbk([404, 'SpecifiedNodeNotFound']);
}
return cbk();
});
}],
// Get existing set of nodes
getNodes: ['checkNode', ({}, cbk) => getSavedNodes({fs: args.fs}, cbk)],
// Encrypt macaroons
lock: ['getNodes', ({getNodes}, cbk) => {
// Exit early when not locking credentials
if (!args.lock_credentials_to.length) {
return cbk();
}
const {nodes} = getNodes;
return encryptSavedMacaroons({
fs: args.fs,
logger: args.logger,
nodes: !args.node ? nodes.map(n => n.node_name) : [args.node],
to: args.lock_credentials_to,
},
cbk);
}],
// Unlock credentials
unlock: ['getNodes', ({getNodes}, cbk) => {
// Exit early when not unlocking credentials
if (!args.is_unlocking) {
return cbk();
}
const {nodes} = getNodes;
return decryptSavedMacaroons({
fs: args.fs,
logger: args.logger,
nodes: !args.node ? nodes.map(n => n.node_name) : [args.node],
},
cbk);
}],
// Get saved nodes
getSaved: ['lock', 'unlock', ({}, cbk) => {
return getSavedNodes({fs: args.fs}, cbk);
}],
},
returnResult({reject, resolve, of: 'getSaved'}, cbk));
});
};

View file

@ -0,0 +1,95 @@
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const asyncMapSeries = require('async/mapSeries');
const {returnResult} = require('asyncjs-util');
const {decryptCiphertext} = require('./../encryption');
const getSavedCredentials = require('./get_saved_credentials');
const putSavedCredentials = require('./put_saved_credentials');
const {isArray} = Array;
/** Decrypt saved macaroons and save as cleartext
{
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
}
logger: <Winston Logger Object>
nodes: [<Node Name String>]
}
@returns via cbk or Promise
*/
module.exports = ({fs, logger, nodes}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!fs) {
return cbk([400, 'ExpectedFileMethodsToDecryptSavedMacaroons']);
}
if (!logger) {
return cbk([400, 'ExpectedLoggerToDecryptSavedMacaroons']);
}
if (!isArray(nodes)) {
return cbk([400, 'ExpectedNodesToDecryptSavedMacaroons']);
}
return cbk();
},
// Get the credentials
getCredentials: ['validate', ({}, cbk) => {
return asyncMap(nodes, (node, cbk) => {
return getSavedCredentials({fs, node}, cbk);
},
cbk);
}],
// Decrypt the encrypted macaroons
decrypt: ['getCredentials', ({getCredentials}, cbk) => {
const encrypted = getCredentials
.filter(n => !!n.credentials.encrypted_macaroon);
return asyncMapSeries(encrypted, ({credentials, node}, cbk) => {
const cipher = credentials.encrypted_macaroon;
logger.info({decrypt_credentials_for: node});
return decryptCiphertext({cipher}, (err, res) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorDecryptingMacaroon', {err}]);
}
return cbk(null, {credentials, macaroon: res.clear, node});
});
},
cbk);
}],
// Save the decrypted credentials over the existing credentials
save: ['decrypt', ({decrypt}, cbk) => {
if (!decrypt.length) {
return cbk();
}
return asyncMap(decrypt, ({credentials, macaroon, node}, cbk) => {
return putSavedCredentials({
fs,
macaroon,
node,
cert: credentials.cert,
socket: credentials.socket,
},
cbk);
},
cbk);
}],
},
returnResult({reject, resolve}));
});
};

View file

@ -0,0 +1,124 @@
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const asyncMapSeries = require('async/mapSeries');
const {returnResult} = require('asyncjs-util');
const decryptSavedMacaroons = require('./decrypt_saved_macaroons');
const {encryptToPublicKeys} = require('./../encryption');
const getSavedCredentials = require('./get_saved_credentials');
const putSavedCredentials = require('./put_saved_credentials');
const ids = n => n.slice().sort().join(',');
const {isArray} = Array;
const notFoundIndex = -1;
/** Encrypt saved macaroons to GPG keys
{
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
}
logger: <Winston Logger Object>
nodes: [<Node Name String>]
to: [<Encrypt to GPG Key Id String>]
}
@returns via cbk or Promise
*/
module.exports = ({fs, logger, nodes, to}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!fs || !fs.getFile || !fs.writeFile) {
return cbk([400, 'ExpectedFilesystemMethodsToSaveEncrypted']);
}
if (!isArray(nodes) || !nodes.length) {
return cbk([400, 'ExpectedNodesToEncryptSavedMacaroonsFor']);
}
if (!isArray(to) || !to.length) {
return cbk([400, 'ExpectedGpgKeyIdsToEncryptSavedMacaroonsTo']);
}
if (to.findIndex(n => typeof(n) !== 'string') !== notFoundIndex) {
return cbk([400, 'ExpectedGpgKeyId']);
}
return cbk();
},
// Get the credentials including encrypted credentials
getAllCredentials: ['validate', ({}, cbk) => {
return asyncMap(nodes, (node, cbk) => {
return getSavedCredentials({fs, node}, cbk);
},
cbk);
}],
// Decrypt macaroons for nodes that have the wrong keys
decryptEncrypted: ['getAllCredentials', ({getAllCredentials}, cbk) => {
const nodes = getAllCredentials
.filter(n => !!n.credentials && !!n.credentials.encrypted_macaroon)
.filter(n => ids(n.credentials.encrypted_to) !== ids(to))
.map(n => n.node);
if (!nodes.length) {
return cbk();
}
return decryptSavedMacaroons({fs, logger, nodes}, cbk);
}],
// Get the credentials
getCredentials: ['decryptEncrypted', ({}, cbk) => {
return asyncMap(nodes, (node, cbk) => {
return getSavedCredentials({fs, node}, cbk);
},
cbk);
}],
// Encrypt unencrypted macaroons
encrypt: ['getCredentials', ({getCredentials}, cbk) => {
const plainCredentials = getCredentials
.filter(n => !!n.credentials && !!n.credentials.macaroon);
return asyncMapSeries(plainCredentials, ({credentials, node}, cbk) => {
const plain = credentials.macaroon;
return encryptToPublicKeys({plain, to}, (err, res) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorEncryptingMacaroon', {err}]);
}
return cbk(null, {credentials, node, cipher: res.cipher});
});
},
cbk);
}],
// Save the encrypted credentials over the existing credentials
save: ['encrypt', ({encrypt}, cbk) => {
if (!encrypt.length) {
return cbk();
}
return asyncMap(encrypt, ({credentials, node, cipher}, cbk) => {
return putSavedCredentials({
fs,
node,
cert: credentials.cert,
encrypted_macaroon: cipher,
encrypted_to: to,
socket: credentials.socket,
},
cbk);
},
cbk);
}],
},
returnResult({reject, resolve}, cbk));
});
};

View file

@ -0,0 +1,97 @@
const {join} = require('path');
const {homedir} = require('os');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const credentials = 'credentials.json';
const home = '.bos';
const {parse} = JSON;
/** Get saved credentials for node
{
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
node: <Node Name String>
}
@returns via cbk or Promise
{
[credentials]: {
cert: <Base64 or Hex Serialized LND TLS Cert>
[encrypted_macaroon]: <Encrypted Macaroon String>
[encrypted_to]: [<Encrypted to GPG Recipient String>]
[macaroon]: <Base64 or Hex Serialized Macaroon String>
socket: <Host:Port Network Address String>
}
node: <Node Name String>
}
*/
module.exports = ({fs, node}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!fs || !fs.getFile) {
return cbk([400, 'ExpectedFileGetMethodToGetSavedCredentials']);
}
if (!node) {
return cbk([400, 'ExpectedNodeNameToGetSavedCredentials']);
}
return cbk();
},
// Get credentials
getCredentials: ['validate', ({}, cbk) => {
const path = join(...[homedir(), home, node, credentials]);
return fs.getFile(path, (err, res) => {
// Exit early on errors, there is no credential found
if (!!err || !res) {
return cbk(null, {node});
}
try {
parse(res.toString());
} catch (err) {
return cbk([400, 'SavedNodeHasInvalidCredentials']);
}
const credentials = parse(res.toString());
if (!credentials.cert) {
return cbk([400, 'SavedNodeMissingCertData']);
}
if (!credentials.macaroon && !credentials.encrypted_macaroon) {
return cbk([400, 'SavedNodeMissingMacaroonData']);
}
if (!!credentials.encrypted_macaroon && !credentials.encrypted_to) {
return cbk([400, 'MissingEncryptToRecipientsInSavedCredentials']);
}
if (!credentials.socket) {
return cbk([400, 'SavedNodeMissingSocket']);
}
return cbk(null, {
node,
credentials: {
cert: credentials.cert,
encrypted_macaroon: credentials.encrypted_macaroon,
encrypted_to: credentials.encrypted_to,
macaroon: credentials.macaroon,
socket: credentials.socket,
},
});
});
}],
},
returnResult({reject, resolve, of: 'getCredentials'}, cbk));
});
};

View file

@ -8,17 +8,17 @@ const {authenticatedLndGrpc} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const credentials = 'credentials.json';
const getSavedCredentials = require('./get_saved_credentials');
const home = '.bos';
const {parse} = JSON;
/** Get a list of saved nodes
{
fs: {
getDirectoryFiles: <Read Directory Contents Function> (path, cbk) => {}
getFile: <Read File Contents Function> (path, cbk) => {}
getFileStatus: <File Status Function> (path, cbk) => {}
getDirectoryFiles: <Read Directory Contents Function> (path, cbk) => {}
}
}
@ -42,6 +42,10 @@ module.exports = ({fs}, cbk) => {
return cbk([400, 'ExpectedFileSystemMethods']);
}
if (!fs.getDirectoryFiles) {
return cbk([400, 'ExpectedGetDirectoryFilesMethod']);
}
if (!fs.getFile) {
return cbk([400, 'ExpectedReadFileFunction']);
}
@ -50,10 +54,6 @@ module.exports = ({fs}, cbk) => {
return cbk([400, 'ExpectedReadFileStatusFunction']);
}
if (!fs.getDirectoryFiles) {
return cbk([400, 'ExpectedGetDirectoryFilesMethod']);
}
return cbk();
},
@ -61,7 +61,7 @@ module.exports = ({fs}, cbk) => {
checkDataDir: ['dataDir', ({dataDir}, cbk) => {
return fs.getFileStatus(dataDir, (err, res) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorCheckingForDataDirectory', {err}]);
return cbk([503, 'UnexpectedErrCheckingForDataDirectory', {err}]);
}
if (!res.isDirectory()) {
@ -92,54 +92,31 @@ module.exports = ({fs}, cbk) => {
// Get node credentials
getNodeCredentials: ['getDirs', ({getDirs}, cbk) => {
const credentialPaths = getDirs.map(dir => {
return {dir, path: join(...[homedir(), home, dir, credentials])};
});
return asyncMap(credentialPaths, ({dir, path}, cbk) => {
return fs.getFile(path, (err, res) => {
if (!!err || !res) {
return cbk();
}
try {
parse(res.toString());
} catch (err) {
return cbk([400, 'SavedNodeHasInvalidCredentials', {err, path}]);
}
const credentials = parse(res.toString());
if (!credentials.cert) {
return cbk([400, 'SavedNodeMissingCertData', {dir}]);
}
if (!credentials.macaroon) {
return cbk([400, 'SavedNodeMissingCertData', {dir}]);
}
if (!credentials.socket) {
return cbk([400, 'SavedNodeMissingSocket', {dir}]);
}
return cbk(null, {credentials, dir});
});
return asyncMap(getDirs, (node, cbk) => {
return getSavedCredentials({fs, node}, cbk);
},
cbk);
}],
// Get node info
getNodes: ['getNodeCredentials', ({getNodeCredentials}, cbk) => {
return asyncMap(getNodeCredentials, ({credentials, dir}, cbk) => {
return asyncMap(getNodeCredentials, ({credentials, node}, cbk) => {
if (!credentials.macaroon) {
return cbk(null, {
node_name: node,
locked_to_keys: credentials.encrypted_to,
});
}
const {lnd} = authenticatedLndGrpc(credentials);
return getWalletInfo({lnd}, (err, res) => {
if (!!err) {
return cbk(null, {node_name: dir});
return cbk(null, {node_name: node});
}
return cbk(null, {
node_name: dir,
node_name: node,
is_online: res.is_synced_to_chain,
});
});

View file

@ -1,3 +1,5 @@
const adjustSavedNodes = require('./adjust_saved_nodes');
const getSavedCredentials = require('./get_saved_credentials');
const getSavedNodes = require('./get_saved_nodes');
module.exports = {getSavedNodes};
module.exports = {adjustSavedNodes, getSavedCredentials, getSavedNodes};

View file

@ -0,0 +1,87 @@
const {join} = require('path');
const {homedir} = require('os');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const credentials = 'credentials.json';
const home = '.bos';
const {isArray} = Array;
const stringify = obj => JSON.stringify(obj, null, 2);
/** Write saved credentials for node
{
cert: <Base64 Encoded Node TLS Certificate String>
[encrypted_macaroon]: <Encrypted Macaroon String>
[encrypted_to]: [<Macaroon Encrypted To Recipient Id String>]
fs: {
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
}
[macaroon]: <Base64 Encoded Macaroon String>
node: <Node Name String>
socket: <Node Socket String>
}
@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.cert) {
return cbk([400, 'ExpectedCertToWriteSavedCredentialsForNode']);
}
if (!!args.encrypted_macaroon && !isArray(args.encrypted_to)) {
return cbk([400, 'ExpectedRecipientIdsForEncryptedMacaroon']);
}
if (!!args.encrypted_macaroon && !!args.macaroon) {
return cbk([400, 'UnexpectedUnencryptedMacaroon']);
}
if (!args.encrypted_macaroon && !args.macaroon) {
return cbk([400, 'ExpectedMacaroonForSavedCredentials']);
}
if (!args.fs || !args.fs.writeFile) {
return cbk([400, 'ExpectedFileSystemMethodsToPutSavedCredentials']);
}
if (!args.node) {
return cbk([400, 'ExpectedNodeNameToPutSavedCredentials']);
}
if (!args.socket) {
return cbk([400, 'ExpectedSocketForNodeToPutSavedCredentials']);
}
return cbk();
},
// Write credentials
writeCredentials: ['validate', ({}, cbk) => {
const file = stringify({
cert: args.cert,
encrypted_macaroon: args.encrypted_macaroon || undefined,
encrypted_to: args.encrypted_to || undefined,
macaroon: args.macaroon || undefined,
socket: args.socket,
});
const path = join(...[homedir(), home, args.node, credentials]);
return args.fs.writeFile(path, file, err => {
if (!!err) {
return cbk([503, 'UnexpectedErrorWritingSavedCredentials']);
}
return cbk();
});
}],
},
returnResult({reject, resolve, of: 'getCredentials'}, cbk));
});
};

262
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "balanceofsatoshis",
"version": "4.4.0",
"version": "4.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -440,9 +440,9 @@
"integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ=="
},
"async-hook-domain": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-1.1.1.tgz",
"integrity": "sha512-nHfgkoCbzXqCPEFshW5/LROfUqKcZdOW4mfvR66V7bdSuvvvt+s2CuHVBmJxHfOVFhCJ29xlRqEJxyNQKQsqBg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-1.1.3.tgz",
"integrity": "sha512-ZovMxSbADV3+biB7oR1GL5lGyptI24alp0LWHlmz1OFc5oL47pz3EiIF6nXOkDW7yLqih4NtsiYduzdDW0i+Wg==",
"dev": true,
"requires": {
"source-map-support": "^0.5.11"
@ -951,12 +951,6 @@
"winston": "^2.3.1"
}
},
"capture-stack-trace": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz",
"integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==",
"dev": true
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@ -1001,19 +995,19 @@
}
},
"chokidar": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz",
"integrity": "sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.2.tgz",
"integrity": "sha512-bw3pm7kZ2Wa6+jQWYP/c7bAZy3i4GwiIiMO2EeRjrE48l8vBqC/WvFhSF0xyM8fQiPEGvwMY/5bqDG7sSEOuhg==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.0",
"fsevents": "~2.1.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.1.3"
"readdirp": "~3.2.0"
}
},
"ci-info": {
@ -5092,9 +5086,9 @@
}
},
"readdirp": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.3.tgz",
"integrity": "sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
"integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
"dev": true,
"requires": {
"picomatch": "^2.0.4"
@ -5419,8 +5413,7 @@
},
"source-map-support": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
"integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
"resolved": "github:tapjs/node-source-map-support#b2e96dd5c02f70a62f06ca2eea7f344bf9f825c8",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
@ -5633,15 +5626,14 @@
}
},
"tap": {
"version": "14.6.9",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.6.9.tgz",
"integrity": "sha512-impxvxJo49knWdQNctdY+hAyQGbLjGR9foNkX4B9NnotgV5tfmJNqLrFu+YluHSNhIjfqF6Ea+Pj08xPAHxfSQ==",
"version": "14.7.1",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.7.1.tgz",
"integrity": "sha512-hbX34cNSduEh0RMNMIUwZPwJksBnE+LQC8FfvvOeZGvkUTXVVxZ4/ADuuDP2AY+9e75t8ZqzezAsfzIQHSLNSg==",
"dev": true,
"requires": {
"async-hook-domain": "^1.1.1",
"async-hook-domain": "^1.1.2",
"bind-obj-methods": "^2.0.0",
"browser-process-hrtime": "^1.0.0",
"capture-stack-trace": "^1.0.0",
"chokidar": "^3.0.2",
"color-support": "^1.1.0",
"coveralls": "^3.0.6",
@ -5667,7 +5659,7 @@
"react": "^16.9.0",
"rimraf": "^2.7.1",
"signal-exit": "^3.0.0",
"source-map-support": "^0.5.13",
"source-map-support": "github:tapjs/node-source-map-support#node-header-length-change",
"stack-utils": "^1.0.2",
"tap-mocha-reporter": "^5.0.0",
"tap-parser": "^10.0.0",
@ -5677,14 +5669,14 @@
"trivial-deferred": "^1.0.1",
"ts-node": "^8.3.0",
"typescript": "^3.6.3",
"which": "^1.3.1",
"which": "^2.0.1",
"write-file-atomic": "^3.0.0",
"yaml": "^1.6.0",
"yapool": "^1.0.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.4.5",
"version": "7.6.3",
"bundled": true,
"dev": true,
"requires": {
@ -5692,19 +5684,19 @@
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.2",
"version": "0.13.3",
"bundled": true,
"dev": true
}
}
},
"@types/prop-types": {
"version": "15.7.1",
"version": "15.7.3",
"bundled": true,
"dev": true
},
"@types/react": {
"version": "16.8.22",
"version": "16.9.5",
"bundled": true,
"dev": true,
"requires": {
@ -5713,9 +5705,12 @@
}
},
"ansi-escapes": {
"version": "3.2.0",
"version": "4.2.1",
"bundled": true,
"dev": true
"dev": true,
"requires": {
"type-fest": "^0.5.2"
}
},
"ansi-regex": {
"version": "2.1.1",
@ -5743,7 +5738,7 @@
"dev": true
},
"auto-bind": {
"version": "2.1.0",
"version": "2.1.1",
"bundled": true,
"dev": true,
"requires": {
@ -6063,15 +6058,22 @@
"dev": true,
"requires": {
"safe-buffer": "~5.1.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
}
}
},
"core-js": {
"version": "2.6.5",
"version": "2.6.10",
"bundled": true,
"dev": true
},
"csstype": {
"version": "2.6.5",
"version": "2.6.7",
"bundled": true,
"dev": true
},
@ -6107,7 +6109,7 @@
"dev": true
},
"esutils": {
"version": "2.0.2",
"version": "2.0.3",
"bundled": true,
"dev": true
},
@ -6157,11 +6159,12 @@
}
},
"ink": {
"version": "2.3.0",
"version": "2.5.0",
"bundled": true,
"dev": true,
"requires": {
"@types/react": "^16.8.6",
"ansi-escapes": "^4.2.1",
"arrify": "^1.0.1",
"auto-bind": "^2.0.0",
"chalk": "^2.4.1",
@ -6171,8 +6174,8 @@
"lodash.throttle": "^4.1.1",
"log-update": "^3.0.0",
"prop-types": "^15.6.2",
"react-reconciler": "^0.20.0",
"scheduler": "^0.13.2",
"react-reconciler": "^0.21.0",
"scheduler": "^0.15.0",
"signal-exit": "^3.0.2",
"slice-ansi": "^1.0.0",
"string-length": "^2.0.0",
@ -6181,11 +6184,6 @@
"yoga-layout-prebuilt": "^1.9.3"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"bundled": true,
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"bundled": true,
@ -6204,24 +6202,6 @@
"supports-color": "^5.3.0"
}
},
"string-width": {
"version": "3.1.0",
"bundled": true,
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"supports-color": {
"version": "5.5.0",
"bundled": true,
@ -6229,16 +6209,6 @@
"requires": {
"has-flag": "^3.0.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
}
}
},
@ -6287,7 +6257,7 @@
"dev": true
},
"lodash": {
"version": "4.17.14",
"version": "4.17.15",
"bundled": true,
"dev": true
},
@ -6297,7 +6267,7 @@
"dev": true
},
"log-update": {
"version": "3.2.0",
"version": "3.3.0",
"bundled": true,
"dev": true,
"requires": {
@ -6306,46 +6276,10 @@
"wrap-ansi": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"ansi-escapes": {
"version": "3.2.0",
"bundled": true,
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"bundled": true,
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"string-width": {
"version": "3.1.0",
"bundled": true,
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
}
}
},
@ -6357,6 +6291,11 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"mimic-fn": {
"version": "1.2.0",
"bundled": true,
"dev": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
@ -6366,7 +6305,7 @@
}
},
"minipass": {
"version": "3.0.0",
"version": "3.0.1",
"bundled": true,
"dev": true,
"requires": {
@ -6416,13 +6355,6 @@
"dev": true,
"requires": {
"mimic-fn": "^1.0.0"
},
"dependencies": {
"mimic-fn": {
"version": "1.2.0",
"bundled": true,
"dev": true
}
}
},
"os-homedir": {
@ -6461,7 +6393,7 @@
"dev": true
},
"react": {
"version": "16.9.0",
"version": "16.10.2",
"bundled": true,
"dev": true,
"requires": {
@ -6471,19 +6403,19 @@
}
},
"react-is": {
"version": "16.8.6",
"version": "16.10.2",
"bundled": true,
"dev": true
},
"react-reconciler": {
"version": "0.20.4",
"version": "0.21.0",
"bundled": true,
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.6"
"scheduler": "^0.15.0"
}
},
"redeyed": {
@ -6521,13 +6453,8 @@
"signal-exit": "^3.0.2"
}
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
},
"scheduler": {
"version": "0.13.6",
"version": "0.15.0",
"bundled": true,
"dev": true,
"requires": {
@ -6700,6 +6627,20 @@
"bundled": true,
"dev": true
},
"type-fest": {
"version": "0.5.2",
"bundled": true,
"dev": true
},
"which": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz",
"integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"widest-line": {
"version": "2.0.1",
"bundled": true,
@ -6708,10 +6649,53 @@
"string-width": "^2.1.1"
}
},
"wrap-ansi": {
"version": "5.1.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"bundled": true,
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"bundled": true,
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"string-width": {
"version": "3.1.0",
"bundled": true,
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"write-file-atomic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.0.tgz",
"integrity": "sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz",
"integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4",
@ -6721,11 +6705,11 @@
}
},
"yaml": {
"version": "1.6.0",
"version": "1.7.1",
"bundled": true,
"dev": true,
"requires": {
"@babel/runtime": "^7.4.5"
"@babel/runtime": "^7.5.5"
}
},
"yoga-layout-prebuilt": {
@ -6944,9 +6928,9 @@
"dev": true
},
"uglify-js": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz",
"integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.2.tgz",
"integrity": "sha512-+gh/xFte41GPrgSMJ/oJVq15zYmqr74pY9VoM69UzMzq9NFk4YDylclb1/bhEzZSaUQjbW5RvniHeq1cdtRYjw==",
"dev": true,
"optional": true,
"requires": {

View file

@ -28,7 +28,7 @@
},
"description": "Lightning balance CLI",
"devDependencies": {
"tap": "14.6.9"
"tap": "14.7.1"
},
"engines": {
"node": ">=10.4.0"
@ -47,7 +47,7 @@
"url": "https://github.com/alexbosworth/balanceofsatoshis.git"
},
"scripts": {
"test": "tap test/arrays/*.js test/balances/*.js test/encryption/*.js test/fiat/*.js test/network/*.js test/responses/*.js test/routing/*.js"
"test": "tap test/arrays/*.js test/balances/*.js test/encryption/*.js test/fiat/*.js test/network/*.js test/nodes/*.js test/responses/*.js test/routing/*.js"
},
"version": "4.4.0"
"version": "4.5.0"
}

View file

@ -82,7 +82,7 @@ module.exports = (args, cbk) => {
// Get authenticated lnd connection
getLnd: ['validate', ({}, cbk) => {
return authenticatedLnd({node: args.node}, cbk);
return authenticatedLnd({logger: args.logger, node: args.node}, cbk);
}],
// Get wallet info

View file

@ -72,6 +72,7 @@ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
confs: <Confirmations to Wait for Deposit Number>
[is_dry_run]: <Avoid Actually Executing Operation Bool>
[is_raw_recovery_shown]: <Show Raw Recovery Transactions Bool>
lnd: <Authenticated LND gRPC API Object>
logger: <Winston Logger Object>
[max_fee]: <Maximum Fee Tokens Number>
[max_wait_blocks]: <Maximum Wait Blocks Number>
@ -103,6 +104,10 @@ module.exports = (args, cbk) => {
return cbk([400, 'ExpectedConfirmationsCountToConsiderReorgSafe']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToInitiateSwapOut']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerForSwapProgressNotifications']);
}
@ -118,13 +123,8 @@ module.exports = (args, cbk) => {
return cbk();
}],
// Get authenticated lnd connection
getLnd: ['validate', ({}, cbk) => {
return authenticatedLnd({node: args.node}, cbk);
}],
// Create a sweep address
createAddress: ['getLnd', 'recover', ({getLnd, recover}, cbk) => {
createAddress: ['recover', 'validate', ({recover}, cbk) => {
if (!!recover && recover.sweep_address) {
return cbk(null, {address: recover.sweep_address});
}
@ -136,24 +136,22 @@ module.exports = (args, cbk) => {
return createChainAddress({
format: 'p2wpkh',
is_unused: true,
lnd: getLnd.lnd,
lnd: args.lnd,
},
cbk);
}],
// Get channels
getChannels: ['getLnd', ({getLnd}, cbk) => {
return getChannels({lnd: getLnd.lnd, is_active: true}, cbk);
getChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.lnd, is_active: true}, cbk);
}],
// Get network
getNetwork: ['getLnd', ({getLnd}, cbk) => {
return getNetwork({lnd: getLnd.lnd}, cbk);
}],
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
// Get wallet info
getWalletInfo: ['getLnd', ({getLnd}, cbk) => {
return getWalletInfo({lnd: getLnd.lnd}, cbk);
getWalletInfo: ['validate', ({}, cbk) => {
return getWalletInfo({lnd: args.lnd}, cbk);
}],
// Get the current block height
@ -162,7 +160,7 @@ module.exports = (args, cbk) => {
}],
// Figure out which channel to use when swapping
channel: ['getChannels', 'getLnd', ({getChannels, getLnd}, cbk) => {
channel: ['getChannels', ({getChannels}, cbk) => {
if (!!args.recovery) {
return cbk();
}
@ -194,7 +192,7 @@ module.exports = (args, cbk) => {
return getNode({
is_omitting_channels: true,
lnd: getLnd.lnd,
lnd: args.lnd,
public_key: channel.partner_public_key,
},
(err, res) => {
@ -339,17 +337,13 @@ module.exports = (args, cbk) => {
}],
// Decode swap execution request
decodeExecutionRequest: [
'getLnd',
'initiateSwap',
({getLnd, initiateSwap}, cbk) =>
{
decodeExecutionRequest: ['initiateSwap', ({initiateSwap}, cbk) => {
if (!!args.recovery) {
return cbk();
}
return decodePaymentRequest({
lnd: getLnd.lnd,
lnd: args.lnd,
request: initiateSwap.swap_execute_request,
},
cbk);
@ -409,17 +403,13 @@ module.exports = (args, cbk) => {
}],
// Decode funding request
decodeFundingRequest: [
'getLnd',
'initiateSwap',
({getLnd, initiateSwap}, cbk) =>
{
decodeFundingRequest: ['initiateSwap', ({initiateSwap}, cbk) => {
if (!!args.recovery) {
return cbk();
}
return decodePaymentRequest({
lnd: getLnd.lnd,
lnd: args.lnd,
request: initiateSwap.swap_fund_request,
},
cbk);
@ -457,9 +447,8 @@ module.exports = (args, cbk) => {
findRouteForExecution: [
'channel',
'decodeExecutionRequest',
'getLnd',
'getStartHeight',
({channel, decodeExecutionRequest, getLnd, getStartHeight}, cbk) =>
({channel, decodeExecutionRequest, getStartHeight}, cbk) =>
{
// Exit early when there is a swap recovery
if (!!args.recovery) {
@ -470,7 +459,7 @@ module.exports = (args, cbk) => {
cltv_delta: decodeExecutionRequest.cltv_delta + cltvBuffer,
destination: decodeExecutionRequest.destination,
ignore: (args.avoid || []).map(n => ({from_public_key: n})),
lnd: getLnd.lnd,
lnd: args.lnd,
logger: args.logger,
max_fee: maxExecutionFeeTokens,
max_timeout_height: getStartHeight + maxCltvDelta,
@ -496,13 +485,11 @@ module.exports = (args, cbk) => {
'channel',
'currency',
'decodeFundingRequest',
'getLnd',
'getStartHeight',
({
channel,
currency,
decodeFundingRequest,
getLnd,
getStartHeight,
}, cbk) =>
{
@ -514,7 +501,7 @@ module.exports = (args, cbk) => {
cltv_delta: decodeFundingRequest.cltv_delta + cltvBuffer,
destination: decodeFundingRequest.destination,
ignore: (args.avoid || []).map(n => ({from_public_key: n})),
lnd: getLnd.lnd,
lnd: args.lnd,
logger: args.logger,
max_fee: round(decodeFundingRequest.tokens / maxRoutingFeeDenominator),
max_timeout_height: getStartHeight + maxCltvDelta,
@ -540,8 +527,7 @@ module.exports = (args, cbk) => {
'channel',
'findRouteForFunding',
'getChannels',
'getLnd',
({channel, findRouteForFunding, getChannels, getLnd}, cbk) =>
({channel, findRouteForFunding, getChannels}, cbk) =>
{
// Exit early when this is a recovery
if (!!args.recovery) {
@ -563,7 +549,7 @@ module.exports = (args, cbk) => {
return getNode({
is_omitting_channels: true,
lnd: getLnd.lnd,
lnd: args.lnd,
public_key: firstHop.public_key,
},
(err, res) => {
@ -588,7 +574,6 @@ module.exports = (args, cbk) => {
'decodeFundingRequest',
'findRouteForExecution',
'findRouteForFunding',
'getLnd',
'getSwapPeer',
'getQuote',
({
@ -597,7 +582,6 @@ module.exports = (args, cbk) => {
decodeFundingRequest,
findRouteForExecution,
findRouteForFunding,
getLnd,
getSwapPeer,
getQuote,
}, cbk) =>
@ -622,7 +606,7 @@ module.exports = (args, cbk) => {
return getChainFeeRate({
confirmation_target: getQuote.cltv_delta,
lnd: getLnd.lnd,
lnd: args.lnd,
},
(err, res) => {
if (!!err) {
@ -661,8 +645,7 @@ module.exports = (args, cbk) => {
'decodeFundingRequest',
'findRouteForExecution',
'findRouteForFunding',
'getLnd',
({decodeFundingRequest, findRouteForFunding, getLnd}, cbk) =>
({decodeFundingRequest, findRouteForFunding}, cbk) =>
{
if (!!args.recovery) {
return cbk();
@ -670,7 +653,7 @@ module.exports = (args, cbk) => {
return payViaRoutes({
id: decodeFundingRequest.id,
lnd: getLnd.lnd,
lnd: args.lnd,
routes: [findRouteForFunding],
},
cbk);
@ -684,7 +667,6 @@ module.exports = (args, cbk) => {
'decodeExecutionRequest',
'findRouteForExecution',
'findRouteForFunding',
'getLnd',
'getMinSweepFee',
'getQuote',
'getStartHeight',
@ -692,7 +674,6 @@ module.exports = (args, cbk) => {
({
channel,
decodeExecutionRequest,
getLnd,
getQuote,
getStartHeight,
initiateSwap,
@ -705,7 +686,7 @@ module.exports = (args, cbk) => {
args.logger.info({paying_execution_request: decodeExecutionRequest.id});
const sub = subscribeToPayViaRequest({
lnd: getLnd.lnd,
lnd: args.lnd,
max_fee: maxExecutionFeeTokens,
max_timeout_height: getStartHeight + maxCltvExpiration,
outgoing_channel: channel.id || undefined,
@ -774,15 +755,14 @@ module.exports = (args, cbk) => {
// Look for deposit
findDeposit: [
'getLnd',
'getWalletInfo',
'initiateSwap',
'network',
'recover',
({getWalletInfo, initiateSwap, getLnd, network, recover}, cbk) =>
({getWalletInfo, initiateSwap, network, recover}, cbk) =>
{
const currentHeight = getWalletInfo.current_block_height;
const sub = subscribeToBlocks({lnd: getLnd.lnd});
const sub = subscribeToBlocks({lnd: args.lnd});
const tokens = !recover ? args.tokens : recover.tokens;
const startHeight = !recover ? currentHeight : recover.start_height;
@ -803,7 +783,7 @@ module.exports = (args, cbk) => {
address: initiateSwap.address,
after: startHeight - fuzzBlocks,
confirmations: args.confs,
lnd: getLnd.lnd,
lnd: args.lnd,
timeout: args.timeout,
},
(err, res) => {
@ -847,7 +827,6 @@ module.exports = (args, cbk) => {
'claim',
'createAddress',
'getHeight',
'getLnd',
'initiateSwap',
'network',
'recover',
@ -855,7 +834,6 @@ module.exports = (args, cbk) => {
claim,
createAddress,
getHeight,
getLnd,
initiateSwap,
network,
recover,
@ -881,7 +859,7 @@ module.exports = (args, cbk) => {
current_height: getHeight + i,
deadline_height: min(maxWaitHeight, maxSafeHeight),
is_dry_run: true,
lnd: getLnd.lnd,
lnd: args.lnd,
max_fee_multiplier: maxFeeMultiplier,
min_fee_rate: minFeeRate,
private_key: claim.private_key,
@ -917,7 +895,6 @@ module.exports = (args, cbk) => {
'claim',
'createAddress',
'getHeight',
'getLnd',
'initiateSwap',
'network',
'rawRecovery',
@ -926,7 +903,6 @@ module.exports = (args, cbk) => {
claim,
createAddress,
getHeight,
getLnd,
initiateSwap,
network,
recover,
@ -940,7 +916,7 @@ module.exports = (args, cbk) => {
args.logger.info({swap_deposit_confirmed: claim.transaction_id});
const blocksSubscription = subscribeToBlocks({lnd: getLnd.lnd});
const blocksSubscription = subscribeToBlocks({lnd: args.lnd});
const tokens = !recover ? args.tokens : recover.tokens;
blocksSubscription.on('end', () => {});
@ -955,7 +931,7 @@ module.exports = (args, cbk) => {
tokens,
current_height: height,
deadline_height: initiateSwap.timeout - args.confs,
lnd: getLnd.lnd,
lnd: args.lnd,
max_fee_multiplier: maxFeeMultiplier,
private_key: claim.private_key,
secret: claim.secret,
@ -993,7 +969,7 @@ module.exports = (args, cbk) => {
address: createAddress.address,
after: getHeight,
confirmations: max(args.confs, minSweepConfs),
lnd: getLnd.lnd,
lnd: args.lnd,
timeout: args.timeout,
transaction_id: claim.transaction_id,
transaction_vout: claim.transaction_vout,
@ -1012,17 +988,16 @@ module.exports = (args, cbk) => {
// Get funding payment
getFundingPayment: [
'decodeFundingRequest',
'getLnd',
'payToFund',
'recover',
'sweep',
({decodeFundingRequest, getLnd, recover}, cbk) =>
({decodeFundingRequest, recover}, cbk) =>
{
const fundingRequest = decodeFundingRequest || {};
const id = fundingRequest.id || sha256(recover.secret).digest('hex');
const sub = subscribeToPastPayment({id, lnd: getLnd.lnd});
const sub = subscribeToPastPayment({id, lnd: args.lnd});
const finished = (err, res) => {
sub.removeAllListeners();
@ -1046,16 +1021,15 @@ module.exports = (args, cbk) => {
// Get execution payment
getExecutionPayment: [
'decodeExecutionRequest',
'getLnd',
'payToExecute',
'recover',
({decodeExecutionRequest, getLnd, recover}, cbk) =>
({decodeExecutionRequest, recover}, cbk) =>
{
const executionRequest = decodeExecutionRequest || {};
const id = executionRequest.id || recover.execution_id;
return getPayment({id, lnd: getLnd.lnd}, cbk);
return getPayment({id, lnd: args.lnd}, cbk);
}],
// Spent offchain

View file

@ -0,0 +1,147 @@
const {test} = require('tap');
const {getSavedNodes} = require('./../../nodes');
const getDirectoryFiles = ({}, cbk) => cbk(null, ['name']);
const tests = [
{
args: {},
description: 'Getting saved nodes requires fs',
error: [400, 'ExpectedFileSystemMethods'],
},
{
args: {fs: {}},
description: 'Filesystem methods requires a directory files function',
error: [400, 'ExpectedGetDirectoryFilesMethod'],
},
{
args: {fs: {getDirectoryFiles: () => {}}},
description: 'Filesystem methods requires a get file function',
error: [400, 'ExpectedReadFileFunction'],
},
{
args: {fs: {getDirectoryFiles: ({}, cbk) => {}, getFile: ({}, cbk) => {}}},
description: 'Filesystem methods requires a read file function',
error: [400, 'ExpectedReadFileStatusFunction'],
},
{
args: {
fs: {
getDirectoryFiles: ({}, cbk) => {},
getFile: ({}, cbk) => {},
getFileStatus: ({}, cbk) => cbk('err'),
},
},
description: 'Error getting data dir returns back error',
error: [503, 'UnexpectedErrCheckingForDataDirectory', {err: 'err'}],
},
{
args: {
fs: {
getDirectoryFiles: ({}, cbk) => {},
getFile: ({}, cbk) => {},
getFileStatus: ({}, cbk) => cbk(null, {isDirectory: () => false}),
},
},
description: 'The home directory must be a directory',
error: [400, 'FailedToFindHomeDataDirectory'],
},
{
args: {
fs: {
getDirectoryFiles: ({}, cbk) => cbk(null, []),
getFile: ({}, cbk) => cbk(),
getFileStatus: ({}, cbk) => cbk(null, {isDirectory: () => true}),
},
},
description: 'A directory with no saved nodes returns an empty array',
expected: {nodes: []},
},
{
args: {
fs: {
getDirectoryFiles,
getFile: ({}, cbk) => cbk(),
getFileStatus: (path, cbk) => {
if (path.slice(-'name'.length) === 'name') {
return cbk('err');
}
return cbk(null, {isDirectory: () => true});
},
},
},
description: 'Errors when getting node dir are passed back',
error: [503, 'UnexpectedErrCheckingForNodeDir', {err: 'err'}],
},
{
args: {
fs: {
getDirectoryFiles,
getFile: ({}, cbk) => cbk(null, 'foo'),
getFileStatus: (path, cbk) => {
return cbk(null, {isDirectory: () => true});
},
},
},
description: 'The saved node has to have JSON credentials',
error: [400, 'SavedNodeHasInvalidCredentials'],
},
{
args: {
fs: {
getDirectoryFiles,
getFile: ({}, cbk) => cbk(null, JSON.stringify({})),
getFileStatus: (path, cbk) => {
return cbk(null, {isDirectory: () => true});
},
},
},
description: 'The saved node has to have JSON credentials',
error: [400, 'SavedNodeMissingCertData'],
},
{
args: {
fs: {
getDirectoryFiles,
getFile: ({}, cbk) => cbk(null, JSON.stringify({cert: 'cert'})),
getFileStatus: (path, cbk) => {
return cbk(null, {isDirectory: () => true});
},
},
},
description: 'The saved node has to have JSON credentials',
error: [400, 'SavedNodeMissingMacaroonData'],
},
{
args: {
fs: {
getDirectoryFiles,
getFile: ({}, cbk) => cbk(null, JSON.stringify({
cert: 'cert',
macaroon: 'macaroon',
})),
getFileStatus: (path, cbk) => {
return cbk(null, {isDirectory: () => true});
},
},
},
description: 'The saved node has to have JSON credentials',
error: [400, 'SavedNodeMissingSocket'],
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({deepIs, end, equal, rejects}) => {
if (!!error) {
rejects(getSavedNodes(args), error, 'Got expected error');
} else {
const {nodes} = await getSavedNodes(args);
deepIs(nodes, expected.nodes, 'Got expected nodes');
}
return end();
});
});