@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
512 lines • 26.4 kB
JavaScript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import chalk from 'chalk';
import { BaseCommand } from './base.js';
import { IllegalArgumentError, SoloError } from '../core/errors.js';
import { Flags as flags } from './flags.js';
import { Listr } from 'listr2';
import * as constants from '../core/constants.js';
import { FREEZE_ADMIN_ACCOUNT } from '../core/constants.js';
import * as helpers from '../core/helpers.js';
import { sleep } from '../core/helpers.js';
import { AccountInfo, HbarUnit, NodeUpdateTransaction, PrivateKey } from '@hashgraph/sdk';
import { ListrLease } from '../core/lease/listr_lease.js';
import { resolveNamespaceFromDeployment } from '../core/resolvers.js';
import { Duration } from '../core/time/duration.js';
import { Templates } from '../core/templates.js';
import { SecretType } from '../core/kube/resources/secret/secret_type.js';
import { Base64 } from 'js-base64';
export class AccountCommand extends BaseCommand {
accountManager;
accountInfo;
systemAccounts;
constructor(opts, systemAccounts = constants.SYSTEM_ACCOUNTS) {
super(opts);
if (!opts || !opts.accountManager)
throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager);
this.accountManager = opts.accountManager;
this.accountInfo = null;
this.systemAccounts = systemAccounts;
}
async closeConnections() {
await this.accountManager.close();
}
async buildAccountInfo(accountInfo, namespace, shouldRetrievePrivateKey) {
if (!accountInfo || !(accountInfo instanceof AccountInfo))
throw new IllegalArgumentError('An instance of AccountInfo is required');
const newAccountInfo = {
accountId: accountInfo.accountId.toString(),
publicKey: accountInfo.key.toString(),
balance: accountInfo.balance.to(HbarUnit.Hbar).toNumber(),
};
if (shouldRetrievePrivateKey) {
const accountKeys = await this.accountManager.getAccountKeysFromSecret(newAccountInfo.accountId, namespace);
newAccountInfo.privateKey = accountKeys.privateKey;
// reconstruct private key to retrieve EVM address if private key is ECDSA type
try {
const privateKey = PrivateKey.fromStringDer(newAccountInfo.privateKey);
newAccountInfo.privateKeyRaw = privateKey.toStringRaw();
}
catch (e) {
this.logger.error(`failed to retrieve EVM address for accountId ${newAccountInfo.accountId}`);
}
}
return newAccountInfo;
}
async createNewAccount(ctx) {
if (ctx.config.ecdsaPrivateKey) {
ctx.privateKey = PrivateKey.fromStringECDSA(ctx.config.ecdsaPrivateKey);
}
else if (ctx.config.ed25519PrivateKey) {
ctx.privateKey = PrivateKey.fromStringED25519(ctx.config.ed25519PrivateKey);
}
else if (ctx.config.generateEcdsaKey) {
ctx.privateKey = PrivateKey.generateECDSA();
}
else {
ctx.privateKey = PrivateKey.generateED25519();
}
return await this.accountManager.createNewAccount(ctx.config.namespace, ctx.privateKey, ctx.config.amount, ctx.config.ecdsaPrivateKey || ctx.config.generateEcdsaKey ? ctx.config.setAlias : false);
}
getAccountInfo(ctx) {
return this.accountManager.accountInfoQuery(ctx.config.accountId);
}
async updateAccountInfo(ctx) {
let amount = ctx.config.amount;
if (ctx.config.ed25519PrivateKey) {
if (!(await this.accountManager.sendAccountKeyUpdate(ctx.accountInfo.accountId, ctx.config.ed25519PrivateKey, ctx.accountInfo.privateKey))) {
this.logger.error(`failed to update account keys for accountId ${ctx.accountInfo.accountId}`);
return false;
}
}
else {
amount = amount || flags.amount.definition.defaultValue;
}
const hbarAmount = Number.parseFloat(amount);
if (amount && isNaN(hbarAmount)) {
throw new SoloError(`The HBAR amount was invalid: ${amount}`);
}
if (hbarAmount > 0) {
if (!(await this.transferAmountFromOperator(ctx.accountInfo.accountId, hbarAmount))) {
this.logger.error(`failed to transfer amount for accountId ${ctx.accountInfo.accountId}`);
return false;
}
this.logger.debug(`sent transfer amount for account ${ctx.accountInfo.accountId}`);
}
return true;
}
async transferAmountFromOperator(toAccountId, amount) {
return await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, toAccountId, amount);
}
async init(argv) {
const self = this;
const tasks = new Listr([
{
title: 'Initialize',
task: async (ctx, task) => {
self.configManager.update(argv);
const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
if (!(await this.k8Factory.default().namespaces().has(namespace))) {
throw new SoloError(`namespace ${namespace.name} does not exist`);
}
ctx.config = {
namespace: namespace,
nodeAliases: helpers.parseNodeAliases(this.configManager.getFlag(flags.nodeAliasesUnparsed)),
};
self.logger.debug('Initialized config', ctx.config);
await self.accountManager.loadNodeClient(ctx.config.namespace, self.getClusterRefs(), self.configManager.getFlag(flags.deployment), self.configManager.getFlag(flags.forcePortForward));
},
},
{
title: 'Update special account keys',
task: (_, task) => {
return new Listr([
{
title: 'Prepare for account key updates',
task: async (ctx) => {
const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
const secrets = await self.k8Factory
.default()
.secrets()
.list(namespace, ['solo.hedera.com/account-id']);
ctx.updateSecrets = secrets.length > 0;
ctx.accountsBatchedSet = self.accountManager.batchAccounts(this.systemAccounts);
ctx.resultTracker = {
rejectedCount: 0,
fulfilledCount: 0,
skippedCount: 0,
};
// do a write transaction to trigger the handler and generate the system accounts to complete genesis
await self.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 1);
},
},
{
title: 'Update special account key sets',
task: ctx => {
const subTasks = [];
const realm = constants.HEDERA_NODE_ACCOUNT_ID_START.realm;
const shard = constants.HEDERA_NODE_ACCOUNT_ID_START.shard;
for (const currentSet of ctx.accountsBatchedSet) {
const accStart = `${realm}.${shard}.${currentSet[0]}`;
const accEnd = `${realm}.${shard}.${currentSet[currentSet.length - 1]}`;
const rangeStr = accStart !== accEnd
? `${chalk.yellow(accStart)} to ${chalk.yellow(accEnd)}`
: `${chalk.yellow(accStart)}`;
subTasks.push({
title: `Updating accounts [${rangeStr}]`,
task: async (ctx) => {
ctx.resultTracker = await self.accountManager.updateSpecialAccountsKeys(ctx.config.namespace, currentSet, ctx.updateSecrets, ctx.resultTracker);
},
});
}
// set up the sub-tasks
return task.newListr(subTasks, {
concurrent: false,
rendererOptions: {
collapseSubtasks: false,
},
});
},
},
{
title: 'Update node admin key',
task: async (ctx) => {
const adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY);
for (const nodeAlias of ctx.config.nodeAliases) {
const nodeId = Templates.nodeIdFromNodeAlias(nodeAlias);
const nodeClient = await self.accountManager.refreshNodeClient(ctx.config.namespace, nodeAlias, self.getClusterRefs(), this.configManager.getFlag(flags.deployment));
try {
let nodeUpdateTx = new NodeUpdateTransaction().setNodeId(nodeId);
const newPrivateKey = PrivateKey.generateED25519();
nodeUpdateTx = nodeUpdateTx.setAdminKey(newPrivateKey.publicKey);
nodeUpdateTx = nodeUpdateTx.freezeWith(nodeClient);
nodeUpdateTx = await nodeUpdateTx.sign(newPrivateKey);
const signedTx = await nodeUpdateTx.sign(adminKey);
const txResp = await signedTx.execute(nodeClient);
const nodeUpdateReceipt = await txResp.getReceipt(nodeClient);
self.logger.debug(`NodeUpdateReceipt: ${nodeUpdateReceipt.toString()} for node ${nodeAlias}`);
// save new key in k8s secret
const data = {
privateKey: Base64.encode(newPrivateKey.toString()),
publicKey: Base64.encode(newPrivateKey.publicKey.toString()),
};
await this.k8Factory
.default()
.secrets()
.create(ctx.config.namespace, Templates.renderNodeAdminKeyName(nodeAlias), SecretType.OPAQUE, data, {
'solo.hedera.com/node-admin-key': 'true',
});
}
catch (e) {
throw new SoloError(`Error updating admin key for node ${nodeAlias}: ${e.message}`, e);
}
}
},
},
{
title: 'Display results',
task: ctx => {
self.logger.showUser(chalk.green(`Account keys updated SUCCESSFULLY: ${ctx.resultTracker.fulfilledCount}`));
if (ctx.resultTracker.skippedCount > 0)
self.logger.showUser(chalk.cyan(`Account keys updates SKIPPED: ${ctx.resultTracker.skippedCount}`));
if (ctx.resultTracker.rejectedCount > 0) {
self.logger.showUser(chalk.yellowBright(`Account keys updates with ERROR: ${ctx.resultTracker.rejectedCount}`));
}
self.logger.showUser(chalk.gray('Waiting for sockets to be closed....'));
if (ctx.resultTracker.rejectedCount > 0) {
throw new SoloError(`Account keys updates failed for ${ctx.resultTracker.rejectedCount} accounts.`);
}
},
},
], {
concurrent: false,
rendererOptions: {
collapseSubtasks: false,
},
});
},
},
], {
concurrent: false,
rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
});
try {
await tasks.run();
}
catch (e) {
throw new SoloError(`Error in creating account: ${e.message}`, e);
}
finally {
await this.closeConnections();
// create two accounts to force the handler to trigger
await self.create({});
await self.create({});
}
return true;
}
async create(argv) {
const self = this;
const lease = await self.leaseManager.create();
const tasks = new Listr([
{
title: 'Initialize',
task: async (ctx, task) => {
self.configManager.update(argv);
const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
const config = {
amount: self.configManager.getFlag(flags.amount),
ecdsaPrivateKey: self.configManager.getFlag(flags.ecdsaPrivateKey),
namespace: namespace,
ed25519PrivateKey: self.configManager.getFlag(flags.ed25519PrivateKey),
setAlias: self.configManager.getFlag(flags.setAlias),
generateEcdsaKey: self.configManager.getFlag(flags.generateEcdsaKey),
createAmount: self.configManager.getFlag(flags.createAmount),
};
if (!config.amount) {
config.amount = flags.amount.definition.defaultValue;
}
if (!(await this.k8Factory.default().namespaces().has(config.namespace))) {
throw new SoloError(`namespace ${config.namespace} does not exist`);
}
// set config in the context for later tasks to use
ctx.config = config;
self.logger.debug('Initialized config', { config });
await self.accountManager.loadNodeClient(ctx.config.namespace, self.getClusterRefs(), self.configManager.getFlag(flags.deployment), self.configManager.getFlag(flags.forcePortForward));
return ListrLease.newAcquireLeaseTask(lease, task);
},
},
{
title: 'create the new account',
task: async (ctx) => {
for (let i = 0; i < ctx.config.createAmount; i++) {
self.accountInfo = await self.createNewAccount(ctx);
const accountInfoCopy = { ...self.accountInfo };
delete accountInfoCopy.privateKey;
this.logger.showJSON('new account created', accountInfoCopy);
if (ctx.config.createAmount > 0) {
await sleep(Duration.ofSeconds(1));
}
}
},
},
], {
concurrent: false,
rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
});
try {
await tasks.run();
}
catch (e) {
throw new SoloError(`Error in creating account: ${e.message}`, e);
}
finally {
await lease.release();
await this.closeConnections();
}
return true;
}
async update(argv) {
const self = this;
const tasks = new Listr([
{
title: 'Initialize',
task: async (ctx, task) => {
self.configManager.update(argv);
await self.configManager.executePrompt(task, [flags.accountId]);
const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
const config = {
accountId: self.configManager.getFlag(flags.accountId),
amount: self.configManager.getFlag(flags.amount),
namespace: namespace,
ecdsaPrivateKey: self.configManager.getFlag(flags.ecdsaPrivateKey),
ed25519PrivateKey: self.configManager.getFlag(flags.ed25519PrivateKey),
};
if (!(await this.k8Factory.default().namespaces().has(config.namespace))) {
throw new SoloError(`namespace ${config.namespace} does not exist`);
}
// set config in the context for later tasks to use
ctx.config = config;
await self.accountManager.loadNodeClient(config.namespace, self.getClusterRefs(), self.configManager.getFlag(flags.deployment), self.configManager.getFlag(flags.forcePortForward));
self.logger.debug('Initialized config', { config });
},
},
{
title: 'get the account info',
task: async (ctx) => {
ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, !!ctx.config.ed25519PrivateKey);
},
},
{
title: 'update the account',
task: async (ctx) => {
if (!(await self.updateAccountInfo(ctx))) {
throw new SoloError(`An error occurred updating account ${ctx.accountInfo.accountId}`);
}
},
},
{
title: 'get the updated account info',
task: async (ctx) => {
self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, false);
this.logger.showJSON('account info', self.accountInfo);
},
},
], {
concurrent: false,
rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
});
try {
await tasks.run();
}
catch (e) {
throw new SoloError(`Error in updating account: ${e.message}`, e);
}
finally {
await this.closeConnections();
}
return true;
}
async get(argv) {
const self = this;
// @ts-ignore
const tasks = new Listr([
{
title: 'Initialize',
task: async (ctx, task) => {
self.configManager.update(argv);
await self.configManager.executePrompt(task, [flags.accountId]);
const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
const config = {
accountId: self.configManager.getFlag(flags.accountId),
namespace: namespace,
privateKey: self.configManager.getFlag(flags.privateKey),
};
if (!(await this.k8Factory.default().namespaces().has(config.namespace))) {
throw new SoloError(`namespace ${config.namespace} does not exist`);
}
// set config in the context for later tasks to use
ctx.config = config;
await self.accountManager.loadNodeClient(config.namespace, self.getClusterRefs(), self.configManager.getFlag(flags.deployment), self.configManager.getFlag(flags.forcePortForward));
self.logger.debug('Initialized config', { config });
},
},
{
title: 'get the account info',
task: async (ctx) => {
self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.privateKey);
this.logger.showJSON('account info', self.accountInfo);
},
},
], {
concurrent: false,
rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
});
try {
await tasks.run();
}
catch (e) {
throw new SoloError(`Error in getting account info: ${e.message}`, e);
}
finally {
await this.closeConnections();
}
return true;
}
/** Return Yargs command definition for 'node' command */
getCommandDefinition() {
const self = this;
return {
command: 'account',
desc: 'Manage Hedera accounts in solo network',
builder: (yargs) => {
return yargs
.command({
command: 'init',
desc: 'Initialize system accounts with new keys',
builder: (y) => flags.setCommandFlags(y, flags.deployment, flags.nodeAliasesUnparsed),
handler: (argv) => {
self.logger.info("==== Running 'account init' ===");
self.logger.info(argv);
self
.init(argv)
.then(r => {
self.logger.info("==== Finished running 'account init' ===");
if (!r)
process.exit(1);
})
.catch(err => {
self.logger.showUserError(err);
process.exit(1);
});
},
})
.command({
command: 'create',
desc: 'Creates a new account with a new key and stores the key in the Kubernetes secrets, if you supply no key one will be generated for you, otherwise you may supply either a ECDSA or ED25519 private key',
builder: (y) => flags.setCommandFlags(y, flags.amount, flags.createAmount, flags.ecdsaPrivateKey, flags.deployment, flags.ed25519PrivateKey, flags.generateEcdsaKey, flags.setAlias),
handler: (argv) => {
self.logger.info("==== Running 'account create' ===");
self.logger.info(argv);
self
.create(argv)
.then(r => {
self.logger.info("==== Finished running 'account create' ===");
if (!r)
process.exit(1);
})
.catch(err => {
self.logger.showUserError(err);
process.exit(1);
});
},
})
.command({
command: 'update',
desc: 'Updates an existing account with the provided info, if you want to update the private key, you can supply either ECDSA or ED25519 but not both\n',
builder: (y) => flags.setCommandFlags(y, flags.accountId, flags.amount, flags.deployment, flags.ecdsaPrivateKey, flags.ed25519PrivateKey),
handler: (argv) => {
self.logger.info("==== Running 'account update' ===");
self.logger.info(argv);
self
.update(argv)
.then(r => {
self.logger.info("==== Finished running 'account update' ===");
if (!r)
process.exit(1);
})
.catch(err => {
self.logger.showUserError(err);
process.exit(1);
});
},
})
.command({
command: 'get',
desc: 'Gets the account info including the current amount of HBAR',
builder: (y) => flags.setCommandFlags(y, flags.accountId, flags.privateKey, flags.deployment),
handler: (argv) => {
self.logger.info("==== Running 'account get' ===");
self.logger.info(argv);
self
.get(argv)
.then(r => {
self.logger.info("==== Finished running 'account get' ===");
if (!r)
process.exit(1);
})
.catch(err => {
self.logger.showUserError(err);
process.exit(1);
});
},
})
.demandCommand(1, 'Select an account command');
},
};
}
close() {
return this.closeConnections();
}
}
//# sourceMappingURL=account.js.map