@holographxyz/cli
Version:
Holograph operator CLI
283 lines (282 loc) • 12.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const inquirer = tslib_1.__importStar(require("inquirer"));
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("node:path"));
const core_1 = require("@oclif/core");
const wallet_1 = require("@ethersproject/wallet");
const networks_1 = require("@holographxyz/networks");
const config_1 = require("../../utils/config");
const utils_1 = require("../../utils/utils");
const aes_encryption_1 = tslib_1.__importDefault(require("../../utils/aes-encryption"));
class Config extends core_1.Command {
static description = 'Initialize the Holograph CLI with a config file. If no flags are passed, the CLI will prompt you for the required information.';
static examples = [
'$ <%= config.bin %> <%= command.id %> --privateKey abc...def',
'$ <%= config.bin %> <%= command.id %> --fromFile ./config.json',
'$ <%= config.bin %> <%= command.id %> --fromJson \'{"version": "beta3", ...}',
];
static flags = {
network: core_1.Flags.string({
options: networks_1.supportedShortNetworks,
description: 'Network to set',
}),
url: core_1.Flags.string({
description: 'Provider URL of network to set',
dependsOn: ['network'],
}),
privateKey: core_1.Flags.string({ description: 'Default account to use when sending all transactions' }),
fromFile: core_1.Flags.string({ description: 'Path to the config file to load' }),
fromJson: core_1.Flags.string({ description: 'JSON object to use as the config' }),
};
/**
* Command Entry Point
*/
async run() {
const { flags } = await this.parse(Config);
let privateKey = flags.privateKey;
let userWallet = null;
let currentConfigFile = null;
let encryption;
let iv = '';
const configPath = path.join(this.config.configDir, config_1.CONFIG_FILE_NAME);
this.debug(`Configuration path ${configPath}`);
let updateNetworksPrompt = { update: false };
let privateKeyPrompt = { update: false };
await this.loadConfigPath(configPath, flags.fromFile);
await this.loadConfigJson(configPath, flags.fromJson);
// Check if config file exists
const isConfigExist = await (0, config_1.checkFileExists)(configPath);
let userConfigTemplate = {
version: 'beta3',
networks: {},
user: {
credentials: {
iv: iv,
privateKey: privateKey,
address: '',
},
},
};
if (isConfigExist) {
this.log(`Updating existing configuration file at ${configPath}`);
currentConfigFile = await (0, config_1.ensureConfigFileIsValid)(configPath, undefined, false);
userConfigTemplate = Object.assign({}, userConfigTemplate, currentConfigFile.configFile);
const prompt = await inquirer.prompt([
{
name: 'shouldContinue',
message: 'Configuration already exist, are you sure you want to override existing values?',
type: 'confirm',
default: false,
},
]);
if (!prompt.shouldContinue) {
this.log('No files were modified');
this.exit();
}
}
else {
this.log(`Creating a new config file file at ${configPath}`);
}
if (isConfigExist) {
// See if the user wants to update network config
updateNetworksPrompt = await inquirer.prompt([
{
name: 'update',
message: 'Would you like to update your network config?',
type: 'confirm',
default: false,
},
]);
}
if (updateNetworksPrompt.update || !isConfigExist) {
// Check what networks the user wants to operate on
const prompt = await inquirer.prompt([
{
type: 'checkbox',
name: 'networks',
message: 'Which networks do you want to operate?',
choices: (0, config_1.generateSupportedNetworksOptions)(),
validate: async (input) => {
if (input.length > 0) {
return true;
}
return 'Please select at least 1 network. Use the arrow keys and space-bar to select.';
},
},
]);
const providedNetworks = prompt.networks;
// Remove networks the user doesn't want to operate on
for (const network of Object.keys(userConfigTemplate.networks)) {
if (!providedNetworks.includes(network)) {
delete userConfigTemplate.networks[network];
}
}
// Add networks to the user config
// It's okay to await in loop because this is a synchronous operation
for (const network of providedNetworks) {
const prompt = await inquirer.prompt([
{
name: 'providerUrl',
message: `Enter the provider url for ${networks_1.networks[network].shortKey}. Leave blank to use ${userConfigTemplate.networks[network]?.providerUrl || networks_1.networks[network].rpc} :`,
type: 'input',
validate: async (input) => {
if ((0, utils_1.isStringAValidURL)(input) || input === '') {
return true;
}
return 'Input is not a valid and secure URL (https or wss)';
},
},
]);
// Leave existing providerUrl if user didn't enter a new one
if (prompt.providerUrl !== '') {
userConfigTemplate.networks[network] = { providerUrl: prompt.providerUrl };
}
else if (!(network in userConfigTemplate.networks)) {
userConfigTemplate.networks[network] = { providerUrl: networks_1.networks[network].rpc };
}
}
}
if (isConfigExist) {
// See if the user wants to update network config
privateKeyPrompt = await inquirer.prompt([
{
name: 'update',
message: 'Would you like to update your private key?',
type: 'confirm',
default: false,
},
]);
}
if (privateKeyPrompt.update || !isConfigExist) {
// Collect private key value
let keyProtected = true;
if (privateKey === undefined) {
keyProtected = false;
const prompt = await inquirer.prompt([
{
name: 'privateKey',
message: 'Default private key to use when sending all transactions (will be password encrypted)',
type: 'password',
validate: async (input) => {
try {
const w = new wallet_1.Wallet(input);
this.debug(w);
return true;
}
catch (error) {
this.debug(error);
return 'Input is not a valid private key';
}
},
},
]);
privateKey = prompt.privateKey;
userWallet = new wallet_1.Wallet(prompt.privateKey);
iv = (0, utils_1.randomASCII)(12);
}
else {
iv = currentConfigFile.user.credentials.iv;
}
await inquirer.prompt([
{
name: 'encryptionPassword',
message: 'Please enter the password to ' + (keyProtected ? 'decrypt' : 'encrypt') + ' the private key with',
type: 'password',
validate: async (input) => {
try {
encryption = new aes_encryption_1.default(input, iv);
if (keyProtected) {
// We need to check that key decoded
userWallet = new wallet_1.Wallet(encryption.decrypt(currentConfigFile.user.credentials.privateKey));
}
else {
privateKey = encryption.encrypt(privateKey || '');
}
return true;
}
catch (error) {
this.debug(error);
return 'Input is not a valid password';
}
},
},
]);
userConfigTemplate.user.credentials.iv = iv;
userConfigTemplate.user.credentials.privateKey = privateKey;
userConfigTemplate.user.credentials.address = userWallet?.address;
}
// Save config object
try {
await fs.outputJSON(configPath, userConfigTemplate, { spaces: 2 });
}
catch (error) {
this.log(`Failed to save file in ${configPath}. Please enable debugger and try again.`);
this.debug(error);
}
this.exit();
}
/**
* Load the config file from the path provided by the user
*/
async loadConfigPath(configPath, filePath) {
// Check if config Dir flag is empty
if (filePath !== undefined) {
try {
const stats = fs.lstatSync(filePath);
this.debug(`Is file: ${stats.isFile()}`);
this.debug(`Is directory: ${stats.isDirectory()}`);
this.debug(`Is symbolic link: ${stats.isSymbolicLink()}`);
this.debug(`Is FIFO: ${stats.isFIFO()}`);
this.debug(`Is socket: ${stats.isSocket()}`);
this.debug(`Is character device: ${stats.isCharacterDevice()}`);
this.debug(`Is block device: ${stats.isBlockDevice()}`);
if (stats.isFile() &&
!stats.isDirectory() &&
!stats.isSymbolicLink() &&
!stats.isFIFO() &&
!stats.isSocket() &&
!stats.isCharacterDevice() &&
!stats.isBlockDevice()) {
const ensureCheck = await (0, config_1.ensureConfigFileIsValid)(filePath, undefined, false);
// Since the json at the desired path is valid, we save it!
await fs.outputJSON(configPath, ensureCheck.configFile, { spaces: 2 });
}
else {
this.error(`filePath is NOT VALID FAIL`);
}
}
catch (error) {
// Handle error
if (error.code === 'ENOENT') {
this.error(`The input ${filePath} is not a valid file path`);
// eslint-disable-next-line no-negated-condition
}
else if (typeof error.message !== 'undefined') {
this.error(error.message);
}
else {
this.error(`Failed to load ${filePath}`);
}
}
this.exit();
}
}
/**
* Load the config file from the JSON provided by the user
*/
async loadConfigJson(configPath, jsonString) {
// Check if config Json flag is empty
if (jsonString !== undefined) {
this.log(`checking jsonString input`);
const output = JSON.parse(jsonString);
await (0, config_1.validateBeta3Schema)(output);
this.log(output);
// Since the json at the desired path is valid, we save it!
await fs.outputJSON(configPath, output, { spaces: 2 });
this.exit();
}
}
}
exports.default = Config;