Розробка дашборда 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(). Завжди актуально, але повільно: кожен протокол вимагає окремих вивізитів, при кількох десятках протоколів latency зростає лінійно.
Рішення — 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 для зберігання історичних portfolio snapshots (для побудови 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 дні). Список цільових протоколів та чейнів, пріоритизація за популярними audience use cases.
Data layer (5–7 днів). Multicall агрегатор, The Graph інтеграції для ключових протоколів, нормалізація у єдину схему позиції.
Backend API (3–5 днів). REST/GraphQL API для фронтенда, кешування, історія портфеля.
Frontend (5–7 днів). Wallet connection, view суммарного balance, деталі за протоколами, графіки.
Орієнтири по строкам
MVP з 5–7 протоколами на 2–3 чейнах — 2–3 тижні. Повнофункціональний дашборд з історією, IL розрахунком, алертами та мобільним view — 6–8 тижнів.







