Агрегация балансов криптовалютного портфеля — одна из самых нетривиальных задач в Web3-разработке. Пользователь может держать активы на десятке сетей (Ethereum, Polygon, Arbitrum, Solana), в сотнях токенов, в DeFi-протоколах вроде Uniswap V3 с уникальной математикой позиций, и на централизованных биржах. Каждый источник требует своего подхода к получению данных. Если не продумать архитектуру с самого начала, система будет торгмозить, расходовать лимиты RPC и выдавать некорректные PnL. Мы набили шишки на 30+ подобных проектах и знаем, как избежать граблей. Multicall3 — один из ключевых инструментов, позволяющий агрегировать данные из EVM-сетей с минимальными затратами.
Разработка портфельного трекера: ключевые архитектурные решения
Как агрегировать балансы из разных сетей?
On-chain балансы
EVM-сети: нативный баланс через eth_getBalance, ERC-20 балансы сложнее — один RPC вызов возвращает баланс одного токена на одном адресе. При 50 токенах × 5 сетей = 250 вызовов. Решение — Multicall3 (задеплоен на всех EVM-сетях по адресу 0xcA11bde05977b3631167028862bE2a173976CA11). Один RPC вызов вместо 50 — это в 50 раз быстрее и снижает затраты на инфраструктуру до 90%. Multicall3 — проверенный контракт, используемый в тысячах проектов. Документация Multicall3 рекомендует batch-запросы для снижения нагрузки.
import { createPublicClient, http, parseAbi } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http() }); const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"; const ERC20_ABI = parseAbi(["function balanceOf(address) view returns (uint256)"]); async function getTokenBalances( walletAddress: `0x${string}`, tokenAddresses: `0x${string}`[] ) { const calls = tokenAddresses.map((tokenAddress) => ({ address: tokenAddress, abi: ERC20_ABI, functionName: "balanceOf" as const, args: [walletAddress], })); return client.multicall({ contracts: calls }); } Альтернатива: Alchemy/Moralis Token API — один запрос возвращает все ERC-20 балансы с метаданными и USD ценой. Платно, но экономит время разработки. Для Solana используем getMultipleAccounts или Anchor для чтения аккаунтов.
DeFi позиции
Это самая сложная часть трекера. Liquidity позиция в Uniswap V3, collateral в Aave, стейкнутые токены в Curve — каждый протокол хранит данные по-своему. Сравним подходы:
| Подход | Скорость | Сложность | Стоимость |
|---|---|---|---|
| Прямые вызовы контрактов | Высокая (зависит от количества запросов) | Высокая (нужна математика тиков) | Бесплатно (только газ) |
| The Graph subgraphs | Средняя (GraphQL запросы) | Средняя (изучение схемы) | Бесплатно (хостинг) |
| Агрегаторы (DeBank, Zapper) | Высокая (кэшированные данные) | Низкая (один API) | Платно (подписка) |
Для большинства проектов мы рекомендуем комбинацию: прямые вызовы для основных протоколов + агрегаторы для long-tail. Например, для Uniswap V3 используем positions и tick контракты, а для Curve — get_balances. Ошибка в расчёте PnL может возникнуть из-за рассинхрона цен и блоков — мы решаем это привязкой снапшотов к номеру блока.
CEX балансы
Биржевые API отдают балансы мгновенно, но требуют read-only API ключ от пользователя. Используем CCXT — библиотеку с единым интерфейсом для 100+ бирж.
import ccxt from "ccxt"; async function getBinanceBalances(apiKey: string, secret: string) { const exchange = new ccxt.binance({ apiKey, secret, sandbox: false }); const balance = await exchange.fetchBalance(); return balance.total; // { BTC: 0.5, ETH: 2.3, USDT: 1000 } } CCXT — популярная библиотека с открытым исходным кодом, поддерживающая 100+ бирж.
Почему важна стратегия обновления данных?
Данные нужно обновлять, но не молотить RPC каждую секунду. Стратегия:
| Тип данных | Частота обновления | Причина |
|---|---|---|
| On-chain балансы | Каждые 30–60 сек | Новый блок |
| CEX балансы | Каждые 30 сек | API лимиты |
| DeFi позиции | Каждые 2–5 мин | Медленно меняются |
| Цены токенов | Каждые 10–30 сек | Критично для P&L |
| Historical P&L | Фоновый job, 1/час | Тяжёлые вычисления |
Background jobs через Redis + BullMQ с отдельными очередями. Результаты кешируем в Redis, фронтенд читает из кеша. Для real-time используем Server-Sent Events или WebSocket с push-обновлениями.
Хранение исторических данных
Для P&L трекинга нужны снапшоты портфеля во времени. TimescaleDB (расширение PostgreSQL) идеально подходит:
CREATE TABLE portfolio_snapshots ( user_id UUID, snapshot_at TIMESTAMPTZ NOT NULL, total_usd NUMERIC(20, 2), breakdown JSONB ); SELECT create_hypertable('portfolio_snapshots', 'snapshot_at'); Запрос P&L за период:
SELECT time_bucket('1 day', snapshot_at) AS day, last(total_usd, snapshot_at) AS end_of_day_value FROM portfolio_snapshots WHERE user_id = $1 AND snapshot_at > NOW() - INTERVAL '30 days' GROUP BY day ORDER BY day; Как настроить Multicall3 в своём проекте
- Установите viem:
npm install viem. - Импортируйте createPublicClient и конфигурацию сети.
- Вызовите
client.multicallс массивом контрактов и функций. - Обработайте результат — он вернёт массив объектов с полями result и error.
Стандартный подход, описанный в документации OpenZeppelin, рекомендует использовать Multicall3 для batch-запросов и снижения нагрузки на RPC.
Что входит в работу
Мы передаём полный комплект документации, исходный код с комментариями, инструкцию по деплою, доступ к Git-репозиторию и поддержку в течение месяца после запуска. При необходимости обучаем вашу команду работе с системой.
Обсудите вашу задачу с нашими инженерами — оценим проект за 1 день и предложим оптимальное решение. Закажите разработку портфельного трекера под ключ с гарантией качества и соблюдением сроков. Свяжитесь с нами, чтобы начать работу.







