Develop a Hedera DApp with MetaMask, HashPack, and Blade… | Hedera Hedera Network Services Token Service Mint and configure tokens and accounts. Consensus Service Verifiable timestamps and ordering of events. Smart Contracts Run Solidity smart contracts. HBAR The Hedera network's native cryptocurrency. Insights How It Works Learn about Hedera from end to end. Explorers View live and historical data on Hedera. Dashboards Analyze network activity and metrics. Network Nodes Understand networks and node types. Devs Start Building Get Started Learn core concepts and build the future. Documentation Review the API and build using your favorite language. Developer Resources Integrations Plugins and microservices for Hedera. Fee Estimator Understand and estimate transaction costs. Open Source Hedera is committed to open, transparent code. Learning Center Learn about web3 and blockchain technologies. Grants Grants & accelerators for your project. Bounties Find bugs. Submit a report. Earn rewards. Ecosystem ECOSYSTEM Hedera Ecosystem Applications, developer tools, network explorers, and more. NFT Ecosystem Metrics Analyze on-chain and market NFT ecosystem metrics. CATEGORIES Web3 Applications Connect into the innovative startups decentralizing the web on Hedera. Enterprise Applications Learn about the Fortune 500 companies decentralizing the web on Hedera. Wallets & Custodians Create a Hedera account to manage HBAR, fungible tokens, and NFTs. Network Explorers Hedera mainnet and testnet graphical network explorers. Developer Tooling Third-party APIs, integrations, and plugins to build apps on Hedera. Grants & Accelerators Boost your project with support from the Hedera ecosystem. Partner Program Explore our partners to bring your vision into reality. Hedera Council Over 30 highly diversified organizations govern Hedera. Use Cases Hedera Solutions Asset Tokenization Studio Open source toolkit for tokenizing assets securely. Stablecoin Studio All-in-one toolkit for stablecoin solutions. Hedera Guardian Auditable carbon markets and traceability. Functional Use Cases Data Integrity & AI Reliable, secure, and ethically governed insights. Sustainability Enabling fair carbon markets with trust. Real-World Asset Tokenization Seamless tokenization of real-world assets and digital at scale. Consumer Engagement & Loyalty Mint, distribute, and redeem loyalty rewards. Decentralized Identity Maintain the lifecycle of credentials. Decentralized Logs Scalable, real-time timestamped events. DeFi Dapps built for the next-generation of finance. NFTs Low, fixed fees. Immutable royalties. Payments Scalable, real-time, and affordable crypto-payments. HBAR Overview Learn about Hedera's token, HBAR. Treasury Management Hedera’s report of the HBAR supply. Governance Decentralized Governance Hedera Council See the world's leading organizations that own Hedera. About Meet Hedera's Board of Directors and team. Journey Watch Hedera's journey to build an empowered digital future for all. Transparent Governance Public Policy Hedera's mission is to inform policy and regulation that impact the industry. Meeting Minutes Immutably recorded on Hedera. Roadmap Follow Hedera's roadmap in its journey to build the future. Resources Company What's New Partners Papers Careers Media Blog Technical Press Podcast Community Events Meetups Store Brand Navigation QUICKSTART Develop a Hedera DApp with MetaMask, HashPack, and Blade Integration technical Jun 27, 2023 by Abi Castro In the dynamic world of decentralized applications (DApps), catering to users with diverse wallet preferences is important. With the increasing adoption of blockchain technology, individuals utilize various wallet solutions to manage their digital assets. Hedera has taken a significant stride towards inclusivity by introducing MetaMask support through the groundbreaking HIP-583. This integration offers an exciting opportunity for Ethereum Virtual Machine (EVM) users to seamlessly transition to the robust and scalable Hedera ecosystem. In this step-by-step guide to DApp development, you will unlock the full potential of multi-wallet Hedera DApps. Armed with React, Material UI, Ethers, and TypeScript, you will be guided through creating a Hedera DApp. By leveraging the power of the Create React App (CRA) Hedera DApp template, designed to simplify your development journey, you can integrate popular wallets such as MetaMask, Hashpack, and Blade. We'll use the Mirror Node API and Hedera Token Service (HTS) throughout this tutorial. You'll gain invaluable insights into querying the mirror node and incorporating HTS functionality into your DApp, allowing users to manage and transact with HTS tokens effortlessly. With this approach, you will have a comprehensive understanding of MetaMask integration and be equipped with the necessary tools to develop your own DApp from start to finish. Prerequisites  A Hedera Testnet Account. Don't have one?  Get one at https://portal.hedera.com/register and receive 10,000 test HBAR every 24 hours Recommended (optional) React Framework HTML, CSS, and fundamental TypeScript knowledge REST API experience Node v.18+ Learning Objectives Query the mirror node for account token balance, token information, and Non-fungible information. Query the mirror node to check if the receiver has associated with a token ID. Associate with HTS tokens with MetaMask, Hashpack, and Blade. Transfer an HTS token with MetaMask, Hashpack, and Blade. Completed dApp We choose to scaffold our project by using the CRA Hedera DApp template, as it offers: Multi-wallet integration out of the box (HashPack, Blade, and MetaMask) Mirror Node Client State management via React Context Material UI Choice of TypeScript or JavaScript This custom template eliminates setup overhead and allows you to dive straight into the core features of your project. The complete TypeScript DApp can be found: https://github.com/hedera-dev/... The complete JavaScript DApp can be found: https://github.com/hedera-dev/... Scaffold your project To begin, execute the following command, replacing   with the directory name you wish to create for the new project: npx create-react-app --template git+ssh://[email protected]/hedera-dev/cra-hedera-dapp-template.git Copy Output when scaffolding your project This command will generate a Create React App project with the Hedera DApp template. Once the project is generated, you can open it in your preferred code editor. Create React App Hedera DApp Template File Directory Fetching Token Data: Writing Mirror Node API Queries Mirror nodes offer access to historical data from the public ledger while optimizing the utilization of network resources. You can effortlessly retrieve essential information like transactions, records, events, and balances. Mirror Node API docs are found here. The template includes a mirror node client located at  src/services/wallets/mirrorNodeClient.ts  This client is specifically configured to target Testnet and provides a predefined query for retrieving account information. We will utilize and expand upon this file throughout the tutorial to help us obtain information about the tokens we currently own. import { AccountId } from "@hashgraph/sdk"; import { NetworkConfig } from "../../config"; export class MirrorNodeClient { url: string; constructor(networkConfig: NetworkConfig) { this.url = networkConfig.mirrorNodeUrl; } async getAccountInfo(accountId: AccountId) { const accountInfo = await fetch(`${this.url}/api/v1/accounts/${accountId}`, { method: "GET" }); const accountInfoJson = await accountInfo.json(); return accountInfoJson; } } Copy Query Account Token Balances by Account ID The information we want to pull from the mirror node is as follows: What tokens do we currently own? How many of those tokens do we own? A token is uniquely identified by its token ID, and we will refer to the number of tokens we own as a balance. To represent the shape of data returned by our mirror node query, let's create an interface outside of the MirrorNodeClient class in  src/services/wallets/mirrorNodeClient.ts export interface MirrorNodeAccountTokenBalance { balance: number, token_id: string, } Copy This interface will allow us to define the specific data fields we need for our DApp, excluding any additional data returned by the HTTP response that is not relevant to our use case. Next, within the MirrorNodeClient class, construct an HTTP GET request using fetch and call the following Mirror Node API endpoint: /api/v1/accounts/{idOrAliasorEVMAddress}/tokens?limit=100 // 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; } Copy The HTTP GET request returns the response in JSON format. We store the JSON response in the variable tokenBalanceInfoJson .  Then cast  tokenBalanceInfoJson.tokens  array to  MirrorNodeAccountTokenBalance[] .  This allows us to work with the token balance data in a strongly typed manner, making it easier to manipulate and use in our DApp. It is important to note that the Mirror Node API paginates its results. To ensure we fetch all the token balance information, we will check the  links.next  field.  Once  links.next  is null, we can be certain that we have retrieved all the relevant token balance information. Finally, this function will return an array of  MirrorNodeAccountTokenBalance  containing the complete set of token balance data. Query Token Information by Token ID In addition to balance and token ID, we need more information about the tokens.  We want to know: What type of token is it? Non-Fungible Token (NFT) or Fungible Token (FT)? How many decimals does our FT have? What is the token name and symbol? We must construct a new HTTP request to query the mirror node for this token information. In  src/services/wallets/mirrorNodeClient.ts , under the  MirrorNodeAccountTokenBalance  interface, create a new interface to represent the shape of data we get from our new mirror node query. export interface MirrorNodeTokenInfo { type: 'FUNGIBLE_COMMON' | 'NON_FUNGIBLE_UNIQUE', decimals: string, name: string, symbol: string token_id: string, } Copy Remember that this HTTP response will return additional data, and our interface only reflects the necessary data we need for our DApp. Next, construct the HTTP GET request using fetch and call the following Mirror Node API endpoint:  /api/v1/tokens/{tokenId} // 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; } Copy Like our previous function, after receiving the HTTP response, we parse the JSON data using the .json() method. Then, we explicitly cast the parsed data as MirrorNodeTokenInfo type. Query Account NFT Information by Account ID In our previous function, we obtained token information; our next objective is to retrieve an account’s NFT information. We will construct a separate HTTP request to fetch all the NFT information on our account.  What we want to know is: Which NFT serial numbers do we own? Create a new interface to represent what the new HTTP response returns. export interface MirrorNodeNftInfo { token_id: string, serial_number: number, } Copy Then, construct an HTTP GET request and call the Mirror Node API endpoint: /api/v1/accounts/{accountId}/nfts?limit=100 // 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; } Copy Similar to our `getAccountTokenBalances()` function, we must ensure we fetch all the token balance information. We will check the `links.next` field until it is null. Unify Account Token Balances and Token Information through Query Data Aggregation We need to combine all of our HTTP response data in order to display our available tokens in our DApp. Let’s create the following interface to represent the combined data. export interface MirrorNodeAccountTokenBalanceWithInfo extends MirrorNodeAccountTokenBalance { info: MirrorNodeTokenInfo, nftSerialNumbers?: number[], } Copy Finally, we will create a function that combines token balances, token information, and NFT information. The function will perform the following main steps: Retrieve all token balances for the specified account. Fetch detailed token information for each token. Retrieve NFT information for the account. Create a mapping of token IDs to their respective NFT serial numbers. Combine token balances, token information, and NFT information. Return the combined data of type MirrorNodeAccountTokenBalance. // 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 { //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(); 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(); 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) } }); } Copy Below is the complete mirrorNodeClient.ts file, which includes all the functions we have created so far to interact with the Mirror Node API and retrieve the necessary data for our DApp. The file should look like this at this stage of the tutorial. 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 { //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(); 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(); 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; } } Copy Next, we will utilize these functions to populate our drop-down menu with the retrieved token data. Create the Available Token Drop-Down Menu In this next section, we will dive into the Home.tsx file and utilize the necessary functions to populate a drop-down menu with the available token information of the connected account. The template provides a wallet interface that handles the supported multiple wallets and is already integrated into the project, simplifying the development process. Replace the existing code in src/pages/Home.tsx with the following code: import { MenuItem, TextField, Typography } from "@mui/material"; import { Stack } from "@mui/system"; import { useWalletInterface } from "../services/wallets/useWalletInterface"; import { useEffect, useState } from "react"; import { AccountId } 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 [amount, setAmount] = useState(0); // include all of this necessary for dropdown const [availableTokens, setAvailableTokens] = useState([]); const [selectedTokenId, setSelectedTokenId] = useState(''); const [serialNumber, setSerialNumber] = useState(UNSELECTED_SERIAL_NUMBER); // 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 ( Let's buidl a dApp on Hedera {walletInterface !== null && ( <> Transfer setSelectedTokenId(e.target.value)} sx={{ width: '250px', height: '50px', }} > Select a token {tokensWithNonZeroBalance.map((token) => { const tokenBalanceAdjustedForDecimals = token.balance / Math.pow(10, Number.parseInt(token.info.decimals)); return ( {token.info.name}({token.token_id}): ({tokenBalanceAdjustedForDecimals}) ); } )} {selectedTokenBalanceWithInfo?.info?.type === "NON_FUNGIBLE_UNIQUE" && ( setSerialNumber(Number.parseInt(e.target.value))} sx={{ width: '190px', height: '50px', }} > Select a Serial Number {selectedTokenBalanceWithInfo.nftSerialNumbers?.map((serialNumber) => { return ( {serialNumber} ); } )} )} {selectedTokenBalanceWithInfo?.info?.type === "FUNGIBLE_COMMON" && ( setAmount(parseInt(e.target.value))} sx={{ maxWidth: '100px' }} /> )} )} ) } Copy The crucial part of the code is found within the useEffect() hook. In this section, we invoke the getAccountTokenBalanceswithTokenInfo() function and update the state variable availableTokens with the returned data. We then apply a filter to remove tokens with a balance of 0 from the availableTokens array. Within the React code, we introduced a TextField component with a select attribute to create a drop-down menu. We use the filtered availableTokens to populate the drop-down options. Depending on whether we select an FT or an NFT, we dynamically render either a TextField of type number or a drop-down menu displaying the available serial numbers, respectively. Create Sender/Receiver Accounts and Start the Project To continue with the rest of this tutorial, we need to create sender and Receiver accounts that will be used for the upcoming steps. You will leverage the following repository to create these accounts. While we will primarily focus on completing the tutorial using the MetaMask wallet, feel free to explore the option of using HashPack or Blade as well. This flexibility ensures you can continue the tutorial using the wallet that best suits your preferences. To proceed, follow these steps: Create a new directory and change into that directory. Create a new file named .env in the directory with the following fields: MY_ACCOUNT_ID= MY_PRIVATE_KEY= Copy 3. Execute the following command in that directory npx github:/hedera-dev/hedera-create-account-and-token-helper Copy Note: Private Keys should never be shared publicly. You will see an output similar to the following: Sample Output Keep this terminal open for the remainder of the tutorial, as you will refer back to it. Next, import the ECDSA Sender and ECDSA Receiver accounts into MetaMask using their respective private keys.  Once both accounts are successfully imported, you can start the DApp by executing the following command: npm run start Copy This will start the DApp on port 3000. Note: To connect to Blade, it is necessary to use HTTPS. The provided template already includes HTTPS support. When starting the DApp, you will encounter a Privacy error in the browser about your connection not being private due to not having a valid SSL certificate. To proceed, click the 'Advanced' option and then select 'Proceed to localhost.' Connection is not Private Browser Warning DApp Homepage Connect to DApp as the Sender The template provides the code to transfer FTs and NFTs to a specific account ID or EVM address. You can find the implementation specific to MetaMask at src/services/wallets/metamask/metamaskClient.tsx . Moving forward with the tutorial, click the ‘Connect Wallet’ button and select MetaMask to connect with the Sender. Choose your wallet Select the Sender Account After a successful connection, you will see the following screen: Sender's Available Tokens Before we transfer, implement a function that verifies whether the Receiver account is associated with the token we intend to transfer. If the receiver is not associated with the token, then the transfer will fail. Needing to associate with a token before receiving it prevents a user from receiving unwanted tokens. In src/services/mirrorNodeClient.ts add the following code: // 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); } Copy Let’s attempt to send the token to the Receiver account. We need to add a Send button to our Home.tsx component. Replace the existing code in src/pages/Home.tsx with the following code: 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(0); const [availableTokens, setAvailableTokens] = useState([]); const [selectedTokenId, setSelectedTokenId] = useState(''); const [serialNumber, setSerialNumber] = useState(UNSELECTED_SERIAL_NUMBER); const [tokenIdToAssociate, setTokenIdToAssociate] = useState(""); // 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]) // 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); // reset amount and serial number when token id changes useEffect(() => { setAmount(0); setSerialNumber(UNSELECTED_SERIAL_NUMBER); }, [selectedTokenId]); return ( Let's buidl a dApp on Hedera {walletInterface !== null && ( <> Transfer setSelectedTokenId(e.target.value)} sx={{ width: '250px', height: '50px', }} > Select a token {tokensWithNonZeroBalance.map((token) => { const tokenBalanceAdjustedForDecimals = token.balance / Math.pow(10, Number.parseInt(token.info.decimals)); return ( {token.info.name}({token.token_id}): ({tokenBalanceAdjustedForDecimals}) ); } )} {selectedTokenBalanceWithInfo?.info?.type === "NON_FUNGIBLE_UNIQUE" && ( setSerialNumber(Number.parseInt(e.target.value))} sx={{ width: '190px', height: '50px', }} > Select a Serial Number {selectedTokenBalanceWithInfo.nftSerialNumbers?.map((serialNumber) => { return ( {serialNumber} ); } )} )} {selectedTokenBalanceWithInfo?.info?.type === "FUNGIBLE_COMMON" && ( setAmount(parseInt(e.target.value))} sx={{ maxWidth: '100px' }} /> )} HTS Token to setToAccountId(e.target.value)} label='account id or evm address' /> )} ) } Copy There are two crucial points in the new code we added. First, within the setTokenIdToAssociate(e.target.value)} /> )} ) } Copy After adding the code, you will notice that the UI updates to display a text box and a button specifically for the association process. Associate Button Has Been Added To associate the Receiver account with the HTS token, enter the NFTTokenID of the Sender's NFT collection in the token ID textbox and click the Associate button. MetaMask will prompt you to sign the transaction. If the extension doesn't appear automatically, you may need to open it manually by clicking on the extension. Note: The template, by default, uses the Hashio JSON RPC Relay URL to work with MetaMask. If you are experiencing degraded performance, I encourage you to follow the steps outlined in this guide to use Arkhia or set up your own local JSON RPC Relay. You can then edit the src/config/networks.ts with the new JSON RPC Relay URL. src/config/network.ts is where you can update the JSON RPC Relay URL Transfer An NFT as the Sender Once the Associate transaction is confirmed, disconnect from the current session and reconnect as the Sender account.  As the Sender, enter the account ID or EVM address of the Receiver account. Select the NFT with serial number 5 from the drop-down menu and click the send button.  Sign the transaction on MetaMask, which will then transfer the NFT from the Sender to the receiver account seamlessly. Transfer the NFT with Serial Number 5 Verify Receiver Received the NFT Disconnect as the Sender and reconnect as the Receiver.  Check the drop-down menu to ensure the Receiver account has NFT serial number 5. Try with HashPack or Blade Optionally, you can import the ED25519 Sender/Receiver accounts into HashPack and Blade and associate and transfer NFT/FT tokens with those additional wallets. Congratulations! You have successfully walked through creating a Hedera DApp that transfers HTS tokens to MetaMask, HashPack, and Blade. Summary We embarked on an exciting adventure together, using the CRA Hedera DApp template. Along the way, we learned how to write queries to fetch account token balances, token info, and NFT details from the mirror node. We even created a drop-down menu that shows the available token balances. In addition, we added a textbox and a button to associate accounts with token IDs. It was an incredible accomplishment, and I encourage everyone to keep learning! Continue Learning Try Tutorials Join the Developer Discord Explore The Hedera Learning Center Share This Back to blog What is gRPC, gRPC-Web, and Proxies? Ed Marquez Pragmatic Blockchain Design Patterns – Integrating Blockchain into Business Processes Michiel Mulders Zero Cost EthereumTransaction on Success: Hedera's New Fee Model for Relay Operators Oliver Thorn Hedera Adopts Chainlink Standard for Cross-Chain Interoperability To Accelerate Ecosystem Adoption Hedera Team Hedera Developer Highlights March 2025 Michiel Mulders Hedera Release Cycle Overview Ed Marquez View All Posts Sign up for the newsletter CONNECT WITH US Transparency Open Source Audits & Standards Sustainability Commitment Carbon Offsets Governance Hedera Council Public Policy Treasury Management Meeting Minutes LLC Agreement Node Requirements Community Events Meetups HBAR Telegram Developer Discord Twitter Community Support FAQ Network Status Developer Discord StackOverflow Brand Brand Guidelines Built on Hedera Logo Hedera Store About Team Partners Journey Roadmap Careers Contact General Inquiry Public Relations © 2018-2025 Hedera Hashgraph, LLC. All trademarks and company names are the property of their respective owners. All rights in the Deutsche Telekom mark are protected by Deutsche Telekom AG. All rights reserved. Hedera uses the third party marks with permission. Terms of Use  |  Privacy Policy