Hedera
  • Welcome to Hedera — let’s build the future
  • Getting Started
    • Environment Setup
    • Web2 Developers
      • Transfer HBAR
      • Create a Token
      • Create a Topic
    • EVM Developers
      • Deploy a Contract
  • Tutorials
    • Smart Contracts
      • How to Mint & Burn an ERC-721 Token Using Hardhat and Ethers (Part 1)
      • How to Set Access Control, a Token URI, Pause, and Transfer an ERC-721 Token Using Hardhat (Part 2)
      • How to Upgrade an ERC-721 Token with OpenZeppelin UUPS Proxies and Hardhat (Part 3)
      • How to Verify a Smart Contract on HashScan
      • Deploy a Smart Contract Using Remix
      • Deploy a Smart Contract Using Hardhat and Hiero JSON-RPC Relay
      • Deploy Your First Smart Contract
      • Deploy a Contract Using the Hedera Token Service
      • Send and Receive HBAR Using Solidity Smart Contracts
      • Deploy By Leveraging Ethereum Developer Tools On Hedera
      • Deploy a Subgraph Using The Graph and Hedera JSON-RPC Relay
      • Deploy Smart Contracts on Hedera Using Truffle
      • The Power of Native Hedera Tokens as ERC-20 Tokens: A step-by-step guide
      • HTS x EVM - Part 1: How to Mint NFTs
      • HTS x EVM - Part 2: KYC & Update
      • HTS x EVM - Part 3: How to Pause, Freeze, Wipe, and Delete NFTs
      • Hedera Smart Contracts Workshop
        • Setup
        • Solidity
        • Hedera SDK JS
        • Hardhat and EthersJs
        • Outro
      • Foundry
        • How to Setup Foundry and Write a Basic Unit Test
        • How to Deploy and Verify a Hedera Smart Contract with Foundry
        • How to Test A Solidity Event
        • How to Fork Testnet on Latest Block
    • Consensus
      • Submit Your First Message
      • Submit Message to Private Topic
      • Query Messages with Mirror Node
    • Tokens
      • Create and Transfer Your First NFT
      • Create and Transfer Your First Fungible Token
      • Create and Transfer an NFT using a Solidity Contract
      • Structure Your Token Metadata Using JSON Schema V2
      • Hedera Token Service - Part 1: How to Mint NFTs
      • Hedera Token Service - Part 2: KYC, Update, and Scheduled Transactions
      • Hedera Token Service - Part 3: How to Pause, Freeze, Wipe, and Delete NFTs
      • Create Your First Frictionless Airdrop Campaign
    • Local Node
      • How to Run Hedera Local Node in a Cloud Development Environment (CDE)
        • Run a Local Node in Gitpod
        • Run a Local Node in Codespaces
      • How to Set Up a Hedera Local Node
      • Set Up a Hedera Local Node using the NPM CLI
    • More Tutorials
      • Create and Fund Your Hedera Testnet Account
      • How to Create a Personal Access Token (API Key) on the Hedera Portal
      • How to Auto-Create Hedera Accounts with HBAR and Token Transfers
      • How to Configure a Mirror Node and Query Data
      • How to Generate a Random Number on Hedera
      • Get Started with the Hedera Consensus Service Fabric Plugin
        • Virtual Environment Setup
      • Schedule Your First Transaction
      • How to Connect to Hedera Networks Over RPC
        • Configuring Hashio RPC endpoints
        • Configuring Hiero JSON-RPC Relay endpoints
        • Configuring Validation Cloud RPC endpoints
      • JavaScript Testing
      • Create a Hedera DApp Integrated with WalletConnect
      • How to Connect MetaMask to Hedera
    • Demo Applications
    • Starter Projects
    • Building on Hedera (course)
  • Networks
    • Mainnet
      • Mainnet Accounts
      • Mainnet Consensus Nodes
        • Node Requirements
          • FAQ
      • Fees
        • Transaction Records
    • Testnets
      • Testnet Accounts
      • Testnet Consensus Nodes
    • Localnet
      • Single Node Configuration
      • Multinode Configuration
    • Network Explorers and Tools
    • Release Notes
      • Consensus Node
      • Hedera Mirror Node
  • Core Concepts
    • Accounts
      • Account Creation
      • Auto Account Creation
      • Account Properties
    • Keys and Signatures
    • Schedule Transaction
    • Smart Contracts
      • Understanding Hedera's EVM Differences and Compatibility
        • For EVM Developers Migrating to Hedera
          • Accounts, Signature Verification & Keys (ECDSA vs. ED25519)
          • JSON-RPC Relay and EVM Tooling
          • Token Management with Hedera Token Service
          • Decimal Handling (8 vs. 18 Decimals)
          • Handling HBAR Transfers in Contracts
        • For Hedera-Native Developers Adding Smart Contract Functionality
          • Integrating ED25519 Accounts and Advanced Features Into Smart Contracts
          • JSON-RPC Relay and State Queries
          • Extending Token Management with Smart Contracts
      • Creating Smart Contracts
      • Compiling Smart Contracts
      • System Smart Contracts
        • Hedera Account Service
        • Hedera Schedule Service
      • Gas and Fees
      • JSON-RPC Relay
      • Deploying Smart Contracts
      • Smart Contract Addresses
      • Verifying Smart Contracts
      • Smart Contract Traceability
      • Tokens Managed by Smart Contracts
        • ERC-20 (Fungible Tokens)
        • ERC-721 (Non-Fungible Token)
        • ERC-3643 Real World Assets (RWA)
        • ERC-1363 (Payable Tokens)
        • Hedera Token Service System Contract
      • Wrapped HBAR (WHBAR)
      • Smart Contract Rent
      • Smart Contract Security
      • EVM Archive Node Queries
    • Tokens
      • Tokenization on Hedera
      • Hedera Token Service (HTS) Native Tokenization
        • Token Types and ID Formats
        • Token Properties
        • Token Creation
        • Custom Fee Schedule
        • Token Airdrops
      • ERC/EVM-Compatible Tokenization
      • Hybrid (HTS + EVM ) Tokenization
    • Staking
      • Staking Program
      • Stake HBAR
    • Hashgraph Consensus Algorithm
      • Gossip About Gossip
      • Virtual Voting
    • Transactions and Queries
      • Transaction Properties
    • State and History
    • Mirror Nodes
      • Hedera Mirror Node
      • One Click Mirror Node Deployment
      • Run Your Own Mirror Node
        • Run Your Own Mirror Node with Google Cloud Storage (GCS)
        • Run Your Mirror Node with Amazon Web Services S3 (AWS)
  • Open Source Solutions and Integrations
    • AI Studio on Hedera
      • ElizaOS Plugin for Hedera
      • Hedera AI Agent Kit
      • MCP Server
      • OpenConvAI
    • AI Tools for Developers
      • Hedera Hivemind
      • Kapa AI
    • Asset Tokenization Studio (ATS)
      • Web User Interface (UI)
      • Frequently Asked Questions (FAQs)
    • HashioDAO
      • Governance Token DAO
      • NFT DAO
      • Multisig DAO
      • DAO Proposals
      • Local Environment Setup
    • Hedera CLI
    • Hedera Contract Builder
    • Hedera Custodians Library
      • How to use it
    • Hedera Developers Code Repository
    • Hedera Developer Playground
    • Hedera Wallet Snap By MetaMask
      • Hedera Wallet Snap Documentation
      • Tutorial: MetaMask Snaps – What Are They and How to Use Them
    • Interoperability and Bridging
      • LayerZero
    • NFT Studio
      • Airdrop List Verifier
      • Metadata Validator
      • NFT Rarity Inspector
      • NFT Token Holders List Builder
      • NFT Risk Calculator
      • Token Balance Snapshot
      • Hedera NFT SDK
    • Oracle Networks
      • Chainlink Oracles
      • Pyth Oracles
      • Supra Oracles
    • Stablecoin Studio
      • Core Concepts
      • Web UI Application
      • CLI Management
      • TypeScript SDK
    • Hedera Guardian
    • Hedera WalletConnect
  • SDKs & APIs
    • SDKs
      • Build Your Hedera Client
      • Set Up Your Local Network
      • Network Address Book
      • Keys
        • Generate a new key pair
        • Import an existing key
        • Create a key list
        • Create a threshold key
        • Generate a mnemonic phrase
        • Recover keys from a mnemonic phrase
      • HBAR
      • Specialized Types
      • Pseudorandom Number Generator
      • Transactions
        • Create a Batch Transaction
        • Transaction ID
        • Modify transaction fields
        • Create an unsigned transaction
        • Manually sign a transaction
        • Submit a transaction
        • Sign a multisignature transaction
        • Get a transaction receipt
        • Get a transaction record
      • Schedule Transaction
        • Schedule ID
        • Create a schedule transaction
        • Sign a scheduled transaction
        • Delete a schedule transaction
        • Get schedule info
        • Network Response Messages
      • Queries
      • General Network Response Messages
      • Accounts and HBAR
        • Create an account
        • Update an account
        • Transfer cryptocurrency
        • Approve an allowance
        • Delete an allowance
        • Delete an account
        • Get account balance
        • Get account info
        • Network Response Messages
      • Consensus Service
        • Create a topic
        • Update a topic
        • Submit a message
        • Delete a topic
        • Get topic messages
        • Get topic info
        • Network Response
      • Token Service
        • Token ID
        • NFT ID
        • Token types
        • Create a token
        • Custom token fees
        • Update a token
        • Update token custom fees
        • Update NFT metadata
        • Transfer tokens
        • Airdrop a token
        • Claim a token
        • Cancel a token
        • Reject a token
        • Delete a token
        • Mint a token
        • Burn a token
        • Freeze an account
        • Unfreeze an account
        • Enable KYC account flag
        • Disable KYC account flag
        • Associate tokens to an account
        • Dissociate tokens from an account
        • Pause a token
        • Unpause a token
        • Wipe a token
        • Atomic swaps
        • Get account token balance
        • Get token info
        • Get NFT info
        • Network Response Messages
      • File Service
        • Create a file
        • Append to a file
        • Update a file
        • Delete a file
        • Get file contents
        • Get file info
        • Network Response Messages
      • Smart Contract Service
        • Delegate Contract ID
        • Create a smart contract
        • Update a smart contract
        • Delete a smart contract
        • Call a smart contract function
        • Ethereum transaction
        • Get a smart contract function
        • Get smart contract bytecode
        • Get smart contract info
        • Hedera Service Solidity Libraries
        • Network Response Messages
      • Signature Provider
        • Provider
        • Signer
        • Wallet
        • Local Provider
    • Mirror Node REST API
      • Accounts
      • Balances
      • Blocks
      • Schedule Transactions
      • Smart Contracts
      • Tokens
      • Topics
      • Transactions
      • Network
    • Hedera Consensus Service gRPC API
    • Hedera APIs
      • Basic Types
        • AccountAmount
        • AccountID
        • ContractID
        • CryptoAllowance
        • CurrentAndNextFeeSchedule
        • FeeComponents
        • FeeData
        • FeeSchedule
        • FileID
        • Fraction
        • HederaFunctionality
        • Key
        • KeyList
        • NftAllowance
        • NftTransfer
        • NodeAddress
        • NodeAddressBook
        • RealmID
        • ScheduleID
        • SemanticVersion
        • ServicesConfigurationList
        • ServiceEndpoint
        • Setting
        • ShardID
        • Signature
        • SignatureList
        • SignatureMap
        • SignaturePair
        • SubType
        • TransferList
        • TransactionID
        • ThresholdKey
        • ThresholdSignature
        • TokenAllowance
        • TokenBalance
        • TokenBalances
        • TokenFreezeStatus
        • TokenPauseStatus
        • TokenID
        • TokenKycStatus
        • TokenRelationship
        • TokenTransferList
        • TokenType
        • TokenSupplyType
        • TopicID
        • TransactionFeeSchedule
      • Cryptocurrency Accounts
        • CryptoService
        • CryptApproveAllowance
        • CryptoDeleteAllowance
        • CryptoCreate
        • CryptoTransfer
        • CryptoUpdate
        • CryptoDelete
        • CryptoGetAccountBalance
        • CryptoGetAccountRecords
        • CryptoGetInfo
        • CryptoGetStakers
      • Consensus Service
        • Consensus Service
        • ConsensusCreateTopic
        • ConsensusUpdateTopic
        • ConsensusSubmitMessage
        • ConsensusDeleteTopic
        • ConsensusTopicInfo
        • ConsensusGetTopicInfo
      • Schedule Service
        • ScheduleService
        • SchedulableTransactionBody
        • ScheduleCreate
        • ScheduleDelete
        • ScheduleSign
        • ScheduleGetInfo
      • Token Service
        • TokenService
        • CustomFees
          • AssessedCustomFee
          • CustomFee
          • FractionalFee
          • FixedFee
          • RoyaltyFee
        • TokenCreate
        • TokenUpdate
        • TokenFeeScheduleUpdate
        • TokenDelete
        • TokenMint
        • TokenBurn
        • TokenFreezeAccount
        • TokenUnfreezeAccount
        • TokenGrantKyc
        • TokenRevokeKyc
        • TokenAssociate
        • TokenDissociate
        • TokenWipeAccount
        • TokenPause
        • TokenUnpause
        • TokenGetInfo
        • TokenGetNftInfo
        • TokenGetNftInfos
        • TokenGetAccountNftInfo
      • File Service
        • FileService
        • FileCreate
        • FileAppend
        • FileUpdate
        • FileDelete
        • FileGetContents
        • FileGetInfo
      • Smart Contracts
        • SmartContractService
        • ContractCall
        • ContractCallLocal
        • ContractCreate
        • ContractUpdate
        • ContractDelete
        • ContractGetByteCode
        • ContractGetInfo
        • ContractGetRecords
      • Miscellaneous
        • Duration
        • ExchangeRate
        • Freeze
        • FreezeType
        • GetByKey
        • GetBySolidityID
        • NetworkGetVersionInfo
        • NetworkService
        • Query
        • QueryHeader
        • Response
        • ResponseCode
        • ResponseHeader
        • SystemDelete
        • SystemUndelete
        • TimeStamp
        • Transaction
        • TransactionBody
        • TransactionContents
        • TransactionGetFastRecord
        • TransactionGetReceipt
        • TransactionGetRecord
        • TransactionReceipt
        • TransactionRecord
        • TransactionResponse
        • UncheckedSubmit
    • Hedera Status API
  • Support & Community
    • Glossary
    • Contributing to Hedera documentation
      • Contribution Guidelines
        • Creating Issues
        • Creating Pull Requests
        • Hedera Improvement Proposal (HIP)
        • Submit Demo Applications
      • Style Guide
        • Understanding different types of documentation
        • Use of HBAR and tinybars
        • Use of web2 and web3
        • Language and grammar
        • Formatting
        • Punctuation
        • GitBook Markdown Syntax
    • Discord
    • GitHub
    • Stack Overflow
    • Hedera Blog
    • Bug Bounty
    • Hedera Help
    • Documentation Survey
    • Meetups
    • Brand Guidelines
    • Status Page
