To make the setup process simple, you'll use a pre-configured token wrapper project from the token-wrapperrepository.
Open a terminal window and navigate to your preferred directory where your project will live. Run the following command to clone the repo and install dependencies to your local machine:
The dotenv package is used to manage environment variables in a separate .env file, which is loaded at runtime. This helps protect sensitive information like your private keys and API secrets, but it's still best practice to add .env to your .gitignore to prevent you from pushing your credentials to GitHub.
Project Configuration
In this step, you will update and configure the Hardhat configuration file that defines tasks, stores Hedera account private key information, and Hashio Testnet RPC URL. First, rename the .env.example file to .env. and update the .env files with the following code.
Environment Variables
The .env file defines environment variables used in the project. The MY_ACCOUNT_ID and MY_PRIVATE_KEY variables contains the ECDSAAccount ID and DER Encoded Private Key, respectively for the Hedera Testnet account. The HEX_ENCODED_PRIVATE_KEY variable contains the HEX Encoded Private Key.
The JSON_RPC_RELAY_URL variable contains the HashIO Testnet endpoint URL. This is the JSON-RPC instance that will submit the transactions to the Hedera test network to test, create and deploy your smart contract.
In this step, you'll examine the descriptions of the project contents in your existing project. If you don't need to review the project contents, you can proceed directly to Running the project.
This directory contains compiled smart contracts used for generating TypeScript bindings with Typechain. We compile these contracts using Hardhat, a versatile Ethereum development tool. You'll find the configuration in hardhat.config.js.
Think of Hardhat as your all-in-one environment for Ethereum development tasks like compiling, deploying, testing, and debugging.
This directory contains the smart contracts that will be compiled and deployed to the Hedera network. This directory contains the smart contracts that will be compiled and deployed to the Hedera network. Let's take a look at the smart contracts in this directory:
ERC20.sol - This is the ERC20 token contract.
HederaResponseCodes.sol - This is a contract that contains the response codes for the Hedera network.
HederaTokenService.sol - This contract provides the transactions to interact with the tokens created on Hedera.
IHederaTokenService.sol - This is the interface for the HederaTokenService contract.
Vault.sol - This contract, when deployed, manages the functionality of a vault and serves as a testing ground to understand how Hedera native tokens interact with the ERC20 contract.
In this directory, you'll find two main scripts: ERC20.ts and utils.ts. They play a crucial role in our interaction with smart contracts. Here's how it all unfolds:
We kick things off by setting up our environment and defining the essential variables. Using the Hedera SDK, we create a new fungible token, which gives us a unique tokenId.
To streamline our interaction with smart contracts, we utilize the Typechain class to create instances of contractERC20 and contractVault. We provide contractERC20 with the tokenId's solidity address and the Ethereum provider. Similarly, we create an instance for contractVault using the vaultContractAddress (after deployed) and the provider. This approach ensures that both contracts are seamlessly integrated into our development process.
With our environment ready, we dive into interactions with the contracts. We demonstrate a token transfer operation, moving native tokens created on Hedera from one account to another through the ERC20 contract. After that, we check how the balance of the receiving account changes. We do this in two ways: first, by calling a function in the SDK, and second, by using the balanceOf function in the ERC20 contract.
In our second example, we deposit 1000 tokens into the Vault contract and then withdraw them. We keep an eye on how this affects the Vault contract's balance. It's worth noting that in Hedera, you need to associate the recipient account with the token before making transfers. To handle this requirement, we use the associate function from the HederaTokenService contract, which establishes the connection between the Vault contract and the token. Once associated, we can easily deposit and withdraw tokens.
ERC20.ts
console.clear();import { AccountId, PrivateKey, TokenAssociateTransaction, Client, AccountBalanceQuery } from"@hashgraph/sdk";import { ethers } from"ethers";import { createFungibleToken, createAccount, deployContract } from"./utils";import { ERC20__factory } from'../typechain-types/factories/ERC20__factory';import { Vault__factory } from'../typechain-types/factories/Vault__factory';import { config } from"dotenv";config();// Provider to connect to the Ethereum networkconstprovider=newethers.providers.JsonRpcProvider(process.env.JSON_RPC_RELAY_URL!);// Wallet to sign the transactionsconstwallet=newethers.Wallet(process.env.HEX_ENCODED_PRIVATE_KEY!, provider);// Client to interact with the Hedera networkconstclient=Client.forTestnet();constoperatorPrKey=PrivateKey.fromStringECDSA(process.env.HEX_ENCODED_PRIVATE_KEY!);constoperatorAccountId=AccountId.fromString(process.env.MY_ACCOUNT_ID!);client.setOperator(operatorAccountId, operatorPrKey);asyncfunctionmain() {// Create a fungible token with the SDK const tokenId = await createFungibleToken("TestToken", "TT", operatorAccountId, operatorPrKey.publicKey, client, operatorPrKey);
// Create an account for Alice with 10 hbar using the SDKconstaliceKey=PrivateKey.generateED25519();constaliceAccountId=awaitcreateAccount(client, aliceKey,10);console.log(`- Alice account id created: ${aliceAccountId!.toString()}`);// Take the address of the tokenIdconsttokenIdAddress= tokenId!.toSolidityAddress();console.log(`- tokenIdAddress`, tokenIdAddress);// We connect to the ERC20 contract using typechainconstaccount=wallet.connect(provider);constaccountAddress=account.address;console.log(`- accountAddress`, accountAddress);constcontractERC20=ERC20__factory.connect( tokenIdAddress, account );// We deploy the Vault contract using the SDK and take the addressconstcontractVaultId=awaitdeployContract(client,Vault__factory.bytecode,4000000);constcontractVaultAddress= contractVaultId!.toSolidityAddress();console.log(`- contractVaultId`, contractVaultId!.toString());// We connect to the Vault contract using typechainconstcontractVault=Vault__factory.connect( contractVaultAddress, wallet );// We set Alice as the operator, now she is the one interacting with the hedera networkconstaliceClient=client.setOperator(aliceAccountId!, aliceKey);// We associate the Alice account with the tokenconsttokenAssociate=awaitnewTokenAssociateTransaction().setAccountId(aliceAccountId!).setTokenIds([tokenId!]).execute(aliceClient);consttokenAssociateReceipt=awaittokenAssociate.getReceipt(aliceClient);console.log(`- tokenAssociateReceipt ${tokenAssociateReceipt.status.toString()}`);constaliceAccountAddress= aliceAccountId!.toSolidityAddress();// We transfer 10 tokens to Alice using the ERC20 contractconsttransfer=awaitcontractERC20.transfer(aliceAccountAddress,10, { gasLimit:1000000 })consttransferReceiptWait=awaittransfer.wait();console.log(`- Transfer`, transferReceiptWait);// We check the balance tokenId from Alice using the SDKconstbalanceAliceNativeToken=newAccountBalanceQuery().setAccountId(aliceAccountId!)consttransactionQuery=awaitbalanceAliceNativeToken.execute(client);constbalanceTokenSDK=transactionQuery.tokens!.get(tokenId!);console.log("- Balance from Alice using the SDK",balanceTokenSDK.toString());// We check the balance tokenId from Alice using the ERC20 contractconstbalanceAliceERC20=awaitcontractERC20.balanceOf(aliceAccountAddress);console.log("- Balance from Alice using the ERC20",parseInt(balanceAliceERC20.toString()));// We associate the Vault contract with the token so we can transfer tokens to itconstassociate=awaitcontractVault.associateFungibleToken(tokenIdAddress, { gasLimit:1000000 })constassociateReceipt=awaitassociate.wait();console.log(`- Associate`, associateReceipt);// We deposit 1000 tokens to the Vault contractconstdeposit=awaitcontractERC20.transfer(contractVaultAddress,1000, { gasLimit:1000000 })constdepositReceipt=awaitdeposit.wait();console.log(`- Deposit tokens to the Vault contract`, depositReceipt);// We check the balance of the Vault contract after the depositlet balanceVaultERC20 =awaitcontractERC20.balanceOf(contractVaultAddress);console.log(`- Balance of the Vault contract before the withdraw`,parseInt(balanceVaultERC20.toString()));// We withdraw 100 tokens from the Vault contractconstwithdraw=awaitcontractVault.withdraw(tokenIdAddress, { gasLimit:1000000 })constwithdrawReceipt=awaitwithdraw.wait();console.log(`- Withdraw tokens from the Vault contract`, withdrawReceipt);// We check again the balance of the Vault contract after the withdraw to see if it has changed balanceVaultERC20 =awaitcontractERC20.balanceOf(contractVaultAddress);console.log(`- Balance of the Vault contract after the withdraw`,parseInt(balanceVaultERC20.toString()));}main();
This directory contains the typescript bindings generated by typechain. These bindings are used to interact with the smart contracts.
Running the project
Now that you have your project set up and configured, we can run it.
To do so, run the following command:
npmruncompilets-nodescripts/ERC20.ts
The first command compiles the smart contracts and generates the typescript bindings. The second command runs the ERC20 script.