Create Multichain Tokens with Interchain Token Service
The next level in Web3 - creating a Multichain Token that is fungible and customizable across all blockchains - is here, made possible by Axelar's new Interchain Token Service.
This service employs Axelar's communication protocols to facilitate cross-chain token transfers. It allows you to incorporate asset bridges and transfers into your interchain dApp and explore many other use cases.
Moralis offers industry-leading tools that make interchain Web3 development more accessible for millions around the world. Let's build a Moralis dapp with our Ethereum Boilerplate that integrates Axelar Interchain Token Service to build multichain tokens. It's effortless!
π‘ You can find the complete code for this tutorial on GitHub.
Prerequisitesβ
You will need:
- A basic understanding of JavaScript and React.
- A MetaMask wallet with FTM and Celo funds for testing. If you don't have these funds, you can get FTM from the Fantom faucet and CELO from the Celo Alfajores faucets (1, 2).
Project setup and installationβ
To quickly set up your development environment, clone this project on GitHub using the following command:
git clone https://github.com/Olanetsoft/multichain-token-example-with-interchain-token-service.git
π‘ Make sure you're on the starter branch.
Install dependenciesβ
Next, change the directory into the cloned folder and install the project dependencies locally using yarn with the following command:
cd multichain-token-example-with-interchain-token-service && yarn install
yarn run dev
yarn run dev will start a Next.js hot-reloading development environment accessible by default at http://localhost:3000.
The cloned project includes several folders:
- contracts: This folder contains the contract ABIs that we'll use in this project to interact with the Interchain Token Service contract.
- src: This folder contains component templates that you'll use to implement the creation of the Interchain Token, its remote deployment, and the cross-chain token transfer.
- pages: This folder contains different pages within the application.
- cypress: This folder contains basic Cypress tests.
Rename .env.local.example to .env.local and provide required data. Get your Web3 API Key from the Moralis dashboard.
Fill the environment variables in your .env.local file in the app root:
- MORALIS_API_KEY: You can get it [here](https://admin.moralis.com/web3apis).
- NEXTAUTH_URL: Your app address. In the development stage, use [http://localhost:3000](http://localhost:3000).
- NEXTAUTH_SECRET: Used for encrypting JWT tokens of users. You can put any value here or generate it on [https://generate-secret.now.sh/32](https://generate-secret.now.sh/32).
Example:
MORALIS_API_KEY=xxxx
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=7197b3e8dbee5ea6274cab37245eec212
Register and deploy a new Interchain Tokenβ
Now that you are fully set up, you can begin implementing the registration and deployment of a new Multichain Token, also known as an Interchain Token. On the navigation bar, you'll see an option for Interchain Tokens. Clicking on it will display a UI similar to the following, but it wonβt be functional yet. You will add functionality in this section of the tutorial.
Import the Interchain Token Factory ABI and contract addressesβ
Navigate to the src/components/interchain/create-token folder and open the create-token.tsx file, then add the following code:
//...
import interchainTokenFactoryContractABI from "../../../../../contracts/InterchainTokenFactoryABI.json";
const interchainTokenFactoryContractAddress =
"0x83a93500d23Fbc3e82B410aD07A6a9F7A0670D66";
const NewInterchainToken: React.FC = () => {
//...
};
Create state variables to save the token info from the UIβ
Add the following code to set up state variables to collect token information from the UI. This data will be used to interact with the contract.
//...
const NewInterchainToken: React.FC = () = >{
//...
const[tokenName, setTokenName] = useState < string > "";
const[tokenSymbol, setTokenSymbol] = useState < string > "";
const[tokenDecimals, setTokenDecimals] = useState < number > 18;
const[initialSupply, setInitialSupply] = useState < number > 0;
const[saltValue, setSaltValue] = useState < string > "";
//...
};
Implement create token functionalityβ
wagmi is already installed in the cloned project. Use useContractWrite() and useWaitForTransaction() to interact with the InterchainTokenFactory contract:
//...
const NewInterchainToken: React.FC = () = >{
//...
// Create a new token
const {
data: createNewToken,
write
} = useContractWrite({
address: interchainTokenFactoryContractAddress,
abi: interchainTokenFactoryContractABI,
functionName: 'deployInterchainToken',
args: [saltValue, // unique salt value
tokenName, // token name
tokenSymbol, // token symbol
tokenDecimals, ethers.utils.parseEther(initialSupply.toString()), // Initial token supply
address, // signer address
],
mode: 'recklesslyUnprepared',
});
const {
data: useWaitForDeployTokenTransactionData,
isSuccess,
isError,
isLoading,
} = useWaitForTransaction({
hash: createNewToken ? .hash,
});
// Method to handle token creation to be used in the 'create' button
// onClick event
const handleCreateToken = async() = >{
if (!tokenName || !tokenSymbol || tokenDecimals < 0 || initialSupply <= 0) {
toast({
title: 'Invalid Input',
description: 'Please fill all the fields correctly.',
status: 'error',
duration: 5000,
isClosable: true,
});
return;
}
write();
toast({
title: 'Transaction Submitted',
description: 'Please confirm the transaction in MetaMask.',
status: 'info',
duration: 5000,
isClosable: true,
});
};
return (
//...
)
}
Implement unique generated value for saltβ
A unique salt value is required when creating a new Interchain Token. This value should be unique across different tokens on different chains. As this value needs to be unique, it should be generated only once when the page mounts.
//...
const NewInterchainToken: React.FC = () = >{
//...
useEffect(() = >{
const localSaltValue = `0x$ {
crypto.randomBytes(32).toString('hex')
}`;
setSaltValue(localSaltValue);
},
[]);
return (
//...
)
}
Track transaction status and update the UIβ
Add the following code to track the transaction status after triggering the deployInterchainToken() method. To do this, you will need to add the following code in another useEffect hook:
//...
const[displayTransactionHash, setDisplayTransactionHash] = useState < string > ('');
const NewInterchainToken: React.FC = () = >{
//...
useEffect(() = >{
if (isSuccess) {
setDisplayTransactionHash(createNewToken ? .hash ? ?'');
toast({
title: 'New Interchain Token Created',
status: 'success',
duration: 5000,
isClosable: true,
});
// Clear only the input fields
setTokenName('');
setTokenSymbol('');
setTokenDecimals(18);
setInitialSupply(0);
setShowNextStep(true);
}
if (isError) {
toast({
title: 'Transaction Error',
description: 'There was an error submitting your transaction.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
if (isLoading) {
toast({
title: 'Transaction Pending',
description: 'Your transaction is pending.',
status: 'info',
duration: 5000,
isClosable: true,
});
}
},
[createNewToken, isSuccess, isError, isLoading, useWaitForDeployTokenTransactionData]);
return (
//...
)
}
Update UI to implement the create token functionalityβ
Thus far, you have successfully implemented the interaction to the deployInterchainToken()function on the InterchainTokenFactory contract. The next step is to connect this function to the user interface you cloned earlier by updating the code with the following snippet:
//...
const NewInterchainToken: React.FC = () => {
//...
return (
<Box
padding="7"
maxW="xxl"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
margin="auto"
marginTop="-20"
>
<Heading size="lg" marginBottom="6" textAlign="center">
Create a New Interchain Token
</Heading>
<VStack spacing={5} align="stretch">
<FormControl>
<FormLabel>Token Name</FormLabel>
<Input
placeholder="Enter token name"
value={tokenName}
onChange={(e) => setTokenName(e.target.value)}
/>
<FormHelperText>Unique name for your token.</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Token Symbol</FormLabel>
<Input
placeholder="Enter token symbol"
value={tokenSymbol}
onChange={(e) => setTokenSymbol(e.target.value)}
/>
<FormHelperText>
Short symbol for your token, like ETH or BTC.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Token Decimals</FormLabel>
<Input
type="number"
placeholder="Enter token decimals"
value={tokenDecimals.toString()}
onChange={(e) => setTokenDecimals(Number(e.target.value))}
/>
<FormHelperText>
Number of decimal places for your token.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Initial Supply</FormLabel>
<Input
type="number"
placeholder="Enter initial supply"
value={initialSupply.toString()}
onChange={(e) => setInitialSupply(Number(e.target.value))}
/>
<FormHelperText>Total initial supply of tokens.</FormHelperText>
</FormControl>
<Text fontSize="sm" color="gray.500">
Unique Salt: {saltValue}
</Text>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Button
colorScheme="cyan"
onClick={handleCreateToken}
isLoading={isLoading}
loadingText="Creating Token"
w="sm"
variant="solid"
disabled={isLoading}
>
Create Token
</Button>
{/* ... */}
</div>
{/* ... */}
</VStack>
</Box>
);
};
Ensure you replace {/ ... /} with the actual code. This placeholder is used to prevent the repetition of existing code in the codebase.
Test the application to create a new Interchain tokenβ
The example below demonstrates how to create a new token named "My New Token" with the symbol "MNT" and an initial supply of 20,000.
Check the transaction on the Fantom testnet scannerβ
Check the Fantom testnet scanner to see if you have successfully created and locally deployed a new Interchain Token on the Fantom testnet.
π‘ Remember to save the unique salt value. You will need it later in this tutorial.
Deploy an Interchain Token to a remote chainβ
You've successfully deployed an Interchain Token to Fantom. Next, deploy it to Celo, which will serve as the remote chain in this tutorial.
π‘ Any two chains can be specified as your local and remote chains.
Navigate to the src/components/interchain/deploy-token folder and open the deploy-token.tsx file. This is where you will implement the remote token deployment functionality.
Create state variables to save the token info from the UIβ
You need to create state variables to collect token information from the UI. This data will be used to interact with the contract and deploy your token remotely. Use the following code to create these variables:
//...
const DeployTokenRemotely = () = >{
//...
const[sourceChain, setSourceChain] = useState < string > ('');
const[destinationChain, setDestinationChain] = useState < string > ('');
const[saltValue, setSaltValue] = useState < string > ('');
//...
}
### Estimate gas fees
To remotely deploy the new Interchain Token, you need to [estimate the gas fee](https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee) for the cross-chain call. You can use the Axelar JS SDK to estimate this fee in your React application.
Update the deploy-token.js file:
//...
const DeployTokenRemotely = () = >{
//...
const api: AxelarQueryAPI = new AxelarQueryAPI({
environment: Environment.TESTNET
});
const[gasAmount, setGasAmount] = useState < number > (0);
// Estimate Gas
const gasEstimator = async() = >{
try {
const gas = await api.estimateGasFee(sourceChain, destinationChain, GasToken.FTM, 700000, 2);
setGasAmount(Number(gas));
} catch(error) {
console.error('Error estimating gas fee: ', error);
}
};
return (
//..
)
}
Implement remote token deploymentβ
Next, implement the remote token deployment functionality. Do this by invoking the deployRemoteInterchainToken() function on the InterchainTokenFactory contract. Use the following code and specify the required parameters: sourceChain, saltValue, address, destinationChain, gasValue, and the cross-chain gas value:
//...
const DeployTokenRemotely = () = >{
//...
// Deploy a token remotely
const {
data: deployTokenRemotely,
write
} = useContractWrite({
address: interchainTokenFactoryContractAddress,
abi: interchainTokenFactoryContractABI,
functionName: 'deployRemoteInterchainToken',
args: [sourceChain, saltValue, address, destinationChain, ethers.BigNumber.from(gasAmount.toString())],
overrides: {
value: ethers.BigNumber.from(gasAmount.toString()),
},
mode: 'recklesslyUnprepared',
});
const {
data: useWaitForDeployTokenRemotelyTransactionData,
isSuccess,
isError,
isLoading,
} = useWaitForTransaction({
// Calling a hook to wait for the transaction to be mined
hash: deployTokenRemotely ? .hash,
});
const handleDeployToken = async() = >{
if (write) {
write();
toast({
title: 'Transaction Submitted',
description: 'Please confirm the transaction in MetaMask.',
status: 'info',
duration: 5000,
isClosable: true,
});
}
if (isError) {
toast({
title: 'Transaction Error',
description: 'There was an error submitting your transaction.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
};
return (
//..
)
}
Track transaction status and update the UIβ
Add the following code to track the transaction status after triggering the deployRemoteInterchainToken() method. To do this, you will need to add the following code in useEffect hook:
//...
const DeployTokenRemotely = () = >{
//...
useEffect(() = >{
gasEstimator();
if (isSuccess) {
setDisplayTransactionHash(deployTokenRemotely ? .hash ? ?'');
toast({
title: 'Token Deployed Remotely',
status: 'success',
duration: 5000,
isClosable: true,
});
setShowNextStep(true);
}
if (isError) {
toast({
title: 'Transaction Error',
description: 'There was an error submitting your transaction.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
if (isLoading) {
toast({
title: 'Transaction Pending',
description: 'Your transaction is pending.',
status: 'info',
duration: 5000,
isClosable: true,
});
}
},
[deployTokenRemotely, isSuccess, isError, isLoading, useWaitForDeployTokenRemotelyTransactionData]);
return (
//..
)
}
Update the UI to implement the remote token deployment functionalityβ
So far, you have successfully implemented the interaction for the deployRemoteInterchainToken() function on the InterchainTokenFactory contract. Now connect this function to the user interface you cloned earlier by updating your code with the following snippet:
//...
const DeployTokenRemotely = () => {
//...
return (
<Box
padding="7"
maxW="xxl"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
margin="auto"
marginTop="-20"
>
{/* ... */}
<VStack spacing={5} align="stretch">
<FormControl>
<FormLabel>Your unique salt value</FormLabel>
<Input
placeholder="Enter Salt Value"
value={saltValue}
onChange={(e) => setSaltValue(e.target.value)}
/>
<FormHelperText>Unique salt value for your token.</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Source chain</FormLabel>
<Stack spacing={3}>
<Select
placeholder="Select source chain"
value={sourceChain}
onChange={(e) => setSourceChain(e.target.value)}
size="md"
>
{/* ... */}
</Select>
</Stack>
<FormHelperText>
Source chain for your token eg. Fantom, binance, Polygon etc.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Destination chain</FormLabel>
<Stack spacing={3}>
<Select
placeholder="Select Destination chain"
value={destinationChain}
onChange={(e) => setDestinationChain(e.target.value)}
size="md"
>
{/* ... */}
</Select>
</Stack>
<FormHelperText>
Destination chain for your token eg. Fantom, binance, Polygon etc.
</FormHelperText>
</FormControl>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Button
colorScheme="cyan"
onClick={handleDeployToken}
isLoading={isLoading}
loadingText="Deploying Token Remotely..."
w="sm"
variant="solid"
disabled={isLoading}
>
Deploy Token Remotely
</Button>
{/* ... */}
</div>
{/* ... */}
</VStack>
</Box>
);
};
π‘ Make sure to replace {/ ... /} with the actual code. This placeholder is used to prevent the repetition of existing code in the codebase.
Test the application to deploy the Interchain Token remotely
Add the salt value you saved from the previous step. Then, select Fantom as the source chain and Celo as the destination chain:
Check the transaction on the Axelar testnet scannerβ
Check the Axelarscan testnet scanner to see if you have successfully created and remotely deployed MNT on the Celo testnet. It should look something like this. Ensure that Axelar shows a successful transaction before continuing to the next step.
If you got this far, good job! You are almost done with the tutorial.
Transfer your token between chainsβ
Now that you've successfully created and deployed your new Interchain Token on both the Fantom and Celo testnets, you can transfer tokens between chains seamlessly via the user interface.
Navigate to the src/components/interchain/transfer-token folder and open the transfer-token.tsx file, where you will implement the token transfer functionality.
Create state variables to save the token info from the UIβ
You need to create state variables to collect token information from the UI:
//...
const TransferToken = () = >{
//...
const[sourceChain, setSourceChain] = useState < string > ('');
const[destinationChain, setDestinationChain] = useState < string > ('');
const[receiverAddress, setReceiverAddress] = useState < string > ('');
const[amountToTransfer, setAmountToTransfer] = useState < number > (0);
const[interchainTokenContractAddress, setInterchainTokenContractAddress] = useState < string > ('');
return (
//...
)
}
Estimate gas feesβ
Youβll need to estimate the gas fee for the cross-chain call to transfer tokens remotely. You can use the Axelar JS SDK to estimate this fee. Update the transfer-token.js file using the following:
//...
const TransferToken = () = >{
//...
const api: AxelarQueryAPI = new AxelarQueryAPI({
environment: Environment.TESTNET
});
const[gasAmount, setGasAmount] = useState < number > (0);
// Estimate Gas
const gasEstimator = async() = >{
try {
const gas = await api.estimateGasFee(sourceChain, destinationChain, GasToken.FTM, 700000, 2);
setGasAmount(Number(gas));
} catch(error) {
console.error('Error estimating gas fee: ', error);
}
};
return (
//..
)
}
Implement token transferβ
To implement the token transfer, invoke the interchainTransfer() function on the created Interchain Token contract. Use the following code and specify the necessary parameters: destinationChain, receiverAddress, amount, gasValue, '0x', and the cross-chain gas value.
//...
const TransferToken = () = >{
//...
// Token Transfer
const {
data: tokenTransfer,
write
} = useContractWrite({
address: interchainTokenContractAddress,
abi: interchainTokenContractABI,
functionName: 'interchainTransfer',
args: [destinationChain, receiverAddress, ethers.utils.parseEther(amountToTransfer.toString()), '0x'],
overrides: {
value: ethers.BigNumber.from(gasAmount.toString()),
},
mode: 'recklesslyUnprepared',
});
const {
data: useWaitForTokenTransferTransactionData,
isSuccess,
isError,
isLoading,
} = useWaitForTransaction({
// Call a hook to wait for the transaction to be mined
hash: tokenTransfer ? .hash,
});
// token transfer
const handleTokenTransfer = async() = >{
if (!sourceChain || !destinationChain || !receiverAddress || !amountToTransfer) {
toast({
title: 'Invalid Input',
description: 'Please fill all the fields correctly.',
status: 'error',
duration: 5000,
isClosable: true,
});
return;
}
if (write) {
write();
toast({
title: 'Transaction Submitted',
description: 'Please confirm the transaction in Metamask.',
status: 'info',
duration: 5000,
isClosable: true,
});
}
if (isError) {
toast({
title: 'Transaction Error',
description: 'There was an error submitting your transaction.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
};
return (
//...
);
};
Track transaction status and update the UIβ
Add the following code to track the transaction status after triggering the interchainTransfer() method. To do this, you will need to add the following code in useEffect hook:
//...
const TransferToken = () = >{
//...
useEffect(() = >{
gasEstimator();
if (isSuccess) {
setDisplayTransactionHash(tokenTransfer ? .hash ? ?'');
toast({
title: 'Token Transfer Initiated',
status: 'success',
duration: 5000,
isClosable: true,
});
}
if (isError) {
toast({
title: 'Transaction Error',
description: 'There was an error submitting your transaction.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
if (isLoading) {
toast({
title: 'Transaction Pending',
description: 'Your transaction is pending.',
status: 'info',
duration: 5000,
isClosable: true,
});
}
},
[tokenTransfer, isSuccess, isError, isLoading, useWaitForTokenTransferTransactionData]);
return (
//..
)
}
Update the UI to implement the token transfer functionalityβ
To connect the token transfer implementation to the UI, update the code using the following snippet:
//...
const TransferToken = () => {
//...
return (
<Box
padding="7"
maxW="xxl"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
margin="auto"
marginTop="-20"
>
{/* ... */}
<VStack spacing={5} align="stretch">
<FormControl>
<FormLabel>Source Chain Name</FormLabel>
<Stack spacing={3}>
<Select
placeholder="Select source chain"
value={sourceChain}
onChange={(e) => setSourceChain(e.target.value)}
size="md"
>
{/* ... */}
</Select>
</Stack>
<FormHelperText>
Source chain for your token eg. Fantom, binance, Polygon etc.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Token Contract Address</FormLabel>
<Input
placeholder="Enter Token Contract Address"
value={interchainTokenContractAddress}
onChange={(e) => setInterchainTokenContractAddress(e.target.value)}
/>
<FormHelperText>
Contract address of the token you want to transfer.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Destination Chain</FormLabel>
<Stack spacing={3}>
<Select
placeholder="Select Destination chain"
value={destinationChain}
onChange={(e) => setDestinationChain(e.target.value)}
size="md"
>
{/* ... */}
</Select>
</Stack>
<FormHelperText>
Destination chain for your token eg. Fantom, binance, Polygon etc.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Receiver Address</FormLabel>
<Input
placeholder="Enter Receiver Address"
value={receiverAddress}
onChange={(e) => setReceiverAddress(e.target.value)}
/>
<FormHelperText>Receiver address for your token.</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Amount to Transfer</FormLabel>
<Input
placeholder="Enter Amount to Transfer"
value={amountToTransfer}
onChange={(e) => setAmountToTransfer(Number(e.target.value))}
/>
<FormHelperText>
Amount to transfer to the receiver address.
</FormHelperText>
</FormControl>
<Button
colorScheme="cyan"
onClick={handleTokenTransfer}
isLoading={isLoading}
loadingText="Transferring Token..."
w="sm"
variant="solid"
disabled={isLoading}
>
Transfer Token
</Button>
{/* ... */}
</VStack>
</Box>
);
};
Ensure you replace {/ ... /} with the actual code. This placeholder prevents the repetition of existing code in the codebase.
Test the application to transfer tokens between chainsβ
Add all the required information below, including the amount you want to transfer.
You can also check the transactions directly from your application. Navigate to the Transactions tab on the menu. You should see a list of transactions done by the connected wallet address, which was implemented using the useEvmWalletTransactions hook from Moralis inside the Transactions.tsx
page. You can learn more about getting transactions by user address here.
Check the transactions using Moralis APIβ
You can also check the transactions directly from your application. Navigate to the Transactions tab on the menu. You should see a list of transactions done by the connected wallet address so far, which was implemented using the useEvmWalletTransactions hook from Moralis inside the Transactions.tsx page. You can learn more about getting transactions by user address here.
When you navigate to the transactions page on your browser, you should see a table similar to the one below.
Check the ERC-20 asset balance with Moralis APIβ
You successfully created and transferred a new ERC-20 token from the Fantom testnet to the Celo testnet. To track the asset balance of the address you transferred to, you can connect the address, navigate to the Balances tab, and click ERC20, which was implemented using the useEvmWalletTokenBalances hook from Moralis.
useEvmWalletTokenBalances is a function that comes with the imported Moralis package and queries all of a userβs ERC-20 tokens for a specific wallet address at a given chain ID.
Learn more about retrieving ERC20 programmatically in dapp here.
When you navigate to the balances page on your browser, you should see a table similar to the one below.
Woohoo! Congratulations, you have just completely built and deployed a multichain token across two blockchains using the Moralis Ethereum Boilerplate. Great job making it this far!
What's nextβ
You can also explore other functionalities of the Interchain Token Service, such as:
- Creating a new custom Interchain Token
- Transforming an existing token into an Interchain token
- Turn deployed tokens on multiple chains into Interchain Tokens