Вы интегрируете криптокошелёк в dApp и видите, что баланс USDC отображается как 0.000000000000000001 вместо 10. Причина — использован formatEther вместо formatUnits с 6 decimals. Или баланс обновляется только после перезагрузки страницы. Такие ошибки мы исправляем регулярно. Наш опыт в интеграции блокчейн-функционала — более 5 лет, реализовано более 50 проектов. В этой статье разберём, как правильно реализовать отображение баланса токенов: от нативного газа до портфеля из 20 ERC-20. Вы узнаете, как избежать типичных ошибок decimals, настроить realtime-обновление и оптимизировать RPC-запросы с помощью multicall. На примере реального кейса покажем, как собрать компонент баланса, который работает в mainnet и testnets.
Отображение баланса токенов: типичные проблемы и решения
- Некорректные decimals: разные токены имеют разное количество знаков (USDC — 6, WBTC — 8, ETH — 18). Использование
formatEtherдля USDC даёт 0.000000000000000001 вместо 10. - N+1 запросы: при отображении портфеля из 10 токенов без multicall делается 10 отдельных RPC-вызовов, что увеличивает TTFB и нагружает провайдера.
- Устаревшие данные: баланс может не обновляться после транзакции. Нужна подписка на события или опрос по блокам.
Получение баланса нативного токена и ERC-20
Для нативного токена (ETH, MATIC, BNB) используем getBalance из viem. Для ERC-20 — контрактный вызов balanceOf. У каждого токена свой decimals, который нужно получать из контракта.
import { formatEther } from 'ethers';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
async function getNativeBalance(address: string): 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);
}
getBalance возвращает bigint в wei. formatEther делит на 10^18. Для отображения нужно округлить до 4–6 значащих цифр.
Для ERC-20 используем multicall, чтобы получить баланс и decimals одним запросом:
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 не опрашивает баланс. Нужно задать refetchInterval. Для Ethereum основной сети интервал 12 000 мс соответствует одному блоку. Альтернатива — подписка на событие Transfer смарт-контракта, но это требует дополнительной инфраструктуры. Мы рекомендуем refetchInterval для простоты и надёжности.
Пример компонента с автообновлением:
import { useBalance, useReadContract } from 'wagmi';
import { erc20Abi, formatUnits } from 'viem';
export function TokenBalance({ address, tokenAddress, symbol }) {
const { data: nativeBalance } = useBalance({
address,
query: { refetchInterval: 12_000 },
});
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 },
});
// ... рендер
}
Форматирование баланса для UI
Сырое formatUnits возвращает строку с 18 знаками после запятой. Для отображения нужна логика. Используйте функцию formatUnits с последующим округлением.
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 });
}
Таблица примеров форматирования:
| Токен | Decimals | Сырой баланс (bigint) | Отформатированное значение |
|---|---|---|---|
| ETH | 18 | 1000000000000000000 | 1.0 ETH |
| USDC | 6 | 10000000 | 10.0 USDC |
| WBTC | 8 | 500000000 | 5.0 WBTC |
Избежание N+1 запросов с помощью multicall
Отметим: когда нужно показать portfolio — балансы 10–20 токенов — важно не делать 20 отдельных RPC-запросов. Multicall3 агрегирует все запросы в один. В viem есть встроенная функция multicall. Сравнение: последовательный вызов для 10 токенов занимает ~2 секунды (при стандартном RPC), а multicall — ~200 мс. Multicall лучше последовательных запросов в 10 раз.
Viem multicall documentation
Таблица сравнения методов:
| Метод | Количество RPC-запросов | Время выполнения | Нагрузка на провайдера |
|---|---|---|---|
| Последовательные вызовы | 10 | ~2 секунды | Высокая |
| Multicall | 1 | ~200 мс | Низкая |
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',
args: [walletAddress],
}));
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),
}));
}
Использование multicall позволяет сократить затраты на RPC-инфраструктуру на 60–70%. Экономия времени разработки — до 2 дней при интеграции портфеля из 10 токенов. Это особенно заметно при высоких нагрузках.
Как ускорить загрузку портфеля токенов?
Используйте multicall для агрегации запросов и добавьте кэширование на стороне клиента. В wagmi можно настроить staleTime, чтобы избежать повторных запросов в течение нескольких секунд. Комбинируйте с refetchInterval для баланса между актуальностью и производительностью.
Изменение decimals у токена: обработка
Decimals токена обычно фиксированы, но могут меняться при апгрейде контракта. Подписывайтесь на события контракта или периодически проверяйте decimals через мультиколл. В подавляющем большинстве случаев decimals не меняются, но для критичных приложений стоит добавить fallback.
Процесс работы и сроки
- Анализ: определяем список токенов и сетей, выбираем провайдер RPC.
- Проектирование: архитектура компонентов, схема кэширования и обновления.
- Реализация: пишем хуки и компоненты, используем wagmi/viem.
- Тестирование: проверяем на mainnet-форке, с разными токенами и адресами.
- Деплой: развёртываем на продакшен, настраиваем мониторинг.
Что входит в работу
- Готовая интеграция компонента отображения баланса (нативного + ERC-20).
- Документация по конфигурации и использованию.
- Доступ к RPC-провайдеру (можем предложить свой).
- Обучение команды заказчика (1 час).
- Гарантия отказоустойчивости и поддержка 2 недели после сдачи.
Срок реализации: один токен с обновлением в реальном времени — половина дня. Полноценный portfolio-виджет с несколькими токенами, форматированием, skeleton-загрузкой и обновлением по блокам — 1–2 дня.
Стоимость рассчитывается индивидуально. Закажите оценку вашего проекта — мы рассчитаем точные сроки и стоимость за 1 день. Свяжитесь с нами для консультации.
Чек-лист для интеграции
- [ ] Определить decimals для каждого токена (можно получить из контракта).
- [ ] Использовать multicall для портфеля из 2+ токенов.
- [ ] Настроить refetchInterval для автообновления (12 000 ms для Ethereum).
- [ ] Форматировать вывод: не более 4 знаков, для малых сумм — экспонента.
- [ ] Добавить skeleton-загрузку и обработку ошибок.