Powered by GitBook
LogoLogo

INTRODUCTION

  • Fees
  • Core Concepts
  • Network Information

TOOLS

  • Bridge
  • Oracles
  • Explorers
  • Developer Portal & Faucet

RESOURCES

  • Status
  • Bug Bounty
  • Build on Hedera (course)
  • Documentation Survey
On this page
  • What you will accomplish
  • Prerequisites
  • Get Started
  • 1. Scaffold your project
  • 2. Fetching Token Data: Writing Mirror Node API Queries
  • 3. Adding in the User Interface
  • 4. Testing DApp Functionality
  • Try with HashPack, Blade or Kabila
  • Complete

Was this helpful?

Edit on GitHub
  1. Tutorials
  2. More Tutorials

Create a Hedera DApp Integrated with WalletConnect

In the dynamic world of decentralized applications (DApps), catering to users with diverse wallet preferences is important.

PreviousJavaScript TestingNextHow to Connect MetaMask to Hedera

Last updated 1 month ago

Was this helpful?

Explore DApp development using the Mirror Node API and Hedera Token Service (HTS). Discover how to integrate HTS functionality into your DApp for seamless token management and transactions. This guide uses React, Material UI, Ethers, and TypeScript with the integrated with WalletConnect, streamlining your development process.

What you will accomplish

