diff --git a/app/api/balances/evm-client.ts b/app/api/balances/evm-client.ts new file mode 100644 index 00000000..98bb4974 --- /dev/null +++ b/app/api/balances/evm-client.ts @@ -0,0 +1,60 @@ +import { createPublicClient, http, Address, erc20Abi } from 'viem'; +import { getViemChain, getDefaultRPC, SupportedNetworks } from '@/utils/networks'; + +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: erc20Abi, + 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',