@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
808 lines (807 loc) • 61.7 kB
JavaScript
// 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);