From e215d92a72b4a0d1d4b235c367c4412a0f4c4083 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 17 Dec 2025 16:02:00 +0200 Subject: [PATCH 1/4] add utilities Signed-off-by: Gerhard Steenkamp --- src/utils/sdk.ts | 2 ++ src/utils/token.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/utils/sdk.ts b/src/utils/sdk.ts index 65622bd7b..cac64bbd5 100644 --- a/src/utils/sdk.ts +++ b/src/utils/sdk.ts @@ -1,6 +1,8 @@ import { BigNumber, providers } from "ethers"; import { EVMBlockFinder } from "@across-protocol/sdk/dist/esm/arch/evm/BlockUtils"; import { toAddressType as _toAddressType } from "@across-protocol/sdk/dist/esm/utils/AddressUtils"; +export { getAssociatedTokenAddress } from "@across-protocol/sdk/dist/esm/arch/svm/SpokeUtils"; +export { toAddress } from "@across-protocol/sdk/dist/esm/arch/svm/utils"; export { getCCTPDepositAccounts } from "@across-protocol/sdk/dist/esm/arch/svm/SpokeUtils"; export { SVMBlockFinder } from "@across-protocol/sdk/dist/esm/arch/svm/BlockUtils"; diff --git a/src/utils/token.ts b/src/utils/token.ts index 4de981d53..9b25e02cb 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -7,6 +7,12 @@ import { getConfig, getChainInfo, parseUnits, + getSVMRpc, + toAddressType, + toAddress, + Address, + getAssociatedTokenAddress, + chainIsSvm, } from "utils"; import { ERC20__factory } from "utils/typechain"; import { SwapToken } from "utils/serverless-api/types"; @@ -49,6 +55,55 @@ export async function getBalance( return balance; } +export function toSolanaKitAddress(address: Address) { + return toAddress(address); +} + +export async function getSvmBalance( + chainId: string | number, + account: string, + token: string +) { + const tokenMint = toAddressType(token, Number(chainId)); + const owner = toAddressType(account, Number(chainId)); + const svmProvider = getSVMRpc(Number(chainId)); + + if (tokenMint.isZeroAddress()) { + const address = toSolanaKitAddress(owner); + const balance = await svmProvider.getBalance(address).send(); + return BigNumber.from(balance.value); + } + + // Get the associated token account address + const tokenAccount = await getAssociatedTokenAddress( + owner.forceSvmAddress(), + tokenMint.forceSvmAddress() + ); + + let balance: BigNumber; + try { + // Get token account info + const tokenAccountInfo = await svmProvider + .getTokenAccountBalance(tokenAccount) + .send(); + balance = BigNumber.from(tokenAccountInfo.value.amount); + } catch (error) { + // If token account doesn't exist or other error, return 0 balance + balance = BigNumber.from(0); + } + return balance; +} + +export async function getTokenBalance( + chainId: number, + account: string, + tokenAddress: string +) { + return chainIsSvm(chainId) + ? getSvmBalance(chainId, account, tokenAddress) + : getBalance(chainId, account, tokenAddress); +} + /** * * @param chainId The chain Id of the chain to query From d2ed72cb86079c4553f7e00081228ae2e267330e Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 17 Dec 2025 16:02:33 +0200 Subject: [PATCH 2/4] add local fetcher hook. Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/hooks/useTokenBalance.ts | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/src/views/SwapAndBridge/hooks/useTokenBalance.ts b/src/views/SwapAndBridge/hooks/useTokenBalance.ts index 5a826c4ac..cc52cbc4f 100644 --- a/src/views/SwapAndBridge/hooks/useTokenBalance.ts +++ b/src/views/SwapAndBridge/hooks/useTokenBalance.ts @@ -1,24 +1,70 @@ import { BigNumber } from "ethers"; import { useUserTokenBalances } from "hooks/useUserTokenBalances"; import { useMemo } from "react"; -import { compareAddressesSimple } from "utils"; +import { + chainIsSvm, + compareAddressesSimple, + getTokenBalance, + isDefined, +} from "utils"; import { EnrichedToken } from "../components/ChainTokenSelector/ChainTokenSelectorModal"; +import { useQuery } from "@tanstack/react-query"; +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { useConnectionSVM } from "hooks/useConnectionSVM"; -// todo: implement local rpc fallback +// returns the connected wallet's balance for particular token export function useTokenBalance( token?: Pick | null | undefined ) { const { data: userBalances } = useUserTokenBalances(); + const { data: localBalance } = useTokenBalanceLocal(token); return useMemo(() => { - return token - ? BigNumber.from( - userBalances?.balances - ?.find((c) => Number(c.chainId) === token.chainId) - ?.balances.find((b) => - compareAddressesSimple(b.address, token.address) - )?.balance ?? 0 - ) - : undefined; - }, [token, userBalances?.balances]); + if (!token) return BigNumber.from(0); + + // Try local first (we update more often) + if (localBalance !== undefined && localBalance !== null) { + return BigNumber.from(localBalance); + } + + // Fall back to API balance + const balanceFromUserBalances = userBalances?.balances + ?.find((c) => Number(c.chainId) === token.chainId) + ?.balances.find((b) => + compareAddressesSimple(b.address, token.address) + )?.balance; + + if ( + balanceFromUserBalances !== undefined && + balanceFromUserBalances !== null + ) { + return BigNumber.from(balanceFromUserBalances); + } + + return BigNumber.from(0); + }, [token, localBalance, userBalances?.balances]); +} + +export function useTokenBalanceLocal( + token?: Pick | null | undefined +) { + const { account: evmAccount } = useConnectionEVM(); + const { account: svmAccount } = useConnectionSVM(); + + const svmAccountString = svmAccount?.toString(); + + return useQuery({ + queryKey: ["tokenBalanceLocal", token, svmAccountString, evmAccount], + queryFn: async () => { + if (!token) return; + const wallet = chainIsSvm(token.chainId) ? svmAccountString : evmAccount; + if (!wallet) return; + return await getTokenBalance(token?.chainId, wallet, token.address); + }, + enabled: + (isDefined(evmAccount) || isDefined(svmAccountString)) && + isDefined(token), + refetchInterval: 10_000, // 10 seconds + staleTime: 5_000, // 5 seconds + }); } From 4f91781f9abb3948527567c7786e7ad9bbfce959 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 17 Dec 2025 16:02:55 +0200 Subject: [PATCH 3/4] use in balance selectors Signed-off-by: Gerhard Steenkamp --- .../components/BalanceSelector.tsx | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index 86290e820..f65abdae8 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -1,14 +1,10 @@ import { AnimatePresence, motion } from "framer-motion"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { BigNumber } from "ethers"; import styled from "@emotion/styled"; -import { - COLORS, - compareAddressesSimple, - formatUnitsWithMaxFractions, -} from "utils"; -import { useUserTokenBalances } from "hooks/useUserTokenBalances"; +import { COLORS, formatUnitsWithMaxFractions } from "utils"; import { useTrackBalanceSelectorClick } from "./useTrackBalanceSelectorClick"; +import { useTokenBalance } from "../hooks/useTokenBalance"; type BalanceSelectorProps = { token: { @@ -31,33 +27,10 @@ export function BalanceSelector({ error = false, }: BalanceSelectorProps) { const [isHovered, setIsHovered] = useState(false); - const tokenBalances = useUserTokenBalances(); + const balance = useTokenBalance(token); const trackBalanceSelectorClick = useTrackBalanceSelectorClick(); - // Derive the balance from the latest token balances - const balance = useMemo(() => { - if (!tokenBalances.data?.balances) { - return BigNumber.from(0); - } - - const chainBalances = tokenBalances.data.balances.find( - (cb) => cb.chainId === String(token.chainId) - ); - - if (!chainBalances) { - return BigNumber.from(0); - } - - const tokenBalance = chainBalances.balances.find((b) => - compareAddressesSimple(b.address, token.address) - ); - - return tokenBalance?.balance - ? BigNumber.from(tokenBalance.balance) - : BigNumber.from(0); - }, [tokenBalances.data, token.chainId, token.address]); - const handlePillClick = (percentage: BalanceSelectorPercentage) => { trackBalanceSelectorClick(percentage); From 19dd30c52b3941fc0fd0136a966b49bb353fc9b5 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Dec 2025 11:56:33 +0200 Subject: [PATCH 4/4] rename to getEvmBalance Signed-off-by: Gerhard Steenkamp --- src/hooks/useBalance/strategies/evm.ts | 4 ++-- src/hooks/useWalletBalanceTrace.ts | 4 ++-- src/utils/token.ts | 4 ++-- src/views/LiquidityPool/hooks/useUserLiquidityPool.ts | 9 +++++++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/hooks/useBalance/strategies/evm.ts b/src/hooks/useBalance/strategies/evm.ts index a73e2bb4b..78e7469f8 100644 --- a/src/hooks/useBalance/strategies/evm.ts +++ b/src/hooks/useBalance/strategies/evm.ts @@ -1,6 +1,6 @@ import { Balance, BalanceStrategy } from "./types"; import { - getBalance, + getEvmBalance, getNativeBalance, getConfig, getProvider, @@ -61,7 +61,7 @@ export class EVMBalanceStrategy implements BalanceStrategy { const balance = tokenInfo?.isNative ? await getNativeBalance(chainId, account, "latest", provider) - : await getBalance(chainId, account, tokenAddress, "latest", provider); + : await getEvmBalance(chainId, account, tokenAddress, "latest", provider); const balanceDecimals = tokenInfo?.decimals ?? 18; return { balance, diff --git a/src/hooks/useWalletBalanceTrace.ts b/src/hooks/useWalletBalanceTrace.ts index fbeae4d9d..86f33bd1a 100644 --- a/src/hooks/useWalletBalanceTrace.ts +++ b/src/hooks/useWalletBalanceTrace.ts @@ -3,7 +3,7 @@ import { useConnection } from "hooks"; import { ChainId, fixedPointAdjustment, - getBalance, + getEvmBalance, getConfig, getNativeBalance, getRoutes, @@ -85,7 +85,7 @@ const calculateUsdBalances = async (account: string) => { fromChainId: Number(chainId), fromTokenSymbol, fromTokenAddress, - balance: await getBalance( + balance: await getEvmBalance( Number(chainId), account, fromTokenAddress diff --git a/src/utils/token.ts b/src/utils/token.ts index 9b25e02cb..250efd2b6 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -40,7 +40,7 @@ export async function getNativeBalance( * @param blockNumber The block number to execute the query. * @returns a Promise that resolves to the balance of the account */ -export async function getBalance( +export async function getEvmBalance( chainId: ChainId, account: string, tokenAddress: string, @@ -101,7 +101,7 @@ export async function getTokenBalance( ) { return chainIsSvm(chainId) ? getSvmBalance(chainId, account, tokenAddress) - : getBalance(chainId, account, tokenAddress); + : getEvmBalance(chainId, account, tokenAddress); } /** diff --git a/src/views/LiquidityPool/hooks/useUserLiquidityPool.ts b/src/views/LiquidityPool/hooks/useUserLiquidityPool.ts index 1e77edda5..9c04ff6f6 100644 --- a/src/views/LiquidityPool/hooks/useUserLiquidityPool.ts +++ b/src/views/LiquidityPool/hooks/useUserLiquidityPool.ts @@ -1,7 +1,12 @@ import { useConnection } from "hooks"; import { useQuery } from "@tanstack/react-query"; -import { getConfig, getBalance, getNativeBalance, hubPoolChainId } from "utils"; +import { + getConfig, + getEvmBalance, + getNativeBalance, + hubPoolChainId, +} from "utils"; import getApiEndpoint from "utils/serverless-api"; const config = getConfig(); @@ -38,7 +43,7 @@ async function fetchUserLiquidityPool( const [l1Balance, poolStateOfUser] = await Promise.all([ tokenSymbol === "ETH" ? getNativeBalance(hubPoolChainId, userAddress) - : getBalance(hubPoolChainId, userAddress, l1TokenAddress), + : getEvmBalance(hubPoolChainId, userAddress, l1TokenAddress), getApiEndpoint().poolsUser(l1TokenAddress, userAddress), ]); return {