How to Approve Fungible Token and NFT Allowances on Hedera – Part 1: Using the SDK
Headshot
Oct 10, 2022
by Ed Marquez
Developer Relations Engineer

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.

Try It Yourself

  • Get a Hedera testnet account
    • This portal acts like a faucet, giving you 10,000 test HBAR every 24 hours
  • Use this Codesandbox to try the example
    • Fork the sandbox
    • Remember to provide testnet account credentials in the .env file
    • Open a new terminal to execute index_ft.js or index_nft.js
  • Get the example code from GitHub

Example: Alice Spends FT and NFT on Behalf of the Treasury

This example guides you through the following steps:

  1. Creating additional Hedera accounts and native tokens (Treasury, Alice, Bob, and the HBAR ROCKS token)
  2. Treasury approving an allowance of HBAR ROCKS tokens for Alice (50 FT / 3 NFT)
  3. Alice performing an approved token transfer from Treasury to Bob (45 FT / 1 NFT)
  4. Treasury deleting the token allowance for Alice

After completing all steps, your console should look something like this (for FT/NFT):

How to Approve Fungible and NFT Allowances on Hedera Part 1 SDK Image 0

Fungible token example output (left) and NFT example output (right)

1. Create Accounts and HTS Token

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.

  • Create new accounts for Treasury, Alice, and Bob. Start by specifying the initial balance of each new account (initBalance) to be 10 HBAR
  • Generate and record the private key for each account. Hedera supports ED25519 and ECDSA keys
  • Use the function accountCreateFcn to create the new accounts
    • The function returns the status of the transaction (treasurySt) and the new account ID (treasuryId
    • The inputs are the newly generated private key (treasuryKey), initBalance, and the client object
  • Output to the console a link to the mirror node explorer, HashScan, showing information about the new accounts
Code Snippet Background
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}`);
  • The code tabs below show the functions called to create the FT (htsTokens.createFtFcn) and NFT (htsTokens.createMintNftFcn).
    • Both functions return the ID of the new token (tokenId), and token information from a query (tokenInfo)
    • For creation of the FT, the inputs are the token name (HBAR ROCKS), token symbol (HROCK), the initial supply (100), the treasury account for the token (treasuryId), the treasury key for transaction authorization (treasuryKey), and the client object
    • For the NFT creation, the inputs are the token name (HBAR ROCKS), token symbol (HROCK), the initial supply (0), the maximum token supply (1000), the treasury account for the token (treasuryId), the treasury key for transaction authorization (treasuryKey), and the client object
  • Output to the console tokenId and the token supply
Fungible Token (FT)
  • Fungible Token (FT)
  • Non-Fungible Token (NFT)
Code Snippet Background
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.

accountCreateFcn
  • accountCreateFcn
  • htsTokens.createFtFcn
  • htsTokens.createMintNftFcn
  • queries.tokenQueryFcn
Code Snippet Background
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):

How to Approve Fungible and NFT Allowances on Hedera Part 1 SDK Image 1

2. Approve FT / NFT Allowance

From the token creation and minting in the previous step, Treasury has an HBAR ROCK balance of 100 FT / 5 NFT.

  • The allowance amount approved for Alice to spend on behalf of Treasury (allowBal) is 50 in the case of the fungible token - and serials #1, #2, and #3 in the case of the NFT
  • For the fungible token example, use the function approvals.ftAllowanceFcn to approve the allowance
    • The function returns the receipt object of the transaction (allowanceApproveFtRx)
    • The inputs are tokenId, the owner account ID (treasuryId), the spender account ID (aliceId), allowBal, the private key of the owner for transaction authorization (treasuryKey), and client
  • For the NFT example, use the function approvals.nftAllowanceFcn to approve the allowance
    • Serials #1, #2, and #3 of the NFT are specified using he NftId class of the SDK
    • The function returns the receipt object of the transaction (allowanceApproveNftRx)
    • The inputs are tokenId, treasuryId, aliceId, the array of NFTs to approve (nft2approve), treasuryKey, and client
  • Output to the console:
    • The status of the allowance approval transaction
    • A mirror node REST API request that shows fungible token allowances approved by Treasury
    • Account balances using queries.balanceCheckerFcn
Fungible Token (FT)
  • Fungible Token (FT)
  • Non-Fungible Token (NFT)
Code Snippet Background
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}`);
Code Snippet Background
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.

