Реализация DeFi-дашборда на сайте
DeFi-дашборд агрегирует данные о позициях пользователя в нескольких протоколах: депозиты в Aave, LP-позиции в Uniswap, стейкинг в Curve, баланс кошелька. Всё на одном экране с обновлением в реальном времени, доходностью и PnL.
Сложность не в отдельных виджетах, а в интеграции: каждый протокол — свой API или набор контрактов, разные форматы данных, разные сети. Плюс ценовые данные для конвертации в USD.
Архитектура дашборда
defi-dashboard/
├── lib/
│ ├── protocols/
│ │ ├── aave.ts # Subgraph / REST API
│ │ ├── uniswap-v3.ts # Subgraph
│ │ └── curve.ts # Curve API
│ ├── prices.ts # CoinGecko / DeFiLlama цены
│ └── multicall.ts # Батчинг on-chain запросов
├── hooks/
│ ├── usePortfolio.ts # Агрегирование всех позиций
│ ├── usePrices.ts # Актуальные цены токенов
│ └── useNetWorth.ts # USD-стоимость портфеля
└── components/
├── NetWorthCard/
├── PositionsList/
├── ProtocolCard/
└── AllocationChart/
Получение ценовых данных
// lib/prices.ts
const COINGECKO_BASE = 'https://api.coingecko.com/api/v3';
// Кэш цен — обновляем раз в минуту, не на каждый рендер
const priceCache = new Map<string, { price: number; ts: number }>();
const CACHE_TTL = 60_000;
export async function getTokenPrices(
tokenIds: string[],
vsCurrency = 'usd',
): Promise<Record<string, number>> {
const stale = tokenIds.filter(id => {
const cached = priceCache.get(id);
return !cached || Date.now() - cached.ts > CACHE_TTL;
});
if (stale.length > 0) {
const res = await fetch(
`${COINGECKO_BASE}/simple/price?ids=${stale.join(',')}&vs_currencies=${vsCurrency}`,
);
const data = await res.json();
for (const [id, prices] of Object.entries(data)) {
priceCache.set(id, { price: (prices as Record<string, number>)[vsCurrency], ts: Date.now() });
}
}
return Object.fromEntries(
tokenIds.map(id => [id, priceCache.get(id)?.price ?? 0]),
);
}
Aave v3 позиции через Subgraph
// lib/protocols/aave.ts
const AAVE_SUBGRAPH = 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3';
interface AavePosition {
reserve: { symbol: string; underlyingAsset: string; decimals: number };
currentATokenBalance: string;
currentVariableDebt: string;
currentStableDebt: string;
reserveLiquidationThreshold: string;
}
export async function getAavePositions(userAddress: string): Promise<AavePosition[]> {
const query = `{
userReserves(
where: { user: "${userAddress.toLowerCase()}", currentATokenBalance_gt: "0" }
) {
reserve { symbol underlyingAsset decimals liquidityRate }
currentATokenBalance
currentVariableDebt
currentStableDebt
}
}`;
const res = await fetch(AAVE_SUBGRAPH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
next: { revalidate: 30 },
});
const { data } = await res.json();
return data.userReserves;
}
Uniswap v3 LP позиции
Uniswap v3 позиции — NFT (ERC-721 в NonfungiblePositionManager). Получаем через Subgraph или Uniswap API:
// lib/protocols/uniswap-v3.ts
const UNISWAP_SUBGRAPH = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';
export async function getUniswapPositions(ownerAddress: string) {
const query = `{
positions(
where: { owner: "${ownerAddress.toLowerCase()}", liquidity_gt: "0" }
orderBy: id
orderDirection: desc
first: 20
) {
id
liquidity
token0 { symbol decimals }
token1 { symbol decimals }
pool { feeTier sqrtPrice tick token0Price token1Price }
depositedToken0
depositedToken1
collectedFeesToken0
collectedFeesToken1
tickLower { tickIdx }
tickUpper { tickIdx }
}
}`;
const res = await fetch(UNISWAP_SUBGRAPH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const { data } = await res.json();
return data.positions;
}
Агрегирование в хуке
// hooks/usePortfolio.ts
import { useQuery } from '@tanstack/react-query';
import { useAccount } from 'wagmi';
import { getAavePositions } from '@/lib/protocols/aave';
import { getUniswapPositions } from '@/lib/protocols/uniswap-v3';
import { getTokenPrices } from '@/lib/prices';
export function usePortfolio() {
const { address } = useAccount();
return useQuery({
queryKey: ['portfolio', address],
queryFn: async () => {
if (!address) return null;
const [aavePositions, uniswapPositions] = await Promise.all([
getAavePositions(address),
getUniswapPositions(address),
]);
const tokenIds = [
...new Set([
...aavePositions.map(p => p.reserve.symbol.toLowerCase()),
'ethereum',
]),
];
const prices = await getTokenPrices(tokenIds);
const aaveUSD = aavePositions.reduce((sum, pos) => {
const balance = Number(pos.currentATokenBalance) / 10 ** pos.reserve.decimals;
const price = prices[pos.reserve.symbol.toLowerCase()] ?? 0;
return sum + balance * price;
}, 0);
return {
aavePositions,
uniswapPositions,
aaveUSD,
prices,
};
},
enabled: !!address,
refetchInterval: 60_000,
});
}
Компоненты дашборда
// components/NetWorthCard.tsx
export function NetWorthCard() {
const { data, isLoading } = usePortfolio();
if (isLoading) return <Skeleton className="h-32" />;
const total = (data?.aaveUSD ?? 0) + (data?.uniswapUSD ?? 0);
return (
<div className="rounded-2xl border border-white/10 bg-gradient-to-br from-neutral-900 to-neutral-800 p-6">
<p className="text-sm text-neutral-400">Чистая стоимость</p>
<p className="mt-1 text-4xl font-bold">
${total.toLocaleString('en-US', { maximumFractionDigits: 2 })}
</p>
<div className="mt-4 flex gap-6 text-sm">
<Metric label="Aave" value={data?.aaveUSD} />
<Metric label="Uniswap LP" value={data?.uniswapUSD} />
</div>
</div>
);
}
Realtime-обновление данных
Для дашборда нужны актуальные данные без ручного обновления страницы:
// Цены — каждую минуту через react-query refetchInterval
// On-chain данные — каждые 12 секунд (один блок Ethereum)
// Subgraph данные — каждые 30 секунд (задержка индексации ~15s)
const { data } = useQuery({
queryKey: ['aavePositions', address],
queryFn: () => getAavePositions(address!),
refetchInterval: 30_000,
staleTime: 15_000,
});
Сроки: дашборд с одним протоколом (например, только Aave), ценами и USD-оценкой — 3–4 дня. Мультипротокольный дашборд с 3–4 протоколами, allocation chart, историей — 1,5–2 недели.







