UNPKG

@hashgraph/hedera-local

Version:

Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).

487 lines (443 loc) 19 kB
// SPDX-License-Identifier: Apache-2.0 import path from 'path'; import { createReadStream } from 'fs'; import csvParser from 'csv-parser'; import { Hbar, PrivateKey } from '@hashgraph/sdk'; import { IOBserver } from '../controller/IObserver'; import { LoggerService } from '../services/LoggerService'; import { ServiceLocator } from '../services/ServiceLocator'; import { EventType } from '../types/EventType'; import { IState } from './IState'; import { CLIService } from '../services/CLIService'; import { Account } from '../types/AccountType'; import { ClientService } from '../services/ClientService'; import { privateKeysAliasECDSA, privateKeysECDSA, privateKeysED25519, } from '../configuration/accountConfiguration.json'; import { ACCOUNT_CREATION_STATE_INIT_MESSAGE, CHECK_SUCCESS, EVM_ADDRESSES_BLOCKLIST_FILE_RELATIVE_PATH, LOADING, SDK_ERRORS, } from '../constants'; import local from '../configuration/local.json'; import { AccountUtils } from '../utils/AccountUtils'; import { RetryUtils } from '../utils/RetryUtils'; /** * Represents the state of account creation. * This class is responsible for initializing the AccountCreationState object. * @implements {IState} */ export class AccountCreationState implements IState { /** * The name of the state. */ private readonly stateName: string; /** * The logger used for logging account creation state information. */ private readonly logger: LoggerService; /** * The CLI service used for account creation. */ private readonly cliService: CLIService; /** * The client service used for account creation. */ private readonly clientService: ClientService; /** * The observer for the account creation state. */ private observer: IOBserver | undefined; /** * Indicates whether the node is being started up. */ private nodeStartup: boolean; /** * Represents the state of account creation. * This class is responsible for initializing the AccountCreationState object. */ constructor() { this.stateName = AccountCreationState.name; this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name); this.cliService = ServiceLocator.Current.get<CLIService>(CLIService.name); this.clientService = ServiceLocator.Current.get<ClientService>(ClientService.name); this.nodeStartup = true; this.logger.trace(ACCOUNT_CREATION_STATE_INIT_MESSAGE, this.stateName); } /** * Subscribes an observer to receive updates from the AccountCreationState. * @param {IOBserver} observer The observer to subscribe. */ public subscribe(observer: IOBserver): void { this.observer = observer; } /** * Starts the account creation state. * * This method retrieves the current arguments, checks if blocklisting is enabled, and if so, gets the count of blocklisted accounts. * It logs the start of the account creation state, retrieves the balance and number of accounts from the arguments, and sets the node startup. * If the mode is asynchronous, it generates accounts asynchronously, otherwise, it generates ECDSA, alias ECDSA, and ED25519 accounts. * Finally, it updates the observer with the finish event type. * * @returns {Promise<void>} A Promise that resolves when the state is started. * @emits {EventType.Finish} When the state is finished. */ public async onStart(): Promise<void> { const { async, blocklisting, accounts, balance, startup } = this.cliService.getCurrentArgv(); this.nodeStartup = startup; let blocklistedAccountsCount = 0; if (blocklisting) { blocklistedAccountsCount = await this.getBlocklistedAccountsCount(); } const mode = async ? 'asynchronous' : 'synchronous'; const blockListedMessage = blocklisting ? `with ${blocklistedAccountsCount} blocklisted accounts` : ''; this.logger.info( `${LOADING} Starting Account Creation state in ${mode} mode ${blockListedMessage}...`, this.stateName ); const promise = this.generateAccounts(balance, accounts); if (!async) { await promise; } this.logger.info(`${CHECK_SUCCESS} Accounts created succefully!`, this.stateName); this.observer!.update(EventType.Finish); } /** * Generates accounts. * * This method generates ECDSA, alias ECDSA, and ED25519 accounts. * * @private * @param {number} balance - The balance for the accounts. * @param {number} accounts - The number of accounts to generate. * @returns {Promise<void>} - A promise that resolves when the accounts have been generated. */ private async generateAccounts(balance: number, accounts: number): Promise<void> { await Promise.all([ this.generateECDSA(balance, accounts), this.generateAliasECDSA(balance, accounts), this.generateED25519(balance, accounts) ]); } /** * Generates ECDSA accounts. * * If the node is in startup mode: * - it uses the private keys from the ECDSA private keys array to create the account. * - otherwise, it generates new ECDSA private keys. * * @private * @param {number} balance - The balance for the accounts. * @param {number} limit - The number of accounts to generate. * @returns {Promise<Account[]>} - A promise that resolves when all the accounts have been created. */ private async generateECDSA(balance: number, limit: number): Promise<Account[]> { const accountData = this.nodeStartup ? privateKeysECDSA.map(privateKeyString => ({ balance, privateKey: PrivateKey.fromStringECDSA(privateKeyString) })) : Array.from({ length: limit }, () => ({ balance, privateKey: PrivateKey.generateECDSA() })); const endIndex = Math.min(accountData.length, limit); return this.createAccounts('ECDSA', accountData.slice(0, endIndex)); } /** * Generates alias ECDSA accounts. * * If the node is in startup mode and the private key for the alias ECDSA account exists: * - it uses the private key to create the account. * - otherwise, it generates new ECDSA private keys. * * If the mode is asynchronous: * - it creates the alias accounts asynchronously and returns a promise that resolves when they have been created, * - otherwise, it creates the alias accounts synchronously and returns them in a resolved promise. * * @private * @param {number} balance - The balance for the accounts. * @param {number} accountNum - The number of accounts to generate. * @returns {Promise<Account[]>} - A promise that resolves when all the alias accounts have been created */ private async generateAliasECDSA(balance: number, accountNum: number): Promise<Account[]> { const accountData = this.nodeStartup ? privateKeysAliasECDSA.map(privateKeyString => ({ balance, privateKey: PrivateKey.fromStringECDSA(privateKeyString) })) : Array.from({ length: accountNum }, () => ({ balance, privateKey: PrivateKey.generateECDSA() })); const endIndex = Math.min(accountData.length, accountNum); return this.createAliasAccounts(accountData.slice(0, endIndex)); } /** * Generates ED25519 accounts. * * If the node is in startup mode: * - it uses the private keys from the ED25519 private keys array to create the account. * - otherwise, it generates new ED25519 private keys. * * @param balance - The balance for the accounts. * @param limit - The number of accounts to generate. * @returns {Promise<Account[]>} - A promise that resolves when all the * accounts have been created if the mode is asynchronous, otherwise void. * @private */ private async generateED25519(balance: number, limit: number): Promise<Account[]> { const accountData = this.nodeStartup ? privateKeysED25519.map(privateKeyString => ({ balance, privateKey: PrivateKey.fromStringED25519(privateKeyString) })) : Array.from({ length: limit }, () => ({ balance, privateKey: PrivateKey.generateED25519() })); const endIndex = Math.min(accountData.length, limit); return this.createAccounts('ED25519', accountData.slice(0, endIndex)); } /** * Generates ED25519 accounts. * * @private * @param {string} title - The title to be logged for the account list. * @param {Array<{ balance: number, privateKey: PrivateKey}>} accountData - The data for the accounts that will be created. * @param {number} accountData.balance - The balance of the account to create. * @param {PrivateKey} accountData.privateKey - The private key of the account to create. * @returns {Promise<Account[]>} - A promise that resolves when all the accounts have been created. */ private async createAccounts(title: string, accountData: { balance: number, privateKey: PrivateKey }[]): Promise<Account[]> { const accountPromises: Promise<Account>[] = []; accountData.forEach((account) => { const { privateKey, balance } = account; const client = this.clientService.getClient(); const publicKey = privateKey.publicKey; const createAccountPromise: Promise<Account> = RetryUtils.retryTask( () => AccountUtils.createAccount(publicKey, balance, client), { shouldRetry: error => this.shouldRetry(error), doOnRetry: error => this.doOnRetry(error) }).then((accountInfo) => { const address = accountInfo.accountId.toSolidityAddress(); return { accountId: accountInfo.accountId.toString(), balance: accountInfo.balance, privateKey, address }; }); accountPromises.push(createAccountPromise); }); return Promise.all(accountPromises) .then((accounts) => { if (accounts) { this.logAccountTitle(title); accounts.forEach((account) => this.logAccount( account.accountId, account.balance, `0x${account.privateKey.toStringRaw()}` )); this.logAccountDivider(); } return accounts; }); } /** * Creates alias accounts. * * @param accountData - The data for the accounts that will be created. * @param accountData.balance - The balance of the account to create. * @param accountData.privateKey - The private key of the account to create. * @returns {Promise<Account[]>} - A promise that resolves when all the alias accounts have been created * @private */ private async createAliasAccounts(accountData: { balance: number, privateKey: PrivateKey }[]): Promise<Account[]> { const accountPromises: Promise<Account>[] = []; // eslint-disable-next-line no-plusplus accountData.forEach(account => { const { privateKey, balance } = account; const client = this.clientService.getClient(); const aliasAccountId = privateKey.publicKey.toAccountId(0, 0); const createAccountPromise: Promise<Account> = RetryUtils.retryTask( () => AccountUtils.createAliasedAccount(aliasAccountId, balance, client), { shouldRetry: error => this.shouldRetry(error), doOnRetry: error => this.doOnRetry(error) }).then((accountInfo) => { const address = privateKey.publicKey.toEvmAddress(); return { accountId: accountInfo.accountId.toString(), balance: accountInfo.balance, privateKey, address }; }); accountPromises.push(createAccountPromise); }); return Promise.all(accountPromises) .then((accounts) => { if (accounts) { this.logAliasAccountTitle(); accounts.forEach((account) => this.logAliasAccount( account.accountId, account.balance, `0x${account.address}`, `0x${account.privateKey.toStringRaw()}` )); this.logAliasAccountDivider(); } return accounts; }); } /** * Retrieves the blocklist file name. * * This method searches the properties of the node configuration for the property with the key 'accounts.blocklist.path' and returns its value. * * @private * @returns {string} - The blocklist file name. */ private blockListFileName(): string { return local.nodeConfiguration.properties .find((prop) => prop.key === 'accounts.blocklist.path')?.value as string; } /** * Retrieves the count of blocklisted accounts. * * This method creates a new promise that resolves with the count of blocklisted accounts. * It initializes the count to 0 and constructs the file path to the blocklist file. * It creates a read stream from the file, pipes it through a CSV parser, and increments the count for each data event. * When the end event is emitted, it resolves the promise with the count. * * @private * @returns {Promise<number>} - A promise that resolves with the count of blocklisted accounts. */ private async getBlocklistedAccountsCount(): Promise<number> { return new Promise((resolve) => { let count = 0; const filepath = path.join( __dirname, EVM_ADDRESSES_BLOCKLIST_FILE_RELATIVE_PATH, this.blockListFileName() ); createReadStream(filepath) .pipe(csvParser()) .on('data', () => { // eslint-disable-next-line no-plusplus count++; }) .on('end', () => { resolve(count); }); }); } /** * Logs an account. * * This method logs the account ID, the private key, and the balance of an account, along with the state name. * * @private * @param {string} accountId - The account ID. * @param {Hbar} balance - The balance of the account. * @param {string} privateKey - The private key of the account. */ private logAccount(accountId: string, balance: Hbar, privateKey: string): void { this.logger.info(`| ${accountId} - ${privateKey} - ${balance} |`, this.stateName); } /** * Logs an alias account. * * This method logs the account ID, the account address, the private key of the account, and the balance of an alias account, along with the state name. * * @private * @param {string} accountId - The account ID. * @param {number} balance - The balance of the account. * @param {string} address - The address of the account. * @param {string} privateKey - The private key of the account. */ private logAliasAccount(accountId: string, balance: Hbar, address: string, privateKey: string): void { this.logger.info(`| ${accountId} - ${address} - ${privateKey} - ${balance} |`, this.stateName); } /** * Logs the title of an account. * * This method logs a divider, the title of the account list with the account type, another divider, the headers for the account ID, private key, and balance, and a final divider. * * @private * @param {string} accountType - The type of the account. */ private logAccountTitle(accountType: string) { this.logAccountDivider(); this.logger.info( `|-----------------------------| Accounts list (${accountType} keys) |----------------------------|`, this.stateName ); this.logAccountDivider(); this.logger.info( '| id | private key | balance |', this.stateName ); this.logAccountDivider(); } /** * Logs the title of an alias account. * * This method logs a divider, the title of the alias account list, another divider, the headers for the account ID, public address, private key, and balance, and a final divider. * * @private */ private logAliasAccountTitle() { this.logAliasAccountDivider(); this.logger.info( '|------------------------------------------------| Accounts list (Alias ECDSA keys) |--------------------------------------------------|', this.stateName ); this.logAliasAccountDivider(); this.logger.info( '| id | public address | private key | balance |', this.stateName ); this.logAliasAccountDivider(); } /** * Logs a divider for an account. * * This method logs a divider line, along with the state name. * * @private */ private logAccountDivider() { this.logger.info( '|-----------------------------------------------------------------------------------------|', this.stateName ); } /** * Logs a divider for an alias account. * * This method logs a divider line, along with the state name. * * @private */ private logAliasAccountDivider() { this.logger.info( '|--------------------------------------------------------------------------------------------------------------------------------------|', this.stateName ); } private shouldRetry = (error: unknown): boolean => { return error?.toString().includes(SDK_ERRORS.FAILED_TO_FIND_A_HEALTHY_NODE) ?? false; } private doOnRetry = (error: unknown): void => { this.logger.warn(`Error occurred during task execution: "${error?.toString()}"`, this.stateName); } }