approvals.ftAllowanceFcn
  • approvals.ftAllowanceFcn
  • approvals.nftAllowanceFcn
  • queries.balanceCheckerFcn
Code Snippet Background
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}`
        );
    }
}

Console Output (FT/NFT):

How to Approve Fungible and NFT Allowances on Hedera Part 1 SDK Image 2

3. Perform Approved Token Transfer

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.

  • To perform an approved transfer of 45 fungible tokens (sendBal), use the function transfers.ftAllowanceFcn
    • The function returns the receipt object of the transaction (allowanceSendFtRx)
    • The inputs are tokenId, the owner account ID (treasuryId), the receiver account ID (bobId), sendBal, the spender account ID (aliceId), the private key of the spender for transaction authorization (aliceKey), and client
  • To perform an approved transfer of NFT serial #3 (nft3), use the function transfers.nftAllowanceFcn
    • The function returns the receipt object of the transaction (allowanceSendNftRx)
    • The inputs are treasuryId, bobId, nft3, aliceId, aliceKey, and client
  • For both cases, output to the console:
    • The status of the approved transfer transaction
    • Account balances using queries.balanceCheckerFcn
Fungible Token (FT)
  • Fungible Token (FT)
  • Non-Fungible Token (NFT)
Code Snippet Background
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`);
Code Snippet Background
await queries.balanceCheckerFcn(treasuryId, tokenId, client);
await queries.balanceCheckerFcn(aliceId, tokenId, client);
await queries.balanceCheckerFcn(bobId, tokenId, client);

Helper functions

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:

  • The methods .addApprovedTokenTransfer() and .addApprovedNftTransfer() are used to specify the amount of fungible token or NFT serial # coming out of the owner’s account, respectively
  • The spender must either generate the transaction ID or be the client submitting the transaction for the approved transfer to be successful
    • In this case, .setTransactionId(TransactionId.generate(spender)) generates the transaction ID with the spender and sets it for the transfer transaction
  • The transaction must be signed with the spender’s private key
transfers.ftAllowanceFcn
  • transfers.ftAllowanceFcn
  • transfers.nftAllowanceFcn
Code Snippet Background
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;
}

Console Output (FT/NFT):

How to Approve Fungible and NFT Allowances on Hedera Part 1 SDK Image 3

4. Delete FT/NFT Allowance

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 function approvals.ftAllowanceFcn from before is used again for the fungible token, but now passing a value of 0 HBAR (allowBal)
  • For the NFT scenario, call the function approvals.nftAllowanceDeleteFcn
    • The function returns the receipt object of the transaction (allowanceDeleteNftRx)
    • The inputs are treasuryId, an array of NFTs to disallow (nft2disallow), the private key of the owner for transaction authorization (treasuryKey), and client
  • Output to the console:
    • Status of the allowance deletion transaction
    • A mirror node REST API request that shows fungible token allowances for Treasury

The last step is to join the Hedera Developer Discord!

Fungible Token (FT)
  • Fungible Token (FT)
  • Non-Fungible Token (NFT)
Code Snippet Background
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}`);
Code Snippet Background
console.log(`
====================================================
THE END - NOW JOIN: https://hedera.com/discord
====================================================\n`);

Helper functions

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.

Code Snippet Background
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;
}

Console Output (FT/NFT):

How to Approve Fungible and NFT Allowances on Hedera Part 1 SDK Image 4

Summary

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.

Continue Learning