@hashgraph/hedera-cli
Version:
CLI tool to manage and setup developer environments for Hedera Hashgraph.
248 lines (214 loc) • 7 kB
text/typescript
import * as fs from 'fs';
import * as path from 'path';
import stateUtils from '../utils/state';
import telemetryUtils from '../utils/telemetry';
import enquirerUtils from '../utils/enquirer';
import stateController from '../state/stateController';
import { Logger } from '../utils/logger';
import type { Command, State } from '../../types';
const logger = Logger.getInstance();
/**
* Remove the private keys and other sensitive info from the state object
* Warning: It does not remove the private keys from scripts
*
* @param data Modify the state object to remove private keys and other sensitive info
* @returns @type {State}
*/
function filterState(data: State) {
const filteredState = { ...data };
filteredState.previewnetOperatorId = '';
filteredState.previewnetOperatorKey = '';
filteredState.testnetOperatorId = '';
filteredState.testnetOperatorKey = '';
filteredState.mainnetOperatorId = '';
filteredState.mainnetOperatorKey = '';
Object.keys(filteredState.tokens).forEach((key) => {
filteredState.tokens[key].keys = {
adminKey: '',
pauseKey: '',
supplyKey: '',
wipeKey: '',
feeScheduleKey: '',
treasuryKey: '',
freezeKey: '',
kycKey: '',
};
});
// Remove the private keys from the accounts
Object.keys(filteredState.accounts).forEach((alias) => {
filteredState.accounts[alias].privateKey = '';
});
logger.log('Warning: The private keys were not removed from scripts');
return filteredState;
}
/**
* Create a backup of the state file
*
* @parm name Name of the backup file
* @param backupAccounts Only backup the accounts from state
* @param safe Remove the private keys from the backup file
* @param storagePath Custom path to store the backup (useful for adding it to a testing suite)
*/
function backupState(
name: string,
backupAccounts: boolean,
safe: boolean,
storagePath: string,
) {
let data;
try {
const statePath = path.join(__dirname, '..', 'state', 'state.json');
data = JSON.parse(fs.readFileSync(statePath, 'utf8')) as State;
} catch (error) {
logger.error('Unable to read state file:', error as object);
process.exit(1);
}
// Create backup filename
const timestamp = Date.now(); // UNIX timestamp in milliseconds
let backupFilename = `state.backup.${timestamp}.json`;
if (name) {
backupFilename = `state.backup.${name}.json`;
}
if (safe) {
data = filterState(data);
}
// Only backup accounts if the user specified the --accounts flag
if (backupAccounts) {
backupFilename = `accounts.backup.${timestamp}.json`;
data = data.accounts;
}
if (storagePath !== '' && !path.isAbsolute(storagePath)) {
throw new Error('Invalid storage path: Must be an absolute path');
}
const backupPath =
storagePath !== ''
? path.join(storagePath, backupFilename) // custom path
: path.join(__dirname, '..', 'state', backupFilename); // default path
try {
fs.writeFileSync(backupPath, JSON.stringify(data, null, 2), 'utf8');
logger.log(`Backup created with filename: ${backupFilename}`);
} catch (error) {
logger.error('Unable to create backup file:', error as object);
process.exit(1);
}
}
/**
* Restore a backup of the state file
*
* @param filename File containing the state backup
*/
function restoreState(
filename: string,
restoreAccounts: boolean,
restoreTokens: boolean,
restoreScripts: boolean,
) {
let data;
try {
const backupPath = path.join(__dirname, '..', 'state', filename);
data = JSON.parse(fs.readFileSync(backupPath, 'utf8')) as State;
} catch (error) {
logger.error('Unable to read backup file:', error as object);
process.exit(1);
}
if (!restoreAccounts && !restoreTokens && !restoreScripts) {
stateController.saveState(data);
logger.log('Backup restored successfully');
return;
}
if (restoreAccounts) {
stateController.saveKey('accounts', data.accounts || {});
}
if (restoreTokens) {
stateController.saveKey('tokens', data.tokens || {});
}
if (restoreScripts) {
stateController.saveKey('scripts', data.scripts || {});
}
logger.log('Backup restored successfully');
}
export default (program: any) => {
const network = program.command('backup');
network
.command('create')
.hook('preAction', async (thisCommand: Command) => {
const command = [
thisCommand.parent.action().name(),
...thisCommand.parent.args,
];
if (stateUtils.isTelemetryEnabled()) {
await telemetryUtils.recordCommand(command.join(' '));
}
})
.description('Create a backup of the state.json file')
.option('--accounts', 'Backup the accounts')
.option('--safe', 'Remove the private keys from the backup')
.option('--name <name>', 'Name of the backup file')
.option('--path <path>', 'Specify a custom path to store the backup')
.action((options: BackupOptions) => {
logger.verbose('Creating backup of state');
backupState(
options.name,
options.accounts,
options.safe,
options.path || '',
);
});
network
.command('restore')
.hook('preAction', async (thisCommand: Command) => {
const command = [
thisCommand.parent.action().name(),
...thisCommand.parent.args,
];
if (stateUtils.isTelemetryEnabled()) {
await telemetryUtils.recordCommand(command.join(' '));
}
})
.description('Restore a backup of the full state')
.option('-f, --file <filename>', 'Filename containing the state backup')
.option('--restore-accounts', 'Restore the accounts', false)
.option('--restore-tokens', 'Restore the tokens', false)
.option('--restore-scripts', 'Restore the scripts', false)
.action(async (options: RestoreOptions) => {
logger.verbose('Restoring backup of state');
let filename = options.file;
if (!options.file) {
const files = fs.readdirSync(path.join(__dirname, '..', 'state'));
// filter out the pattern state.backup.TIMESTAMP.json
const pattern = /^state\.backup\.\d+\.json$/;
const backups = files.filter((file) => pattern.test(file));
if (backups.length === 0) {
logger.error('No backup files found');
process.exit(1);
}
try {
filename = await enquirerUtils.createPrompt(
backups,
'Choose a backup:',
);
} catch (error) {
logger.error('Unable to read backup file:', error as object);
process.exit(1);
}
}
restoreState(
filename,
options.restoreAccounts,
options.restoreTokens,
options.restoreScripts,
);
});
};
interface BackupOptions {
accounts: boolean;
safe: boolean;
name: string;
path: string;
}
interface RestoreOptions {
file: string;
restoreAccounts: boolean;
restoreTokens: boolean;
restoreScripts: boolean;
}