Prerequisites

Before you begin, you should be familiar with the following:

Also, you should have the following set up on your computer ⬇
Check your prerequisites set up ⬇

Open your terminal, and enter the following commands.

git --version
code --version
node --version
npm --version

Each of these commands should output some text that includes a version number, for example:

git --version
git version 2.39.2 (Apple Git-143)

code --version
1.81.1
6c3e3dba23e8fadc360aed75ce363ba185c49794
arm64

node --version
v20.6.1

npm --version
9.8.1

If the output contains text similar to command not found, please install that item.

Get Started

This custom template eliminates setup overhead and allows you to dive straight into the core features of your project.

Template Project Repos

1. Scaffold your project

Open a terminal and run the following command to set up your project structure, replacing my-app-name with your desired directory name.

npx create-react-app <my-app-name> --template git+ssh://git@github.com/hedera-dev/cra-hedera-dapp-template.git
Scaffolding project expected output

Open your newly created react app project with visual studio code. You should see the following file structure.

2. Fetching Token Data: Writing Mirror Node API Queries

In vscode open the file located at src/services/wallets/mirrorNodeClient.ts.

This file creates a mirror node client and is used to fetch data from the mirror nodes. We will add new code to help us obtain information about the tokens we currently own.

This client is configured for the Hedera Testnet. For further configuration, go to src/config/network.ts.

