Fetching multichain token balances
Fetching blockchain balances for an account address is a critical aspect of most dapps. The problem is fetching balances can be difficult, slow, or expensive and inaccurate when relying on 3rd party data services. Fetching balances from a node is fast and accurate, but can be difficult to setup and maintain. This article will show you how to fetch balances from a node from 15 different EVM chains using only RPC providers.
Lets get started!
First, lets discuss the tradeoffs of different approaches to fetching balances.
Approaches
- Fetching balances from a 3rd party data service
Pros
- Easy to setup
- Fast
- Comprehensive token coverage
Cons
- Expensive
- Can be inaccurate due to indexing lag
- Centralized
- Prone to spam
- Minimal chain support
- Fetch balances from a node
Pros
- Fast
- Accurate
- Decentralized
- Cheap
- Comprehensive chain coverage
Cons
- Less comprensive token coverage (only fetches balances for specified tokens)
In this post we will just cover the second approach, fetching balances from a node. In order to do this we will want to batch our requests to the node to reduce the number of requests we need to make. There are a few different ways we can batch our requests.
Methods of batching requests
- Parallelized HTTP requests (HTTP Layer)
- Batched JSON-RPC requests (JSON-RPC Layer)
- Batched contract calls (EVM Layer)
Using batched contract calls we can fetch balances from a node in a single request. This is the fastest and most accurate method of fetching balances. We can use helpful utility contracts such as eth-balance-checker.
The eth-balance-checker contract is deployed on many chains, and can be used to fetch balances for any ERC20 token. It is also possible to deploy your own instance of the contract, and use it to fetch balances for any token on any EVM chain. The contract simply takes an array of addresses and an array of token addresses, and returns an array of balances by iterating over the array and performing balanceOf
calls to the specified contracts.
const EVM_BALANCE_CHECKER_CONTRACT_ADDRESS_MAP: { [key: number]: string } = {
[ChainId.ARBITRUM]: '0x151E24A486D7258dd7C33Fb67E4bB01919B7B32c',
[ChainId.AURORA]: '0x100665685d533F65bdD0BD1d65ca6387FC4F4FDB',
[ChainId.AVALANCHE]: '0xD023D153a0DFa485130ECFdE2FAA7e612EF94818',
[ChainId.BASE]: '0x6f755772f2669800245929cf9d0d9d860fc16e40',
[ChainId.BNB]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4',
[ChainId.CRONOS]: '0x8B14C79f24986B127EC7208cE4e93E8e45125F8f',
[ChainId.ETHEREUM]: '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39',
[ChainId.FANTOM]: '0x07f697424ABe762bB808c109860c04eA488ff92B',
[ChainId.GNOSIS]: '0x6f755772f2669800245929cf9d0d9d860fc16e40',
[ChainId.LINEA]: '0x6f755772f2669800245929cf9d0d9d860fc16e40',
[ChainId.MOONBEAM]: '0xf614056a46e293DD701B9eCeBa5df56B354b75f9',
[ChainId.MOONRIVER]: '0xDEAa846cca7FEc9e76C8e4D56A55A75bb0973888',
[ChainId.OPTIMISM]: '0xB1c568e9C3E6bdaf755A60c7418C269eb11524FC',
[ChainId.POLYGON]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4',
[ChainId.POLYGON_ZKEVM]: '0x6f755772f2669800245929cf9d0d9d860fc16e40',
[ChainId.ZK_SYNC]: '0xb3eD078a5024596F912a96D5Cf864Cb241a95884',
}
Next we setup our RPC provider. We will use Ankr as our RPC provider. Ankr provides free RPC endpoints for many EVM chains making it easy and convenient to interative with various chains from one API. We also will use the ethers.js library to interact with the RPC provider.
const ANKR_RPC_API_DOMAIN = 'rpc.ankr.com'
const ANKR_CHAIN_PATH_MAP: { [key: number]: string } = {
[ChainId.ARBITRUM]: 'arbitrum',
[ChainId.AURORA]: 'aurora',
[ChainId.AVALANCHE]: 'avalanche',
[ChainId.BASE]: 'base',
[ChainId.BNB]: 'bsc',
[ChainId.CRONOS]: 'cronos',
[ChainId.ETHEREUM]: 'eth',
[ChainId.FANTOM]: 'fantom',
[ChainId.GNOSIS]: 'gnosis',
[ChainId.LINEA]: 'linea',
[ChainId.MOONBEAM]: 'moonbeam',
[ChainId.MOONRIVER]: 'moonriver',
[ChainId.OPTIMISM]: 'optimism',
[ChainId.POLYGON]: 'polygon',
[ChainId.POLYGON_ZKEVM]: 'polygon_zkevm',
[ChainId.ZK_SYNC]: 'zksync_era',
}
export const generateAnkrRpcUrl = (chainId: number) => {
if (!Object.keys(ANKR_CHAIN_PATH_MAP).includes(chainId.toString())) {
throw Error(`ChainId ${chainId} is not supported by Ankr`)
}
const chainPath = ANKR_CHAIN_PATH_MAP[chainId]
const rpcUrl = `https://${ANKR_RPC_API_DOMAIN}/${chainPath}`
return rpcUrl
}
Putting it all together we can fetch balances for an account address and an array of token addresses.
export const getAccountErc20Balances = async (chainId: number, accountAddress: string, tokenAddresses: string[]) => {
const balanceCheckerContractAddress = EVM_BALANCE_CHECKER_CONTRACT_ADDRESS_MAP[chainId]
const rpcUrl = generateAnkrRpcUrl(chainId)
const provider = new ethers.providers.JsonRpcProvider(rpcUrl)
const balanceCheckerContract = new ethers.Contract(balanceCheckerContractAddress, balanceCheckerAbi, provider)
const balanceMap = await balanceCheckerContract.balances([accountAddress], tokenAddresses)
const accountBalances = formatTokenBalances(balanceMap, [accountAddress], tokenAddresses)[accountAddress]
return accountBalances
}
The result is a simple function that can be used to fetch balances for any account address on any EVM chain.
const accountAddress = '0x000000'
const tokenAddresses = ['0x000000', '0x000000']
const tokenBalances = getAccountErc20Balances(ChainId.ETHEREUM, accountAddress, tokenAddresses)