UNPKG

@hashgraph/hedera-cli

Version:

CLI tool to manage and setup developer environments for Hedera Hashgraph.

378 lines (331 loc) 10.7 kB
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); };