Allowances grant another account (spender) the right to transfer HBAR, fungible tokens (FT), and non-fungible tokens (NFTs) from your account (owner). The ability to approve allowances is important because it enables applications like exchanges and wallets to perform transfers on behalf of their customers without requiring a customer to sign every single transaction in advance. You can approve allowances and perform approved transfers on Hedera as you build things like NFT exchanges, marketplaces for carbon assets, games, and more.
This tutorial series shows you how to approve allowances for fungible tokens and NFTs. Part 1 focuses on approving token allowances using the Hedera JavaScript SDK. Part 2 and Part 3 show how to approve token allowances using Solidity HTS precompiles and ERC standard calls, respectively.
To learn how to approve HBAR allowances, check out How to Approve HBAR Allowances on Hedera Using the SDK.
This example guides you through the following steps:
After completing all steps, your console should look something like this (for FT/NFT):
Fungible token example output (left) and NFT example output (right)
There are five entities in this scenario: Operator, Treasury, Alice, Bob, and the HBAR ROCKS token. Your testnet credentials from the Hedera portal should be used for the operator variables, which are used to initialize the Hedera client that submits transactions to the network and gets confirmations.
console.log(`\nSTEP 1 ===================================\n`); console.log(`- Creating Hedera accounts...\n`); const initBalance = new Hbar(10); const treasuryKey = PrivateKey.generateED25519(); const [treasurySt, treasuryId] = await accountCreateFcn(treasuryKey, initBalance, client); console.log(`- Treasury's account: https://hashscan.io/#/testnet/account/${treasuryId}`); const aliceKey = PrivateKey.generateED25519(); const [aliceSt, aliceId] = await accountCreateFcn(aliceKey, initBalance, client); console.log(`- Alice's account: https://hashscan.io/#/testnet/account/${aliceId}`); const bobKey = PrivateKey.generateED25519(); const [bobSt, bobId] = await accountCreateFcn(bobKey, initBalance, client); console.log(`- Bob's account: https://hashscan.io/#/testnet/account/${bobId}`);
const [tokenId, tokenInfo] = await htsTokens.createFtFcn("HBAR ROCKS", "HROCK", 100, treasuryId, treasuryKey, client); console.log(`\n- Token ID: ${tokenId}`); console.log(`- Initial token supply: ${tokenInfo.totalSupply.low}`);
const [tokenId, tokenInfo] = await htsTokens.createMintNftFcn("HBAR ROCKS", "HROCK", 0, 1000, treasuryId, treasuryKey, client); console.log(`\n- Token ID: ${tokenId}`); console.log(`- Token supply after minting NFTs: ${tokenInfo.totalSupply.low}`);
Helper Functions
The helper function accountCreateFcn uses the AccountCreateTransaction() class of the SDK to generate the new accounts for Treasury, Alice, and Bob.
The functions htsTokens.createFtFcn and htsTokens.createMintNftFcn both use the TokenCreateTransaction() class of the SDK. However, each instance is customized appropriately to create a fungible or non-fungible token. In the NFT case, the helper function also uses the TokenMintTransaction() class to mint a batch of 5 NFTs in a single transaction. For additional details on using the Hedera Token Service (HTS) for fungible and non-fungible tokens, be sure to read the tutorial series Get Started with the Hedera Token Service.
You can easily reuse the helper functions in this example in case you need to do tasks like creating more accounts or tokens in the future. We’ll use this modular approach throughout the article.
async function accountCreateFcn(pvKey, iBal, client) { const response = await new AccountCreateTransaction() .setInitialBalance(iBal) .setKey(pvKey.publicKey) .setMaxAutomaticTokenAssociations(10) .execute(client); const receipt = await response.getReceipt(client); return [receipt.status, receipt.accountId]; }
export async function createFtFcn(tName, tSymbol, iSupply, id, pvKey, client) { const tokenCreateTx = new TokenCreateTransaction() .setTokenName(tName) .setTokenSymbol(tSymbol) .setDecimals(0) .setInitialSupply(iSupply) .setTreasuryAccountId(id) .setAdminKey(pvKey.publicKey) .setSupplyKey(pvKey.publicKey) .freezeWith(client); const tokenCreateSign = await tokenCreateTx.sign(pvKey); const tokenCreateSubmit = await tokenCreateSign.execute(client); const tokenCreateRx = await tokenCreateSubmit.getReceipt(client); const tokenId = tokenCreateRx.tokenId; const tokenInfo = await queries.tokenQueryFcn(tokenId, client); return [tokenId, tokenInfo]; }
export async function createMintNftFcn(tName, tSymbol, iSupply, maxSupply, id, pvKey, client) { const nftCreate = new TokenCreateTransaction() .setTokenName(tName) .setTokenSymbol(tSymbol) .setTokenType(TokenType.NonFungibleUnique) .setSupplyType(TokenSupplyType.Finite) .setDecimals(0) .setInitialSupply(iSupply) .setTreasuryAccountId(id) .setSupplyKey(pvKey.publicKey) .setMaxSupply(maxSupply) // .setCustomFees([nftCustomFee]) // .setAdminKey(adminKey) // .setPauseKey(pauseKey) // .setFreezeKey(freezeKey) // .setWipeKey(wipeKey) .freezeWith(client); const nftCreateTxSign = await nftCreate.sign(pvKey); const nftCreateSubmit = await nftCreateTxSign.execute(client); const nftCreateRx = await nftCreateSubmit.getReceipt(client); const tokenId = nftCreateRx.tokenId; // // MINT NEW BATCH OF NFTs const CID = [ Buffer.from("ipfs://QmNPCiNA3Dsu3K5FxDPMG5Q3fZRwVTg14EXA92uqEeSRXn"), Buffer.from("ipfs://QmZ4dgAgt8owvnULxnKxNe8YqpavtVCXmc1Lt2XajFpJs9"), Buffer.from("ipfs://QmPzY5GxevjyfMUF5vEAjtyRoigzWp47MiKAtLBduLMC1T"), Buffer.from("ipfs://Qmd3kGgSrAwwSrhesYcY7K54f3qD7MDo38r7Po2dChtQx5"), Buffer.from("ipfs://QmWgkKz3ozgqtnvbCLeh7EaR1H8u5Sshx3ZJzxkcrT3jbw"), ]; const mintTx = new TokenMintTransaction() .setTokenId(tokenId) .setMetadata(CID) //Batch minting - UP TO 10 NFTs in single tx .freezeWith(client); const mintTxSign = await mintTx.sign(pvKey); const mintTxSubmit = await mintTxSign.execute(client); const mintRx = await mintTxSubmit.getReceipt(client); const tokenInfo = await queries.tokenQueryFcn(tokenId, client); return [tokenId, tokenInfo]; }
export async function tokenQueryFcn(tkId, client) { let info = await new TokenInfoQuery().setTokenId(tkId).execute(client); return info; }
Console Output (FT/NFT):
From the token creation and minting in the previous step, Treasury has an HBAR ROCK balance of 100 FT / 5 NFT.
console.log(`\nSTEP 2 ===================================\n`); console.log(`- Treasury approving fungible token allowance for Alice...\n`); let allowBal = 50; const allowanceApproveFtRx = await approvals.ftAllowanceFcn(tokenId, treasuryId, aliceId, allowBal, treasuryKey, client); console.log(`- Allowance approval status: ${allowanceApproveFtRx.status}`); console.log(`- https://testnet.mirrornode.hedera.com/api/v1/accounts/${treasuryId}/allowances/tokens \n`);
console.log(`\nSTEP 2 ===================================\n`); console.log(`- Treasury approving NFT allowance for Alice...\n`); // Can approve all serials under a NFT collection // Or can approve individual serials under a NFT collection const nft1 = new NftId(tokenId, 1); const nft2 = new NftId(tokenId, 2); const nft3 = new NftId(tokenId, 3); const nft2approve = [nft1, nft2, nft3]; const allowanceApproveNftRx = await approvals.nftAllowanceFcn(tokenId, treasuryId, aliceId, nft2approve, treasuryKey, client); console.log(`- Allowance approval status: ${allowanceApproveNftRx.status}`);
await queries.balanceCheckerFcn(treasuryId, tokenId, client); await queries.balanceCheckerFcn(aliceId, tokenId, client); await queries.balanceCheckerFcn(bobId, tokenId, client);
Helper functions
The helper functions approvals.ftAllowanceFcn and approvals.nftAllowanceFcn both use AccountAllowanceApproveTransaction() from the SDK to grant the token allowances for the spender from an owner’s account balance. The difference is the methods used - .approveTokenAllowance() is used to approve fungible token allowances, whereas .approveTokenNftAllowance() is used to approve NFT allowances by serial number. Note that for NFTs you also have the option to approve all serial numbers by using the .approveTokenNftAllowanceAllSerials() method.
The function queries.balanceCheckerFcn uses AccountBalanceQuery() to check and display the HBAR and token balances for a given account ID or contract ID.
export async function ftAllowanceFcn(tId, owner, spender, allowBal, pvKey, client) { const allowanceTx = new AccountAllowanceApproveTransaction().approveTokenAllowance(tId, owner, spender, allowBal).freezeWith(client); const allowanceSign = await allowanceTx.sign(pvKey); const allowanceSubmit = await allowanceSign.execute(client); const allowanceRx = await allowanceSubmit.getReceipt(client); return allowanceRx; }
export async function nftAllowanceFcn(tId, owner, spender, nft2Approve, pvKey, client) { const allowanceTx = new AccountAllowanceApproveTransaction() // .approveTokenNftAllowanceAllSerials(tId, owner, spender) // Can approve all serials under a NFT collection .approveTokenNftAllowance(nft2Approve[0], owner, spender) // Or can approve individual serials under a NFT collection .approveTokenNftAllowance(nft2Approve[1], owner, spender) .approveTokenNftAllowance(nft2Approve[2], owner, spender) .freezeWith(client); const allowanceSign = await allowanceTx.sign(pvKey); const allowanceSubmit = await allowanceSign.execute(client); const allowanceRx = await allowanceSubmit.getReceipt(client); return allowanceRx; }
export async function balanceCheckerFcn(acId, tkId, client) { let balanceCheckTx = []; try { balanceCheckTx = await new AccountBalanceQuery().setAccountId(acId).execute(client); console.log( `- Balance of account ${acId}: ${balanceCheckTx.hbars.toString()} + ${balanceCheckTx.tokens._map.get( tkId.toString() )} unit(s) of token ${tkId}` ); } catch { balanceCheckTx = await new AccountBalanceQuery().setContractId(acId).execute(client); console.log( `- Balance of contract ${acId}: ${balanceCheckTx.hbars.toString()} + ${balanceCheckTx.tokens._map.get( tkId.toString() )} unit(s) of token ${tkId}` ); } }
In this step, Alice spends tokens from the allowance granted by Treasury. This means that Alice transfers tokens from Treasury’s account to Bob’s.
console.log(`\nSTEP 3 ===================================\n`); console.log(`- Alice performing allowance transfer from Treasury to Bob...\n`); const sendBal = 45; // Spender must generate the TX ID or be the client const allowanceSendFtRx = await transfers.ftAllowanceFcn(tokenId, treasuryId, bobId, sendBal, aliceId, aliceKey, client); console.log(`- Allowance transfer status: ${allowanceSendFtRx.status} \n`);
console.log(`\nSTEP 3 ===================================\n`); console.log(`- Alice performing allowance transfer from Treasury to Bob...\n`); const allowanceSendNftRx = await transfers.nftAllowanceFcn(treasuryId, bobId, nft3, aliceId, aliceKey, client); console.log(`- Allowance transfer status: ${allowanceSendNftRx.status} \n`);
The functions transfers.ftAllowanceFcn and transfers.nftAllowanceFcn both use TransferTransaction() from the SDK to enable a spender to use an allowance approved by an owner. Notice the following:
export async function ftAllowanceFcn(tId, owner, receiver, sendBal, spender, spenderPvKey, client) { const approvedSendTx = new TransferTransaction() .addApprovedTokenTransfer(tId, owner, -sendBal) .addTokenTransfer(tId, receiver, sendBal) .setTransactionId(TransactionId.generate(spender)) // Spender must generate the TX ID or be the client .freezeWith(client); const approvedSendSign = await approvedSendTx.sign(spenderPvKey); const approvedSendSubmit = await approvedSendSign.execute(client); const approvedSendRx = await approvedSendSubmit.getReceipt(client); return approvedSendRx; }
export async function nftAllowanceFcn(owner, receiver, nft2Send, spender, spenderPvKey, client) { const approvedSendTx = new TransferTransaction() .addApprovedNftTransfer(nft2Send, owner, receiver) .setTransactionId(TransactionId.generate(spender)) // Spender must generate the TX ID or be the client .freezeWith(client); const approvedSendSign = await approvedSendTx.sign(spenderPvKey); const approvedSendSubmit = await approvedSendSign.execute(client); const approvedSendRx = await approvedSendSubmit.getReceipt(client); return approvedSendRx; }
In this step, Treasury removes the token allowances for Alice. Fungible token allowances are removed by simply setting the allowance value to zero. On the other hand, NFT allowances are removed by specifying the NFT serials that should be disallowed.
The last step is to join the Hedera Developer Discord!
console.log(`\nSTEP 4 ===================================\n`); console.log(`- Treasury deleting fungible token allowance for Alice...\n`); allowBal = 0; const allowanceDeleteFtRx = await approvals.ftAllowanceFcn(tokenId, treasuryId, aliceId, allowBal, treasuryKey, client); console.log(`- Allowance deletion status: ${allowanceDeleteFtRx.status}`); console.log(`- https://testnet.mirrornode.hedera.com/api/v1/accounts/${treasuryId}/allowances/tokens`);
console.log(`\nSTEP 4 ===================================\n`); console.log(`- Treasury deleting NFT allowance for Alice...\n`); const nft2disallow = [nft1, nft2]; const allowanceDeleteNftRx = await approvals.nftAllowanceDeleteFcn(treasuryId, nft2disallow, treasuryKey, client); console.log(`- Allowance deletion status: ${allowanceDeleteNftRx.status}`);
console.log(` ==================================================== THE END - NOW JOIN: https://hedera.com/discord ====================================================\n`);
The function approvals.nftAllowanceDeleteFcn uses AccountAllowanceDeleteTransaction() from the SDK to remove an allowance previously approved by an owner. Keep in mind that 20 is the maximum number of NFT serials that can be disallowed in a single transaction.
export async function nftAllowanceDeleteFcn(owner, nft2disallow, pvKey, client) { const allowanceTx = new AccountAllowanceDeleteTransaction() .deleteAllTokenNftAllowances(nft2disallow[0], owner) .deleteAllTokenNftAllowances(nft2disallow[1], owner) .freezeWith(client); const allowanceSign = await allowanceTx.sign(pvKey); const allowanceSubmit = await allowanceSign.execute(client); const allowanceRx = await allowanceSubmit.getReceipt(client); return allowanceRx; }
Now you know how to approve allowances for fungible tokens and NFTs on Hedera using the JavaScript SDK. You can try this example with the other officially supported SDKs for Java, Go, and Swift.
In Part 2 and Part 3 of this tutorial series, you will learn how to approve token allowances using Solidity HTS precompiles and ERC standard call.