From a718277e299aaf8bcc56a581c0abffa153e4b8c8 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Feb 2024 21:58:56 +0000 Subject: [PATCH 01/10] solo account create Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 220 ++++++++++++++++++++++++ solo/src/commands/flags.mjs | 52 +++++- solo/src/commands/index.mjs | 5 +- solo/src/commands/prompts.mjs | 67 ++++++-- solo/src/core/account_manager.mjs | 45 ++++- solo/src/core/constants.mjs | 4 +- solo/test/e2e/commands/account.test.mjs | 88 ++++++++++ solo/test/test_util.js | 7 +- 8 files changed, 467 insertions(+), 21 deletions(-) create mode 100644 solo/src/commands/account.mjs create mode 100644 solo/test/e2e/commands/account.test.mjs diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs new file mode 100644 index 000000000..4e2b12b6b --- /dev/null +++ b/solo/src/commands/account.mjs @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { BaseCommand } from './base.mjs' +import { FullstackTestingError, IllegalArgumentError } from '../core/errors.mjs' +import { flags } from './index.mjs' +import { Listr } from 'listr2' +import * as prompts from './prompts.mjs' +import { constants } from '../core/index.mjs' +import chalk from 'chalk' +import { sleep } from '../core/helpers.mjs' +import { PrivateKey } from '@hashgraph/sdk' + +export class AccountCommand extends BaseCommand { + constructor (opts) { + super(opts) + + if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager) + + this.accountManager = opts.accountManager + } + + async create (argv) { + const self = this + + const tasks = new Listr([ + { + title: 'Initialize', + task: async (ctx, task) => { + self.configManager.load(argv) + await prompts.execute(task, self.configManager, [ + flags.namespace + ]) + + const config = { + namespace: self.configManager.getFlag(flags.namespace), + privateKey: self.configManager.getFlag(flags.privateKey), + amount: self.configManager.getFlag(flags.amount), + stdout: self.configManager.getFlag(flags.stdout) + } + + if (!await this.k8.hasNamespace(config.namespace)) { + throw new FullstackTestingError(`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.loadTreasuryAccount(ctx) + await self.loadNodeClient(ctx) + } + }, + { + title: 'create the new account', + task: async (ctx, task) => { + await self.createNewAccount(ctx) + } + } + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }) + + try { + await tasks.run() + } catch (e) { + throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) + } finally { + if (this.nodeClient) { + this.nodeClient.close() + await sleep(5) // sleep a couple of ticks for connections to close + } + await this.accountManager.stopPortForwards() + await sleep(5) // sleep a couple of ticks for connections to close + } + + return true + } + + async createNewAccount (ctx) { + if (ctx.config.privateKey) { + ctx.privateKey = PrivateKey.fromStringED25519(ctx.config.privateKey) + } else { + ctx.privateKey = PrivateKey.generateED25519() + } + + const accountInfo = await this.accountManager.createNewAccount(ctx.config.namespace, + ctx.nodeClient, ctx.privateKey, ctx.config.amount) + + const userMessagePrefix = `new account created [accountId=${accountInfo.accountId}, amount=${ctx.config.amount} HBAR, publicKey=${accountInfo.publicKey}` + const userMessagePostfix = ctx.config.stdout ? `, privateKey=${accountInfo.privateKey}]` : ']' + + this.logger.showUser(chalk.cyan(userMessagePrefix + userMessagePostfix)) + } + + async loadNodeClient (ctx) { + const serviceMap = await this.accountManager.getNodeServiceMap(ctx.config.namespace) + + ctx.nodeClient = await this.accountManager.getNodeClient(ctx.config.namespace, + serviceMap, ctx.treasuryAccountId, ctx.treasuryPrivateKey) + this.nodeClient = ctx.nodeClient // store in class so that we can make sure connections are closed + } + + async loadTreasuryAccount (ctx) { + ctx.treasuryAccountId = constants.TREASURY_ACCOUNT_ID + // check to see if the treasure account is in the secrets + const accountInfo = await this.accountManager.getAccountKeysFromSecret(ctx.treasuryAccountId, ctx.config.namespace) + + // if it isn't in the secrets we can load genesis key + if (accountInfo) { + ctx.treasuryPrivateKey = accountInfo.privateKey + } else { + ctx.treasuryPrivateKey = constants.GENESIS_KEY + } + } + + async update (argv) { + const self = this + } + + async get (argv) { + const self = this + } + + /** + * Return Yargs command definition for 'node' command + * @param accountCmd an instance of NodeCommand + */ + static getCommandDefinition (accountCmd) { + return { + command: 'account', + desc: 'Manage Hedera accounts in fullstack testing network', + builder: yargs => { + return yargs + .command({ + command: 'create', + desc: 'Creates a new account with a new key and stores the key in the Kubernetes secrets', + builder: y => flags.setCommandFlags(y, + flags.namespace, + flags.privateKey, + flags.amount, + flags.stdout + ), + handler: argv => { + accountCmd.logger.debug("==== Running 'account create' ===") + accountCmd.logger.debug(argv) + + accountCmd.create(argv).then(r => { + accountCmd.logger.debug("==== Finished running 'account create' ===") + if (!r) process.exit(1) + }).catch(err => { + accountCmd.logger.showUserError(err) + process.exit(1) + }) + } + }) + .command({ + command: 'update', + desc: 'Updates an existing account with the provided info\n', + builder: y => flags.setCommandFlags(y, + flags.namespace, + flags.accountId, + flags.newPrivateKey, + flags.amount, + flags.stdout + ), + handler: argv => { + accountCmd.logger.debug("==== Running 'account update' ===") + accountCmd.logger.debug(argv) + + accountCmd.update(argv).then(r => { + accountCmd.logger.debug("==== Finished running 'account update' ===") + if (!r) process.exit(1) + }).catch(err => { + accountCmd.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.namespace, + flags.accountId, + flags.privateKey + ), + handler: argv => { + accountCmd.logger.debug("==== Running 'account get' ===") + accountCmd.logger.debug(argv) + + accountCmd.get(argv).then(r => { + accountCmd.logger.debug("==== Finished running 'account get' ===") + if (!r) process.exit(1) + }).catch(err => { + accountCmd.logger.showUserError(err) + process.exit(1) + }) + } + }) + .demandCommand(1, 'Select an account command') + } + } + } +} diff --git a/solo/src/commands/flags.mjs b/solo/src/commands/flags.mjs index 781641702..f9c862314 100644 --- a/solo/src/commands/flags.mjs +++ b/solo/src/commands/flags.mjs @@ -388,6 +388,51 @@ export const updateAccountKeys = { } } +export const privateKey = { + name: 'private-key', + definition: { + describe: 'private key for the Hedera account', + defaultValue: '', + type: 'string' + } +} + +export const accountId = { + name: 'account-id', + definition: { + describe: 'The Hedera account id, e.g.: 0.0.1001', + defaultValue: '', + type: 'string' + } +} + +export const newPrivateKey = { + name: 'new-private-key', + definition: { + describe: 'Private key to assign to the Hedera account', + defaultValue: '', + type: 'string' + } +} + +export const amount = { + name: 'amount', + definition: { + describe: 'Amount of HBAR to add', + defaultValue: 100, + type: 'number' + } +} + +export const stdout = { + name: 'stdout', + definition: { + describe: 'Send the account keys to stdout for the user', + defaultValue: false, + type: 'boolean' + } +} + export const allFlags = [ devMode, clusterName, @@ -425,7 +470,12 @@ export const allFlags = [ bootstrapProperties, settingTxt, log4j2Xml, - updateAccountKeys + updateAccountKeys, + privateKey, + accountId, + newPrivateKey, + amount, + stdout ] export const allFlagsMap = new Map(allFlags.map(f => [f.name, f])) diff --git a/solo/src/commands/index.mjs b/solo/src/commands/index.mjs index 8682951cf..c6028e486 100644 --- a/solo/src/commands/index.mjs +++ b/solo/src/commands/index.mjs @@ -19,6 +19,7 @@ import { InitCommand } from './init.mjs' import { NetworkCommand } from './network.mjs' import { NodeCommand } from './node.mjs' import { RelayCommand } from './relay.mjs' +import { AccountCommand } from './account.mjs' import * as flags from './flags.mjs' /* @@ -31,13 +32,15 @@ function Initialize (opts) { const networkCommand = new NetworkCommand(opts) const nodeCmd = new NodeCommand(opts) const relayCmd = new RelayCommand(opts) + const accountCmd = new AccountCommand(opts) return [ InitCommand.getCommandDefinition(initCmd), ClusterCommand.getCommandDefinition(clusterCmd), NetworkCommand.getCommandDefinition(networkCommand), NodeCommand.getCommandDefinition(nodeCmd), - RelayCommand.getCommandDefinition(relayCmd) + RelayCommand.getCommandDefinition(relayCmd), + AccountCommand.getCommandDefinition(accountCmd) ] } diff --git a/solo/src/commands/prompts.mjs b/solo/src/commands/prompts.mjs index 56e487b75..042bcf2e6 100644 --- a/solo/src/commands/prompts.mjs +++ b/solo/src/commands/prompts.mjs @@ -23,7 +23,9 @@ import * as helpers from '../core/helpers.mjs' async function prompt (type, task, input, defaultValue, promptMessage, emptyCheckMessage, flagName) { try { - const needsPrompt = type === 'toggle' ? (input === undefined || typeof input !== 'boolean') : !input + let needsPrompt = type === 'toggle' ? (input === undefined || typeof input !== 'boolean') : !input + needsPrompt = type === 'number' ? typeof input !== 'number' : needsPrompt + if (needsPrompt) { input = await task.prompt(ListrEnquirerPromptAdapter).run({ type, @@ -262,19 +264,11 @@ export async function promptOperatorKey (task, input) { } export async function promptReplicaCount (task, input) { - try { - if (typeof input !== 'number') { - input = await task.prompt(ListrEnquirerPromptAdapter).run({ - type: 'number', - default: flags.replicaCount.definition.defaultValue, - message: 'How many replica do you want?' - }) - } - - return input - } catch (e) { - throw new FullstackTestingError(`input failed: ${flags.replicaCount.name}`, e) - } + return await prompt('number', task, input, + flags.replicaCount.definition.defaultValue, + 'How many replica do you want? ', + null, + flags.replicaCount.name) } export async function promptGenerateGossipKeys (task, input) { @@ -341,6 +335,46 @@ export async function promptUpdateAccountKeys (task, input) { flags.updateAccountKeys.name) } +export async function promptPrivateKey (task, input) { + return await promptText(task, input, + flags.privateKey.definition.defaultValue, + 'Enter the private key: ', + null, + flags.privateKey.name) +} + +export async function promptAccountId (task, input) { + return await promptText(task, input, + flags.accountId.definition.defaultValue, + 'Enter the account id: ', + null, + flags.accountId.name) +} + +export async function promptNewPrivateKey (task, input) { + return await promptText(task, input, + flags.newPrivateKey.definition.defaultValue, + 'Enter the new private key: ', + null, + flags.newPrivateKey.name) +} + +export async function promptAmount (task, input) { + return await prompt('number', task, input, + flags.amount.definition.defaultValue, + 'How much HBAR do you want to add? ', + null, + flags.amount.name) +} + +export async function promptStdout (task, input) { + return await promptToggle(task, input, + flags.stdout.definition.defaultValue, + 'Would you like to send account keys to stdout for the user? ', + null, + flags.stdout.name) +} + export function getPromptMap () { return new Map() .set(flags.nodeIDs.name, promptNodeIds) @@ -372,6 +406,11 @@ export function getPromptMap () { .set(flags.keyFormat.name, promptKeyFormat) .set(flags.fstChartVersion.name, promptFstChartVersion) .set(flags.updateAccountKeys.name, promptUpdateAccountKeys) + .set(flags.privateKey.name, promptPrivateKey) + .set(flags.accountId.name, promptAccountId) + .set(flags.newPrivateKey.name, promptNewPrivateKey) + .set(flags.amount.name, promptAmount) + .set(flags.stdout.name, promptStdout) } // build the prompt registry diff --git a/solo/src/core/account_manager.mjs b/solo/src/core/account_manager.mjs index 33b26e86a..1137ddb58 100644 --- a/solo/src/core/account_manager.mjs +++ b/solo/src/core/account_manager.mjs @@ -16,9 +16,10 @@ */ import * as constants from './constants.mjs' import { + AccountCreateTransaction, AccountId, AccountInfoQuery, AccountUpdateTransaction, - Client, + Client, Hbar, HbarUnit, KeyList, PrivateKey, Status } from '@hashgraph/sdk' @@ -97,7 +98,7 @@ export class AccountManager { await this.updateSpecialAccountsKeys(namespace, nodeClient, constants.SYSTEM_ACCOUNTS) // update the treasury account last - await this.updateSpecialAccountsKeys(namespace, nodeClient, constants.TREASURY_ACCOUNT) + await this.updateSpecialAccountsKeys(namespace, nodeClient, constants.TREASURY_ACCOUNTS) nodeClient.close() await this.stopPortForwards() @@ -422,4 +423,44 @@ export class AccountManager { socket.destroy() await sleep(1) // gives a few ticks for connections to close } + + /** + * creates a new Hedera account + * @param namespace the namespace to store the Kubernetes key secret into + * @param nodeClient the active and network configured node client + * @param privateKey the private key of type PrivateKey + * @param amount the amount of HBAR to add to the account + * @returns {Promise<{accountId: AccountId, privateKey: string, publicKey: string}>} a + * custom object with the account information in it + */ + async createNewAccount (namespace, nodeClient, privateKey, amount) { + const newAccount = await new AccountCreateTransaction() + .setKey(privateKey) + .setInitialBalance(Hbar.from(amount, HbarUnit.Hbar)) + .execute(nodeClient) + + // Get the new account ID + const getReceipt = await newAccount.getReceipt(nodeClient) + const accountInfo = { + accountId: getReceipt.accountId, + privateKey: privateKey.toString(), + publicKey: privateKey.publicKey.toString() + } + + if (!(await this.k8.createSecret( + Templates.renderAccountKeySecretName(accountInfo.accountId), + namespace, 'Opaque', { + privateKey: accountInfo.privateKey, + publicKey: accountInfo.publicKey + }, + Templates.renderAccountKeySecretLabelObject(accountInfo.accountId), true)) + ) { + this.logger.error(`new account created [accountId=${accountInfo.accountId}, amount=${amount} HBAR, publicKey=${accountInfo.publicKey}, privateKey=${accountInfo.privateKey}] but failed to create secret in Kubernetes`) + + throw new FullstackTestingError(`failed to create secret for accountId ${accountInfo.accountId.toString()}, keys were sent to log file`) + } + this.logger.debug(`created k8s secret for account ${accountInfo.accountId}`) + + return accountInfo + } } diff --git a/solo/src/core/constants.mjs b/solo/src/core/constants.mjs index 241a7c553..2213836f3 100644 --- a/solo/src/core/constants.mjs +++ b/solo/src/core/constants.mjs @@ -76,8 +76,10 @@ export const DEFAULT_CHART_REPO = new Map() export const OPERATOR_ID = process.env.SOLO_OPERATOR_ID || '0.0.2' export const OPERATOR_KEY = process.env.SOLO_OPERATOR_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' export const OPERATOR_PUBLIC_KEY = process.env.SOLO_OPERATOR_PUBLIC_KEY || '302a300506032b65700321000aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92' +export const TREASURY_ACCOUNT_ID = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.2` +export const GENESIS_KEY = process.env.GENESIS_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' export const SYSTEM_ACCOUNTS = [[3, 100], [200, 349], [400, 750], [900, 1000]] // do account 0.0.2 last and outside the loop -export const TREASURY_ACCOUNT = [[2, 2]] +export const TREASURY_ACCOUNTS = [[2, 2]] export const LOCAL_NODE_START_PORT = process.env.LOCAL_NODE_START_PORT || 30212 export const ACCOUNT_KEYS_UPDATE_PAUSE = process.env.ACCOUNT_KEYS_UPDATE_PAUSE || 5 diff --git a/solo/test/e2e/commands/account.test.mjs b/solo/test/e2e/commands/account.test.mjs new file mode 100644 index 000000000..13a274f90 --- /dev/null +++ b/solo/test/e2e/commands/account.test.mjs @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { beforeAll, describe, expect, it } from '@jest/globals' +import { + ChartManager, + ConfigManager, + constants, DependencyManager, + Helm, + K8 +} from '../../../src/core/index.mjs' +import { getTestCacheDir, testLogger } from '../../test_util.js' +import path from 'path' +import { AccountManager } from '../../../src/core/account_manager.mjs' +import { AccountCommand } from '../../../src/commands/account.mjs' +import { flags } from '../../../src/commands/index.mjs' + +describe('account commands should work correctly', () => { + let accountCmd + let accountManager + let configManager + let k8 + let helm + let chartManager + let depManager + const argv = {} + + beforeAll(() => { + configManager = new ConfigManager(testLogger, path.join(getTestCacheDir('accountCmd'), 'solo.config')) + k8 = new K8(configManager, testLogger) + accountManager = new AccountManager(testLogger, k8, constants) + helm = new Helm(testLogger) + chartManager = new ChartManager(helm, testLogger) + depManager = new DependencyManager(testLogger) + accountCmd = new AccountCommand({ + logger: testLogger, + helm, + k8, + chartManager, + configManager, + depManager, + accountManager + }) + + argv[flags.cacheDir.name] = getTestCacheDir('accountCmd') + argv[flags.namespace.name] = 'solo-e2e' + argv[flags.clusterName.name] = 'kind-solo-e2e' + argv[flags.clusterSetupNamespace.name] = 'solo-e2e-cluster' + configManager.update(argv, true) + }) + + it('account create with no options', async () => { + await expect(accountCmd.create(argv)).resolves.toBeTruthy() + }) + + it('account create with private key, amount, and stdout options', () => { + + }) + + it('account update with account and amount options', () => { + + }) + + it('account update with account, new private key, and standard out options', () => { + + }) + + it('account get with account option', () => { + + }) + + it('account get with account id and private key options', () => { + + }) +}) diff --git a/solo/test/test_util.js b/solo/test/test_util.js index b7b35d294..8d05cb4df 100644 --- a/solo/test/test_util.js +++ b/solo/test/test_util.js @@ -21,8 +21,11 @@ import { logging } from '../src/core/index.mjs' export const testLogger = logging.NewLogger('debug') -export function getTestCacheDir () { - const d = 'test/data/tmp' +export function getTestCacheDir (appendDir) { + let d = 'test/data/tmp' + if (appendDir) { + d = path.join(d, appendDir) + } if (!fs.existsSync(d)) { fs.mkdirSync(d) } From 94bf9bab34dade095d03f84bb606278d66e12f7f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 15:12:54 +0000 Subject: [PATCH 02/10] added solo account get Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 99 +++++++++++++++++++++---- solo/src/commands/init.mjs | 4 +- solo/src/core/account_manager.mjs | 21 ++++-- solo/test/e2e/commands/account.test.mjs | 13 +++- 4 files changed, 115 insertions(+), 22 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index 4e2b12b6b..bf2d65ada 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -20,9 +20,8 @@ import { flags } from './index.mjs' import { Listr } from 'listr2' import * as prompts from './prompts.mjs' import { constants } from '../core/index.mjs' -import chalk from 'chalk' import { sleep } from '../core/helpers.mjs' -import { PrivateKey } from '@hashgraph/sdk' +import { HbarUnit, PrivateKey } from '@hashgraph/sdk' export class AccountCommand extends BaseCommand { constructor (opts) { @@ -31,6 +30,8 @@ export class AccountCommand extends BaseCommand { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager) this.accountManager = opts.accountManager + this.nodeClient = null + this.ctx = null } async create (argv) { @@ -40,7 +41,8 @@ export class AccountCommand extends BaseCommand { { title: 'Initialize', task: async (ctx, task) => { - self.configManager.load(argv) + self.ctx = ctx // useful for validation in e2e testing + self.configManager.update(argv) await prompts.execute(task, self.configManager, [ flags.namespace ]) @@ -52,6 +54,10 @@ export class AccountCommand extends BaseCommand { stdout: self.configManager.getFlag(flags.stdout) } + if (!config.amount) { + config.amount = flags.amount.definition.defaultValue + } + if (!await this.k8.hasNamespace(config.namespace)) { throw new FullstackTestingError(`namespace ${config.namespace} does not exist`) } @@ -81,12 +87,7 @@ export class AccountCommand extends BaseCommand { } catch (e) { throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) } finally { - if (this.nodeClient) { - this.nodeClient.close() - await sleep(5) // sleep a couple of ticks for connections to close - } - await this.accountManager.stopPortForwards() - await sleep(5) // sleep a couple of ticks for connections to close + await this.closeConnections() } return true @@ -99,13 +100,15 @@ export class AccountCommand extends BaseCommand { ctx.privateKey = PrivateKey.generateED25519() } - const accountInfo = await this.accountManager.createNewAccount(ctx.config.namespace, + ctx.accountInfo = await this.accountManager.createNewAccount(ctx.config.namespace, ctx.nodeClient, ctx.privateKey, ctx.config.amount) - const userMessagePrefix = `new account created [accountId=${accountInfo.accountId}, amount=${ctx.config.amount} HBAR, publicKey=${accountInfo.publicKey}` - const userMessagePostfix = ctx.config.stdout ? `, privateKey=${accountInfo.privateKey}]` : ']' + const accountInfoCopy = { ...ctx.accountInfo } + if (!ctx.config.stdout) { + delete accountInfoCopy.privateKey + } - this.logger.showUser(chalk.cyan(userMessagePrefix + userMessagePostfix)) + this.logger.showJSON('new account created', accountInfoCopy) } async loadNodeClient (ctx) { @@ -129,12 +132,73 @@ export class AccountCommand extends BaseCommand { } } + async getAccountInfo (ctx) { + return this.accountManager.accountInfoQuery(ctx.config.accountId, ctx.nodeClient) + } + async update (argv) { const self = this } async get (argv) { const self = this + + const tasks = new Listr([ + { + title: 'Initialize', + task: async (ctx, task) => { + self.ctx = ctx // useful for validation in e2e testing + self.configManager.update(argv) + await prompts.execute(task, self.configManager, [ + flags.namespace, + flags.accountId + ]) + + const config = { + namespace: self.configManager.getFlag(flags.namespace), + accountId: self.configManager.getFlag(flags.accountId), + privateKey: self.configManager.getFlag(flags.privateKey) + } + + if (!await this.k8.hasNamespace(config.namespace)) { + throw new FullstackTestingError(`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 }) + } + }, + { + title: 'get the account info', + task: async (ctx, task) => { + await self.loadTreasuryAccount(ctx) + await self.loadNodeClient(ctx) + const accountInfo = await self.getAccountInfo(ctx) + ctx.accountInfo = { + accountId: accountInfo.accountId.toString(), + publicKey: accountInfo.key.toString(), + balance: accountInfo.balance.to(HbarUnit.Hbar).toNumber() + } + // TODO private key requires k8 secret + this.logger.showJSON('account info', ctx.accountInfo) + } + } + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }) + + try { + await tasks.run() + } catch (e) { + throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) + } finally { + await this.closeConnections() + } + + return true } /** @@ -217,4 +281,13 @@ export class AccountCommand extends BaseCommand { } } } + + async closeConnections () { + if (this.nodeClient) { + this.nodeClient.close() + await sleep(5) // sleep a couple of ticks for connections to close + } + await this.accountManager.stopPortForwards() + await sleep(5) // sleep a couple of ticks for connections to close + } } diff --git a/solo/src/commands/init.mjs b/solo/src/commands/init.mjs index 893274ea0..f7316c5b3 100644 --- a/solo/src/commands/init.mjs +++ b/solo/src/commands/init.mjs @@ -159,8 +159,8 @@ export class InitCommand extends BaseCommand { flags.clusterSetupNamespace, flags.cacheDir, flags.chartDirectory, - flags.keyFormat, - ) + flags.keyFormat + ) }, handler: (argv) => { initCmd.init(argv).then(r => { diff --git a/solo/src/core/account_manager.mjs b/solo/src/core/account_manager.mjs index 1137ddb58..86453e858 100644 --- a/solo/src/core/account_manager.mjs +++ b/solo/src/core/account_manager.mjs @@ -339,6 +339,18 @@ export class AccountManager { } } + /** + * gets the account info from Hedera network + * @param accountId the account + * @param nodeClient the active and configured node client + * @returns {AccountInfo} the private key of the account + */ + async accountInfoQuery (accountId, nodeClient) { + return await new AccountInfoQuery() + .setAccountId(accountId) + .execute(nodeClient) + } + /** * gets the account private and public key from the Kubernetes secret from which it is stored * @param accountId the account @@ -346,9 +358,7 @@ export class AccountManager { * @returns {Promise} the private key of the account */ async getAccountKeys (accountId, nodeClient) { - const accountInfo = await new AccountInfoQuery() - .setAccountId(accountId) - .execute(nodeClient) + const accountInfo = await this.accountInfoQuery(accountId, nodeClient) let keys if (accountInfo.key instanceof KeyList) { @@ -442,9 +452,10 @@ export class AccountManager { // Get the new account ID const getReceipt = await newAccount.getReceipt(nodeClient) const accountInfo = { - accountId: getReceipt.accountId, + accountId: getReceipt.accountId.toString(), privateKey: privateKey.toString(), - publicKey: privateKey.publicKey.toString() + publicKey: privateKey.publicKey.toString(), + amount } if (!(await this.k8.createSecret( diff --git a/solo/test/e2e/commands/account.test.mjs b/solo/test/e2e/commands/account.test.mjs index 13a274f90..919817165 100644 --- a/solo/test/e2e/commands/account.test.mjs +++ b/solo/test/e2e/commands/account.test.mjs @@ -27,6 +27,7 @@ import path from 'path' import { AccountManager } from '../../../src/core/account_manager.mjs' import { AccountCommand } from '../../../src/commands/account.mjs' import { flags } from '../../../src/commands/index.mjs' +import { HEDERA_NODE_ACCOUNT_ID_START } from '../../../src/core/constants.mjs' describe('account commands should work correctly', () => { let accountCmd @@ -64,6 +65,13 @@ describe('account commands should work correctly', () => { it('account create with no options', async () => { await expect(accountCmd.create(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).not.toBeNull() + expect(accountInfo.privateKey).not.toBeNull() + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.amount).toEqual(flags.amount.definition.defaultValue) }) it('account create with private key, amount, and stdout options', () => { @@ -78,8 +86,9 @@ describe('account commands should work correctly', () => { }) - it('account get with account option', () => { - + it('account get with account option', async () => { + argv[flags.accountId.name] = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.1001` + await expect(accountCmd.get(argv)).resolves.toBeTruthy() }) it('account get with account id and private key options', () => { From 76e7db69ab9ad350e08ac514bae4955a5877862f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 15:57:12 +0000 Subject: [PATCH 03/10] finished get command Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 10 ++++-- solo/src/commands/flags.mjs | 2 +- solo/test/e2e/commands/account.test.mjs | 42 ++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index bf2d65ada..0378ae364 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -157,7 +157,7 @@ export class AccountCommand extends BaseCommand { const config = { namespace: self.configManager.getFlag(flags.namespace), accountId: self.configManager.getFlag(flags.accountId), - privateKey: self.configManager.getFlag(flags.privateKey) + stdout: self.configManager.getFlag(flags.stdout) } if (!await this.k8.hasNamespace(config.namespace)) { @@ -181,7 +181,11 @@ export class AccountCommand extends BaseCommand { publicKey: accountInfo.key.toString(), balance: accountInfo.balance.to(HbarUnit.Hbar).toNumber() } - // TODO private key requires k8 secret + + if (ctx.config.stdout) { + const accountKeys = await this.accountManager.getAccountKeysFromSecret(ctx.accountInfo.accountId, ctx.config.namespace) + ctx.accountInfo.privateKey = accountKeys.privateKey + } this.logger.showJSON('account info', ctx.accountInfo) } } @@ -262,7 +266,7 @@ export class AccountCommand extends BaseCommand { builder: y => flags.setCommandFlags(y, flags.namespace, flags.accountId, - flags.privateKey + flags.stdout ), handler: argv => { accountCmd.logger.debug("==== Running 'account get' ===") diff --git a/solo/src/commands/flags.mjs b/solo/src/commands/flags.mjs index f9c862314..18773bef8 100644 --- a/solo/src/commands/flags.mjs +++ b/solo/src/commands/flags.mjs @@ -425,7 +425,7 @@ export const amount = { } export const stdout = { - name: 'stdout', + name: 'stdout-private-key', definition: { describe: 'Send the account keys to stdout for the user', defaultValue: false, diff --git a/solo/test/e2e/commands/account.test.mjs b/solo/test/e2e/commands/account.test.mjs index 919817165..8b09a66e8 100644 --- a/solo/test/e2e/commands/account.test.mjs +++ b/solo/test/e2e/commands/account.test.mjs @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { beforeAll, describe, expect, it } from '@jest/globals' +import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals' import { ChartManager, ConfigManager, @@ -37,7 +37,7 @@ describe('account commands should work correctly', () => { let helm let chartManager let depManager - const argv = {} + let argv = {} beforeAll(() => { configManager = new ConfigManager(testLogger, path.join(getTestCacheDir('accountCmd'), 'solo.config')) @@ -55,7 +55,11 @@ describe('account commands should work correctly', () => { depManager, accountManager }) + }) + beforeEach(() => { + configManager.reset() + argv = {} argv[flags.cacheDir.name] = getTestCacheDir('accountCmd') argv[flags.namespace.name] = 'solo-e2e' argv[flags.clusterName.name] = 'kind-solo-e2e' @@ -74,8 +78,20 @@ describe('account commands should work correctly', () => { expect(accountInfo.amount).toEqual(flags.amount.definition.defaultValue) }) - it('account create with private key, amount, and stdout options', () => { + it('account create with private key, amount, and stdout options', async () => { + argv[flags.privateKey.name] = constants.GENESIS_KEY + argv[flags.amount.name] = 777 + argv[flags.stdout.name] = true + configManager.update(argv, true) + await expect(accountCmd.create(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).not.toBeNull() + expect(accountInfo.privateKey.toString()).toEqual(constants.GENESIS_KEY) + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.amount).toEqual(777) }) it('account update with account and amount options', () => { @@ -88,10 +104,28 @@ describe('account commands should work correctly', () => { it('account get with account option', async () => { argv[flags.accountId.name] = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.1001` + configManager.update(argv, true) + await expect(accountCmd.get(argv)).resolves.toBeTruthy() + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toBeUndefined() + expect(accountInfo.publicKey).toBeTruthy() + expect(accountInfo.balance).toEqual(100) }) - it('account get with account id and private key options', () => { + it('account get with account id and stdout private key options', async () => { + argv[flags.accountId.name] = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.1001` + argv[flags.stdout.name] = true + configManager.update(argv, true) + await expect(accountCmd.get(argv)).resolves.toBeTruthy() + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toBeTruthy() + expect(accountInfo.publicKey).toBeTruthy() + expect(accountInfo.balance).toEqual(100) }) }) From 91dfd625a74dc36ecc57cbadeb7781ea92bc9d68 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 19:13:39 +0000 Subject: [PATCH 04/10] working version, I think.... Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 122 ++++++++++++++-- solo/src/core/account_manager.mjs | 42 +++++- solo/test/e2e/commands/account.test.mjs | 179 +++++++++++++++++------- 3 files changed, 275 insertions(+), 68 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index 0378ae364..ae25f752d 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -93,6 +93,21 @@ export class AccountCommand extends BaseCommand { return true } + async buildAccountInfo (accountInfo, namespace, shouldRetrievePrivateKey) { + 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 + } + + return newAccountInfo + } + async createNewAccount (ctx) { if (ctx.config.privateKey) { ctx.privateKey = PrivateKey.fromStringED25519(ctx.config.privateKey) @@ -136,11 +151,29 @@ export class AccountCommand extends BaseCommand { return this.accountManager.accountInfoQuery(ctx.config.accountId, ctx.nodeClient) } - async update (argv) { - const self = this + async updateAccountInfo (ctx) { + let amount = ctx.config.amount + if (ctx.config.newPrivateKey) { + if (!(await this.accountManager.sendAccountKeyUpdate(ctx.accountInfo.accountId, ctx.config.newPrivateKey, ctx.nodeClient, ctx.accountInfo.privateKey))) { + this.logger.error(`failed to update account keys for accountId ${ctx.accountInfo.accountId}`) + return false + } + this.logger.debug(`sent account key update for account ${ctx.accountInfo.accountId}`) + } else { + amount = amount || flags.amount.definition.defaultValue + } + + if (amount) { + if (!(await this.transferAmountFromOperator(ctx.nodeClient, ctx.accountInfo.accountId, amount))) { + 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 get (argv) { + async update (argv) { const self = this const tasks = new Listr([ @@ -157,6 +190,8 @@ export class AccountCommand extends BaseCommand { const config = { namespace: self.configManager.getFlag(flags.namespace), accountId: self.configManager.getFlag(flags.accountId), + newPrivateKey: self.configManager.getFlag(flags.newPrivateKey), + amount: self.configManager.getFlag(flags.amount), stdout: self.configManager.getFlag(flags.stdout) } @@ -175,17 +210,76 @@ export class AccountCommand extends BaseCommand { task: async (ctx, task) => { await self.loadTreasuryAccount(ctx) await self.loadNodeClient(ctx) - const accountInfo = await self.getAccountInfo(ctx) - ctx.accountInfo = { - accountId: accountInfo.accountId.toString(), - publicKey: accountInfo.key.toString(), - balance: accountInfo.balance.to(HbarUnit.Hbar).toNumber() + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.newPrivateKey) + } + }, + { + title: 'update the account', + task: async (ctx, task) => { + if (!(await self.updateAccountInfo(ctx))) { + throw new FullstackTestingError(`An error occurred updating account ${ctx.accountInfo.accountId}`) + } + } + }, + { + title: 'get the updated account info', + task: async (ctx, task) => { + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.newPrivateKey) + this.logger.showJSON('account info', ctx.accountInfo) + } + } + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }) + + try { + await tasks.run() + } catch (e) { + throw new FullstackTestingError(`Error in updating account: ${e.message}`, e) + } finally { + await this.closeConnections() + } + + return true + } + + async get (argv) { + const self = this + + const tasks = new Listr([ + { + title: 'Initialize', + task: async (ctx, task) => { + self.ctx = ctx // useful for validation in e2e testing + self.configManager.update(argv) + await prompts.execute(task, self.configManager, [ + flags.namespace, + flags.accountId + ]) + + const config = { + namespace: self.configManager.getFlag(flags.namespace), + accountId: self.configManager.getFlag(flags.accountId), + stdout: self.configManager.getFlag(flags.stdout) } - if (ctx.config.stdout) { - const accountKeys = await this.accountManager.getAccountKeysFromSecret(ctx.accountInfo.accountId, ctx.config.namespace) - ctx.accountInfo.privateKey = accountKeys.privateKey + if (!await this.k8.hasNamespace(config.namespace)) { + throw new FullstackTestingError(`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 }) + } + }, + { + title: 'get the account info', + task: async (ctx, task) => { + await self.loadTreasuryAccount(ctx) + await self.loadNodeClient(ctx) + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.stdout) this.logger.showJSON('account info', ctx.accountInfo) } } @@ -197,7 +291,7 @@ export class AccountCommand extends BaseCommand { try { await tasks.run() } catch (e) { - throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) + throw new FullstackTestingError(`Error in getting account info: ${e.message}`, e) } finally { await this.closeConnections() } @@ -294,4 +388,8 @@ export class AccountCommand extends BaseCommand { await this.accountManager.stopPortForwards() await sleep(5) // sleep a couple of ticks for connections to close } + + async transferAmountFromOperator (nodeClient, toAccountId, amount) { + return await this.accountManager.transferAmount(nodeClient, constants.TREASURY_ACCOUNT_ID, toAccountId, amount) + } } diff --git a/solo/src/core/account_manager.mjs b/solo/src/core/account_manager.mjs index 86453e858..eb47812ac 100644 --- a/solo/src/core/account_manager.mjs +++ b/solo/src/core/account_manager.mjs @@ -21,7 +21,7 @@ import { AccountInfoQuery, AccountUpdateTransaction, Client, Hbar, HbarUnit, KeyList, - PrivateKey, Status + PrivateKey, Status, TransferTransaction } from '@hashgraph/sdk' import { FullstackTestingError } from './errors.mjs' import { sleep } from './helpers.mjs' @@ -156,7 +156,7 @@ export class AccountManager { return nodeClient } catch (e) { - throw new FullstackTestingError('failed to setup node client', e) + throw new FullstackTestingError(`failed to setup node client: ${e.message}`, e) } } @@ -376,13 +376,21 @@ export class AccountManager { * @param accountId the account that will get it's keys updated * @param newPrivateKey the new private key * @param nodeClient the active and configured node client - * @param genesisKey the genesis key that is the current key + * @param oldPrivateKey the genesis key that is the current key * @returns {Promise} whether the update was successful */ - async sendAccountKeyUpdate (accountId, newPrivateKey, nodeClient, genesisKey) { + async sendAccountKeyUpdate (accountId, newPrivateKey, nodeClient, oldPrivateKey) { this.logger.debug( `Updating account ${accountId.toString()} with new public and private keys`) + if (typeof newPrivateKey === 'string') { + newPrivateKey = PrivateKey.fromStringED25519(newPrivateKey) + } + + if (typeof oldPrivateKey === 'string') { + oldPrivateKey = PrivateKey.fromStringED25519(oldPrivateKey) + } + // Create the transaction to update the key on the account const transaction = await new AccountUpdateTransaction() .setAccountId(accountId) @@ -390,7 +398,7 @@ export class AccountManager { .freezeWith(nodeClient) // Sign the transaction with the old key and new key - const signTx = await (await transaction.sign(genesisKey)).sign( + const signTx = await (await transaction.sign(oldPrivateKey)).sign( newPrivateKey) // SIgn the transaction with the client operator private key and submit to a Hedera network @@ -440,7 +448,7 @@ export class AccountManager { * @param nodeClient the active and network configured node client * @param privateKey the private key of type PrivateKey * @param amount the amount of HBAR to add to the account - * @returns {Promise<{accountId: AccountId, privateKey: string, publicKey: string}>} a + * @returns {{accountId: AccountId, privateKey: string, publicKey: string, balance: number}} a * custom object with the account information in it */ async createNewAccount (namespace, nodeClient, privateKey, amount) { @@ -455,7 +463,7 @@ export class AccountManager { accountId: getReceipt.accountId.toString(), privateKey: privateKey.toString(), publicKey: privateKey.publicKey.toString(), - amount + balance: amount } if (!(await this.k8.createSecret( @@ -474,4 +482,24 @@ export class AccountManager { return accountInfo } + + async transferAmount (nodeClient, fromAccountId, toAccountId, hbarAmount) { + try { + const transaction = new TransferTransaction() + .addHbarTransfer(fromAccountId, new Hbar(-1 * hbarAmount)) + .addHbarTransfer(toAccountId, new Hbar(hbarAmount)) + + const txResponse = await transaction.execute(nodeClient) + + const receipt = await txResponse.getReceipt(nodeClient) + + this.logger.debug(`The transfer from account ${fromAccountId} to account ${toAccountId} for amount ${hbarAmount} was ${receipt.status.toString()} `) + + return receipt.status === Status.Success + } catch (e) { + const errorMessage = `transfer amount failed with an error: ${e.toString()}` + this.logger.error(errorMessage) + throw new FullstackTestingError(errorMessage, e) + } + } } diff --git a/solo/test/e2e/commands/account.test.mjs b/solo/test/e2e/commands/account.test.mjs index 8b09a66e8..374d55fbe 100644 --- a/solo/test/e2e/commands/account.test.mjs +++ b/solo/test/e2e/commands/account.test.mjs @@ -14,11 +14,19 @@ * limitations under the License. * */ -import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals' +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it +} from '@jest/globals' import { ChartManager, ConfigManager, - constants, DependencyManager, + constants, + DependencyManager, Helm, K8 } from '../../../src/core/index.mjs' @@ -27,7 +35,7 @@ import path from 'path' import { AccountManager } from '../../../src/core/account_manager.mjs' import { AccountCommand } from '../../../src/commands/account.mjs' import { flags } from '../../../src/commands/index.mjs' -import { HEDERA_NODE_ACCOUNT_ID_START } from '../../../src/core/constants.mjs' +import { sleep } from '../../../src/core/helpers.mjs' describe('account commands should work correctly', () => { let accountCmd @@ -38,6 +46,8 @@ describe('account commands should work correctly', () => { let chartManager let depManager let argv = {} + let accountId1 + let accountId2 beforeAll(() => { configManager = new ConfigManager(testLogger, path.join(getTestCacheDir('accountCmd'), 'solo.config')) @@ -67,65 +77,136 @@ describe('account commands should work correctly', () => { configManager.update(argv, true) }) + afterEach(() => { + sleep(5).then().catch() // give a few ticks so that connections can close + }) + it('account create with no options', async () => { - await expect(accountCmd.create(argv)).resolves.toBeTruthy() - - const accountInfo = accountCmd.ctx.accountInfo - expect(accountInfo).not.toBeNull() - expect(accountInfo.accountId).not.toBeNull() - expect(accountInfo.privateKey).not.toBeNull() - expect(accountInfo.publicKey).not.toBeNull() - expect(accountInfo.amount).toEqual(flags.amount.definition.defaultValue) + try { + await expect(accountCmd.create(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).not.toBeNull() + accountId1 = accountInfo.accountId + expect(accountInfo.privateKey).not.toBeNull() + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.balance).toEqual(flags.amount.definition.defaultValue) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) it('account create with private key, amount, and stdout options', async () => { - argv[flags.privateKey.name] = constants.GENESIS_KEY - argv[flags.amount.name] = 777 - argv[flags.stdout.name] = true - configManager.update(argv, true) - - await expect(accountCmd.create(argv)).resolves.toBeTruthy() - - const accountInfo = accountCmd.ctx.accountInfo - expect(accountInfo).not.toBeNull() - expect(accountInfo.accountId).not.toBeNull() - expect(accountInfo.privateKey.toString()).toEqual(constants.GENESIS_KEY) - expect(accountInfo.publicKey).not.toBeNull() - expect(accountInfo.amount).toEqual(777) + try { + argv[flags.privateKey.name] = constants.GENESIS_KEY + argv[flags.amount.name] = 777 + argv[flags.stdout.name] = true + configManager.update(argv, true) + + await expect(accountCmd.create(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).not.toBeNull() + accountId2 = accountInfo.accountId + expect(accountInfo.privateKey.toString()).toEqual(constants.GENESIS_KEY) + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.balance).toEqual(777) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) - it('account update with account and amount options', () => { - + it('account update with account', async () => { + try { + argv[flags.accountId.name] = accountId1 + configManager.update(argv, true) + + await expect(accountCmd.update(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toBeUndefined() + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.balance).toEqual(200) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) - it('account update with account, new private key, and standard out options', () => { - + it('account update with account, amount, new private key, and standard out options', async () => { + try { + argv[flags.accountId.name] = accountId2 + argv[flags.newPrivateKey.name] = constants.GENESIS_KEY + argv[flags.amount.name] = 333 + argv[flags.stdout.name] = true + configManager.update(argv, true) + + await expect(accountCmd.update(argv)).resolves.toBeTruthy() + + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toEqual(constants.GENESIS_KEY) + expect(accountInfo.publicKey).not.toBeNull() + expect(accountInfo.balance).toEqual(1110) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) it('account get with account option', async () => { - argv[flags.accountId.name] = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.1001` - configManager.update(argv, true) - - await expect(accountCmd.get(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo - expect(accountInfo).not.toBeNull() - expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) - expect(accountInfo.privateKey).toBeUndefined() - expect(accountInfo.publicKey).toBeTruthy() - expect(accountInfo.balance).toEqual(100) + try { + argv[flags.accountId.name] = accountId1 + configManager.update(argv, true) + + await expect(accountCmd.get(argv)).resolves.toBeTruthy() + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toBeUndefined() + expect(accountInfo.publicKey).toBeTruthy() + expect(accountInfo.balance).toEqual(200) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) it('account get with account id and stdout private key options', async () => { - argv[flags.accountId.name] = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.1001` - argv[flags.stdout.name] = true - configManager.update(argv, true) - - await expect(accountCmd.get(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo - expect(accountInfo).not.toBeNull() - expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) - expect(accountInfo.privateKey).toBeTruthy() - expect(accountInfo.publicKey).toBeTruthy() - expect(accountInfo.balance).toEqual(100) + try { + argv[flags.accountId.name] = accountId2 + argv[flags.stdout.name] = true + configManager.update(argv, true) + + await expect(accountCmd.get(argv)).resolves.toBeTruthy() + const accountInfo = accountCmd.ctx.accountInfo + expect(accountInfo).not.toBeNull() + expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) + expect(accountInfo.privateKey).toBeTruthy() + expect(accountInfo.publicKey).toBeTruthy() + expect(accountInfo.balance).toEqual(1110) + } catch (e) { + testLogger.showUserError(e) + expect(e).toBeNull() + } finally { + await accountCmd.closeConnections() + } }) }) From 7342c77603afe6e780e71e816a96f3e7ea2930e7 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 20:49:00 +0000 Subject: [PATCH 05/10] added test sequencer to ensure that node test runs before account test Signed-off-by: Jeromy Cannon --- solo/jest.config.mjs | 17 ++++++++-- solo/package-lock.json | 1 + solo/package.json | 1 + .../{node.test.mjs => 01_node.test.mjs} | 0 .../{account.test.mjs => 02_account.test.mjs} | 0 solo/test/e2e/jestCustomSequencer.cjs | 33 +++++++++++++++++++ 6 files changed, 49 insertions(+), 3 deletions(-) rename solo/test/e2e/commands/{node.test.mjs => 01_node.test.mjs} (100%) rename solo/test/e2e/commands/{account.test.mjs => 02_account.test.mjs} (100%) create mode 100644 solo/test/e2e/jestCustomSequencer.cjs diff --git a/solo/jest.config.mjs b/solo/jest.config.mjs index ab20d9c9c..3cb258013 100644 --- a/solo/jest.config.mjs +++ b/solo/jest.config.mjs @@ -15,9 +15,20 @@ * */ const config = { - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(mjs?)$', moduleFileExtensions: ['js', 'mjs'], - verbose: true + verbose: true, + testSequencer: './test/e2e/jestCustomSequencer.cjs', + projects: [ + { + rootDir: '/test/e2e', + displayName: 'end-to-end', + testMatch: ['/**/*.test.mjs'] + }, + { + rootDir: '/test/unit', + displayName: 'unit', + testMatch: ['/**/*.test.mjs'] + } + ] } - export default config diff --git a/solo/package-lock.json b/solo/package-lock.json index c702f1268..f8094ad6d 100644 --- a/solo/package-lock.json +++ b/solo/package-lock.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@jest/test-sequencer": "^29.7.0", "eslint": "^8.53.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-headers": "^1.1.0", diff --git a/solo/package.json b/solo/package.json index 5a2061aba..20968d273 100644 --- a/solo/package.json +++ b/solo/package.json @@ -46,6 +46,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@jest/test-sequencer": "^29.7.0", "eslint": "^8.53.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-headers": "^1.1.0", diff --git a/solo/test/e2e/commands/node.test.mjs b/solo/test/e2e/commands/01_node.test.mjs similarity index 100% rename from solo/test/e2e/commands/node.test.mjs rename to solo/test/e2e/commands/01_node.test.mjs diff --git a/solo/test/e2e/commands/account.test.mjs b/solo/test/e2e/commands/02_account.test.mjs similarity index 100% rename from solo/test/e2e/commands/account.test.mjs rename to solo/test/e2e/commands/02_account.test.mjs diff --git a/solo/test/e2e/jestCustomSequencer.cjs b/solo/test/e2e/jestCustomSequencer.cjs new file mode 100644 index 000000000..282666cc6 --- /dev/null +++ b/solo/test/e2e/jestCustomSequencer.cjs @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const Sequencer = require('@jest/test-sequencer').default + +const isEndToEnd = (test) => { + const contextConfig = test.context.config + return contextConfig.displayName.name === 'end-to-end' +} + +class CustomSequencer extends Sequencer { + sort (tests) { + const copyTests = Array.from(tests) + const normalTests = copyTests.filter((t) => !isEndToEnd(t)) + const endToEndTests = copyTests.filter((t) => isEndToEnd(t)) + return super.sort(normalTests).concat(endToEndTests.sort((a, b) => (a.path > b.path ? 1 : -1))) + } +} + +module.exports = CustomSequencer From 7ef3523a9ae86acbd7c9dadc0130ec3500e30cb9 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 21:14:54 +0000 Subject: [PATCH 06/10] prep for pull request review Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 150 ++++++++++----------- solo/src/commands/flags.mjs | 12 +- solo/src/commands/prompts.mjs | 9 -- solo/src/core/account_manager.mjs | 19 ++- solo/test/e2e/commands/02_account.test.mjs | 2 +- solo/test/e2e/core/k8_e2e.test.mjs | 2 +- solo/test/test_util.js | 7 +- 7 files changed, 97 insertions(+), 104 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index ae25f752d..efda2400e 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -34,63 +34,13 @@ export class AccountCommand extends BaseCommand { this.ctx = null } - async create (argv) { - const self = this - - const tasks = new Listr([ - { - title: 'Initialize', - task: async (ctx, task) => { - self.ctx = ctx // useful for validation in e2e testing - self.configManager.update(argv) - await prompts.execute(task, self.configManager, [ - flags.namespace - ]) - - const config = { - namespace: self.configManager.getFlag(flags.namespace), - privateKey: self.configManager.getFlag(flags.privateKey), - amount: self.configManager.getFlag(flags.amount), - stdout: self.configManager.getFlag(flags.stdout) - } - - if (!config.amount) { - config.amount = flags.amount.definition.defaultValue - } - - if (!await this.k8.hasNamespace(config.namespace)) { - throw new FullstackTestingError(`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.loadTreasuryAccount(ctx) - await self.loadNodeClient(ctx) - } - }, - { - title: 'create the new account', - task: async (ctx, task) => { - await self.createNewAccount(ctx) - } - } - ], { - concurrent: false, - rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }) - - try { - await tasks.run() - } catch (e) { - throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) - } finally { - await this.closeConnections() + async closeConnections () { + if (this.nodeClient) { + this.nodeClient.close() + await sleep(5) // sleep a couple of ticks for connections to close } - - return true + await this.accountManager.stopPortForwards() + await sleep(5) // sleep a couple of ticks for connections to close } async buildAccountInfo (accountInfo, namespace, shouldRetrievePrivateKey) { @@ -153,8 +103,8 @@ export class AccountCommand extends BaseCommand { async updateAccountInfo (ctx) { let amount = ctx.config.amount - if (ctx.config.newPrivateKey) { - if (!(await this.accountManager.sendAccountKeyUpdate(ctx.accountInfo.accountId, ctx.config.newPrivateKey, ctx.nodeClient, ctx.accountInfo.privateKey))) { + if (ctx.config.privateKey) { + if (!(await this.accountManager.sendAccountKeyUpdate(ctx.accountInfo.accountId, ctx.config.privateKey, ctx.nodeClient, ctx.accountInfo.privateKey))) { this.logger.error(`failed to update account keys for accountId ${ctx.accountInfo.accountId}`) return false } @@ -173,6 +123,69 @@ export class AccountCommand extends BaseCommand { return true } + async transferAmountFromOperator (nodeClient, toAccountId, amount) { + return await this.accountManager.transferAmount(nodeClient, constants.TREASURY_ACCOUNT_ID, toAccountId, amount) + } + + async create (argv) { + const self = this + + const tasks = new Listr([ + { + title: 'Initialize', + task: async (ctx, task) => { + self.ctx = ctx // useful for validation in e2e testing + self.configManager.update(argv) + await prompts.execute(task, self.configManager, [ + flags.namespace + ]) + + const config = { + namespace: self.configManager.getFlag(flags.namespace), + privateKey: self.configManager.getFlag(flags.privateKey), + amount: self.configManager.getFlag(flags.amount), + stdout: self.configManager.getFlag(flags.stdout) + } + + if (!config.amount) { + config.amount = flags.amount.definition.defaultValue + } + + if (!await this.k8.hasNamespace(config.namespace)) { + throw new FullstackTestingError(`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.loadTreasuryAccount(ctx) + await self.loadNodeClient(ctx) + } + }, + { + title: 'create the new account', + task: async (ctx, task) => { + await self.createNewAccount(ctx) + } + } + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }) + + try { + await tasks.run() + } catch (e) { + throw new FullstackTestingError(`Error in creating account: ${e.message}`, e) + } finally { + await this.closeConnections() + } + + return true + } + async update (argv) { const self = this @@ -190,7 +203,7 @@ export class AccountCommand extends BaseCommand { const config = { namespace: self.configManager.getFlag(flags.namespace), accountId: self.configManager.getFlag(flags.accountId), - newPrivateKey: self.configManager.getFlag(flags.newPrivateKey), + privateKey: self.configManager.getFlag(flags.privateKey), amount: self.configManager.getFlag(flags.amount), stdout: self.configManager.getFlag(flags.stdout) } @@ -210,7 +223,7 @@ export class AccountCommand extends BaseCommand { task: async (ctx, task) => { await self.loadTreasuryAccount(ctx) await self.loadNodeClient(ctx) - ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.newPrivateKey) + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.privateKey) } }, { @@ -224,7 +237,7 @@ export class AccountCommand extends BaseCommand { { title: 'get the updated account info', task: async (ctx, task) => { - ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.newPrivateKey) + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.privateKey) this.logger.showJSON('account info', ctx.accountInfo) } } @@ -337,7 +350,7 @@ export class AccountCommand extends BaseCommand { builder: y => flags.setCommandFlags(y, flags.namespace, flags.accountId, - flags.newPrivateKey, + flags.privateKey, flags.amount, flags.stdout ), @@ -379,17 +392,4 @@ export class AccountCommand extends BaseCommand { } } } - - async closeConnections () { - if (this.nodeClient) { - this.nodeClient.close() - await sleep(5) // sleep a couple of ticks for connections to close - } - await this.accountManager.stopPortForwards() - await sleep(5) // sleep a couple of ticks for connections to close - } - - async transferAmountFromOperator (nodeClient, toAccountId, amount) { - return await this.accountManager.transferAmount(nodeClient, constants.TREASURY_ACCOUNT_ID, toAccountId, amount) - } } diff --git a/solo/src/commands/flags.mjs b/solo/src/commands/flags.mjs index 18773bef8..167a8d76d 100644 --- a/solo/src/commands/flags.mjs +++ b/solo/src/commands/flags.mjs @@ -406,17 +406,8 @@ export const accountId = { } } -export const newPrivateKey = { - name: 'new-private-key', - definition: { - describe: 'Private key to assign to the Hedera account', - defaultValue: '', - type: 'string' - } -} - export const amount = { - name: 'amount', + name: 'hbar-amount', definition: { describe: 'Amount of HBAR to add', defaultValue: 100, @@ -473,7 +464,6 @@ export const allFlags = [ updateAccountKeys, privateKey, accountId, - newPrivateKey, amount, stdout ] diff --git a/solo/src/commands/prompts.mjs b/solo/src/commands/prompts.mjs index 042bcf2e6..d8521aa7a 100644 --- a/solo/src/commands/prompts.mjs +++ b/solo/src/commands/prompts.mjs @@ -351,14 +351,6 @@ export async function promptAccountId (task, input) { flags.accountId.name) } -export async function promptNewPrivateKey (task, input) { - return await promptText(task, input, - flags.newPrivateKey.definition.defaultValue, - 'Enter the new private key: ', - null, - flags.newPrivateKey.name) -} - export async function promptAmount (task, input) { return await prompt('number', task, input, flags.amount.definition.defaultValue, @@ -408,7 +400,6 @@ export function getPromptMap () { .set(flags.updateAccountKeys.name, promptUpdateAccountKeys) .set(flags.privateKey.name, promptPrivateKey) .set(flags.accountId.name, promptAccountId) - .set(flags.newPrivateKey.name, promptNewPrivateKey) .set(flags.amount.name, promptAmount) .set(flags.stdout.name, promptStdout) } diff --git a/solo/src/core/account_manager.mjs b/solo/src/core/account_manager.mjs index eb47812ac..3205ce815 100644 --- a/solo/src/core/account_manager.mjs +++ b/solo/src/core/account_manager.mjs @@ -18,10 +18,15 @@ import * as constants from './constants.mjs' import { AccountCreateTransaction, AccountId, - AccountInfoQuery, AccountUpdateTransaction, - Client, Hbar, HbarUnit, + AccountInfoQuery, + AccountUpdateTransaction, + Client, + Hbar, + HbarUnit, KeyList, - PrivateKey, Status, TransferTransaction + PrivateKey, + Status, + TransferTransaction } from '@hashgraph/sdk' import { FullstackTestingError } from './errors.mjs' import { sleep } from './helpers.mjs' @@ -483,6 +488,14 @@ export class AccountManager { return accountInfo } + /** + * transfer the specified amount of HBAR from one account to another + * @param nodeClient the configured and active network node client + * @param fromAccountId the account to pull the HBAR from + * @param toAccountId the account to put the HBAR + * @param hbarAmount the amount of HBAR + * @returns {Promise} if the transaction was successfully posted + */ async transferAmount (nodeClient, fromAccountId, toAccountId, hbarAmount) { try { const transaction = new TransferTransaction() diff --git a/solo/test/e2e/commands/02_account.test.mjs b/solo/test/e2e/commands/02_account.test.mjs index 374d55fbe..c0230afd7 100644 --- a/solo/test/e2e/commands/02_account.test.mjs +++ b/solo/test/e2e/commands/02_account.test.mjs @@ -148,7 +148,7 @@ describe('account commands should work correctly', () => { it('account update with account, amount, new private key, and standard out options', async () => { try { argv[flags.accountId.name] = accountId2 - argv[flags.newPrivateKey.name] = constants.GENESIS_KEY + argv[flags.privateKey.name] = constants.GENESIS_KEY argv[flags.amount.name] = 333 argv[flags.stdout.name] = true configManager.update(argv, true) diff --git a/solo/test/e2e/core/k8_e2e.test.mjs b/solo/test/e2e/core/k8_e2e.test.mjs index a2cd671dc..2158ced04 100644 --- a/solo/test/e2e/core/k8_e2e.test.mjs +++ b/solo/test/e2e/core/k8_e2e.test.mjs @@ -99,7 +99,7 @@ describe('K8', () => { await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves fs.rmdirSync(tmpDir, { recursive: true }) - }, 50000) + }, 120000) it('should be able to port forward gossip port', (done) => { const podName = Templates.renderNetworkPodName('node0') diff --git a/solo/test/test_util.js b/solo/test/test_util.js index 8d05cb4df..a75100759 100644 --- a/solo/test/test_util.js +++ b/solo/test/test_util.js @@ -22,10 +22,9 @@ import { logging } from '../src/core/index.mjs' export const testLogger = logging.NewLogger('debug') export function getTestCacheDir (appendDir) { - let d = 'test/data/tmp' - if (appendDir) { - d = path.join(d, appendDir) - } + const baseDir = 'test/data/tmp' + const d = appendDir ? path.join(baseDir, appendDir) : baseDir + if (!fs.existsSync(d)) { fs.mkdirSync(d) } From 00079a9e47ec2d114498c36c1b6093a18584465f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Feb 2024 21:33:13 +0000 Subject: [PATCH 07/10] patched a bug, and upped timeout because of pipelines being slow Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 2 +- solo/test/e2e/commands/02_account.test.mjs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index efda2400e..824511ebf 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -237,7 +237,7 @@ export class AccountCommand extends BaseCommand { { title: 'get the updated account info', task: async (ctx, task) => { - ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.privateKey) + ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.stdout) this.logger.showJSON('account info', ctx.accountInfo) } } diff --git a/solo/test/e2e/commands/02_account.test.mjs b/solo/test/e2e/commands/02_account.test.mjs index c0230afd7..5c3becfef 100644 --- a/solo/test/e2e/commands/02_account.test.mjs +++ b/solo/test/e2e/commands/02_account.test.mjs @@ -38,6 +38,7 @@ import { flags } from '../../../src/commands/index.mjs' import { sleep } from '../../../src/core/helpers.mjs' describe('account commands should work correctly', () => { + const defaultTimeout = 20000 let accountCmd let accountManager let configManager @@ -98,7 +99,7 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) it('account create with private key, amount, and stdout options', async () => { try { @@ -122,7 +123,7 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) it('account update with account', async () => { try { @@ -143,7 +144,7 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) it('account update with account, amount, new private key, and standard out options', async () => { try { @@ -167,7 +168,7 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) it('account get with account option', async () => { try { @@ -187,7 +188,7 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) it('account get with account id and stdout private key options', async () => { try { @@ -208,5 +209,5 @@ describe('account commands should work correctly', () => { } finally { await accountCmd.closeConnections() } - }) + }, defaultTimeout) }) From 2034f38c0cbc75d7342f887fc9aabea8fefbc6fd Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 15 Feb 2024 13:32:37 +0000 Subject: [PATCH 08/10] patched a bug, and upped timeout because of pipelines being slow Signed-off-by: Jeromy Cannon --- solo/src/commands/account.mjs | 55 ++++++++++------------ solo/src/commands/flags.mjs | 12 +---- solo/src/commands/prompts.mjs | 9 ---- solo/test/e2e/commands/02_account.test.mjs | 19 ++++---- 4 files changed, 33 insertions(+), 62 deletions(-) diff --git a/solo/src/commands/account.mjs b/solo/src/commands/account.mjs index 824511ebf..b594dde50 100644 --- a/solo/src/commands/account.mjs +++ b/solo/src/commands/account.mjs @@ -31,7 +31,7 @@ export class AccountCommand extends BaseCommand { this.accountManager = opts.accountManager this.nodeClient = null - this.ctx = null + this.accountInfo = null } async closeConnections () { @@ -65,15 +65,8 @@ export class AccountCommand extends BaseCommand { ctx.privateKey = PrivateKey.generateED25519() } - ctx.accountInfo = await this.accountManager.createNewAccount(ctx.config.namespace, + return await this.accountManager.createNewAccount(ctx.config.namespace, ctx.nodeClient, ctx.privateKey, ctx.config.amount) - - const accountInfoCopy = { ...ctx.accountInfo } - if (!ctx.config.stdout) { - delete accountInfoCopy.privateKey - } - - this.logger.showJSON('new account created', accountInfoCopy) } async loadNodeClient (ctx) { @@ -113,8 +106,13 @@ export class AccountCommand extends BaseCommand { amount = amount || flags.amount.definition.defaultValue } - if (amount) { - if (!(await this.transferAmountFromOperator(ctx.nodeClient, ctx.accountInfo.accountId, amount))) { + const hbarAmount = Number.parseFloat(amount) + if (amount && isNaN(hbarAmount)) { + throw new FullstackTestingError(`The HBAR amount was invalid: ${amount}`) + } + + if (hbarAmount > 0) { + if (!(await this.transferAmountFromOperator(ctx.nodeClient, ctx.accountInfo.accountId, hbarAmount))) { this.logger.error(`failed to transfer amount for accountId ${ctx.accountInfo.accountId}`) return false } @@ -134,7 +132,6 @@ export class AccountCommand extends BaseCommand { { title: 'Initialize', task: async (ctx, task) => { - self.ctx = ctx // useful for validation in e2e testing self.configManager.update(argv) await prompts.execute(task, self.configManager, [ flags.namespace @@ -143,8 +140,7 @@ export class AccountCommand extends BaseCommand { const config = { namespace: self.configManager.getFlag(flags.namespace), privateKey: self.configManager.getFlag(flags.privateKey), - amount: self.configManager.getFlag(flags.amount), - stdout: self.configManager.getFlag(flags.stdout) + amount: self.configManager.getFlag(flags.amount) } if (!config.amount) { @@ -167,7 +163,11 @@ export class AccountCommand extends BaseCommand { { title: 'create the new account', task: async (ctx, task) => { - await self.createNewAccount(ctx) + self.accountInfo = await self.createNewAccount(ctx) + const accountInfoCopy = { ...self.accountInfo } + delete accountInfoCopy.privateKey + + this.logger.showJSON('new account created', accountInfoCopy) } } ], { @@ -193,7 +193,6 @@ export class AccountCommand extends BaseCommand { { title: 'Initialize', task: async (ctx, task) => { - self.ctx = ctx // useful for validation in e2e testing self.configManager.update(argv) await prompts.execute(task, self.configManager, [ flags.namespace, @@ -204,8 +203,7 @@ export class AccountCommand extends BaseCommand { namespace: self.configManager.getFlag(flags.namespace), accountId: self.configManager.getFlag(flags.accountId), privateKey: self.configManager.getFlag(flags.privateKey), - amount: self.configManager.getFlag(flags.amount), - stdout: self.configManager.getFlag(flags.stdout) + amount: self.configManager.getFlag(flags.amount) } if (!await this.k8.hasNamespace(config.namespace)) { @@ -237,8 +235,8 @@ export class AccountCommand extends BaseCommand { { title: 'get the updated account info', task: async (ctx, task) => { - ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.stdout) - this.logger.showJSON('account info', ctx.accountInfo) + self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, false) + this.logger.showJSON('account info', self.accountInfo) } } ], { @@ -264,7 +262,6 @@ export class AccountCommand extends BaseCommand { { title: 'Initialize', task: async (ctx, task) => { - self.ctx = ctx // useful for validation in e2e testing self.configManager.update(argv) await prompts.execute(task, self.configManager, [ flags.namespace, @@ -273,8 +270,7 @@ export class AccountCommand extends BaseCommand { const config = { namespace: self.configManager.getFlag(flags.namespace), - accountId: self.configManager.getFlag(flags.accountId), - stdout: self.configManager.getFlag(flags.stdout) + accountId: self.configManager.getFlag(flags.accountId) } if (!await this.k8.hasNamespace(config.namespace)) { @@ -292,8 +288,8 @@ export class AccountCommand extends BaseCommand { task: async (ctx, task) => { await self.loadTreasuryAccount(ctx) await self.loadNodeClient(ctx) - ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.stdout) - this.logger.showJSON('account info', ctx.accountInfo) + self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, false) + this.logger.showJSON('account info', self.accountInfo) } } ], { @@ -328,8 +324,7 @@ export class AccountCommand extends BaseCommand { builder: y => flags.setCommandFlags(y, flags.namespace, flags.privateKey, - flags.amount, - flags.stdout + flags.amount ), handler: argv => { accountCmd.logger.debug("==== Running 'account create' ===") @@ -351,8 +346,7 @@ export class AccountCommand extends BaseCommand { flags.namespace, flags.accountId, flags.privateKey, - flags.amount, - flags.stdout + flags.amount ), handler: argv => { accountCmd.logger.debug("==== Running 'account update' ===") @@ -372,8 +366,7 @@ export class AccountCommand extends BaseCommand { desc: 'Gets the account info including the current amount of HBAR', builder: y => flags.setCommandFlags(y, flags.namespace, - flags.accountId, - flags.stdout + flags.accountId ), handler: argv => { accountCmd.logger.debug("==== Running 'account get' ===") diff --git a/solo/src/commands/flags.mjs b/solo/src/commands/flags.mjs index 167a8d76d..9c2dae8ba 100644 --- a/solo/src/commands/flags.mjs +++ b/solo/src/commands/flags.mjs @@ -415,15 +415,6 @@ export const amount = { } } -export const stdout = { - name: 'stdout-private-key', - definition: { - describe: 'Send the account keys to stdout for the user', - defaultValue: false, - type: 'boolean' - } -} - export const allFlags = [ devMode, clusterName, @@ -464,8 +455,7 @@ export const allFlags = [ updateAccountKeys, privateKey, accountId, - amount, - stdout + amount ] export const allFlagsMap = new Map(allFlags.map(f => [f.name, f])) diff --git a/solo/src/commands/prompts.mjs b/solo/src/commands/prompts.mjs index d8521aa7a..7a3dc1321 100644 --- a/solo/src/commands/prompts.mjs +++ b/solo/src/commands/prompts.mjs @@ -359,14 +359,6 @@ export async function promptAmount (task, input) { flags.amount.name) } -export async function promptStdout (task, input) { - return await promptToggle(task, input, - flags.stdout.definition.defaultValue, - 'Would you like to send account keys to stdout for the user? ', - null, - flags.stdout.name) -} - export function getPromptMap () { return new Map() .set(flags.nodeIDs.name, promptNodeIds) @@ -401,7 +393,6 @@ export function getPromptMap () { .set(flags.privateKey.name, promptPrivateKey) .set(flags.accountId.name, promptAccountId) .set(flags.amount.name, promptAmount) - .set(flags.stdout.name, promptStdout) } // build the prompt registry diff --git a/solo/test/e2e/commands/02_account.test.mjs b/solo/test/e2e/commands/02_account.test.mjs index 5c3becfef..b6cb3e1e0 100644 --- a/solo/test/e2e/commands/02_account.test.mjs +++ b/solo/test/e2e/commands/02_account.test.mjs @@ -86,7 +86,7 @@ describe('account commands should work correctly', () => { try { await expect(accountCmd.create(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).not.toBeNull() accountId1 = accountInfo.accountId @@ -101,16 +101,15 @@ describe('account commands should work correctly', () => { } }, defaultTimeout) - it('account create with private key, amount, and stdout options', async () => { + it('account create with private key and hbar amount options', async () => { try { argv[flags.privateKey.name] = constants.GENESIS_KEY argv[flags.amount.name] = 777 - argv[flags.stdout.name] = true configManager.update(argv, true) await expect(accountCmd.create(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).not.toBeNull() accountId2 = accountInfo.accountId @@ -132,7 +131,7 @@ describe('account commands should work correctly', () => { await expect(accountCmd.update(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) expect(accountInfo.privateKey).toBeUndefined() @@ -151,12 +150,11 @@ describe('account commands should work correctly', () => { argv[flags.accountId.name] = accountId2 argv[flags.privateKey.name] = constants.GENESIS_KEY argv[flags.amount.name] = 333 - argv[flags.stdout.name] = true configManager.update(argv, true) await expect(accountCmd.update(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) expect(accountInfo.privateKey).toEqual(constants.GENESIS_KEY) @@ -176,7 +174,7 @@ describe('account commands should work correctly', () => { configManager.update(argv, true) await expect(accountCmd.get(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) expect(accountInfo.privateKey).toBeUndefined() @@ -190,14 +188,13 @@ describe('account commands should work correctly', () => { } }, defaultTimeout) - it('account get with account id and stdout private key options', async () => { + it('account get with account id option', async () => { try { argv[flags.accountId.name] = accountId2 - argv[flags.stdout.name] = true configManager.update(argv, true) await expect(accountCmd.get(argv)).resolves.toBeTruthy() - const accountInfo = accountCmd.ctx.accountInfo + const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) expect(accountInfo.privateKey).toBeTruthy() From 195dcbd2c7f8825a9324b826e3dc103c30d86995 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 15 Feb 2024 14:08:46 +0000 Subject: [PATCH 09/10] removing todo, no longer needed Signed-off-by: Jeromy Cannon --- solo/src/core/account_manager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solo/src/core/account_manager.mjs b/solo/src/core/account_manager.mjs index 3205ce815..7419cdcc9 100644 --- a/solo/src/core/account_manager.mjs +++ b/solo/src/core/account_manager.mjs @@ -142,7 +142,7 @@ export class AccountManager { `Expected service ${serviceObject.name} to have a loadBalancerIP set for basepath ${this.k8.kubeClient.basePath}`) } const host = this.isLocalhost() ? '127.0.0.1' : serviceObject.loadBalancerIp - const port = serviceObject.grpcPort // TODO: add grpcs logic in https://github.com/hashgraph/full-stack-testing/issues/752 + const port = serviceObject.grpcPort const targetPort = this.isLocalhost() ? localPort : port if (this.isLocalhost()) { From f079d8ad8c09728f6798bccee0c2ecaf6c35eb70 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 15 Feb 2024 14:56:39 +0000 Subject: [PATCH 10/10] fix test case failure from recent change Signed-off-by: Jeromy Cannon --- solo/test/e2e/commands/02_account.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solo/test/e2e/commands/02_account.test.mjs b/solo/test/e2e/commands/02_account.test.mjs index b6cb3e1e0..916d89a7c 100644 --- a/solo/test/e2e/commands/02_account.test.mjs +++ b/solo/test/e2e/commands/02_account.test.mjs @@ -157,7 +157,7 @@ describe('account commands should work correctly', () => { const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) - expect(accountInfo.privateKey).toEqual(constants.GENESIS_KEY) + expect(accountInfo.privateKey).toBeUndefined() expect(accountInfo.publicKey).not.toBeNull() expect(accountInfo.balance).toEqual(1110) } catch (e) { @@ -197,7 +197,7 @@ describe('account commands should work correctly', () => { const accountInfo = accountCmd.accountInfo expect(accountInfo).not.toBeNull() expect(accountInfo.accountId).toEqual(argv[flags.accountId.name]) - expect(accountInfo.privateKey).toBeTruthy() + expect(accountInfo.privateKey).toBeUndefined() expect(accountInfo.publicKey).toBeTruthy() expect(accountInfo.balance).toEqual(1110) } catch (e) {