From 79410f0c52119f29980c193ab1b45d671612b0ce Mon Sep 17 00:00:00 2001 From: JeffreyDallas <39912573+JeffreyDallas@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:36:52 -0600 Subject: [PATCH] feat: add solo smoke test to test flow (#905) Signed-off-by: Jeffrey Tang Signed-off-by: Jeromy Cannon Co-authored-by: Jeromy Cannon --- .github/workflows/flow-task-test.yaml | 2 + .github/workflows/script/solo_smoke_test.sh | 135 ++++++++++++++++++++ examples/create-topic.js | 62 +++++++++ src/commands/account.ts | 42 +++++- src/commands/flags.ts | 34 +++++ 5 files changed, 269 insertions(+), 6 deletions(-) create mode 100755 .github/workflows/script/solo_smoke_test.sh create mode 100644 examples/create-topic.js diff --git a/.github/workflows/flow-task-test.yaml b/.github/workflows/flow-task-test.yaml index ebdb99514..7499454b3 100644 --- a/.github/workflows/flow-task-test.yaml +++ b/.github/workflows/flow-task-test.yaml @@ -73,4 +73,6 @@ jobs: - name: Run Example Task File Test run: | task default-with-relay + sleep 10 + .github/workflows/script/solo_smoke_test.sh task clean diff --git a/.github/workflows/script/solo_smoke_test.sh b/.github/workflows/script/solo_smoke_test.sh new file mode 100755 index 000000000..8d0c12450 --- /dev/null +++ b/.github/workflows/script/solo_smoke_test.sh @@ -0,0 +1,135 @@ +#!/bin/bash +set -eo pipefail + +# +# This script should be called after solo has been deployed with mirror node and relay node deployed, +# and should be called from the root of the solo repository +# +# This uses solo account creation function to repeatedly generate background transactions +# Then run smart contract test, and also javascript sdk sample test to interact with solo network +# + +function_name="" + +function enable_port_forward () +{ + kubectl port-forward -n solo-e2e svc/haproxy-node1-svc 50211:50211 > /dev/null 2>&1 & + kubectl port-forward -n solo-e2e svc/hedera-explorer 8080:80 > /dev/null 2>&1 & + kubectl port-forward -n solo-e2e svc/relay-node1-hedera-json-rpc-relay 7546:7546 > /dev/null 2>&1 & + kubectl port-forward -n solo-e2e svc/mirror-grpc 5600:5600 > /dev/null 2>&1 & +} + +function clone_smart_contract_repo () +{ + echo "Clone hedera-smart-contracts" + if [ -d "hedera-smart-contracts" ]; then + echo "Directory hedera-smart-contracts exists." + else + echo "Directory hedera-smart-contracts does not exist." + git clone https://github.com/hashgraph/hedera-smart-contracts --branch only-erc20-tests + fi +} + +function setup_smart_contract_test () +{ + echo "Setup smart contract test" + cd hedera-smart-contracts + + echo "Remove previous .env file" + rm -f .env + + npm install + npx hardhat compile || return 1: + + echo "Build .env file" + + echo "PRIVATE_KEYS=\"$CONTRACT_TEST_KEYS\"" > .env + echo "RETRY_DELAY=5000 # ms" >> .env + echo "MAX_RETRY=5" >> .env + cat .env + cd - +} + +function start_background_transactions () +{ + echo "Start background transaction" + # generate accounts as background traffic for two minutes + # so record stream files can be kept pushing to mirror node + cd solo + npm run solo-test -- account create -n solo-e2e --create-amount 15 > /dev/null 2>&1 & + cd - +} + +function start_contract_test () +{ + cd hedera-smart-contracts + echo "Wait a few seconds for background transactions to start" + sleep 5 + echo "Run smart contract test" + npm run hh:test + result=$? + + cd - + return $result +} + +function create_test_account () +{ + echo "Create test account with solo network" + cd solo + + # create new account and extract account id + npm run solo-test -- account create -n solo-e2e --hbar-amount 100 --generate-ecdsa-key --set-alias > test.log + export OPERATOR_ID=$(grep "accountId" test.log | awk '{print $2}' | sed 's/"//g'| sed 's/,//g') + echo "OPERATOR_ID=${OPERATOR_ID}" + rm test.log + + # get private key of the account + npm run solo-test -- account get -n solo-e2e --account-id ${OPERATOR_ID} --private-key > test.log + export OPERATOR_KEY=$(grep "privateKey" test.log | awk '{print $2}' | sed 's/"//g'| sed 's/,//g') + export CONTRACT_TEST_KEY_ONE=0x$(grep "privateKeyRaw" test.log | awk '{print $2}' | sed 's/"//g'| sed 's/,//g') + echo "CONTRACT_TEST_KEY_ONE=${CONTRACT_TEST_KEY_ONE}" + rm test.log + + npm run solo-test -- account create -n solo-e2e --hbar-amount 100 --generate-ecdsa-key --set-alias > test.log + export SECOND_KEY=$(grep "accountId" test.log | awk '{print $2}' | sed 's/"//g'| sed 's/,//g') + npm run solo-test -- account get -n solo-e2e --account-id ${SECOND_KEY} --private-key > test.log + export CONTRACT_TEST_KEY_TWO=0x$(grep "privateKeyRaw" test.log | awk '{print $2}' | sed 's/"//g'| sed 's/,//g') + echo "CONTRACT_TEST_KEY_TWO=${CONTRACT_TEST_KEY_TWO}" + rm test.log + + export CONTRACT_TEST_KEYS=${CONTRACT_TEST_KEY_ONE},$'\n'${CONTRACT_TEST_KEY_TWO} + export HEDERA_NETWORK="local-node" + + echo "OPERATOR_KEY=${OPERATOR_KEY}" + echo "HEDERA_NETWORK=${HEDERA_NETWORK}" + echo "CONTRACT_TEST_KEYS=${CONTRACT_TEST_KEYS}" + + cd - +} + +function start_sdk_test () +{ + cd solo + node examples/create-topic.js + result=$? + + cd - + return $result +} + +echo "Restart port-forward" +task helper:clean:port-forward +enable_port_forward + + +echo "Change to parent directory" +cd ../ +create_test_account +clone_smart_contract_repo +setup_smart_contract_test +start_background_transactions +start_contract_test +start_sdk_test +echo "Sleep a while to wait background transactions to finish" +sleep 30 diff --git a/examples/create-topic.js b/examples/create-topic.js new file mode 100644 index 000000000..f44a957a3 --- /dev/null +++ b/examples/create-topic.js @@ -0,0 +1,62 @@ +/** + * 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 {Wallet, LocalProvider, TopicCreateTransaction, TopicMessageSubmitTransaction} from '@hashgraph/sdk'; + +import dotenv from 'dotenv'; + +dotenv.config(); + +async function main() { + if (process.env.OPERATOR_ID === null || process.env.OPERATOR_KEY === null || process.env.HEDERA_NETWORK === null) { + throw new Error('Environment variables OPERATOR_ID, HEDERA_NETWORK, and OPERATOR_KEY are required.'); + } + + console.log(`Hedera network = ${process.env.HEDERA_NETWORK}`); + const provider = new LocalProvider(); + + const wallet = new Wallet(process.env.OPERATOR_ID, process.env.OPERATOR_KEY, provider); + + try { + console.log('before create topic'); + // create topic + let transaction = await new TopicCreateTransaction().freezeWithSigner(wallet); + transaction = await transaction.signWithSigner(wallet); + console.log('after sign transaction'); + const createResponse = await transaction.executeWithSigner(wallet); + const createReceipt = await createResponse.getReceiptWithSigner(wallet); + + console.log(`topic id = ${createReceipt.topicId.toString()}`); + + // send one message + let topicMessageSubmitTransaction = await new TopicMessageSubmitTransaction({ + topicId: createReceipt.topicId, + message: 'Hello World', + }).freezeWithSigner(wallet); + topicMessageSubmitTransaction = await topicMessageSubmitTransaction.signWithSigner(wallet); + const sendResponse = await topicMessageSubmitTransaction.executeWithSigner(wallet); + + const sendReceipt = await sendResponse.getReceiptWithSigner(wallet); + + console.log(`topic sequence number = ${sendReceipt.topicSequenceNumber.toString()}`); + } catch (error) { + console.error(error); + } + + provider.close(); +} + +void main(); diff --git a/src/commands/account.ts b/src/commands/account.ts index 76c0fd27e..2bb0bb0e4 100644 --- a/src/commands/account.ts +++ b/src/commands/account.ts @@ -26,6 +26,8 @@ import {FREEZE_ADMIN_ACCOUNT} from '../core/constants.js'; import {type Opts} from '../types/command_types.js'; import {ListrLease} from '../core/lease/listr_lease.js'; import {type CommandBuilder} from '../types/aliases.js'; +import {sleep} from '../core/helpers.js'; +import {Duration} from '../core/time/duration.js'; export class AccountCommand extends BaseCommand { private readonly accountManager: AccountManager; @@ -57,7 +59,13 @@ export class AccountCommand extends BaseCommand { if (!accountInfo || !(accountInfo instanceof AccountInfo)) throw new IllegalArgumentError('An instance of AccountInfo is required'); - const newAccountInfo: {accountId: string; balance: number; publicKey: string; privateKey?: string} = { + const newAccountInfo: { + accountId: string; + balance: number; + publicKey: string; + privateKey?: string; + privateKeyRaw?: string; + } = { accountId: accountInfo.accountId.toString(), publicKey: accountInfo.key.toString(), balance: accountInfo.balance.to(HbarUnit.Hbar).toNumber(), @@ -66,6 +74,14 @@ export class AccountCommand extends BaseCommand { if (shouldRetrievePrivateKey) { const accountKeys = await this.accountManager.getAccountKeysFromSecret(newAccountInfo.accountId, namespace); newAccountInfo.privateKey = accountKeys.privateKey; + + // reconstruct private key to retrieve EVM address if private key is ECDSA type + try { + const privateKey = PrivateKey.fromStringDer(newAccountInfo.privateKey); + newAccountInfo.privateKeyRaw = privateKey.toStringRaw(); + } catch (e: Error | any) { + this.logger.error(`failed to retrieve EVM address for accountId ${newAccountInfo.accountId}`); + } } return newAccountInfo; @@ -73,6 +89,7 @@ export class AccountCommand extends BaseCommand { async createNewAccount(ctx: { config: { + generateEcdsaKey: boolean; ecdsaPrivateKey?: string; ed25519PrivateKey?: string; namespace: string; @@ -85,6 +102,8 @@ export class AccountCommand extends BaseCommand { ctx.privateKey = PrivateKey.fromStringECDSA(ctx.config.ecdsaPrivateKey); } else if (ctx.config.ed25519PrivateKey) { ctx.privateKey = PrivateKey.fromStringED25519(ctx.config.ed25519PrivateKey); + } else if (ctx.config.generateEcdsaKey) { + ctx.privateKey = PrivateKey.generateECDSA(); } else { ctx.privateKey = PrivateKey.generateED25519(); } @@ -93,7 +112,7 @@ export class AccountCommand extends BaseCommand { ctx.config.namespace, ctx.privateKey, ctx.config.amount, - ctx.config.ecdsaPrivateKey ? ctx.config.setAlias : false, + ctx.config.ecdsaPrivateKey || ctx.config.generateEcdsaKey ? ctx.config.setAlias : false, ); } @@ -300,6 +319,8 @@ export class AccountCommand extends BaseCommand { ed25519PrivateKey: string; namespace: string; setAlias: boolean; + generateEcdsaKey: boolean; + createAmount: number; }; privateKey: PrivateKey; } @@ -318,6 +339,8 @@ export class AccountCommand extends BaseCommand { namespace: self.configManager.getFlag(flags.namespace) as string, ed25519PrivateKey: self.configManager.getFlag(flags.ed25519PrivateKey) as string, setAlias: self.configManager.getFlag(flags.setAlias) as boolean, + generateEcdsaKey: self.configManager.getFlag(flags.generateEcdsaKey) as boolean, + createAmount: self.configManager.getFlag(flags.createAmount) as number, }; if (!config.amount) { @@ -341,10 +364,15 @@ export class AccountCommand extends BaseCommand { { title: 'create the new account', task: async ctx => { - self.accountInfo = await self.createNewAccount(ctx); - const accountInfoCopy = {...self.accountInfo}; - delete accountInfoCopy.privateKey; - this.logger.showJSON('new account created', accountInfoCopy); + for (let i = 0; i < ctx.config.createAmount; i++) { + self.accountInfo = await self.createNewAccount(ctx); + const accountInfoCopy = {...self.accountInfo}; + delete accountInfoCopy.privateKey; + this.logger.showJSON('new account created', accountInfoCopy); + if (ctx.config.createAmount > 0) { + await sleep(Duration.ofSeconds(1)); + } + } }, }, ], @@ -553,9 +581,11 @@ export class AccountCommand extends BaseCommand { flags.setCommandFlags( y, flags.amount, + flags.createAmount, flags.ecdsaPrivateKey, flags.namespace, flags.ed25519PrivateKey, + flags.generateEcdsaKey, flags.setAlias, ), handler: (argv: any) => { diff --git a/src/commands/flags.ts b/src/commands/flags.ts index 4931f11d2..23adfdfd9 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -1065,6 +1065,17 @@ export class Flags { }, }; + static readonly generateEcdsaKey: CommandFlag = { + constName: 'generateEcdsaKey', + name: 'generate-ecdsa-key', + definition: { + describe: 'Generate ECDSA private key for the Hedera account', + defaultValue: false, + type: 'boolean', + }, + prompt: undefined, + }; + static readonly ecdsaPrivateKey: CommandFlag = { constName: 'ecdsaPrivateKey', name: 'ecdsa-private-key', @@ -1137,6 +1148,27 @@ export class Flags { }, }; + static readonly createAmount: CommandFlag = { + constName: 'createAmount', + name: 'create-amount', + definition: { + describe: 'Amount of new account to create', + defaultValue: 1, + type: 'number', + }, + prompt: async function promptCreateAmount(task: ListrTaskWrapper, input: any) { + return await Flags.prompt( + 'number', + task, + input, + Flags.createAmount.definition.defaultValue, + 'How many account to create? ', + null, + Flags.createAmount.name, + ); + }, + }; + static readonly nodeAlias: CommandFlag = { constName: 'nodeAlias', name: 'node-alias', @@ -1609,6 +1641,7 @@ export class Flags { Flags.endpointType, Flags.soloChartVersion, Flags.generateGossipKeys, + Flags.generateEcdsaKey, Flags.generateTlsKeys, Flags.gossipEndpoints, Flags.gossipPrivateKey, @@ -1623,6 +1656,7 @@ export class Flags { Flags.namespace, Flags.newAccountNumber, Flags.newAdminKey, + Flags.createAmount, Flags.nodeAlias, Flags.nodeAliasesUnparsed, Flags.operatorId,