@hashgraph/hedera-cli
Version:
CLI tool to manage and setup developer environments for Hedera Hashgraph.
469 lines (409 loc) • 12.8 kB
text/typescript
import { Client, AccountId, PrivateKey } from '@hashgraph/sdk';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { Logger } from '../utils/logger';
import stateController from '../state/stateController';
import type { Account, DownloadState, Script, Token, Topic } from '../../types';
const logger = Logger.getInstance();
/**
* Generates a UUID when it doesn't exist
*/
function createUUID(): void {
const uuid = stateController.get('uuid');
if (uuid === '' || !uuid) {
const newUUID = uuidv4();
stateController.saveKey('uuid', newUUID);
}
}
/**
* Returns the current telemetry setting
* @returns {boolean} telemetry
*/
function isTelemetryEnabled(): boolean {
const telemetry = stateController.get('telemetry');
return telemetry === 1;
}
function getMirrorNodeURL(): string {
const network = stateController.get('network');
let mirrorNodeURL = stateController.get('mirrorNodeTestnet');
switch (network) {
case 'testnet':
mirrorNodeURL = stateController.get('mirrorNodeTestnet');
break;
case 'mainnet':
mirrorNodeURL = stateController.get('mirrorNodeMainnet');
break;
case 'previewnet':
mirrorNodeURL = stateController.get('mirrorNodePreviewnet');
break;
case 'localnet':
mirrorNodeURL = stateController.get('mirrorNodeLocalnet');
break;
default:
logger.error('Invalid network name');
process.exit(1);
}
return mirrorNodeURL;
}
function getAvailableNetworks(): string[] {
const mainnet = stateController.get('mainnetOperatorKey');
const testnet = stateController.get('testnetOperatorKey');
const previewnet = stateController.get('previewnetOperatorKey');
const localnet = stateController.get('localnetOperatorKey');
const networks = [];
if (mainnet) {
networks.push('mainnet');
}
if (testnet) {
networks.push('testnet');
}
if (previewnet) {
networks.push('previewnet');
}
if (localnet) {
networks.push('localnet');
}
return networks;
}
function getMirrorNodeURLByNetwork(network: string): string {
let mirrorNodeURL = stateController.get('mirrorNodeTestnet');
switch (network) {
case 'testnet':
mirrorNodeURL = stateController.get('mirrorNodeTestnet');
break;
case 'mainnet':
mirrorNodeURL = stateController.get('mirrorNodeMainnet');
break;
case 'previewnet':
mirrorNodeURL = stateController.get('mirrorNodePreviewnet');
break;
case 'localnet':
mirrorNodeURL = stateController.get('mirrorNodeLocalnet');
break;
default:
logger.error('Invalid network name');
process.exit(1);
}
return mirrorNodeURL;
}
function getHederaClient(): Client {
const state = stateController.getAll();
let client: Client;
let operatorId, operatorKey;
switch (state.network) {
case 'mainnet':
client = Client.forMainnet();
operatorId = state.mainnetOperatorId;
operatorKey = state.mainnetOperatorKey;
break;
case 'testnet':
client = Client.forTestnet();
operatorId = state.testnetOperatorId;
operatorKey = state.testnetOperatorKey;
break;
case 'previewnet':
client = Client.forPreviewnet();
operatorId = state.previewnetOperatorId;
operatorKey = state.previewnetOperatorKey;
break;
case 'localnet':
const node = {
[state.localNodeAddress]: AccountId.fromString(
state.localNodeAccountId,
),
};
client = Client.forNetwork(node).setMirrorNetwork(
state.localNodeMirrorAddressGRPC,
);
operatorId = state.localnetOperatorId;
operatorKey = state.localnetOperatorKey;
break;
default:
logger.error('Invalid network name');
process.exit(1);
}
if (operatorId === '' || operatorKey === '') {
logger.error(`operator key and ID not set for ${state.network}`);
process.exit(1);
}
return client.setOperator(
AccountId.fromString(operatorId),
PrivateKey.fromStringDer(operatorKey),
);
}
/**
* @returns {string} network name
*/
function getNetwork() {
const state = stateController.getAll();
return state.network;
}
function getOperator(): { operatorId: string; operatorKey: string } {
const state = stateController.getAll();
let operatorId, operatorKey;
switch (state.network) {
case 'mainnet':
operatorId = state.mainnetOperatorId;
operatorKey = state.mainnetOperatorKey;
break;
case 'testnet':
operatorId = state.testnetOperatorId;
operatorKey = state.testnetOperatorKey;
break;
case 'previewnet':
operatorId = state.previewnetOperatorId;
operatorKey = state.previewnetOperatorKey;
break;
case 'localnet':
operatorId = state.localnetOperatorId;
operatorKey = state.localnetOperatorKey;
break;
default:
logger.error('Invalid network name');
process.exit(1);
}
if (operatorId === '' || operatorKey === '') {
logger.error(`operator key and ID not set for ${state.network}`);
process.exit(1);
}
return {
operatorId,
operatorKey,
};
}
function switchNetwork(name: string) {
const networks = getAvailableNetworks();
if (!networks.includes(name)) {
logger.error(
'Invalid network name. Available networks: ' + networks.join(', '),
);
process.exit(1);
}
const state = stateController.getAll();
let operatorId, operatorKey;
switch (name) {
case 'mainnet':
operatorId = state.mainnetOperatorId;
operatorKey = state.mainnetOperatorKey;
break;
case 'testnet':
operatorId = state.testnetOperatorId;
operatorKey = state.testnetOperatorKey;
break;
case 'previewnet':
operatorId = state.previewnetOperatorId;
operatorKey = state.previewnetOperatorKey;
break;
case 'localnet':
operatorId = state.localnetOperatorId;
operatorKey = state.localnetOperatorKey;
break;
}
if (operatorId === '' || operatorKey === '') {
logger.error(`operator key and ID not set for ${name}`);
process.exit(1);
}
stateController.saveKey('network', name);
}
function addTokenAssociation(
tokenId: string,
accountId: string,
alias: string,
) {
const tokens = stateController.get('tokens');
if (!tokens[tokenId]) {
logger.log(
`Token ${tokenId} not found in state. Skipping storing the token associations.`,
);
return;
}
const token: Token = tokens[tokenId];
token.associations.push({ alias, accountId });
tokens[tokenId] = token;
stateController.saveKey('tokens', tokens);
}
/* Accounts */
function getAccountById(accountId: string): Account | undefined {
const accounts: Record<string, Account> = stateController.get('accounts');
const account = Object.values(accounts).find(
(el: Account) => el.accountId === accountId,
);
return account;
}
function getAccountByAlias(alias: string): Account | undefined {
const accounts: Record<string, Account> = stateController.get('accounts');
return accounts[alias];
}
function getAccountByIdOrAlias(accountIdOrAlias: string): Account {
const accountIdPattern = /^0\.0\.\d+$/;
const match = accountIdOrAlias.match(accountIdPattern);
let account;
if (match) {
account = getAccountById(accountIdOrAlias);
} else {
account = getAccountByAlias(accountIdOrAlias);
}
if (!account) {
logger.error(`Account not found: ${accountIdOrAlias}`);
process.exit(1);
}
return account;
}
function startScriptExecution(name: string): void {
const state = stateController.getAll();
state.scriptExecutionName = name;
state.scriptExecution = 1;
stateController.saveState(state);
}
function stopScriptExecution(): void {
const state = stateController.getAll();
state.scripts[`script-${state.scriptExecutionName}`].args = {};
state.scriptExecutionName = '';
state.scriptExecution = 0;
stateController.saveState(state);
}
function clearState(): void {
const state = stateController.getAll();
state.accounts = {};
state.tokens = {};
state.scripts = {};
state.topics = {};
state.scriptExecution = 0;
state.scriptExecutionName = '';
stateController.saveState(state);
}
async function downloadState(url: string): Promise<DownloadState> {
let data;
try {
const response = await axios.get(url);
data = response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error(error.message);
} else {
logger.error('Unexpected error downloading file', error as object);
}
process.exit(1);
}
return data;
}
function addAccounts(importedAccounts: Account[], merge: boolean) {
const accounts: Record<string, Account> = stateController.get('accounts');
Object.values(importedAccounts).forEach((account: Account) => {
const existingAccount = accounts[account.alias];
if (!merge && existingAccount) {
logger.error(`Account with name ${account} already exists`);
process.exit(1);
}
if (merge && existingAccount) {
logger.log(
`Account "${account.alias}" already exists, merging it with the new account details`,
);
}
accounts[account.alias] = account;
stateController.saveKey('accounts', accounts);
logger.log(
`Account "${account.alias}" with ID ${account.accountId} added successfully`,
);
});
}
function addTokens(importedTokens: Token[], merge: boolean) {
const tokens: Record<string, Token> = stateController.get('tokens');
Object.values(importedTokens).forEach((token: Token) => {
const existingToken = tokens[token.tokenId];
if (!merge && existingToken) {
logger.error(`Token with ID ${token.tokenId} already exists`);
process.exit(1);
}
if (merge && existingToken) {
logger.log(`Token ${token.tokenId} already exists, overwriting it`);
}
tokens[token.tokenId] = token;
stateController.saveKey('tokens', tokens);
logger.log(
`Token ${token.tokenId} with name "${token.name}" added successfully`,
);
});
}
function addTopics(importedTopics: Topic[], merge: boolean) {
const topics: Record<string, Topic> = stateController.get('topics');
Object.values(importedTopics).forEach((topic: Topic) => {
const existingTopic = topics[topic.topicId];
if (!merge && existingTopic) {
logger.error(`Topic with ID ${topic.topicId} already exists`);
process.exit(1);
}
if (merge && existingTopic) {
logger.log(`Topic ${topic.topicId} already exists, overwriting it`);
}
topics[topic.topicId] = topic;
stateController.saveKey('topics', topics);
logger.log(`Topic ${topic.topicId} added successfully`);
});
}
function addScripts(importedScripts: Script[], merge: boolean) {
const scripts: Record<string, Script> = stateController.get('scripts');
Object.values(importedScripts).forEach((script: Script) => {
const scriptName = `script-${script.name}`;
const existingScript = scripts[scriptName];
if (!merge && existingScript) {
logger.error(`Script with name ${scriptName} already exists`);
process.exit(1);
}
if (merge && existingScript) {
// continue to add values to existing state (merging)
logger.log(`Script "${script.name}" already exists, overwriting it`);
}
scripts[scriptName] = {
name: script.name,
creation: Date.now(),
commands: script.commands,
args: {},
};
stateController.saveKey('scripts', scripts);
logger.log(`Script "${script.name}" added successfully`);
});
}
function importState(data: any, overwrite: boolean, merge: boolean) {
if (overwrite) {
stateController.saveKey('accounts', data.accounts || {});
stateController.saveKey('tokens', data.tokens || {});
stateController.saveKey('scripts', data.scripts || {});
stateController.saveKey('topics', data.topics || {});
logger.log('State overwritten successfully');
process.exit(0);
}
if (data.accounts && Object.entries(data.accounts).length > 0) {
addAccounts(data.accounts, merge);
}
if (data.tokens && Object.entries(data.tokens).length > 0) {
addTokens(data.tokens, merge);
}
if (data.scripts && Object.entries(data.scripts).length > 0) {
addScripts(data.scripts, merge);
}
if (data.topics && Object.entries(data.topics).length > 0) {
addTopics(data.topics, merge);
}
}
const stateUtils = {
createUUID,
isTelemetryEnabled,
getMirrorNodeURL,
getMirrorNodeURLByNetwork,
getHederaClient,
getOperator,
switchNetwork,
getNetwork,
addTokenAssociation,
getAccountById,
getAccountByAlias,
getAccountByIdOrAlias,
startScriptExecution,
stopScriptExecution,
clearState,
downloadState,
importState,
getAvailableNetworks,
};
export default stateUtils;