2.1 Query Account Token Balances by Account ID

We'll use the Mirror Node API to query information about the tokens we currently own and the quantities of those tokens.

Open src/services/wallets/mirrorNodeClient.ts and paste the below interface outside of and above the MirrorNodeClient class.

export interface MirrorNodeAccountTokenBalance {
  balance: number,
  token_id: string,
}

This interface defines the data fields we need for our DApp, filtering out any extra data from the mirror node response.

Paste the below HTTP GET request outside of and below the MirrorNodeClient class in the src/services/wallets/mirrorNodeClient.ts file.

 // Purpose: get token balances for an account
 // Returns: an array of MirrorNodeAccountTokenBalance
 async getAccountTokenBalances(accountId: AccountId) {
   // get token balances
   const tokenBalanceInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}/tokens?limit=100`, { method: "GET" });
   const tokenBalanceInfoJson = await tokenBalanceInfo.json();

   const tokenBalances = [...tokenBalanceInfoJson.tokens] as MirrorNodeAccountTokenBalance[];

   // because the mirror node API paginates results, we need to check if there are more results
   // if links.next is not null, then there are more results and we need to fetch them until links.next is null
   let nextLink = tokenBalanceInfoJson.links.next;
   while (nextLink !== null) {
     const nextTokenBalanceInfo = await fetch(`${this.url}${nextLink}`, { method: "GET" });
     const nextTokenBalanceInfoJson = await nextTokenBalanceInfo.json();
     tokenBalances.push(...nextTokenBalanceInfoJson.tokens);
     nextLink = nextTokenBalanceInfoJson.links.next;
   }

   return tokenBalances;
 }
File Checkpoint

To ensure you're on the right track, your `src/services/wallets/mirrorNodeClient.ts` file should look like below.

import { AccountId } from "@hashgraph/sdk";
import { NetworkConfig } from "../../config";

export interface MirrorNodeAccountTokenBalance {
  balance: number,
  token_id: string,
}

export class MirrorNodeClient {
  url: string;
  constructor(networkConfig: NetworkConfig) {
    this.url = networkConfig.mirrorNodeUrl;
  }

 // Purpose: get token balances for an account
 // Returns: an array of MirrorNodeAccountTokenBalance
 async getAccountTokenBalances(accountId: AccountId) {
  // get token balances
  const tokenBalanceInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}/tokens?limit=100`, { method: "GET" });
  const tokenBalanceInfoJson = await tokenBalanceInfo.json();

  const tokenBalances = [...tokenBalanceInfoJson.tokens] as MirrorNodeAccountTokenBalance[];

  // because the mirror node API paginates results, we need to check if there are more results
  // if links.next is not null, then there are more results and we need to fetch them until links.next is null
  let nextLink = tokenBalanceInfoJson.links.next;
  while (nextLink !== null) {
    const nextTokenBalanceInfo = await fetch(`${this.url}${nextLink}`, { method: "GET" });
    const nextTokenBalanceInfoJson = await nextTokenBalanceInfo.json();
    tokenBalances.push(...nextTokenBalanceInfoJson.tokens);
    nextLink = nextTokenBalanceInfoJson.links.next;
  }

  return tokenBalances;
}

  async getAccountInfo(accountId: AccountId) {
    const accountInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}`, { method: "GET" });
    const accountInfoJson = await accountInfo.json();
    return accountInfoJson;
  }
}

2.2 Query Token Information by Token ID

In the previous step we wrote code to obtain the current token balance of an account. Next we will retieve the type of token (Non-Fungible or Fungible), decimal precision, token name and symbol.

Open src/services/wallets/mirrorNodeClient.ts and paste the interface outside of and above the MirrorNodeClient class.

export interface MirrorNodeTokenInfo {
  type: 'FUNGIBLE_COMMON' | 'NON_FUNGIBLE_UNIQUE',
  decimals: string,
  name: string,
  symbol: string
  token_id: string,
}

Paste the below HTTP GET request outside of and below the getAccountTokenBalances function in the src/services/wallets/mirrorNodeClient.ts file.

// Purpose: get token info for a token
// Returns: a MirrorNodeTokenInfo 
async getTokenInfo(tokenId: string) {
  const tokenInfo = await fetch(`${this.url}/api/v1/tokens/${tokenId}`, { method: "GET" });
  const tokenInfoJson = await tokenInfo.json() as MirrorNodeTokenInfo;
  return tokenInfoJson;
}

2.3 Query Account NFT Information by AccountID

In the previous step we wrote code to obtain the token details (token type, decimals, name, and symbol). Next we will retrieve the NFT serial numbers that are owned.

Open src/services/wallets/mirrorNodeClient.ts and paste the interface outside of and above the MirrorNodeClient class.

export interface MirrorNodeNftInfo {
  token_id: string,
  serial_number: number,
}

Paste the below HTTP GET request outside of and below the getTokenInfo function in the src/services/wallets/mirrorNodeClient.ts file.

// Purpose: get NFT Infor for an account
// Returns: an array of NFTInfo
async getNftInfo(accountId: AccountId) {
  const nftInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}/nfts?limit=100`, { method: "GET" });
  const nftInfoJson = await nftInfo.json();

  const nftInfos = [...nftInfoJson.nfts] as MirrorNodeNftInfo[];

  // because the mirror node API paginates results, we need to check if there are more results
  // if links.next is not null, then there are more results and we need to fetch them until links.next is null
  let nextLink = nftInfoJson.links.next;
  while (nextLink !== null) {
    const nextNftInfo = await fetch(`${this.url}${nextLink}`, { method: "GET" });
    const nextNftInfoJson = await nextNftInfo.json();
    nftInfos.push(...nextNftInfoJson.nfts);
    nextLink = nextNftInfoJson.links.next;
  }
  return nftInfos;
}

