@hashgraph/hedera-cli
Version:
CLI tool to manage and setup developer environments for Hedera Hashgraph.
378 lines (331 loc) • 10.7 kB
text/typescript
import * as path from 'path';
import {
TokenCreateTransaction,
TokenType,
PrivateKey,
CustomFee,
} from '@hashgraph/sdk';
import accountUtils from '../../utils/account';
import tokenUtils from '../../utils/token';
import stateUtils from '../../utils/state';
import telemetryUtils from '../../utils/telemetry';
import feeUtils from '../../utils/fees';
import { Logger } from '../../utils/logger';
import stateController from '../../state/stateController';
import dynamicVariablesUtils from '../../utils/dynamicVariables';
import type {
Account,
Command,
Token,
Keys,
CustomFeeInput,
FixedFee,
FractionalFee,
} from '../../../types';
import signUtils from '../../utils/sign';
const logger = Logger.getInstance();
interface CreateTokenFromFileOptions {
file: string;
args: string[];
}
interface TokenInput {
name: string;
symbol: string;
decimals: number;
supplyType: 'finite' | 'infinite';
initialSupply: number;
keys: Keys;
maxSupply: number;
treasuryId?: string;
treasuryKey: string;
customFees: CustomFeeInput[];
memo: string;
}
function getTreasuryIdByTreasuryKey(treasuryKey: string): string {
const account = accountUtils.findAccountByPrivateKey(treasuryKey);
if (!account) {
logger.error('Treasury account not found');
process.exit(1);
}
return account.accountId;
}
async function createAccountForToken(
key: string,
initialBalance: number,
type: string,
alias: string,
): Promise<{ key: string; account: Account }> {
const account = await accountUtils.createAccount(initialBalance, type, alias);
return { key, account };
}
function resolveTokenFilePath(filename: string): string {
return path.join(__dirname, '../..', 'input', `token.${filename}.json`);
}
function initializeToken(tokenInput: TokenInput): Token {
const token: Token = {
network: stateUtils.getNetwork(),
associations: [],
tokenId: '',
name: tokenInput.name,
symbol: tokenInput.symbol,
treasuryId: tokenInput.treasuryId || '',
decimals: tokenInput.decimals,
initialSupply: tokenInput.initialSupply,
supplyType: 'infinite',
maxSupply: tokenInput.maxSupply || 0,
keys: {
adminKey: tokenInput.keys.adminKey,
pauseKey: tokenInput.keys.pauseKey,
kycKey: tokenInput.keys.kycKey,
wipeKey: tokenInput.keys.wipeKey,
freezeKey: tokenInput.keys.freezeKey,
supplyKey: tokenInput.keys.supplyKey,
feeScheduleKey: tokenInput.keys.feeScheduleKey,
treasuryKey: tokenInput.keys.treasuryKey,
},
customFees: tokenInput.customFees,
};
return token;
}
/**
* Find alias pattern in keys on token and replace with private key
* @param keys
* @return updated keys
*/
function replaceAliasPattern(keys: Keys): Keys {
const accounts = stateController.get('accounts');
const aliasPattern = /<alias:([a-zA-Z0-9_-]+)>/;
let newKeys = { ...keys };
Object.keys(newKeys).forEach((key) => {
const match = newKeys[key as keyof typeof newKeys].match(aliasPattern);
if (match) {
const alias = match[1];
if (accounts[alias]) {
newKeys[key as keyof typeof newKeys] = accounts[alias].privateKey;
}
}
});
return newKeys;
}
/**
* Create new accounts for keys that match the `newkey` pattern
* and replace the pattern with the new private key
*
* @param keys
* @returns promise array of new accounts
*/
function findNewKeyPattern(
keys: Keys,
): Promise<{ key: string; account: Account }>[] {
let newAccountPromises: Promise<{ key: string; account: Account }>[] = [];
const newKeyPattern = /<newkey:(ecdsa|ECDSA|ed25519|ED25519):(\d+)>/;
Object.keys(keys).forEach((key) => {
const match = keys[key as keyof typeof keys].match(newKeyPattern);
if (match) {
const keyType = match[1]; // 'ecdsa' or 'ed25519' (can be capitals)
const initialBalance = Number(match[2]); // Initial balance in tinybars
newAccountPromises.push(
createAccountForToken(key, initialBalance, keyType, 'random'), // Random alias because you can create an account upfront in scripts and give it an alias to be used in the template
);
}
});
return newAccountPromises;
}
/**
* Replace keys that match the `newkey` pattern with the new private key
* @param keys
* @returns updated keys
*/
async function handleNewKeyPattern(keys: Keys): Promise<Keys> {
let newAccountPromises: Promise<{ key: string; account: Account }>[] =
findNewKeyPattern(keys);
// Create new accounts if pattern is detected
let newKeys = { ...keys };
if (newAccountPromises.length > 0) {
try {
const newAccounts = await Promise.all(newAccountPromises);
newAccounts.forEach((newAccount) => {
newKeys[newAccount.key as keyof typeof newKeys] =
newAccount.account.privateKey;
});
} catch (error) {
logger.error(
'Failed to create new account(s) for token',
error as object,
);
process.exit(1);
}
}
return newKeys;
}
async function replaceKeysForToken(token: Token): Promise<Token> {
let newToken = { ...token };
// Look for alias pattern in keys on token
newToken.keys = replaceAliasPattern(newToken.keys);
// Look for `newkey` pattern in keys on token
newToken.keys = await handleNewKeyPattern(newToken.keys);
return newToken;
}
async function prepareTokenCreation(
token: Token,
tokenInput: TokenInput,
): Promise<Token> {
token.supplyType = tokenInput.supplyType;
token = await replaceKeysForToken(token);
if (token.treasuryId === '') {
token.treasuryId = getTreasuryIdByTreasuryKey(token.keys.treasuryKey);
}
return token;
}
function addKeysToTokenCreateTx(
tokenCreateTx: TokenCreateTransaction,
token: Token,
) {
// Mapping key names to their corresponding setter methods
const keySetters = {
adminKey: tokenCreateTx.setAdminKey,
pauseKey: tokenCreateTx.setPauseKey,
kycKey: tokenCreateTx.setKycKey,
wipeKey: tokenCreateTx.setWipeKey,
freezeKey: tokenCreateTx.setFreezeKey,
supplyKey: tokenCreateTx.setSupplyKey,
feeScheduleKey: tokenCreateTx.setFeeScheduleKey,
};
Object.entries(keySetters).forEach(([key, setter]) => {
const keyValue = token.keys[key as keyof typeof token.keys];
if (keyValue && keyValue !== '') {
setter.call(tokenCreateTx, PrivateKey.fromStringDer(keyValue).publicKey);
}
});
}
async function createTokenOnNetwork(token: Token) {
const client = stateUtils.getHederaClient();
try {
const tokenCreateTx = new TokenCreateTransaction()
.setTokenName(token.name)
.setTokenSymbol(token.symbol)
.setDecimals(token.decimals)
.setInitialSupply(token.initialSupply)
.setTokenType(TokenType.FungibleCommon)
.setSupplyType(tokenUtils.getSupplyType(token.supplyType))
.setTreasuryAccountId(token.treasuryId);
if (token.supplyType === 'finite') {
tokenCreateTx.setMaxSupply(token.maxSupply);
}
// Add keys
addKeysToTokenCreateTx(tokenCreateTx, token);
// Add custom fees
let fees: CustomFee[] = token.customFees.map((fee) => {
switch (fee.type) {
case 'fixed':
return feeUtils.createCustomFixedFee(fee as FixedFee);
case 'fractional':
return feeUtils.createCustomFractionalFee(fee as FractionalFee);
default:
logger.error(`Unsupported fee type: ${fee.type}`);
client.close();
process.exit(1);
}
});
tokenCreateTx.setCustomFees(fees);
// Signing
tokenCreateTx.freezeWith(client);
const signedTokenCreateTx = await signUtils.signByType(
tokenCreateTx,
'tokenCreate',
{
adminKey: token.keys.adminKey,
treasuryKey: token.keys.treasuryKey,
},
);
// Execute
let tokenCreateSubmit = await signedTokenCreateTx.execute(client);
let tokenCreateRx = await tokenCreateSubmit.getReceipt(client);
if (tokenCreateRx.tokenId == null) {
logger.error('Token was not created');
client.close();
process.exit(1);
}
token.tokenId = tokenCreateRx.tokenId.toString();
logger.log(`Token ID: ${token.tokenId}`);
client.close();
} catch (error) {
logger.error(error as object);
client.close();
process.exit(1);
}
}
function updateTokenState(token: Token) {
const tokens: Record<string, Token> = stateController.get('tokens');
const updatedTokens = {
...tokens,
[token.tokenId]: token,
};
stateController.saveKey('tokens', updatedTokens);
stateUtils.getHederaClient().close();
}
async function createTokenFromFile(tokenInput: TokenInput): Promise<Token> {
try {
let token = initializeToken(tokenInput);
token = await prepareTokenCreation(token, tokenInput);
await createTokenOnNetwork(token);
updateTokenState(token);
return token;
} catch (error) {
logger.error(error as object);
stateUtils.getHederaClient().close();
process.exit(1);
}
}
async function createToken(options: CreateTokenFromFileOptions) {
logger.verbose(`Creating token from template with name: ${options.file}`);
options = dynamicVariablesUtils.replaceOptions(options);
const filepath = resolveTokenFilePath(options.file);
const tokenDefinition = require(filepath);
const token = await createTokenFromFile(tokenDefinition);
// Store dynamic script variables
dynamicVariablesUtils.storeArgs(
options.args,
dynamicVariablesUtils.commandActions.token.createFromFile.action,
{
tokenId: token.tokenId,
name: token.name,
symbol: token.symbol,
treasuryId: token.treasuryId,
adminKey: token.keys.adminKey,
pauseKey: token.keys.pauseKey,
kycKey: token.keys.kycKey,
wipeKey: token.keys.wipeKey,
freezeKey: token.keys.freezeKey,
supplyKey: token.keys.supplyKey,
feeScheduleKey: token.keys.feeScheduleKey,
treasuryKey: token.keys.treasuryKey,
},
);
}
export default (program: any) => {
program
.command('create-from-file')
.hook('preAction', async (thisCommand: Command) => {
const command = [
thisCommand.parent.action().name(),
...thisCommand.parent.args,
];
if (stateUtils.isTelemetryEnabled()) {
await telemetryUtils.recordCommand(command.join(' '));
}
})
.description('Create a new token from a file')
.requiredOption(
'-f, --file <filename>',
'Filename containing the token information',
)
.option(
'--args <args>',
'Store arguments for scripts',
(value: string, previous: string) =>
previous ? previous.concat(value) : [value],
[],
)
.action(createToken);
};