UNPKG

@hashgraph/solo

Version:

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

808 lines (807 loc) 61.7 kB
// SPDX-License-Identifier: Apache-2.0 var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var AccountCommand_1; import chalk from 'chalk'; import * as fs from 'node:fs'; import { BaseCommand } from './base.js'; import { IllegalArgumentError } from '../core/errors/illegal-argument-error.js'; import { SoloError } from '../core/errors/solo-error.js'; import { Flags as flags } from './flags.js'; import { Listr } from 'listr2'; import * as constants from '../core/constants.js'; import * as helpers from '../core/helpers.js'; import { entityId } from '../core/helpers.js'; import { AccountId, AccountInfo, Hbar, HbarUnit, Long, NodeUpdateTransaction, PrivateKey, } from '@hiero-ledger/sdk'; import { resolveNamespaceFromDeployment } from '../core/resolvers.js'; import { NamespaceName } from '../types/namespace/namespace-name.js'; import { Templates } from '../core/templates.js'; import { SecretType } from '../integration/kube/resources/secret/secret-type.js'; import { Base64 } from 'js-base64'; import { inject, injectable } from 'tsyringe-neo'; import { InjectTokens } from '../core/dependency-injection/inject-tokens.js'; import { patchInject } from '../core/dependency-injection/container-helper.js'; import { Duration } from '../core/time/duration.js'; import { PREDEFINED_ACCOUNT_GROUPS, predefinedEcdsaAccountsWithAlias, } from './one-shot/predefined-accounts.js'; import { ContainerReference } from '../integration/kube/resources/container/container-reference.js'; import { LedgerPhase } from '../data/schema/model/remote/ledger-phase.js'; import { DeploymentPhase } from '../data/schema/model/remote/deployment-phase.js'; import { container } from 'tsyringe-neo'; import { PvcReference } from '../integration/kube/resources/pvc/pvc-reference.js'; import { PvcName } from '../integration/kube/resources/pvc/pvc-name.js'; import * as CommandHelpers from './command-helpers.js'; import { invokeSoloCommand } from './command-helpers.js'; import { NodeCommandTasks } from './node/tasks.js'; import { ContainerName } from '../integration/kube/resources/container/container-name.js'; import { ConsensusCommandDefinition } from './command-definitions/consensus-command-definition.js'; import { OneShotCommandDefinition } from './command-definitions/one-shot-command-definition.js'; let AccountCommand = class AccountCommand extends BaseCommand { static { AccountCommand_1 = this; } accountManager; systemAccounts; static ACCOUNT_KEY_USER_MESSAGE = 'where:\n' + '- privateKey: the hex-encoded private key which is used to sign transactions with in the Hiero SDKs\n' + '- privateKeyRaw: the Ethereum compatible private key, without the `0x` prefix\n' + '- for more information see: https://docs.hedera.com/hedera/core-concepts/keys-and-signatures'; accountInfo; constructor(accountManager, systemAccounts) { super(); this.accountManager = accountManager; this.systemAccounts = systemAccounts; this.accountManager = patchInject(accountManager, InjectTokens.AccountManager, this.constructor.name); this.accountInfo = undefined; this.systemAccounts = patchInject(systemAccounts, InjectTokens.SystemAccounts, this.constructor.name); } static INIT_FLAGS_LIST = { required: [flags.deployment], optional: [flags.nodeAliasesUnparsed, flags.clusterRef], }; static RESET_FLAGS_LIST = { required: [], optional: [flags.deployment, flags.nodeAliasesUnparsed, flags.clusterRef], }; static CREATE_FLAGS_LIST = { required: [flags.deployment], optional: [ flags.amount, flags.createAmount, flags.ecdsaPrivateKey, flags.privateKey, flags.ed25519PrivateKey, flags.generateEcdsaKey, flags.setAlias, flags.clusterRef, ], }; static UPDATE_FLAGS_LIST = { required: [flags.accountId, flags.deployment], optional: [flags.amount, flags.ecdsaPrivateKey, flags.ed25519PrivateKey, flags.clusterRef], }; static GET_FLAGS_LIST = { required: [flags.accountId, flags.deployment], optional: [flags.privateKey, flags.clusterRef], }; static PREDEFINED_FLAGS_LIST = { required: [flags.deployment], optional: [flags.clusterRef, flags.forcePortForward, flags.cacheDir, flags.devMode, flags.quiet], }; 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 { throw new SoloError(`failed to retrieve EVM address for accountId ${newAccountInfo.accountId}`); } } return newAccountInfo; } async createNewAccount(context_) { if (context_.config.ecdsaPrivateKey) { context_.privateKey = PrivateKey.fromStringECDSA(context_.config.ecdsaPrivateKey); } else if (context_.config.ed25519PrivateKey) { context_.privateKey = PrivateKey.fromStringED25519(context_.config.ed25519PrivateKey); } else if (context_.config.generateEcdsaKey) { context_.privateKey = PrivateKey.generateECDSA(); } else { context_.privateKey = PrivateKey.generateED25519(); } return await this.accountManager.createNewAccount(context_.config.namespace, context_.privateKey, context_.config.amount, context_.config.ecdsaPrivateKey || context_.config.generateEcdsaKey ? context_.config.setAlias : false, context_.config.contextName); } getAccountInfo(context_) { return this.accountManager.accountInfoQuery(context_.config.accountId); } async updateAccountInfo(context_) { let amount = context_.config.amount; if (context_.config.ed25519PrivateKey) { if (!(await this.accountManager.sendAccountKeyUpdate(context_.accountInfo.accountId, context_.config.ed25519PrivateKey, context_.accountInfo.privateKey))) { throw new SoloError(`failed to update account keys for accountId ${context_.accountInfo.accountId}`); } } else { const defaultAmount = flags.amount.definition.defaultValue; amount = amount || defaultAmount; } const hbarAmount = Number.parseFloat(amount.toString()); if (Number.isNaN(hbarAmount)) { throw new SoloError(`The HBAR amount was invalid: ${amount}`); } if (hbarAmount > 0) { const deployment = context_.config.deployment; if (!(await this.transferAmountFromOperator(context_.accountInfo.accountId, hbarAmount, deployment))) { throw new SoloError(`failed to transfer amount for accountId ${context_.accountInfo.accountId}`); } this.logger.debug(`sent transfer amount for account ${context_.accountInfo.accountId}`); } return true; } async transferAmountFromOperator(toAccountId, amount, deploymentName) { const operatorAccountId = this.accountManager.getOperatorAccountId(deploymentName); return await this.accountManager.transferAmount(operatorAccountId, toAccountId, amount); } async init(argv) { const tasks = new Listr([ { title: 'Initialize', task: async (context_, task) => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); this.configManager.update(argv); flags.disablePrompts([flags.clusterRef]); const clusterReference = this.getClusterReference(); const contextName = this.getClusterContext(clusterReference); const config = { deployment: this.configManager.getFlag(flags.deployment), clusterRef: clusterReference, contextName, namespace: await this.resolveNamespaceFromDeployment(task), nodeAliases: helpers.parseNodeAliases(this.configManager.getFlag(flags.nodeAliasesUnparsed), this.remoteConfig.getConsensusNodes(), this.configManager), }; await this.throwIfNamespaceIsMissing(config.contextName, config.namespace); // set config in the context for later tasks to use context_.config = config; await this.accountManager.loadNodeClient(config.namespace, this.remoteConfig.getClusterRefs(), this.configManager.getFlag(flags.deployment), this.configManager.getFlag(flags.forcePortForward)); }, }, { title: 'Update special account keys', task: () => { return new Listr([ { title: 'Prepare for account key updates', task: async (context_) => { const config = context_.config; context_.updateSecrets = await this.k8Factory .getK8(config.contextName) .secrets() .list(config.namespace, ['solo.hedera.com/account-id']) .then((secrets) => secrets.length > 0); context_.accountsBatchedSet = this.accountManager.batchAccounts(this.systemAccounts); context_.resultTracker = { rejectedCount: 0, fulfilledCount: 0, skippedCount: 0, }; // do a write transaction to trigger the handler and generate the system accounts to complete genesis const deployment = config.deployment; const treasuryAccountId = this.accountManager.getTreasuryAccountId(deployment); const freezeAccountId = this.accountManager.getFreezeAccountId(deployment); await this.accountManager.transferAmount(treasuryAccountId, freezeAccountId, 1); }, }, { title: 'Update special account key sets', task: (context_, task) => { const config = context_.config; const subTasks = []; const realm = this.localConfig.configuration.realmForDeployment(config.deployment); const shard = this.localConfig.configuration.shardForDeployment(config.deployment); for (const currentSet of context_.accountsBatchedSet) { const accountStart = entityId(shard, realm, currentSet[0]); const accountEnd = entityId(shard, realm, currentSet.at(-1)); const rangeString = accountStart === accountEnd ? `${chalk.yellow(accountStart)}` : `${chalk.yellow(accountStart)} to ${chalk.yellow(accountEnd)}`; subTasks.push({ title: `Updating accounts [${rangeString}]`, task: async (context_) => { const config = context_.config; context_.resultTracker = await this.accountManager.updateSpecialAccountsKeys(config.namespace, currentSet, context_.updateSecrets, context_.resultTracker, config.deployment); }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: false, rendererOptions: { collapseSubtasks: false, }, }); }, }, { title: 'Update node admin key', task: async ({ config }) => { const adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY); for (const nodeAlias of config.nodeAliases) { const nodeId = Templates.nodeIdFromNodeAlias(nodeAlias); const nodeClient = await this.accountManager.refreshNodeClient(config.namespace, this.remoteConfig.getClusterRefs(), nodeAlias, config.deployment); try { let nodeUpdateTx = new NodeUpdateTransaction().setNodeId(new Long(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); this.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 .getK8(config.contextName) .secrets() .create(config.namespace, Templates.renderNodeAdminKeyName(nodeAlias), SecretType.OPAQUE, data, { 'solo.hedera.com/node-admin-key': 'true' }); } catch (error) { throw new SoloError(`Error updating admin key for node ${nodeAlias}: ${error.message}`, error); } } }, }, { title: 'Display results', task: ({ resultTracker: { fulfilledCount, skippedCount, rejectedCount } }) => { this.logger.showUser(chalk.green(`Account keys updated SUCCESSFULLY: ${fulfilledCount}`)); if (skippedCount > 0) { this.logger.showUser(chalk.cyan(`Account keys updates SKIPPED: ${skippedCount}`)); } if (rejectedCount > 0) { this.logger.showUser(chalk.yellowBright(`Account keys updates with ERROR: ${rejectedCount}`)); } this.logger.showUser(chalk.gray('Waiting for sockets to be closed....')); if (rejectedCount > 0) { throw new SoloError(`Account keys updates failed for ${rejectedCount} accounts.`); } }, }, ], { concurrent: false, rendererOptions: { collapseSubtasks: false, }, }); }, }, ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT); try { await tasks.run(); } catch (error) { throw new SoloError(`Error in creating account: ${error.message}`, error); } finally { await this.closeConnections(); // create two accounts to force the handler to trigger await this.create(argv); await this.create(argv); } return true; } async resetSystem(argv) { const shouldSkipConsensusPodRestart = process.env.SOLO_LEDGER_RESET_SKIP_POD_RESTART !== 'false'; const tasks = new Listr([ { title: 'Identify nodes', task: async (context_, task) => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); this.configManager.update(argv); const deployment = this.configManager.getFlag(flags.deployment) ?? OneShotCommandDefinition.COMMAND_NAME; const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); const nodeAliases = helpers.parseNodeAliases(this.configManager.getFlag(flags.nodeAliasesUnparsed), this.remoteConfig.getConsensusNodes(), this.configManager); const nodeTasks = container.resolve(NodeCommandTasks); const resolvedNodeAliases = nodeAliases.length > 0 ? nodeAliases : await nodeTasks.getExistingNodeAliases(namespace, deployment); if (resolvedNodeAliases.length === 0) { throw new SoloError('No consensus nodes found to reset; check your deployment or --node-aliases input.'); } context_.config = { deployment, namespace, nodeAliases: resolvedNodeAliases, }; this.logger.debug(`context_.config = ${JSON.stringify(context_.config)}`); }, }, { title: 'Stop consensus nodes', task: async (context_, task) => invokeSoloCommand('Stop consensus nodes', `${ConsensusCommandDefinition.COMMAND_NAME} ${ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME} ${ConsensusCommandDefinition.NODE_STOP}`, () => { const commandArgv = CommandHelpers.newArgv(); commandArgv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_STOP, CommandHelpers.optionFromFlag(flags.deployment), context_.config.deployment, CommandHelpers.optionFromFlag(flags.nodeAliasesUnparsed), context_.config.nodeAliases.join(',')); return commandArgv; }, this.taskList).task(context_, task), }, { title: 'Change node state to frozen in remote config', task: async (context_) => { for (const nodeAlias of context_.config.nodeAliases) { this.remoteConfig.configuration.components.changeNodePhase(Templates.renderComponentIdFromNodeAlias(nodeAlias), DeploymentPhase.FROZEN); } await this.remoteConfig.persist(); }, }, { title: 'Scale down block node StatefulSet(s)', skip: () => this.remoteConfig.configuration.state.blockNodes.length === 0, task: async () => { for (const blockNode of this.remoteConfig.configuration.state.blockNodes) { const context = this.remoteConfig.getClusterRefs().get(blockNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for block node ${blockNode.metadata.id}`); } const namespace = blockNode.metadata.namespace.toString(); const statefulSetName = Templates.renderBlockNodeName(blockNode.metadata.id); await this.k8Factory.getK8(context).manifests().scaleStatefulSet(namespace, statefulSetName, 0); } }, }, { title: 'Scale down mirror importer deployment(s)', skip: () => this.remoteConfig.configuration.state.mirrorNodes.length === 0, task: async () => { for (const mirrorNode of this.remoteConfig.configuration.state.mirrorNodes) { const context = this.remoteConfig.getClusterRefs().get(mirrorNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for mirror node ${mirrorNode.metadata.id}`); } const namespaceName = NamespaceName.of(mirrorNode.metadata.namespace); const { mirrorNodeReleaseName } = await this.inferMirrorNodeData(namespaceName, context); const importerDeploymentName = `${mirrorNodeReleaseName}-importer`; await this.k8Factory .getK8(context) .manifests() .scaleDeployment(namespaceName.toString(), importerDeploymentName, 0); } }, }, { title: 'Reset mirror object storage streams', skip: () => this.remoteConfig.configuration.state.mirrorNodes.length === 0, task: async () => { for (const mirrorNode of this.remoteConfig.configuration.state.mirrorNodes) { const context = this.remoteConfig.getClusterRefs().get(mirrorNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for mirror node ${mirrorNode.metadata.id}`); } const namespace = NamespaceName.of(mirrorNode.metadata.namespace); const k8 = this.k8Factory.getK8(context); const minioPods = await k8.pods().list(namespace, ['v1.min.io/tenant=minio']); for (const minioPod of minioPods) { await k8 .containers() .readByRef(ContainerReference.of(minioPod.podReference, ContainerName.of('minio'))) .execContainer(['sh', '-c', 'rm -rf /export/data/solo-streams/*']); } } }, }, { title: 'Truncate mirror postgres data', skip: () => this.remoteConfig.configuration.state.mirrorNodes.length === 0, task: async () => { const truncateSql = fs.readFileSync(constants.MIRROR_POSTGRES_TRUNCATE_SQL_FILE, 'utf8'); for (const mirrorNode of this.remoteConfig.configuration.state.mirrorNodes) { const context = this.remoteConfig.getClusterRefs().get(mirrorNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for mirror node ${mirrorNode.metadata.id}`); } const namespace = NamespaceName.of(mirrorNode.metadata.namespace); const k8 = this.k8Factory.getK8(context); const postgresPods = await k8.pods().list(namespace, [constants.SOLO_MIRROR_POSTGRES_NAME_LABEL]); if (postgresPods.length === 0) { throw new SoloError(`postgres pod not found in namespace ${namespace}`); } const postgresPod = postgresPods[0]; const postgresContainerReference = ContainerReference.of(postgresPod.podReference, ContainerName.of('postgresql')); const mirrorPasswordsSecret = await k8.secrets().read(namespace, 'mirror-passwords'); const ownerKey = Object.keys(mirrorPasswordsSecret.data).find((key) => key.endsWith('_MIRROR_IMPORTER_DB_OWNER')); if (!ownerKey) { throw new SoloError('Could not find MIRROR_IMPORTER_DB_OWNER in mirror-passwords secret.'); } const environmentVariablePrefix = ownerKey.replace('_MIRROR_IMPORTER_DB_OWNER', ''); const databaseOwner = Base64.decode(mirrorPasswordsSecret.data[`${environmentVariablePrefix}_MIRROR_IMPORTER_DB_OWNER`]); const databaseOwnerPassword = Base64.decode(mirrorPasswordsSecret.data[`${environmentVariablePrefix}_MIRROR_IMPORTER_DB_OWNERPASSWORD`]); const databaseName = Base64.decode(mirrorPasswordsSecret.data[`${environmentVariablePrefix}_MIRROR_IMPORTER_DB_NAME`]); await k8 .containers() .readByRef(postgresContainerReference) .execContainer([ 'psql', `postgresql://${databaseOwner}:${databaseOwnerPassword}@localhost:5432/${databaseName}`, '-v', 'ON_ERROR_STOP=1', '-c', truncateSql, ]); } }, }, { title: 'Flush mirror redis cache', skip: () => this.remoteConfig.configuration.state.mirrorNodes.length === 0, task: async () => { for (const mirrorNode of this.remoteConfig.configuration.state.mirrorNodes) { const context = this.remoteConfig.getClusterRefs().get(mirrorNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for mirror node ${mirrorNode.metadata.id}`); } const namespace = NamespaceName.of(mirrorNode.metadata.namespace); const k8 = this.k8Factory.getK8(context); const redisPods = await k8.pods().list(namespace, [constants.SOLO_MIRROR_REDIS_NAME_LABEL]); for (const redisPod of redisPods) { const redisContainerReference = ContainerReference.of(redisPod.podReference, ContainerName.of('redis')); await k8 .containers() .readByRef(redisContainerReference) .execContainer([ 'bash', '-c', // Credentials are read from the mounted secret file or the REDIS_PASSWORD env var // already present inside the container — never passed as a CLI argument. // REDISCLI_AUTH is the env var that redis-cli reads natively, so the password // never appears in the process argument list and is not visible in `ps` output. 'PASSWORD_FILE="${REDIS_PASSWORD_FILE:-/opt/bitnami/redis/secrets/redis-password}"; ' + 'export REDISCLI_AUTH="${REDIS_PASSWORD:-$(cat "$PASSWORD_FILE" 2>/dev/null)}"; ' + 'if [ -z "$REDISCLI_AUTH" ]; then echo "REDIS password not found" >&2; exit 1; fi; ' + 'if command -v redis-cli >/dev/null 2>&1; then ' + ' redis-cli FLUSHALL; ' + 'else ' + ' /opt/bitnami/redis/bin/redis-cli FLUSHALL; ' + 'fi', ]); } } }, }, { title: 'Delete ledger account secrets', task: async (context_) => { for (const [, context] of this.remoteConfig.getClusterRefs()) { const secrets = await this.k8Factory .getK8(context) .secrets() .list(context_.config.namespace, ['solo.hedera.com/account-id']); for (const secret of secrets) { await this.k8Factory.getK8(context).secrets().delete(context_.config.namespace, secret.name); } } }, }, { title: 'Clear consensus node saved state', task: async (context_, task) => { const subTasks = []; this.logger.debug(`context_.config = ${JSON.stringify(context_.config)}`); const nodeAliases = context_.config.nodeAliases; if (!nodeAliases || nodeAliases.length === 0) { throw new SoloError('No consensus nodes found to reset; check your deployment or --node-aliases input.'); } for (const nodeAlias of nodeAliases) { const resolvedContext = this.remoteConfig.extractContextFromConsensusNodes(nodeAlias) ?? this.k8Factory.default().contexts().readCurrent(); const k8 = this.k8Factory.getK8(resolvedContext); const pods = await k8 .pods() .list(context_.config.namespace, [ `solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=network-node', ]); for (const pod of pods) { const containerReference = ContainerReference.of(pod.podReference, constants.ROOT_CONTAINER); subTasks.push({ title: `Node ${nodeAlias}: ${pod.podReference.name}`, task: async () => { await k8 .containers() .readByRef(containerReference) .execContainer([ 'bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/*; ` + 'rm -rf /opt/hgcapp/recordStreams/* /opt/hgcapp/recordStreams/.[!.]* /opt/hgcapp/recordStreams/..?*; ' + 'rm -rf /opt/hgcapp/eventsStreams/* /opt/hgcapp/eventsStreams/.[!.]* /opt/hgcapp/eventsStreams/..?*; ' + 'rm -rf /opt/hgcapp/blockStreams/* /opt/hgcapp/blockStreams/.[!.]* /opt/hgcapp/blockStreams/..?*; ' + `if [ -f ${constants.HEDERA_HAPI_PATH}/data/config/.archive/genesis-network.json ]; then ` + `cp ${constants.HEDERA_HAPI_PATH}/data/config/.archive/genesis-network.json ${constants.HEDERA_HAPI_PATH}/data/config/genesis-network.json; ` + `else echo "ERROR: missing ${constants.HEDERA_HAPI_PATH}/data/config/.archive/genesis-network.json" >&2; exit 1; fi`, ]); }, }); } } return task.newListr(subTasks, constants.LISTR_DEFAULT_OPTIONS.WITH_CONCURRENCY); }, }, { title: 'Optional: recreate consensus node pods and reset persisted state', skip: () => shouldSkipConsensusPodRestart, task: async (context_, task) => { const nodeAliases = context_.config.nodeAliases; const subTasks = nodeAliases.map((nodeAlias) => ({ title: `Recreate ${nodeAlias}`, task: async () => { const resolvedContext = this.remoteConfig.extractContextFromConsensusNodes(nodeAlias) ?? this.k8Factory.default().contexts().readCurrent(); const k8 = this.k8Factory.getK8(resolvedContext); const labels = [ `solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=network-node', ]; const pods = await k8.pods().list(context_.config.namespace, labels); for (const pod of pods) { const podName = pod.podReference.name.toString(); await k8.pods().delete(pod.podReference); // Reset the PVC-backed stream and saved-state storage, but leave stable pod readiness // checks to the later node-start path. That path already waits for a stable ready pod // immediately before exec'ing into it, so only waiting for replacement pod creation // here avoids paying the same 15s settle cost twice. const resetPvcNames = [ `hgcapp-record-streams-pvc-${podName}`, `hgcapp-event-streams-pvc-${podName}`, `hgcapp-blockstream-pvc-${podName}`, `hgcapp-data-saved-pvc-${podName}`, `hgcapp-state-pvc-${podName}`, ]; await Promise.all(resetPvcNames.map(async (pvcName) => { try { await k8.pvcs().delete(PvcReference.of(context_.config.namespace, PvcName.of(pvcName))); } catch (error) { this.logger.debug(`Skipping reset PVC deletion for ${pvcName}: ${error instanceof Error ? error.message : String(error)}`); } })); } await k8.pods().waitForRunningPhase(context_.config.namespace, labels, 120, 1000); }, })); return task.newListr(subTasks, constants.LISTR_DEFAULT_OPTIONS.WITH_CONCURRENCY); }, }, { title: 'Reset block node PVCs', skip: () => this.remoteConfig.configuration.state.blockNodes.length === 0, task: async () => { for (const blockNode of this.remoteConfig.configuration.state.blockNodes) { const context = this.remoteConfig.getClusterRefs().get(blockNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for block node ${blockNode.metadata.id}`); } const releaseName = Templates.renderBlockNodeName(blockNode.metadata.id); const pvcs = await this.k8Factory .getK8(context) .pvcs() .list(NamespaceName.of(blockNode.metadata.namespace), [`app.kubernetes.io/instance=${releaseName}`]); for (const pvc of pvcs) { await this.k8Factory .getK8(context) .pvcs() .delete(PvcReference.of(NamespaceName.of(blockNode.metadata.namespace), PvcName.of(pvc))); } } }, }, { title: 'Reset ledger phase to uninitialized', task: async () => { this.remoteConfig.configuration.state.ledgerPhase = LedgerPhase.UNINITIALIZED; await this.remoteConfig.persist(); }, }, { title: 'Bring services back online', task: async (_context_, task) => task.newListr([ { title: 'Scale up block node StatefulSet(s)', skip: () => this.remoteConfig.configuration.state.blockNodes.length === 0, task: async () => { for (const blockNode of this.remoteConfig.configuration.state.blockNodes) { const context = this.remoteConfig .getClusterRefs() .get(blockNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for block node ${blockNode.metadata.id}`); } const namespace = blockNode.metadata.namespace.toString(); const statefulSetName = Templates.renderBlockNodeName(blockNode.metadata.id); await this.k8Factory.getK8(context).manifests().scaleStatefulSet(namespace, statefulSetName, 1); } }, }, { title: 'Scale up mirror importer deployment(s)', skip: () => this.remoteConfig.configuration.state.mirrorNodes.length === 0, task: async () => { for (const mirrorNode of this.remoteConfig.configuration.state.mirrorNodes) { const context = this.remoteConfig .getClusterRefs() .get(mirrorNode.metadata.cluster); if (!context) { throw new SoloError(`No cluster context found for mirror node ${mirrorNode.metadata.id}`); } const namespaceName = NamespaceName.of(mirrorNode.metadata.namespace); const { mirrorNodeReleaseName } = await this.inferMirrorNodeData(namespaceName, context); const importerDeploymentName = `${mirrorNodeReleaseName}-importer`; const k8 = this.k8Factory.getK8(context); await k8.manifests().scaleDeployment(namespaceName.toString(), importerDeploymentName, 1); await k8 .pods() .waitForReadyStatus(namespaceName, [ 'app.kubernetes.io/name=importer', 'app.kubernetes.io/component=importer', `app.kubernetes.io/instance=${mirrorNodeReleaseName}`, ], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY); } }, }, { title: 'Start consensus node services', task: async (context_, task) => { const nodeAliases = context_.config.nodeAliases; if (!nodeAliases || nodeAliases.length === 0) { throw new SoloError('No consensus nodes found to start; check your deployment or --node-aliases input.'); } return invokeSoloCommand('Start consensus nodes', ConsensusCommandDefinition.START_COMMAND, () => { const argv = CommandHelpers.newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_START, CommandHelpers.optionFromFlag(flags.deployment), context_.config.deployment, CommandHelpers.optionFromFlag(flags.nodeAliasesUnparsed), nodeAliases.join(',')); return argv; }, this.taskList).task(context_, task); }, }, ], constants.LISTR_DEFAULT_OPTIONS.WITH_CONCURRENCY), }, ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT); await tasks.run(); return true; } async create(argv) { const tasks = new Listr([ { title: 'Initialize', task: async (context_, task) => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); this.configManager.update(argv); flags.disablePrompts([flags.clusterRef]); const config = { amount: this.configManager.getFlag(flags.amount), ecdsaPrivateKey: this.configManager.getFlag(flags.ecdsaPrivateKey), namespace: await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task), deployment: this.configManager.getFlag(flags.deployment), ed25519PrivateKey: this.configManager.getFlag(flags.ed25519PrivateKey), setAlias: this.configManager.getFlag(flags.setAlias), generateEcdsaKey: this.configManager.getFlag(flags.generateEcdsaKey), privateKey: this.configManager.getFlag(flags.privateKey), createAmount: this.configManager.getFlag(flags.createAmount), clusterRef: this.configManager.getFlag(flags.clusterRef), }; config.contextName = this.localConfig.configuration.clusterRefs.get(config.clusterRef)?.toString() ?? this.k8Factory.default().contexts().readCurrent(); if (!config.amount) { config.amount = flags.amount.definition.defaultValue; } if (!(await this.k8Factory.getK8(config.contextName).namespaces().has(config.namespace))) { throw new SoloError(`namespace ${config.namespace} does not exist`); } // set config in the context for later tasks to use context_.config = config; await this.accountManager.loadNodeClient(context_.config.namespace, this.remoteConfig.getClusterRefs(), config.deployment, this.configManager.getFlag(flags.forcePortForward)); }, }, { title: 'create the new account', task: async (context_, task) => { const subTasks = []; for (let index = 0; index < context_.config.createAmount; index++) { subTasks.push({ title: `Create accounts [${index}]`, task: async (context_) => { this.accountInfo = await this.createNewAccount(context_); const accountInfoCopy = { ...this.accountInfo }; if (!context_.config.privateKey) { delete accountInfoCopy.privateKey; } this.logger.showJSON('new account created', accountInfoCopy); if (context_.config.privateKey) { this.logger.showUser(AccountCommand_1.ACCOUNT_KEY_USER_MESSAGE); } }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: 8, rendererOptions: { collapseSubtasks: false, }, }); }, }, ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT); try { await tasks.run(); } catch (error) { throw new SoloError(`Error in creating account: ${error.message}`, error); } finally { await this.closeConnections(); } return true; } async update(argv) { const tasks = new Listr([ { title: 'Initialize', task: async (context_, task) => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); this.configManager.update(argv); flags.disablePrompts([flags.clusterRef]); await this.configManager.executePrompt(task, [flags.accountId]); const config = { accountId: this.configManager.getFlag(flags.accountId), amount: this.configManager.getFlag(flags.amount), namespace: await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task), deployment: this.configManager.getFlag(flags.deployment), ecdsaPrivateKey: this.configManager.getFlag(flags.ecdsaPrivateKey), ed25519PrivateKey: this.configManager.getFlag(flags.ed25519PrivateKey), clusterRef: this.configManager.getFlag(flags.clusterRef), contextName: '', }; config.contextName = this.localConfig.configuration.clusterRefs.get(config.clusterRef)?.toString() ?? this.k8Factory.default().contexts().readCurrent(); if (!(await this.k8Factory.getK8(config.contextName).namespaces().has(config.namespace))) { throw new SoloError(`namespace ${config.namespace} does not exist`); } // set config in the context for later tasks to use context_.config = config; await this.accountManager.loadNodeClient(config.namespace, this.remoteConfig.getClusterRefs(), config.deployment, this.configManager.getFlag(flags.forcePortForward)); }, }, { title: 'get the account info', task: async (context_) => { context_.accountInfo = await this.buildAccountInfo(await this.getAccountInfo(context_), context_.config.namespace, !!context_.config.ed25519PrivateKey);