2.4 Combine Account Token Balances and Token Information via Data Aggregation

We need to combine all of our HTTP response data in order to display our available tokens in our DApp.

Open src/services/wallets/mirrorNodeClient.ts and paste the interface outside of and above the MirrorNodeClient class.

export interface MirrorNodeAccountTokenBalanceWithInfo extends MirrorNodeAccountTokenBalance {
  info: MirrorNodeTokenInfo,
  nftSerialNumbers?: number[],
}

Paste the function outside of and below the getNftInfo function in the src/services/wallets/mirrorNodeClient.ts file.

// Purpose: get token balances for an account with token info in order to display token balance, token type, decimals, etc.
// Returns: an array of MirrorNodeAccountTokenBalanceWithInfo
async getAccountTokenBalancesWithTokenInfo(accountId: AccountId): Promise<MirrorNodeAccountTokenBalanceWithInfo[]> {
  //1.  Retrieve all token balances in the account
  const tokens = await this.getAccountTokenBalances(accountId);
  //2. Create a map of token IDs to token info and fetch token info for each token
  const tokenInfos = new Map<string, MirrorNodeTokenInfo>();
  for (const token of tokens) {
    const tokenInfo = await this.getTokenInfo(token.token_id);
    tokenInfos.set(tokenInfo.token_id, tokenInfo);
  }

  //3. Fetch all NFT info in account
  const nftInfos = await this.getNftInfo(accountId);

  //4. Create a map of token Ids to arrays of serial numbers
  const tokenIdToSerialNumbers = new Map<string, number[]>();
  for (const nftInfo of nftInfos) {
    const tokenId = nftInfo.token_id;
    const serialNumber = nftInfo.serial_number;

    // if we haven't seen this token_id before, create a new array with the serial number
    if (!tokenIdToSerialNumbers.has(tokenId)) {
      tokenIdToSerialNumbers.set(tokenId, [serialNumber]);
    } else {
      // if we have seen this token_id before, add the serial number to the array
      tokenIdToSerialNumbers.get(tokenId)!.push(serialNumber);
    }
  }

  //5. Combine token balances, token info, and NFT info and return
  return tokens.map(token => {
    return {
      ...token,
      info: tokenInfos.get(token.token_id)!,
      nftSerialNumbers: tokenIdToSerialNumbers.get(token.token_id)
    }
  });
}

The getAccountTokenBalancesWithTokenInfo combines token balances, token info and, NFT info in order to display our available tokens in our DApp.

Complete `mirrorNodeClient.ts` file Checkpoint

To ensure you're on the right track, your `src/services/wallets/mirrorNodeClient.ts` file should look like below.

import { AccountId } from "@hashgraph/sdk";
import { NetworkConfig } from "../../config";

export interface MirrorNodeAccountTokenBalance {
  balance: number,
  token_id: string,
}

export interface MirrorNodeTokenInfo {
  type: 'FUNGIBLE_COMMON' | 'NON_FUNGIBLE_UNIQUE',
  decimals: string,
  name: string,
  symbol: string
  token_id: string,
}

export interface MirrorNodeNftInfo {
  token_id: string,
  serial_number: number,
}

export interface MirrorNodeAccountTokenBalanceWithInfo extends MirrorNodeAccountTokenBalance {
  info: MirrorNodeTokenInfo,
  nftSerialNumbers?: number[],
}

export class MirrorNodeClient {
  url: string;
  constructor(networkConfig: NetworkConfig) {
    this.url = networkConfig.mirrorNodeUrl;
  }



