Реалізація відображення балансу токенів на веб-сайті
Показати баланс токена — три рядки коду. Показати його правильно — це вже інша історія: форматування з урахуванням decimals, оновлення в реальному часі, контекст мультичейну, кеш з інвалідацією. Давайте йти від простого до складного.
Отримання балансу нативного токена
import { formatEther } from 'ethers';
import { usePublicClient } from 'wagmi';
async function getNativeBalance(address: string, chainId: number): Promise<string> {
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.ETH_RPC_URL),
});
const balance = await client.getBalance({ address: address as `0x${string}` });
return formatEther(balance); // "1.234567890123456789"
}
getBalance повертає bigint у wei. formatEther ділить на 10^18. Для відображення зазвичай потрібно округлити до 4–6 значущих цифр.
Баланс ERC-20 з урахуванням decimals
Різні токени мають різну кількість decimals: USDC — 6, більшість ERC-20 — 18, WBTC — 8. Використання formatEther для USDC дасть неправильний результат.
import { erc20Abi, formatUnits } from 'viem';
async function getTokenBalance(
tokenAddress: `0x${string}`,
walletAddress: `0x${string}`,
client: PublicClient,
): Promise<{ formatted: string; raw: bigint; decimals: number }> {
const [balance, decimals] = await client.multicall({
contracts: [
{
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [walletAddress],
},
{
address: tokenAddress,
abi: erc20Abi,
functionName: 'decimals',
},
],
});
const raw = balance.result as bigint;
const dec = decimals.result as number;
return {
raw,
decimals: dec,
formatted: formatUnits(raw, dec),
};
}
multicall — один RPC-виклик замість двох. На проектах із 10+ токенами це критично для продуктивності.
Компонент з автоматичним оновленням через wagmi
// components/TokenBalance.tsx
import { useBalance, useReadContract } from 'wagmi';
import { erc20Abi, formatUnits } from 'viem';
interface TokenBalanceProps {
address: `0x${string}`;
tokenAddress?: `0x${string}`; // якщо не встановлено — показати нативний токен
symbol?: string;
}
export function TokenBalance({ address, tokenAddress, symbol }: TokenBalanceProps) {
// Нативний баланс
const { data: nativeBalance } = useBalance({
address,
query: { refetchInterval: 12_000 }, // оновлення кожен блок (~12s на Ethereum)
});
// Баланс ERC-20
const { data: tokenBalance } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address],
query: { enabled: !!tokenAddress, refetchInterval: 12_000 },
});
const { data: decimals } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'decimals',
query: { enabled: !!tokenAddress, staleTime: Infinity }, // decimals не змінюються
});
if (tokenAddress && tokenBalance != null && decimals != null) {
const formatted = formatUnits(tokenBalance as bigint, decimals as number);
return <BalanceDisplay value={formatted} symbol={symbol ?? 'TOKEN'} />;
}
if (nativeBalance) {
return <BalanceDisplay value={nativeBalance.formatted} symbol={nativeBalance.symbol} />;
}
return <BalanceSkeleton />;
}
Форматування для UI
Сирий formatUnits повертає рядок з 18 знаками після коми. Для відображення потрібна логіка:
export function formatTokenAmount(
value: string | bigint,
decimals: number,
opts?: { maxDecimals?: number; compact?: boolean },
): string {
const raw = typeof value === 'bigint' ? formatUnits(value, decimals) : value;
const num = parseFloat(raw);
if (num === 0) return '0';
const maxDec = opts?.maxDecimals ?? 4;
if (opts?.compact && num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(2)}M`;
}
if (opts?.compact && num >= 1_000) {
return `${(num / 1_000).toFixed(2)}K`;
}
// Для дуже малих значень — показати значущі цифри
if (num < 0.0001 && num > 0) {
return num.toExponential(2);
}
return num.toLocaleString('en-US', {
maximumFractionDigits: maxDec,
minimumFractionDigits: 0,
});
}
Відображення кількох токенів
Коли потрібно показати портфель — баланси 10–20 токенів — важливо не робити 20 окремих RPC-запитів. Контракт multicall3 (розгорнутий на більшості EVM-мереж) агрегує всі запити в один:
import { multicall } from 'viem/actions';
async function getPortfolioBalances(
tokens: Array<{ address: `0x${string}`; decimals: number; symbol: string }>,
walletAddress: `0x${string}`,
client: PublicClient,
) {
const calls = tokens.map(token => ({
address: token.address,
abi: erc20Abi,
functionName: 'balanceOf' as const,
args: [walletAddress] as [`0x${string}`],
}));
const results = await multicall(client, { contracts: calls });
return tokens.map((token, i) => ({
...token,
balance: results[i].result as bigint,
formatted: formatUnits(results[i].result as bigint, token.decimals),
}));
}
Часова шкала: один токен з оновленнями в реальному часі — половина дня. Повноцінний портфельний віджет із кількома токенами, форматуванням, завантаженням скелету та оновленнями блоку — 1–2 дні.







