UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

512 lines 26.4 kB
/** * 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