@hashgraph/hedera-local
Version:
Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).
487 lines (443 loc) • 19 kB
text/typescript
// 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);
}
}