Разработка дашборда DeFi-портфеля
Пользователь держит USDC в Aave, ETH-USDC LP в Uniswap V3, wstETH в Lido, и открытую перпетуал позицию на GMX. Четыре разных протокола, четыре разных способа представления позиций, четыре разных API или субграфа. Задача дашборда — агрегировать всё это в единый экран с реальными цифрами P&L.
Разработка такого инструмента — это не только фронтенд. 80% усилий уходит на data layer: нормализацию данных из разных источников и корректный расчёт stale/live балансов.
Источники данных и их специфика
On-chain direct calls vs индексаторы
Самый точный способ получить баланс — прямой eth_call к контракту. Для токен-баланса — balanceOf(). Для позиции Aave — getUserAccountData(). Это всегда актуально, но медленно: каждый протокол требует отдельных вызовов, а при нескольких десятках протоколов латентность растёт линейно.
Решение — Multicall3 (0xcA11bde05977b3631167028862bE2a173976CA11, деплой на всех major EVM чейнах): батч из 50+ вызовов в одной транзакции. Время ответа — как один RPC вызов вместо 50.
import { multicall } from 'viem'
const results = await multicall(client, {
contracts: [
{ address: AAVE_POOL, abi: aavePoolAbi, functionName: 'getUserAccountData', args: [userAddress] },
{ address: USDC_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] },
{ address: UNISWAP_POSITION_MANAGER, abi: nftAbi, functionName: 'balanceOf', args: [userAddress] },
]
})
Для исторических данных (история транзакций, PnL по времени) прямые вызовы не работают — нужны индексаторы.
The Graph для исторических данных
Uniswap, Aave, Compound, Curve, Balancer — все имеют официальные subgraphs в The Graph Network. Subgraph предоставляет GraphQL API для запроса исторических событий: deposits, withdrawals, swaps, liquidations.
query UserPositions($user: String!) {
aaveV3_deposits(where: { user: $user }, orderBy: timestamp, orderDirection: desc) {
amount
reserve { symbol, decimals, priceInUSD }
timestamp
}
aaveV3_borrows(where: { user: $user }) {
amount
reserve { symbol }
currentVariableBorrowRate
}
}
Проблема: у разных версий протоколов разные subgraphs. Aave V2 на Ethereum, Aave V3 на Polygon, Aave V3 на Arbitrum — три разных субграфа с разными схемами. Нормализация — основная инженерная задача дашборда.
Alchemy и Moralis как API-over-RPC
Alchemy API предоставляет готовые методы: getTokenBalances() возвращает все ERC-20 балансы адреса без перебора контрактов. getAssetTransfers() — история трансферов. Это значительно упрощает начальную реализацию, но стоит денег при высокой нагрузке.
Moralis дополнительно агрегирует данные о NFT позициях и DeFi protocol positions через их DeFi API — платный, но экономит месяцы разработки кастомного data layer.
Для MVP оправдан Alchemy + The Graph для ключевых протоколов. Для production с десятками тысяч пользователей — собственный indexer.
Расчёт P&L и impermanent loss
Самая сложная часть — корректный расчёт unrealized P&L по LP позициям.
Для Uniswap V3 позиция — это NFT с определёнными tickLower, tickUpper, liquidity. Текущие amounts token0 и token1 зависят от текущего sqrtPriceX96 пула. Формула нетривиальна:
function getAmountsFromLiquidity(
sqrtPriceX96: bigint,
sqrtRatioAX96: bigint,
sqrtRatioBX96: bigint,
liquidity: bigint
): [bigint, bigint] {
if (sqrtPriceX96 <= sqrtRatioAX96) {
// Вся ликвидность в token0
const amount0 = (liquidity * (sqrtRatioBX96 - sqrtRatioAX96) * Q96)
/ (sqrtRatioBX96 * sqrtRatioAX96)
return [amount0, 0n]
} else if (sqrtPriceX96 < sqrtRatioBX96) {
const amount0 = (liquidity * (sqrtRatioBX96 - sqrtPriceX96) * Q96)
/ (sqrtRatioBX96 * sqrtPriceX96)
const amount1 = (liquidity * (sqrtPriceX96 - sqrtRatioAX96)) / Q96
return [amount0, amount1]
} else {
// Вся ликвидность в token1
const amount1 = (liquidity * (sqrtRatioBX96 - sqrtRatioAX96)) / Q96
return [0n, amount1]
}
}
Impermanent loss считается как разница между текущей стоимостью позиции и стоимостью, если бы те же активы просто держались с момента входа. Для дашборда нужно хранить entry price и initial amounts при открытии позиции.
Мультичейн агрегация
Типичный пользователь активен на Ethereum mainnet, Arbitrum, Polygon, Base. Дашборд должен показывать суммарный портфель поперёк чейнов.
Схема: параллельные запросы к RPC каждого чейна через Promise.all(), нормализация балансов в USD через единый price oracle. Coingecko API или DefiLlama Price API для получения актуальных цен по token address + chain ID.
Проблема cross-chain identity: адрес пользователя одинаков на всех EVM чейнах (ECDSA), но смарт-контракт кошелёк (Safe, Argent) может иметь разные адреса на разных чейнах при несинхронизированном деплое. Нужно явно поддерживать multi-address mode.
Стек и производительность
Backend: Node.js + TypeScript с viem для RPC. Redis для кэша балансов (TTL 30 секунд для live данных, 5 минут для исторических). PostgreSQL для хранения исторических snapshot-ов портфеля (для построения equity curve).
Frontend: React + wagmi v2 для wallet connection, Recharts или TradingView Lightweight Charts для графиков, Tanstack Query для data fetching с автоматическим refetch каждые 30 секунд.
WebSocket для real-time обновлений: подписка на eth_subscribe("newHeads") для триггера обновления балансов при новом блоке — выглядит живо без лишних poll-запросов.
Процесс работы
Аналитика (1–2 дня). Список целевых протоколов и чейнов, приоритизация по popular use cases аудитории.
Data layer (5–7 дней). Multicall агрегатор, The Graph интеграции для ключевых протоколов, нормализация в единую схему позиции.
Backend API (3–5 дней). REST/GraphQL API для фронтенда, кэширование, история портфеля.
Frontend (5–7 дней). Wallet connection, суммарный balance view, детали по протоколам, графики.
Ориентиры по срокам
MVP с 5–7 протоколами на 2–3 чейнах — 2–3 недели. Полноценный дашборд с историей, IL расчётом, алертами и мобильным view — 6–8 недель.