  // Purpose: get token balances for an account
  // Returns: an array of MirrorNodeAccountTokenBalance
  async getAccountTokenBalances(accountId: AccountId) {
    // get token balances
    const tokenBalanceInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}/tokens?limit=100`, { method: "GET" });
    const tokenBalanceInfoJson = await tokenBalanceInfo.json();

    const tokenBalances = [...tokenBalanceInfoJson.tokens] as MirrorNodeAccountTokenBalance[];

    // because the mirror node API paginates results, we need to check if there are more results
    // if links.next is not null, then there are more results and we need to fetch them until links.next is null
    let nextLink = tokenBalanceInfoJson.links.next;
    while (nextLink !== null) {
      const nextTokenBalanceInfo = await fetch(`${this.url}${nextLink}`, { method: "GET" });
      const nextTokenBalanceInfoJson = await nextTokenBalanceInfo.json();
      tokenBalances.push(...nextTokenBalanceInfoJson.tokens);
      nextLink = nextTokenBalanceInfoJson.links.next;
    }

    return tokenBalances;
  }

  // Purpose: get token info for a token
  // Returns: a MirrorNodeTokenInfo 
  async getTokenInfo(tokenId: string) {
    const tokenInfo = await fetch(`${this.url}/api/v1/tokens/${tokenId}`, { method: "GET" });
    const tokenInfoJson = await tokenInfo.json() as MirrorNodeTokenInfo;
    return tokenInfoJson;
  }

  // Purpose: get NFT Infor for an account
  // Returns: an array of NFTInfo
  async getNftInfo(accountId: AccountId) {
    const nftInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}/nfts?limit=100`, { method: "GET" });
    const nftInfoJson = await nftInfo.json();

    const nftInfos = [...nftInfoJson.nfts] as MirrorNodeNftInfo[];

    // because the mirror node API paginates results, we need to check if there are more results
    // if links.next is not null, then there are more results and we need to fetch them until links.next is null
    let nextLink = nftInfoJson.links.next;
    while (nextLink !== null) {
      const nextNftInfo = await fetch(`${this.url}${nextLink}`, { method: "GET" });
      const nextNftInfoJson = await nextNftInfo.json();
      nftInfos.push(...nextNftInfoJson.nfts);
      nextLink = nextNftInfoJson.links.next;
    }
    return nftInfos;
  }

  // Purpose: get token balances for an account with token info in order to display token balance, token type, decimals, etc.
  // Returns: an array of MirrorNodeAccountTokenBalanceWithInfo
  async getAccountTokenBalancesWithTokenInfo(accountId: AccountId): Promise<MirrorNodeAccountTokenBalanceWithInfo[]> {
    //1.  Retrieve all token balances in the account
    const tokens = await this.getAccountTokenBalances(accountId);
    //2. Create a map of token IDs to token info and fetch token info for each token
    const tokenInfos = new Map<string, MirrorNodeTokenInfo>();
    for (const token of tokens) {
      const tokenInfo = await this.getTokenInfo(token.token_id);
      tokenInfos.set(tokenInfo.token_id, tokenInfo);
    }

    //3. Fetch all NFT info in account
    const nftInfos = await this.getNftInfo(accountId);

    //4. Create a map of token Ids to arrays of serial numbers
    const tokenIdToSerialNumbers = new Map<string, number[]>();
    for (const nftInfo of nftInfos) {
      const tokenId = nftInfo.token_id;
      const serialNumber = nftInfo.serial_number;

      // if we haven't seen this token_id before, create a new array with the serial number
      if (!tokenIdToSerialNumbers.has(tokenId)) {
        tokenIdToSerialNumbers.set(tokenId, [serialNumber]);
      } else {
        // if we have seen this token_id before, add the serial number to the array
        tokenIdToSerialNumbers.get(tokenId)!.push(serialNumber);
      }
    }

    //5. Combine token balances, token info, and NFT info and return
    return tokens.map(token => {
      return {
        ...token,
        info: tokenInfos.get(token.token_id)!,
        nftSerialNumbers: tokenIdToSerialNumbers.get(token.token_id)
      }
    });
  }

  async getAccountInfo(accountId: AccountId) {
    const accountInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}`, { method: "GET" });
    const accountInfoJson = await accountInfo.json();
    return accountInfoJson;
  }

}

2.5 Add Token Association Support

Before a user can receive a new token, they must associate with it. This association helps protect users from receiving unwanted tokens.

Open src/services/wallets/mirrorNodeClient.ts and paste the function below the getAccountTokenBalancesWithTokenInfo function.

// Purpose: check if an account is associated with a token
// Returns: true if the account is associated with the token, false otherwise
async isAssociated(accountId: AccountId, tokenId: string) {
  const accountTokenBalance = await this.getAccountTokenBalances(accountId);
  return accountTokenBalance.some(token => token.token_id === tokenId);
}

3. Adding in the User Interface

In this step, we'll copy and paste the home.tsx file, which contains all the necessary code for adding UI components that enable token transfers and association with a token.

Open src/pages/Home.tsx and replace the existing code by pasting the below code:

`Home.tx` file
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import { Stack } from "@mui/system";
import { useWalletInterface } from "../services/wallets/useWalletInterface";
import SendIcon from '@mui/icons-material/Send';
import { useEffect, useState } from "react";
import { AccountId, TokenId } from "@hashgraph/sdk";
import { MirrorNodeAccountTokenBalanceWithInfo, MirrorNodeClient } from "../services/wallets/mirrorNodeClient";
import { appConfig } from "../config";

const UNSELECTED_SERIAL_NUMBER = -1;

export default function Home() {
  const { walletInterface, accountId } = useWalletInterface();
  const [toAccountId, setToAccountId] = useState("");
  const [amount, setAmount] = useState<number>(0);
  // include all of this necessary for dropdown
  const [availableTokens, setAvailableTokens] = useState<MirrorNodeAccountTokenBalanceWithInfo[]>([]);
  const [selectedTokenId, setSelectedTokenId] = useState<string>('');
  const [serialNumber, setSerialNumber] = useState<number>(UNSELECTED_SERIAL_NUMBER);

  const [tokenIdToAssociate, setTokenIdToAssociate] = useState("");

  // include all of this necessary for dropdown
  // Purpose: Get the account token balances with token info for the current account and set them to state
  useEffect(() => {
    if (accountId === null) {
      return;
    }
    const mirrorNodeClient = new MirrorNodeClient(appConfig.networks.testnet);
    // Get token balance with token info for the current account
    mirrorNodeClient.getAccountTokenBalancesWithTokenInfo(AccountId.fromString(accountId)).then((tokens) => {
      // set to state
      setAvailableTokens(tokens);
      console.log(tokens);
    }).catch((error) => {
      console.error(error);
    });
  }, [accountId])

  // include all of this necessary for dropdown
  // Filter out tokens with a balance of 0
  const tokensWithNonZeroBalance = availableTokens.filter((token) => token.balance > 0);
  // include all of this necessary for dropdown
  // Get the selected token balance with info
  const selectedTokenBalanceWithInfo = availableTokens.find((token) => token.token_id === selectedTokenId);

  // include all of this necessary for dropdown
  // reset amount and serial number when token id changes
  useEffect(() => {
    setAmount(0);
    setSerialNumber(UNSELECTED_SERIAL_NUMBER);
  }, [selectedTokenId]);

  return (
    <Stack alignItems="center" spacing={4}>
      <Typography
        variant="h4"
        color="white"
      >
        Let's buidl a dApp on Hedera
      </Typography>
      {walletInterface !== null && (
        <>
          <Stack
            direction='row'
            gap={2}
            alignItems='center'
          >
            <Typography>
              Transfer
            </Typography>
            <TextField
              label='Available Tokens'
              value={selectedTokenId}
              select
              onChange={(e) => setSelectedTokenId(e.target.value)}
              sx={{
                width: '250px',
                height: '50px',
              }}
            >
              <MenuItem
                value={''}
              >
                Select a token
              </MenuItem>
              {tokensWithNonZeroBalance.map((token) => {
                const tokenBalanceAdjustedForDecimals = token.balance / Math.pow(10, Number.parseInt(token.info.decimals));
                return (
                  <MenuItem
                    key={token.token_id}
                    value={token.token_id}
                  >
                    {token.info.name}({token.token_id}): ({tokenBalanceAdjustedForDecimals})
                  </MenuItem>
                );
              }
              )}
            </TextField>
            {selectedTokenBalanceWithInfo?.info?.type === "NON_FUNGIBLE_UNIQUE" && (
              <TextField
                label='Serial Number'
                select
                value={serialNumber.toString()}
                onChange={(e) => setSerialNumber(Number.parseInt(e.target.value))}
                sx={{
                  width: '190px',
                  height: '50px',
                }}
              >
                <MenuItem
                  value={UNSELECTED_SERIAL_NUMBER}
                >
                  Select a Serial Number
                </MenuItem>
                {selectedTokenBalanceWithInfo.nftSerialNumbers?.map((serialNumber) => {
                  return (
                    <MenuItem
                      key={serialNumber}
                      value={serialNumber}
                    >
                      {serialNumber}
                    </MenuItem>
                  );
                }
                )}
              </TextField>
            )}
            {selectedTokenBalanceWithInfo?.info?.type === "FUNGIBLE_COMMON" && (
              <TextField
                type='number'
                label='amount'
                value={amount}
                onChange={(e) => setAmount(parseInt(e.target.value))}
                sx={{
                  maxWidth: '100px'
                }}
              />
            )}
            {/* not included in the dropdown stage. this is in the association/send stage */}
            <Typography>
              HTS Token
              to
            </Typography>
            <TextField
              value={toAccountId}
              onChange={(e) => setToAccountId(e.target.value)}
              label='account id or evm address'
            />
            <Button
              variant='contained'
              onClick={async () => {
                if (selectedTokenBalanceWithInfo === undefined) {
                  console.log(`Token Id is empty.`)
                  return;
                }
                
                // check if receiver has associated
                const mirrorNodeClient = new MirrorNodeClient(appConfig.networks.testnet);
                const isAssociated = await mirrorNodeClient.isAssociated(AccountId.fromString(toAccountId), selectedTokenId);
                if (!isAssociated) {
                  console.log(`Receiver is not associated with token id: ${selectedTokenId}`);
                  return;
                }
                if (selectedTokenBalanceWithInfo.info.type === "NON_FUNGIBLE_UNIQUE") {
                  await walletInterface.transferNonFungibleToken(
                    AccountId.fromString(toAccountId),
                    TokenId.fromString(selectedTokenId),
                    serialNumber);
                } else {
                  const amountWithDecimals = amount * Math.pow(10, Number.parseInt(selectedTokenBalanceWithInfo.info.decimals));
                  await walletInterface.transferFungibleToken(
                    AccountId.fromString(toAccountId),
                    TokenId.fromString(selectedTokenId),
                    amountWithDecimals);
                }
              }}
            >
              <SendIcon />
            </Button>
          </Stack>
          <Stack
            direction='row'
            gap={2}
            alignItems='center'
          >
            <TextField
              value={tokenIdToAssociate}
              label='token id'
              onChange={(e) => setTokenIdToAssociate(e.target.value)}
            />
            <Button
              variant='contained'
              onClick={async () => {
                if (tokenIdToAssociate === "") {
                  console.log(`Token Id is empty.`)
                  return;
                }
                await walletInterface.associateToken(TokenId.fromString(tokenIdToAssociate));
              }}
            >
              Associate Token
            </Button>
          </Stack>
        </>
      )}
    </Stack>
  )
}

The crucial part of the code is found within the following code:

// include all of this necessary for dropdown
  // Purpose: Get the account token balances with token info for the current account and set them to state
  useEffect(() => {
    if (accountId === null) {
      return;
    }
    const mirrorNodeClient = new MirrorNodeClient(appConfig.networks.testnet);
    // Get token balance with token info for the current account
    mirrorNodeClient.getAccountTokenBalancesWithTokenInfo(AccountId.fromString(accountId)).then((tokens) => {
      // set to state
      setAvailableTokens(tokens);
      console.log(tokens);
    }).catch((error) => {
      console.error(error);
    });
  }, [accountId])

This code fetches and updates the list of tokens a user owns providing the available tokens to the dropdown menu.

4. Testing DApp Functionality

The application is ready to be started and tested. You will be testing:

4.1 Test Setup

You'll be creating four Hedera Testnet accounts, each with a balance of 10 HBAR. Two of these accounts will come pre-loaded with their own fungible tokens, and four accounts will come pre-loaded with their own non-fungible tokens (NFTs).

Open a new terminal window and create a new directory and change into that directory

mkdir hedera-test-accounts && cd hedera-test-accounts

Open hedera-test-accounts folder in a new visual studio code window.

Create a new file and name it .env with the following contents. Remember to enter your account ID and your private key.

MY_ACCOUNT_ID=<enter your account id>
MY_PRIVATE_KEY=<enter your DER private key>

Within the hedera-test-accounts home directory, execute the following command in the terminal,

npx github:/hedera-dev/hedera-create-account-and-token-helper

Keep this terminal open for the remainder of the tutorial, as you will refer back to it.

Test Accounts Sample Output
{
  "ed25519": {
    "sender": {
      "accountId": "0.0.xxxxxxxx",
      "privateKey": "302...",
      "FungibleTokenId": "0.0.xxxxxxxx",
      "NftTokenId": "0.0.xxxxxxxx"
    },
    "receiver": {
      "accountId": "0.0.xxxxxxxx",
      "privateKey": "302..."
    }
  },
  "ecdsaWithAlias": {
    "sender": {
      "accountId": "0.0.xxxxxxxx",
      "privateKey": "...hexadecimal string of length 64...",
      "FungibleTokenId": "0.0.xxxxxxxx",
      "NftTokenId": "0.0.xxxxxxxx"
    },
    "receiver": {
      "accountId": "0.0.xxxxxxxx",
      "privateKey": "...hexadecimal string of length 64..."
    }
  }
}

4.2 Import Sender and Receiver accounts

Rename your imported accounts within your preferred wallet to keep track which is the sender and receiver account.

4.3 Start the DApp

Navigate back to your application in Visual Studio Code, and in the terminal, run the following command

npm run start

4.4 Connect to DApp as the Receiver

Click the Connect Wallet button in the upper right and select MetaMask and select the Sender account.

WalletConnect Instructions

Open HashPack and unlock your wallet

Go back to the DApp homepage and click 'Connect Wallet'

Choose WalletConnect

Copy the connection string

Open HashPack and connect to DApp by clicking on the world in the upper right

Paste the walletconnect string into HashPack pairing string textbox

Select the Receiver account to connect with the DApp

✅ You're connected!

4.5 Associate Receiver Account with Sender Account NFT

Open the output of the test accounts you created earlier and copy the ecdsaWithAlias Sender's account NftTokenId

Paste the NftTokenId in the DApps associate token textbox and click the button Associate

MetaMask will prompt you to sign the transaction. If the extension does not automatically open, you will need to manually click on the MetaMask extension.

Confirm the transaction

4.6 Transfer NFT to Receiver Account

Disconnect as the Receiver account and reconnect with the Sender account. To do this, open the MetaMask extension, click on the three dots in the upper right, select "Connected Sites," and then disconnect the Receiver account. All other wallets disconnect by clicking on your account ID in the upper right of the DApp homepage.

Connect to the DApp as the Sender Account.

As the Sender,

  • Select from available tokens the HederaNFT

  • Select the NFT with serial number 5 from the drop-down menu.

  • Enter the account ID or EVM address of the Receiver account.

  • Click the "send" button.

  • Sign the transaction on MetaMask to complete the transfer of the NFT from the Sender to the receiver account.

4.7 Verify Receiver Account Receieved the NFT

Disconnect as the Sender account and reconnect as the Receiver account.

Check the dropdown menu and ensure the Receiver account has NFT serial number 5.

Try with HashPack, Blade or Kabila

Optionally, import your accounts into any of the above Hedera native wallets and test out transferring more tokens.

Complete

🎉 Congratulations! You have successfully walked through creating a Hedera DApp that transfers HTS tokens using MetaMask, HashPack, Blade, or Kabila.

You have learned how to:


Recommended:

Recommended:

Recommended for Linux & Mac:

Recommended for Windows:

We choose to scaffold our project by using the , as it offers:

The complete TypeScript project can be found on GitHub .

The complete JavaScript project can be found on GitHub .

offer access to historical data from the Hedera network while optimizing the use of network resources. You can easily retrieve information like transactions, records, events, and balances. Visit the to learn more.

Import the sender and receiver accounts that were just outputted into your preferred wallet application. (, , , or )

For assistance on how to import a Hedera account into MetaMask refer to our documentation .

The react template uses the Hashio JSON RPC Relay URL to work with MetaMask. If you are experiencing degraded performance, follow this to switch to Arkhia or set up your own JSON RPC Relay. Edit the src/config/networks.ts with the new JSON RPC Relay URL.

JavaScript
TypeScript
React
REST API
Install Git (Github)
VS Code. Install VS Code (Visual Studio)
nvm
nvm-windows
CRA Hedera DApp template
here
here
Mirror nodes
mirror node API docs
MetaMask
HashPack
Blade
Kabila
here
guide

Writer: Abi Castro, DevRel Engineer

Create React App (CRA) Hedera DApp template
HashPack
Kabila
Blade
MetaMask

|

GitHub
LinkedIn
Completed DAPP User Interface
Hedera React DApp File Structure
DApp homepage
Select your wallet
Connect with receiver account
Successfully connected to dapp view
Associate textbox
Sign the Associate transaction
Transfer NFT to Receiver Account
Transfer NFT to Receiver Account Success
Completed dapp user interface to transfer and associate a token.
The file structure showing node_modules, public and src folder, along with .env and .gitignore.
homepage of the application with a single button called connect
a modal with two choices: walletconnect or metamask
metamask select your account
a dropdown of available tokens with a transfer button and a textbox to associate tokens with connected account.
a textbox to enter your tokenid to associate with
MetaMask Modal asking to confirm/sign the transaction
Homepage showing the transfer of NFT serial number 5
Homepage showing the transfer of NFT serial number 5 was received by Receiver account