This tutorial will walk you through a TokenCreateTransaction and create a fungible Hedera Token Service (HTS) token. You will learn how to configure essential token properties, set up necessary keys and permissions, and submit your transaction to the Hedera network.
What you will accomplish
By the end of this tutorial, you will be able to:
Create a new fungible token using HTS.
Query the transaction via Mirror Node API.
View your transaction on a Mirror Node Explorer.
Prerequisites
Before you begin, you should have completed the following tutorials:
Step 1: Navigate to the hts example in the project directory
From the root directory of the hedera-future-world project CD (change directories) to the token create transaction example.
cdhts
If you completed a previous example in the series you can use to go back to the root directory and cd into this example.
cd../hts
If you want to get back to the root directory, you can CD out from any directory with this command.
cd../
You can follow along through the code walkthrough or skip ahead to execute the program here.
Step 2: Guided Code Walkthrough
Open the HTS token script (/hts/script-hts-ft...) in a code editor like VS Code, IntelliJ, or a Gitpod instance. The imports at the top include modules for interacting with the Hedera network via the SDK. The @hashgraph/sdk enables account management and transactions like creating a token while the dotenv package loads environment variables from the .env file, such as the operator account ID, private key, and name variables.
script-hts-ft.js
import { Client, PrivateKey, AccountId, TokenCreateTransaction, TokenType,} from'@hashgraph/sdk';import dotenv from'dotenv';import { createLogger,} from'../util/util.js';constlogger=awaitcreateLogger({ scriptId:'htsFt', scriptCategory:'task',});let client;asyncfunctionscriptHtsFungibleToken() {logger.logStart('Hello Future World - HTS Fungible Token - start');// Read in environment variables from `.env` file in parent directorydotenv.config({ path:'../.env' });logger.log('Read .env file');// Initialize the operator accountconstoperatorIdStr=process.env.OPERATOR_ACCOUNT_ID;constoperatorKeyStr=process.env.OPERATOR_ACCOUNT_PRIVATE_KEY;if (!operatorIdStr ||!operatorKeyStr) {thrownewError('Must set OPERATOR_ACCOUNT_ID and OPERATOR_ACCOUNT_PRIVATE_KEY environment variables'); }constoperatorId=AccountId.fromString(operatorIdStr);constoperatorKey=PrivateKey.fromStringECDSA(operatorKeyStr); client =Client.forTestnet().setOperator(operatorId, operatorKey);logger.log('Using account:', operatorIdStr);}
packagemainimport ("encoding/json""fmt""log""os""strings""time""github.com/hashgraph/hedera-sdk-go/v2""github.com/imroc/req/v3""github.com/joho/godotenv")typeTokenMNAPIResponsestruct { Name string`json:"name"` TotalSupply string`json:"total_supply"`}funcmain() { fmt.Println("🏁 Hello Future World - HTS Fungible Token - start")// Load environment variables from .env file err := godotenv.Load("../.env")if err !=nil { log.Fatal("Error loading .env file") }// Initialize the operator account operatorIdStr := os.Getenv("OPERATOR_ACCOUNT_ID") operatorKeyStr := os.Getenv("OPERATOR_ACCOUNT_PRIVATE_KEY")if operatorIdStr ==""|| operatorKeyStr =="" { log.Fatal("Must set OPERATOR_ACCOUNT_ID, OPERATOR_ACCOUNT_PRIVATE_KEY") } operatorId, _ := hedera.AccountIDFromString(operatorIdStr)// Necessary because Go SDK v2.37.0 does not handle the `0x` prefix automatically// Ref: https://github.com/hashgraph/hedera-sdk-go/issues/1057 operatorKeyStr = strings.TrimPrefix(operatorKeyStr, "0x") operatorKey, _ := hedera.PrivateKeyFromStringECDSA(operatorKeyStr) fmt.Printf("Using account: %s\n", operatorId) fmt.Printf("Using operatorKey: %s\n", operatorKeyStr)}
Create a Hedera Testnet Client
To set up your Hedera Testnet client, create the client and configure the operator using your Testnet account ID and private key. The operator account covers transaction and query fees in HBAR, with all transactions requiring a signature from the operator's private key for authorization.
script-hts-ft.js
// The client operator ID and key is the account that will be automatically set to pay for the transaction fees for each transactionclient =Client.forTestnet().setOperator(operatorId, operatorKey);//Set the default maximum transaction fee (in Hbar)client.setDefaultMaxTransactionFee(newHbar(100));//Set the maximum payment for queries (in Hbar)client.setDefaultMaxQueryPayment(newHbar(50));
ScriptHtsFt.java
// The client operator ID and key is the account that will be automatically set to pay for the transaction fees for each transactionClient client =Client.forTestnet().setOperator(operatorId, operatorKey);//Set the default maximum transaction fee (in HBAR)client.setDefaultMaxTransactionFee(newHbar(100));//Set the default maximum payment for queries (in HBAR)client.setDefaultMaxQueryPayment(newHbar(50));
// The client operator ID and key is the account that will be automatically set to pay for the transaction fees for each transactionclient := hedera.ClientForTestnet()client.SetOperator(operatorId, operatorKey)// Set the default maximum transaction fee (in HBAR)client.SetDefaultMaxTransactionFee(hedera.HbarFrom(100, hedera.HbarUnits.Hbar))// Set the default maximum payment for queries (in HBAR)client.SetDefaultMaxQueryPayment(hedera.HbarFrom(50, hedera.HbarUnits.Hbar))
To avoid encountering the INSUFFICIENT_TX_FEE error while executing transactions, you can also specify the maximum transaction fee limit through the .setDefaultMaxTransactionFee() method and the maximum query payment through the .setDefaultMaxQueryPayment() method to control costs, ensuring your client operates within your desired financial limits on the Hedera Testnet.
🚨 How to resolve the INSUFFICIENT_TX_FEE error
To resolve this error, you must adjust the max transaction fee to a higher value suitable for your needs.
Here is a simple example addition to your code:
Copy
constmaxTransactionFee=newHbar(XX); // replace XX with desired fee in Hbar
In this example, you can set maxTransactionFee to any value greater than 5 HBAR (or 500,000,000 tinybars) to avoid the "INSUFFICIENT_TX_FEE" error for transactions greater than 5 HBAR. Please replace XX with the desired value.
To implement this new max transaction fee, you use the setDefaultMaxTransactionFee() method as shown below:
To create a fungible token using the HTS, start by instantiating a TokenCreateTransaction. Set the token type to TokenType.FungibleCommon, which functions similarly to ERC-20 tokens on Ethereum, meaning all token units are interchangeable.
Configure the token with the required properties:
Token Name: The name of the token.
Token Symbol: A publicly visible symbol for the token (e.g., hBARK).
Treasury Account ID: The account holding the initial token supply.
Other optional fields
Default values will apply if left unspecified:
No admin key makes the token immutable (unchangeable).
No supply key fixes the token's supply (no minting or burning).
No token type defaults to fungible.
Unlike NFTs, fungible tokens don’t require decimals or an initial supply of zero. For example, an initial supply of 10,000 units is represented as 1000000 in code to account for two decimals.
script-hts-ft.js
// Create the token create transactionconsttokenCreateTx=awaitnewTokenCreateTransaction()//Set the transaction memo.setTransactionMemo(`Hello Future World token - ${logger.version}`)// HTS `TokenType.FungibleCommon` behaves similarly to ERC20.setTokenType(TokenType.FungibleCommon)// Configure token options: name, symbol, decimals, initial supply.setTokenName(`${yourName} coin`)//Set the token symbol.setTokenSymbol(logger.scriptId)//Set the token decimals to 2.setDecimals(2)//Set the initial supply of the token to 10,000.setInitialSupply(1000000)// Configure token access permissions: treasury account, admin, freezing.setTreasuryAccountId(operatorId)//Set the admin key of the the token to the operator account.setAdminKey(operatorKey) //Set the freeze default value to false.setFreezeDefault(false)// Freeze the transaction to prepare for signing.freezeWith(client);// Get the transaction ID of the transaction. // The SDK automatically generates and assigns a transaction ID when the transaction is createdconsttokenCreateTxId=tokenCreateTx.transactionId;logger.log('The token create transaction ID: ',tokenCreateTxId.toString());
ScriptHtsFt.java
// Create a HTS token create transactionTokenCreateTransaction tokenCreateTx =newTokenCreateTransaction()//Set the transaction memo.setTransactionMemo("Hello Future World token - xyz")// HTS "TokenType.FungibleCommon" behaves similarly to ERC20.setTokenType(TokenType.FUNGIBLE_COMMON)// Configure token options: name, symbol, decimals, initial supply.setTokenName("htsFt coin")// Set the token symbol.setTokenSymbol("HTSFT")// Set the token decimals to 2.setDecimals(2)// Set the initial supply of the token to 1,000,000.setInitialSupply(1_000_000)// Configure token access permissions: treasury account, admin, freezing.setTreasuryAccountId(operatorId)// Set the freeze default value to false.setFreezeDefault(false)//Freeze the transaction and prepare for signing.freezeWith(client);// Get the transaction ID of the transaction. The SDK automatically generates and assigns a transaction ID when the transaction is createdTransactionId tokenCreateTxId =tokenCreateTx.getTransactionId();System.out.println("The token create transaction ID: "+tokenCreateTxId.toString());
// Create the token create transactiontokenCreateTx, _ := hedera.NewTokenCreateTransaction().//Set the transaction memoSetTransactionMemo("Hello Future World token - xyz").// HTS `TokenType.FungibleCommon` behaves similarly to ERC20SetTokenType(hedera.TokenTypeFungibleCommon).// Configure token options: name, symbol, decimals, initial supplySetTokenName(`htsFt coin`).// Set the token symbolSetTokenSymbol("HTSFT").// Set the token decimals to 2SetDecimals(2).// Set the initial supply of the token to 1,000,000SetInitialSupply(1_000_000).// Configure token access permissions: treasury account, admin, freezingSetTreasuryAccountID(operatorId).// Set the freeze default value to falseSetFreezeDefault(false).//Freeze the transaction and prepare for signingFreezeWith(client)// Get the transaction ID of the transaction. The SDK automatically generates and assigns a transaction ID when the transaction is createdtokenCreateTxId := tokenCreateTx.GetTransactionID()fmt.Printf("The token create transaction ID: %s\n", tokenCreateTxId.String())
Key terminology for HTS token create transaction
Token Type: Fungible tokens, declared using TokenType.FungibleCommon, may be thought of as analogous to ERC20 tokens. Note that HTS also supports another token type, TokenType.NonFungibleUnique, which may be thought of as analogous to ERC721 tokens.
Token Name: This is the full name of the token. For example, "Singapore Dollar".
Token Symbol: This is the abbreviation of the token's name. For example, "SGD".
Decimals: This is the number of decimal places the currency uses. For example, 2 mimics "cents", where the smallest unit of the token is 0.01 (1/100) of a single token.
Initial Supply: This is the number of units of the token to "mint" when first creating the token. Note that this is specified in the smallest units, so 1_000_000 initial supply when decimals is 2, results in 10_000 full units of the token being minted. It might be easier to think about it as "one million cents equals ten thousand dollars".
Treasury Account ID: This is the account for which the initial supply of the token is credited. For example, using operatorId in this examplewould mean that your specified testnet account receives all the tokens when they are minted.
Admin Key: This is the key that is authorized to administrate this token. For example, using operatorKey would mean that your testnet account key would authorize (required to sign related transactions) to perform actions such as minting additional supply.
Sign and Submit the Token Create Transaction
The transaction must be signed using the operator's private key. This step ensures that the transaction is authenticated and authorized by the operator. Once signed, the transaction is submitted to the Hedera Testnet, where it will be processed and validated.
After submission, get the transaction receipt. The receipt contains important information about the transaction, such as its status and any resulting data. The receipt is used to confirm that the transaction was successfully processed and to get the new token ID. The token ID uniquely identifies the newly created token on the Hedera network and is required for any subsequent operations with the token.
script-hts-ft.js
// Sign the transaction with the operator's private keyconsttokenCreateTxSigned=awaittokenCreateTx.sign(operatorKey);// Submit the signed transaction to the Hedera networkconsttokenCreateTxSubmitted=awaittokenCreateTxSigned.execute(client);// Get the transaction receiptconsttokenCreateTxReceipt=awaittokenCreateTxSubmitted.getReceipt(client);// Get and log the newly created token ID to the consoleconsttokenId=tokenCreateTxReceipt.tokenId;logger.log('tokenId:',tokenId.toString());
ScriptHtsFt.java
// Sign the transaction with the operator's private keyTokenCreateTransaction tokenCreateTxSigned =tokenCreateTx.sign(operatorKey);// Submit the transaction to the Hedera networkTransactionResponse tokenCreateTxSubmitted =tokenCreateTxSigned.execute(client);// Get the transaction receiptTransactionReceipt tokenCreateTxReceipt =tokenCreateTxSubmitted.getReceipt(client);// Get the token IDTokenId tokenId =tokenCreateTxReceipt.tokenId;System.out.println("Token ID: "+tokenId.toString());
script-hts-ft.go
// Sign the transaction with the operator's private keytokenCreateTxSigned := tokenCreateTx.Sign(operatorKey)// Submit the transaction to the Hedera networktokenCreateTxSubmitted, _ := tokenCreateTxSigned.Execute(client)// Get the transaction receipttokenCreateTxReceipt, _ := tokenCreateTxSubmitted.GetReceipt(client)// Get the newly created token IDtokenId := tokenCreateTxReceipt.TokenIDfmt.Printf("Token ID: %s\n", tokenId.String())
Query the Account Token Balance Mirror Node API
Mirror nodes store the history of transactions that took place on the network. To query the token balance of your account, use the Mirror Node API with the path /api/v1/tokens/{tokenId}. This API endpoint allows you to get the balance information about a specific token by replacing {tokenId} with your actual token ID. Since the treasury account was configured as your own account, it will hold the entire initial supply of the token.
Specify tokenId within the URL path
The constructed tokenVerifyMirrorNodeApiUrl string should look like this:
You can perform the same Mirror Node API query as tokenVerifyMirrorNodeApiUrl above. This is what the relevant part of the Swagger page would look like when doing so:
➡ You can learn more about the Mirror Nodes via its documentation: REST API.
Step 3: Run the Token Create Transaction Script
In the terminal, cd into the ./hts directory and run the token create transaction script:
nodescript-hts-ft.js
gradlerun
gomodtidygorunscript-hts-ft.go
Sample output:
🏁 Hello Future World - HTS Fungible Token - start …
Read .env file
Using account: 0.0.1455
🟣 Creating new HTS token …
↪️ file:///workspace/hello-future-world-x/hts/script-hts-ft...
The token create transaction ID: 0.0.46495@1722971043.397070956
tokenId: 0.0.5878530
🟣 View the token on HashScan …
↪️ file:///workspace/hello-future-world-x/hts/script-hts-ft...
Paste URL in browser:
https://hashscan.io/testnet/token/0.0.46599
🟣 Get token data from the Hedera Mirror Node …
↪️ file:///workspace/hello-future-world-x/hts/script-hts-ft...
The token Hedera Mirror Node API URL:
https://testnet.mirrornode.hedera.com/api/v1/tokens/0.0.46599
The name of this token: bguiz coin
The total supply of this token: 1000000
🎉 Hello Future World - HTS Fungible Token - complete …
Copy and paste the HashScan URL in your browser to view the token creation transaction details and verify that:
The token should exist, and its "token ID" should match tokenId. (1)
The "name" and "symbol" should be shown as the same values derived from your name (or nickname) that you chose earlier. (2)
The "treasury account" should match operatorId. (3)
Both the "total supply" and "initial supply" should be 10,000. (4)
Note: "total supply" and "initial supply" are not displayed as 1,000,000 because of the two decimal places configured. Instead, these are displayed as 10,000.00.