@hashgraph/hedera-cli
Version:
CLI tool to manage and setup developer environments for Hedera Hashgraph.
428 lines (359 loc) • 11.7 kB
text/typescript
import {
PrivateKey,
AccountCreateTransaction,
Hbar,
AccountId,
} from '@hashgraph/sdk';
import stateController from '../state/stateController';
import stateUtils from '../utils/state';
import { display } from '../utils/display';
import { Logger } from '../utils/logger';
import api from '../api';
import type { Account } from '../../types';
const logger = Logger.getInstance();
function clearAddressBook(): void {
stateController.saveKey('accounts', {});
}
function deleteAccount(accountIdOrAlias: string): void {
const account = stateUtils.getAccountByIdOrAlias(accountIdOrAlias);
if (!account) {
logger.error('Account not found');
process.exit(1);
}
const accounts = stateController.get('accounts');
delete accounts[account.alias];
stateController.saveKey('accounts', accounts);
}
function generateRandomAlias(): string {
const length = 20; // Define the length of the random string
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
async function createAccount(
balance: number,
type: string,
alias: string,
setMaxAutomaticTokenAssociations: number = 0,
): Promise<Account> {
// Validate balance
if (isNaN(balance) || balance <= 0) {
logger.error('Invalid balance. Balance must be a positive number.');
process.exit(1);
}
// Validate type
if (!['ecdsa', 'ed25519'].includes(type.toLowerCase())) {
logger.error('Invalid type. Type must be either "ecdsa" or "ed25519".');
process.exit(1);
}
// Validate alias: Not allowed to use "operator" as an alias or part of an alias
if (alias.toLowerCase().includes('operator')) {
logger.error('Invalid alias. Alias cannot contain the word "operator".');
process.exit(1);
}
// Get client from config
const accounts: Record<string, Account> = stateController.get('accounts');
const client = stateUtils.getHederaClient();
// Generate random alias if "random" is provided
let isRandomAlias = false;
if (alias.toLowerCase() === 'random') {
isRandomAlias = true;
let newAlias = generateRandomAlias();
alias = newAlias; // Implement this function to generate a random string
}
// Check if name is unique
if (!isRandomAlias && accounts && accounts[alias]) {
logger.error('An account with this alias already exists.');
client.close();
process.exit(1);
}
// Handle different types of account creation
let newAccountPrivateKey, newAccountPublicKey;
if (type.toLowerCase() === 'ed25519') {
newAccountPrivateKey = PrivateKey.generateED25519();
newAccountPublicKey = newAccountPrivateKey.publicKey;
} else {
newAccountPrivateKey = PrivateKey.generateECDSA();
newAccountPublicKey = newAccountPrivateKey.publicKey;
}
let newAccountId;
try {
const newAccount = await new AccountCreateTransaction()
.setKey(newAccountPublicKey)
.setInitialBalance(Hbar.fromTinybars(balance))
.setMaxAutomaticTokenAssociations(setMaxAutomaticTokenAssociations)
.execute(client);
// Get the new account ID
const getReceipt = await newAccount.getReceipt(client);
newAccountId = getReceipt.accountId;
} catch (error) {
logger.error('Error creating new account:', error as object);
client.close();
process.exit(1);
}
if (newAccountId == null) {
logger.error('Account was not created');
client.close();
process.exit(1);
}
// Store the new account in the config
const newAccountDetails = {
network: stateUtils.getNetwork(),
alias,
accountId: newAccountId.toString(),
type: type.toUpperCase(),
publicKey: newAccountPrivateKey.publicKey.toString(),
evmAddress:
type.toLowerCase() === 'ed25519'
? ''
: newAccountPrivateKey.publicKey.toEvmAddress(),
solidityAddress: `${newAccountId.toSolidityAddress()}`,
solidityAddressFull: `0x${newAccountId.toSolidityAddress()}`,
privateKey: newAccountPrivateKey.toString(),
};
// Add the new account to the accounts object in the config
const updatedAccounts = { ...accounts, [alias]: newAccountDetails };
stateController.saveKey('accounts', updatedAccounts);
// Log the account ID
logger.log(`The new account ID is: ${newAccountId}, with alias: ${alias}`);
client.close();
return newAccountDetails;
}
function listAccounts(showPrivateKeys: boolean = false): void {
const accounts: Record<string, Account> = stateController.get('accounts');
// Check if there are any accounts in the config
if (!accounts || Object.keys(accounts).length === 0) {
logger.log('No accounts found.');
process.exit(0);
}
// Log details for each account
for (const [alias, account] of Object.entries(accounts)) {
if (showPrivateKeys) {
logger.log('Alias, account ID, type, private key (DER)\n');
logger.log(
`${alias}, ${account.accountId}, ${account.type.toUpperCase()}, ${
account.privateKey
}`,
);
} else {
logger.log('Alias, account ID, type\n');
logger.log(
`${alias}, ${account.accountId}, ${account.type.toUpperCase()}`,
);
}
}
}
/**
* @description Returns the type of a private key
* @param privateKey Input private key
* @returns {string} key type {ed25519, ecdsa, Unknown key type}
*/
function getKeyType(privateKey: string): string {
try {
PrivateKey.fromStringED25519(privateKey);
return 'ed25519';
} catch (e) {
// Not an Ed25519 private key
}
try {
PrivateKey.fromStringECDSA(privateKey);
return 'ecdsa';
} catch (e) {
// Not an ECDSA private key
}
return 'Unknown key type';
}
function importAccount(id: string, key: string, alias: string): Account {
const accounts = stateController.get('accounts');
// Check if name is unique
if (accounts && accounts[alias]) {
logger.error('An account with this alias already exists.');
process.exit(1);
}
let privateKey, type;
const accountId = AccountId.fromString(id);
switch (getKeyType(key)) {
case 'ecdsa':
type = 'ECDSA';
privateKey = PrivateKey.fromStringECDSA(key);
break;
case 'ed25519':
type = 'ED25519';
privateKey = PrivateKey.fromStringED25519(key);
break;
default:
logger.error(
'Invalid key type. Only ECDSA and ED25519 keys are supported.',
);
process.exit(1);
}
// No Solidity and EVM address for ED25519 keys
const updatedAccounts = { ...accounts };
updatedAccounts[alias] = {
network: stateUtils.getNetwork(),
alias,
accountId: id,
type,
publicKey: privateKey.publicKey.toString(),
evmAddress:
type.toLowerCase() === 'ed25519'
? ''
: privateKey.publicKey.toEvmAddress(),
solidityAddress: `${accountId.toSolidityAddress()}`,
solidityAddressFull: `0x${accountId.toSolidityAddress()}`,
privateKey: key,
};
stateController.saveKey('accounts', updatedAccounts);
return updatedAccounts[alias];
}
function importAccountId(id: string, alias: string): Account {
const accounts = stateController.get('accounts');
// Check if name is unique
if (accounts && accounts[alias]) {
logger.error('An account with this alias already exists.');
process.exit(1);
}
const accountId = AccountId.fromString(id);
const updatedAccounts = { ...accounts };
updatedAccounts[alias] = {
network: stateUtils.getNetwork(),
alias,
accountId: id,
type: '',
publicKey: '',
evmAddress: '',
solidityAddress: `${accountId.toSolidityAddress()}`,
solidityAddressFull: `0x${accountId.toSolidityAddress()}`,
privateKey: '',
};
stateController.saveKey('accounts', updatedAccounts);
return updatedAccounts[alias];
}
async function getAccountBalance(
accountIdOrAlias: string,
onlyHbar: boolean = false,
tokenId?: string,
): Promise<void> {
const accounts = stateController.get('accounts');
const client = stateUtils.getHederaClient();
let accountId;
// Check if input is an alias or an account ID
const accountIdPattern = /^0\.0\.\d+$/;
if (accountIdPattern.test(accountIdOrAlias)) {
accountId = accountIdOrAlias;
} else if (accounts && accounts[accountIdOrAlias]) {
accountId = accounts[accountIdOrAlias].accountId;
} else {
logger.error('Invalid account ID or alias not found in address book.');
client.close();
process.exit(1);
}
const response = await api.account.getAccountInfo(accountId);
display('displayBalance', response, { onlyHbar, tokenId });
client.close();
return;
}
async function getAccountHbarBalanceByNetwork(
accountId: string,
network: string,
): Promise<number> {
const response = await api.account.getAccountInfoByNetwork(
accountId,
network,
);
if (!response) {
logger.error('Error getting account balance');
process.exit(1);
}
return response.data.balance.balance;
}
async function getAccountHbarBalance(accountId: string): Promise<number> {
const response = await api.account.getAccountInfo(accountId);
if (!response) {
logger.error('Error getting account balance');
process.exit(1);
}
return response.data.balance.balance;
}
function findAccountByPrivateKey(privateKey: string): Account {
const accounts: Record<string, Account> = stateController.get('accounts');
if (!accounts) {
logger.error('No accounts found in state');
process.exit(1);
}
let matchingAccount: Account | null = null;
for (const [, account] of Object.entries(accounts)) {
if (account.privateKey === privateKey) {
matchingAccount = account;
break; // Exit the loop once a matching account is found
}
}
if (!matchingAccount) {
logger.error('No matching account found for private key');
process.exit(1);
}
return matchingAccount;
}
function findAccountByAlias(inputAlias: string): Account {
const accounts: Record<string, Account> = stateController.get('accounts');
if (!accounts) {
logger.error('No accounts found in state');
process.exit(1);
}
let matchingAccount: Account | null = null;
for (const [, account] of Object.entries(accounts)) {
if (account.alias === inputAlias) {
matchingAccount = account;
break; // Exit the loop once a matching account is found
}
}
if (!matchingAccount) {
logger.error('No matching account found for alias');
process.exit(1);
}
return matchingAccount;
}
function getPublicKeyFromPrivateKey(privateKey: string): string {
const keyType = getKeyType(privateKey);
if (keyType === 'ed25519') {
return PrivateKey.fromStringED25519(privateKey).publicKey.toString();
}
if (keyType === 'ecdsa') {
return PrivateKey.fromStringECDSA(privateKey).publicKey.toString();
}
logger.error('Invalid private key');
process.exit(1);
}
function getPrivateKeyObject(privateKey: string): PrivateKey {
const keyType = getKeyType(privateKey);
if (keyType === 'ed25519') {
return PrivateKey.fromStringED25519(privateKey);
}
if (keyType === 'ecdsa') {
return PrivateKey.fromStringECDSA(privateKey);
}
logger.error('Invalid private key');
process.exit(1);
}
const accountUtils = {
createAccount,
listAccounts,
importAccount,
importAccountId,
getAccountBalance,
getAccountHbarBalance,
getAccountHbarBalanceByNetwork,
getKeyType,
getPublicKeyFromPrivateKey,
getPrivateKeyObject,
generateRandomAlias,
clearAddressBook,
deleteAccount,
findAccountByPrivateKey,
findAccountByAlias,
};
export default accountUtils;