nem-cli
Version:
NEM-cli is a collection of command line tools for the NEM Blockchain features using the Terminal
446 lines (390 loc) • 12.5 kB
JavaScript
/**
* Part of the evias/nem-cli package.
*
* NOTICE OF LICENSE
*
* Licensed under MIT License.
*
* This source file is subject to the MIT License that is
* bundled with this package in the LICENSE file.
*
* @package evias/nem-cli
* @author Grégory Saive <greg@evias.be> (https://github.com/evias)
* @license MIT License
* @copyright (c) 2017, Grégory Saive <greg@evias.be>
* @link https://github.com/evias/nem-cli
*/
"use strict";
import ConsoleInput from "./console-input";
import NEMNetworkConnection from "./nem-connection";
import * as JSONBeautifier from "prettyjson";
const { URLSearchParams } = require('url');
var Menu = require("simple-terminal-menu");
var Table = require("easy-table");
var chalk = require("chalk");
/**
* The BaseCommand class will be extended by all scripts/*.js
* JS Command classes.
*
* This class is responsible for handling *command line arguments*
* passed to the CLI as well as hold a *connection object*, an SDK
* instance and a *node object*, which can all be used directly
* in child classes.
*/
class BaseCommand {
/**
* Construct the BaseCommand object
*
* Default properties will be initialized with this
* constructor.
*
* @param {object} _package
*/
constructor(_package) {
/**
* The terminal utility to ask for user input.
*
* @var {ConsoleInput}
*/
this.io = new ConsoleInput();
/**
* The NPM package of the NEM-CLI. This is used
* for the --help command.
*
* @var {object}
*/
this.npmPackage = _package;
/**
* The command signature (Example: "wallet", "api" or "export")
*
* @var {string}
*/
this.signature = "";
/**
* The command description
*
* @var {string}
*/
this.description = "";
/**
* The available command line arguments for
* this command.
*
* Each option object must contain a `signature` and
* `description` key.
*
* @var {array}
*/
this.options = [];
/**
* This is an array of examples for the said command.
*
* @var {array}
*/
this.examples = [];
/**
* This will contain the command line arguments
* passed to the command.
*
* @var {object}
*/
this.argv = {};
}
/**
* This method outputs the help message corresponding to
* the `./nem-cli <command> --help` command.
*
* It will display a list of examples use cases of this API wrapper.
*
* @return void
*/
help() {
const warning = chalk.red;
const keyword = chalk.yellow;
const label = chalk.green;
const normal = chalk.reset;
const log = console.log;
log("")
log(" " + label("Usage: ") + keyword("nem-cli " + this.signature + " [options]"));
log("");
log(" " + label("Description:"));
log("");
log(normal(this.description));
log("");
log("");
log(" " + label("Options: "));
log("");
log(" " + keyword("-n, --node [node]") + normal("\t\tSet custom [node] for NIS API"));
log(" " + keyword("-p, --port [port]") + normal("\t\tSet custom [port] for NIS API"));
log(" " + keyword("-N, --network [network]") + normal("\t\tSet network (Mainnet|Testnet|Mijin)"));
log(" " + keyword("-S, --force-ssl") + normal("\t\tUse SSL (HTTPS)"));
log(" " + keyword("-d, --verbose") + normal("\t\tSet verbose command execution (more logs)"));
log("");
for (let i = 0; i < this.options.length; i++) {
let opt = this.options[i];
log(" " + keyword(opt.signature) + normal("\t\t" + opt.description));
}
log("");
if (this.examples.length) {
log("");
log(" " + label("Examples: "));
log("");
for (let j = 0; j < this.examples.length; j++) {
let example = this.examples[j];
log(" $ " + example);
}
}
}
/**
* This method should *execute* the action proper to the subcommand.
*
* Example:
*
* run(env) { console.log("Command running!"); }
*
* @param {string} subcommand
* @param {object} opts
* @return {void}
*/
run(subcommand, opts) {
throw new Error("Please specify a run(env) method in your subclass of BaseCommand.");
}
/**
* This method should end the command execution process.
*
* Example:
*
* end() { return process.exit(); }
*
* @return void
*/
end() {
throw new Error("Please specify a end() method in your subclass of BaseCommand.");
}
/**
* This method will initialize the *connection* object and
* the SDK object as well as the node object (SDK endpoint).
*
* After this method has been called, the command can fully
* interact with the NEM blockchain node.
*
* @param {object} options Can contain keys "network", "node", "port", "forceSsl"
*/
init(options) {
this.argv = options;
// prepare connection to NEM network
let defaultNodes = {
"mainnet": "hugealice.nem.ninja",
"testnet": "bigalice2.nem.ninja"
};
let network = "testnet";
let port = this.argv.port ? parseInt(this.argv.port) : 7890;
let node = this.argv.node && this.argv.node.length ? this.argv.node : "bigalice2.nem.ninja";
if (this.argv.network) {
// --network has precedence over --node
network = this.argv.network.toLowerCase();
if (! defaultNodes.hasOwnProperty(network))
network = "testnet";
node = defaultNodes[network];
}
let nsch = node.match(/^http/) ? node.replace(/:\/\/.*/, '') : null;
node = node.replace(/https?:\/\//, '');
let scheme = this.argv.forceSsl ? "https" : (nsch ? nsch : "http");
// set connection object
this.conn = new NEMNetworkConnection(network, scheme + "://" + node, port);
this.SDK = this.conn.SDK;
this.node = this.SDK.model.objects.create("endpoint")(this.conn.getHost(), this.conn.getPort());
this.network = network;
this.networkId = this.SDK.model.network.data[network].id;
}
/**
* Getter for the `options` property.
*
* This property holds the commands' specific argument
* line options.
*
* @return {array}
*/
getOptions() {
return this.options;
}
/**
* Getter for the `signature` property.
*
* The signature property explained:
*
* $ ./nem-cli api
* $ ./nem-cli list
*
* In these 2 examples, signatures are *api* and *list*.
*
* The signature is used to register a subcommand to the commander
* arguments helper.
*
* @return {string}
*/
getSignature() {
return this.signature;
}
/**
* Getter for the `description` property.
*
* @return {string}
*/
getDescription() {
return this.description;
}
/**
* Getter for the `io` property.
*
* This property holds a helper for class `ConsoleInput`
* such that input can be asked for from the terminal.
*
* @see {ConsoleInput}
* @return {array}
*/
getInput() {
return this.io;
}
/**
* The switchNetworkByQS method will identify potential a `address`
* parameter in the query string of the URL provided.
*
* @param {string} url
*/
switchNetworkByQS(url) {
let hasQuery = url && url.length ? url.match(/\?[a-z0-9=_\-\+%]+$/i) : false;
if (! hasQuery)
return "testnet";
// most common use case: endpoint?address=..
let query = url.replace(/(.*)(\?[a-z0-9=_\-\+%]+)$/i, "$2");
let urlParams = new URLSearchParams(query);
if (urlParams.has("address")) {
// address parameter found, we will determine the network by
// the address parameter whenever an address is identified.
let addr = urlParams.get("address");
return this.switchNetworkByAddress(addr);
}
}
/**
* Switch the currently set NEM NETWORK to the one retrieved
* from the given `address`.
*
* @param {String} address NEM Wallet Address
*/
switchNetworkByAddress(address) {
let network = this.conn.getNetworkForAddress(address);
if (network != this.network)
// re-init with new network identified by address.
this.init({"network": network});
}
/**
* This method will display a terminal menu with
* the given `items`. Items will be indexed, the
* `items` parameter should be an array with choices
* texts.
*
* @param {Array} items
*/
displayMenu(menuTitle, items, quitCallback, addQuit, cbParams) {
let self = this;
let menu = new Menu({
x: 3,
y: 2
});
menu.writeTitle("NEM CLI v" + this.npmPackage.version);
menu.writeSubtitle(menuTitle);
menu.writeSeparator();
for (let i = 0, m = Object.keys(items).length; i < m; i++) {
let choice = items[i].title;
let c = choice.substr(0, 1);
// add menu item with callback
menu.add(choice, items[i].callback);
}
if (addQuit === true) {
menu.add("Quit", function() {
menu.close();
return quitCallback ? quitCallback() : null;
});
}
}
/**
* This method will display a table in the terminal.
*
* The arguments `headers` and `rows` are mandatory. The
* `headers` array should have keys representing row field
* names and values representing header titles.
*
* @param {*} headers
* @param {*} rows
*/
displayTable(title, headers, data) {
let self = this;
let table = new Table();
if (typeof data === 'object' && data.length) {
// isArray
data.forEach(function(row) {
self.addRow(table, headers, row);
});
}
else {
self.addRow(table, headers, data);
}
console.log("");
console.log(' ' + title + ' ');
console.log("");
console.log(table.toString());
}
/**
* This method will check the type of data that is
* currently being added and will format (color) the
* text accordingly.
*
* @see chalk
* @see easy-table
* @param {Table} table
* @param {Array} fields
* @param {Object} data
*/
addRow(table, headers, data) {
let fields = Object.keys(headers);
for (let f in fields) {
let field = fields[f];
let header = headers[field];
let value = data[field];
if (typeof value === 'boolean')
// YES/NO flags
value = value === true ? chalk.green("YES") : chalk.red("NO")
else if (typeof value === 'number')
// numbers
value = chalk.yellow(value);
else if (typeof value === 'string'
&& (parseFloat(value) == value
|| parseInt(value) == value)) {
// numbers (but not typed right)
value = chalk.yellow(value);
}
table.cell(header, value);
}
table.newRow();
return table;
}
/**
* This helper method will return a beautified JSON
* to be outputted to the console.
*
* @param {string} json
* @return {string}
*/
beautifyJSON(json) {
let parsed = JSON.parse(json);
let beautified = JSONBeautifier.render(parsed, {
keysColor: 'green',
dashColor: 'green',
stringColor: 'yellow'
});
return beautified;
}
}
exports.BaseCommand = BaseCommand;
export default BaseCommand;