From 0cf11176cce1fdcccc4a3a678475ddcd5fd54900 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 29 Oct 2025 14:56:13 +0800 Subject: [PATCH 1/2] fix: fetch hyperevm balances with multicall --- app/api/balances/evm-client.ts | 70 ++++++++++++++++++++++++++++++++++ app/api/balances/route.ts | 14 +++++-- 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 app/api/balances/evm-client.ts diff --git a/app/api/balances/evm-client.ts b/app/api/balances/evm-client.ts new file mode 100644 index 00000000..33cf84e2 --- /dev/null +++ b/app/api/balances/evm-client.ts @@ -0,0 +1,70 @@ +import { createPublicClient, http, Address } from 'viem'; +import { getViemChain, getDefaultRPC, SupportedNetworks } from '@/utils/networks'; + +const ERC20_ABI = [ + { + inputs: [{ name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +] as const; + +type TokenBalance = { + address: string; + balance: string; +}; + +/** + * Fetches ERC20 token balances for a given address on HyperEVM by directly calling balanceOf on each token contract + */ +export async function getHyperEVMBalances( + userAddress: string, + tokenAddresses: string[], +): Promise { + const client = createPublicClient({ + chain: getViemChain(SupportedNetworks.HyperEVM), + transport: http(getDefaultRPC(SupportedNetworks.HyperEVM)), + }); + + // Create multicall contracts for all token addresses + const contracts = tokenAddresses.map((tokenAddress) => ({ + address: tokenAddress as Address, + abi: ERC20_ABI, + functionName: 'balanceOf', + args: [userAddress as Address], + })); + + try { + // Use multicall to batch all balance queries into a single RPC call + const results = await client.multicall({ + contracts, + allowFailure: true, + }); + + // Filter out failed calls and zero balances, then format the response + const tokens: TokenBalance[] = []; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + + if (result.status === 'success' && result.result !== undefined) { + const balance = result.result as bigint; + + // Only include non-zero balances + if (balance > 0n) { + tokens.push({ + address: tokenAddresses[i].toLowerCase(), + balance: balance.toString(10), + }); + } + } + } + + return tokens; + } catch (error) { + console.error('Failed to fetch HyperEVM balances via multicall:', error); + throw new Error('Failed to fetch balances from HyperEVM'); + } +} diff --git a/app/api/balances/route.ts b/app/api/balances/route.ts index 6ba866f8..af499b6b 100644 --- a/app/api/balances/route.ts +++ b/app/api/balances/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { SupportedNetworks, getDefaultRPC } from '@/utils/networks'; import { supportedTokens } from '@/utils/tokens'; +import { getHyperEVMBalances } from './evm-client'; type TokenBalance = { contractAddress: string; @@ -17,7 +18,8 @@ export async function GET(req: NextRequest) { } try { - const alchemyUrl = getDefaultRPC(Number(chainId) as SupportedNetworks); + const chainIdNum = Number(chainId) as SupportedNetworks; + const alchemyUrl = getDefaultRPC(chainIdNum); if (!alchemyUrl) { throw new Error(`Chain ${chainId} not supported`); } @@ -25,14 +27,20 @@ export async function GET(req: NextRequest) { // Get supported token addresses for this chain const tokenAddresses = supportedTokens .filter(token => - token.networks.some(network => network.chain.id === Number(chainId)) + token.networks.some(network => network.chain.id === chainIdNum) ) .flatMap(token => token.networks - .filter(network => network.chain.id === Number(chainId)) + .filter(network => network.chain.id === chainIdNum) .map(network => network.address) ); + // Special handling for HyperEVM - use direct balanceOf calls via multicall + if (chainIdNum === SupportedNetworks.HyperEVM) { + const tokens = await getHyperEVMBalances(address, tokenAddresses); + return NextResponse.json({ tokens }); + } + // Get token balances for specific tokens only const balancesResponse = await fetch(alchemyUrl, { method: 'POST', From 77867026a225bf576cd04a82a7ff808daae3773c Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 29 Oct 2025 14:58:09 +0800 Subject: [PATCH 2/2] chore: remove abi --- app/api/balances/evm-client.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/api/balances/evm-client.ts b/app/api/balances/evm-client.ts index 33cf84e2..98bb4974 100644 --- a/app/api/balances/evm-client.ts +++ b/app/api/balances/evm-client.ts @@ -1,16 +1,6 @@ -import { createPublicClient, http, Address } from 'viem'; +import { createPublicClient, http, Address, erc20Abi } from 'viem'; import { getViemChain, getDefaultRPC, SupportedNetworks } from '@/utils/networks'; -const ERC20_ABI = [ - { - inputs: [{ name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, -] as const; - type TokenBalance = { address: string; balance: string; @@ -31,7 +21,7 @@ export async function getHyperEVMBalances( // Create multicall contracts for all token addresses const contracts = tokenAddresses.map((tokenAddress) => ({ address: tokenAddress as Address, - abi: ERC20_ABI, + abi: erc20Abi, functionName: 'balanceOf', args: [userAddress as Address], }));