WIP stats page, market header oracle price

This commit is contained in:
Riordan Panayides 2021-07-03 02:18:57 +01:00
commit aea7b84b21
10 changed files with 684 additions and 340 deletions

115
components/Chart.tsx Normal file
View File

@ -0,0 +1,115 @@
import { useState } from 'react'
import { AreaChart, Area, XAxis, YAxis, Tooltip, BarChart, Bar } from 'recharts'
import useDimensions from 'react-cool-dimensions'
const Chart = ({ title, xAxis, yAxis, data, labelFormat, type }) => {
const [mouseData, setMouseData] = useState<string | null>(null)
// @ts-ignore
const { observe, width, height } = useDimensions()
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
return (
<div className="h-full w-full" ref={observe}>
<div className="absolute h-full w-full pb-4">
<div className="pb-0.5 text-xs text-th-fgd-3">{title}</div>
{mouseData ? (
<>
<div className="pb-1 text-xl text-th-fgd-1">
{labelFormat(mouseData[yAxis])}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{new Date(mouseData[xAxis]).toDateString()}
</div>
</>
) : data.length > 0 && data[data.length - 1][yAxis] ? (
<>
<div className="pb-1 text-xl text-th-fgd-1">
{labelFormat(data[data.length - 1][yAxis])}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{new Date(data[data.length - 1][xAxis]).toDateString()}
</div>
</>
) : (
<>
<div className="animate-pulse bg-th-bkg-3 h-8 mt-1 rounded w-48" />
<div className="animate-pulse bg-th-bkg-3 h-4 mt-1 rounded w-24" />
</>
)}
</div>
{width > 0 && type === 'area' ? (
<AreaChart
width={width}
height={height}
data={data}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FF9C24" stopOpacity={0.5} />
<stop offset="100%" stopColor="#FF9C24" stopOpacity={0} />
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yAxis}
stroke="#FF9C24"
fill="url(#gradientArea)"
/>
<XAxis dataKey={xAxis} hide />
<YAxis dataKey={yAxis} hide />
</AreaChart>
) : null}
{width > 0 && type === 'bar' ? (
<BarChart
width={width}
height={height}
data={data}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
fill: '#fff',
opacity: 0.2,
}}
content={<></>}
/>
<defs>
<linearGradient id="gradientBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FF9C24" stopOpacity={1} />
<stop offset="100%" stopColor="#FF9C24" stopOpacity={0.5} />
</linearGradient>
</defs>
<Bar
isAnimationActive={false}
type="monotone"
dataKey={yAxis}
fill="url(#gradientBar)"
/>
<XAxis dataKey={xAxis} hide />
<YAxis dataKey={yAxis} hide />
</BarChart>
) : null}
</div>
)
}
export default Chart

View File

@ -5,10 +5,10 @@ import useInterval from '../hooks/useInterval'
import ChartApi from '../utils/chartDataConnector'
import UiLock from './UiLock'
import ManualRefresh from './ManualRefresh'
// import useOraclePrice from '../hooks/useOraclePrice'
import useOraclePrice from '../hooks/useOraclePrice'
const MarketHeader = () => {
// const oraclePrice = useOraclePrice()
const oraclePrice = useOraclePrice()
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const baseSymbol = marketConfig.baseSymbol
const selectedMarketName = marketConfig.name
@ -95,7 +95,7 @@ const MarketHeader = () => {
<div className="pr-4 sm:pr-0 sm:w-24">
<div className="text-th-fgd-4 text-xs">Oracle price</div>
<div className="font-semibold mt-0.5">
{/* {oraclePrice ? oraclePrice.toFixed(2) : '--'} */}
{oraclePrice ? oraclePrice.toFixed(2) : '--'}
</div>
</div>
<div className="pr-4 sm:pr-0 sm:w-24">

View File

@ -0,0 +1,188 @@
import { useState } from 'react'
import useMangoStats from '../../hooks/useMangoStats'
import Chart from '../Chart'
const icons = {
BTC: '/assets/icons/btc.svg',
ETH: '/assets/icons/eth.svg',
SOL: '/assets/icons/sol.svg',
SRM: '/assets/icons/srm.svg',
USDT: '/assets/icons/usdt.svg',
USDC: '/assets/icons/usdc.svg',
WUSDT: '/assets/icons/usdt.svg',
}
export default function StatsAssets() {
const [selectedAsset, setSelectedAsset] = useState<string>('BTC')
const { latestStats, stats } = useMangoStats()
const selectedStatsData = stats.filter(
(stat) => stat.symbol === selectedAsset
)
return (
<>
<div className="flex flex-col-reverse items-center sm:flex-row sm:justify-between sm:h-12 mb-4 w-full">
<AssetHeader asset={selectedAsset} />
<div className="flex pb-4 sm:pb-0">
{latestStats.map((stat) => (
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-3
${
selectedAsset === stat.symbol
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => setSelectedAsset(stat.symbol)}
key={stat.symbol as string}
>
{stat.symbol}
</div>
))}
</div>
</div>
<div className="grid grid-flow-col grid-cols-1 grid-rows-4 md:grid-cols-2 md:grid-rows-2 gap-4 pb-8">
<div
className="border border-th-bkg-3 relative md:mb-0 p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Total Deposits"
xAxis="time"
yAxis="totalDeposits"
data={selectedStatsData}
labelFormat={(x) =>
x && x.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
type="area"
/>
</div>
<div
className="border border-th-bkg-3 relative p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Deposit Interest"
xAxis="time"
yAxis="depositInterest"
data={selectedStatsData}
labelFormat={(x) => `${(x * 100).toFixed(5)}%`}
type="bar"
/>
</div>
<div
className="border border-th-bkg-3 relative md:mb-0 p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Total Borrows"
xAxis="time"
yAxis="totalBorrows"
data={selectedStatsData}
labelFormat={(x) =>
x && x.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
type="area"
/>
</div>
<div
className="border border-th-bkg-3 relative p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Borrow Interest"
xAxis="time"
yAxis="borrowInterest"
data={selectedStatsData}
labelFormat={(x) => `${(x * 100).toFixed(5)}%`}
type="bar"
/>
</div>
</div>
</>
)
}
const AssetHeader = ({ asset }) => {
switch (asset) {
case 'BTC':
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
Bitcoin
</div>
)
case 'ETH':
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
Ethereum
</div>
)
case 'SOL':
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
Solana
</div>
)
case 'SRM':
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
Serum
</div>
)
case 'USDC':
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
USD Coin
</div>
)
default:
return (
<div className="flex items-center text-xl text-th-fgd-1">
<img
src={icons[asset]}
alt={icons[asset]}
width="24"
height="24"
className="mr-2.5"
/>
Bitcoin
</div>
)
}
}

View File

@ -0,0 +1,217 @@
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import { formatBalanceDisplay, tokenPrecision } from '../../utils/index'
import useMangoStats from '../../hooks/useMangoStats'
import useHistoricPrices from '../../hooks/useHistoricPrices'
import useMarketList from '../../hooks/useMarketList'
import useMangoStore from '../../stores/useMangoStore'
import Chart from '../Chart'
const icons = {
BTC: '/assets/icons/btc.svg',
ETH: '/assets/icons/eth.svg',
SOL: '/assets/icons/sol.svg',
SRM: '/assets/icons/srm.svg',
USDT: '/assets/icons/usdt.svg',
USDC: '/assets/icons/usdc.svg',
WUSDT: '/assets/icons/usdt.svg',
}
export default function StatsTotals() {
const { latestStats, stats } = useMangoStats()
const { prices } = useHistoricPrices()
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
// TODO: fix this
const backupPrices = [0, 0]
const { getTokenIndex, symbols } = useMarketList()
const startTimestamp = 1622905200000
const trimmedStats = stats.filter(
(stat) => new Date(stat.hourly).getTime() >= startTimestamp
)
// get deposit and borrow values from stats
const depositValues = []
const borrowValues = []
if (prices) {
for (let i = 0; i < trimmedStats.length; i++) {
// use the current price if no match for hour from the prices api
const price =
trimmedStats[i].symbol !== 'USDC' &&
prices[trimmedStats[i].symbol][trimmedStats[i].hourly]
? prices[trimmedStats[i].symbol][trimmedStats[i].hourly]
: backupPrices[getTokenIndex(symbols[trimmedStats[i].symbol])]
const depositValue =
trimmedStats[i].symbol === 'USDC'
? trimmedStats[i].totalDeposits
: trimmedStats[i].totalDeposits * price
const borrowValue =
trimmedStats[i].symbol === 'USDC'
? trimmedStats[i].totalBorrows
: trimmedStats[i].totalBorrows * price
depositValues.push({
symbol: trimmedStats[i].symbol,
value: depositValue,
time: trimmedStats[i].hourly,
})
borrowValues.push({
symbol: trimmedStats[i].symbol,
value: borrowValue,
time: trimmedStats[i].hourly,
})
}
}
const formatValues = (values) => {
// get value for each symbol every hour
const hours = values.reduce((acc, d) => {
const found = acc.find((a) => a.time === d.time && a.symbol === d.symbol)
const value = {
value: d.value,
symbol: d.symbol,
time: d.time,
}
if (!found) {
acc.push(value)
} else {
found.value = d.value
}
return acc
}, [])
// sum the values for each hour
const holder = {}
hours.forEach(function (d) {
if (d.time in holder) {
holder[d.time] = holder[d.time] + d.value
} else {
holder[d.time] = d.value
}
})
const points = []
for (const prop in holder) {
points.push({ time: prop, value: holder[prop] })
}
return points
}
return (
<>
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 md:grid-cols-2 md:grid-rows-1 gap-4 pb-8">
<div
className="border border-th-bkg-3 relative md:mb-0 p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Total Deposit Value"
xAxis="time"
yAxis="value"
data={formatValues(depositValues)}
labelFormat={(x) =>
x &&
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
}
type="area"
/>
</div>
<div
className="border border-th-bkg-3 relative p-4 rounded-md"
style={{ height: '300px' }}
>
<Chart
title="Total Borrow Value"
xAxis="time"
yAxis="value"
data={formatValues(borrowValues)}
labelFormat={(x) =>
x &&
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
}
type="area"
/>
</div>
</div>
<div className="md:flex md:flex-col min-w-full">
<Table className="min-w-full divide-y divide-th-bkg-2">
<Thead>
<Tr className="text-th-fgd-3 text-xs">
<Th scope="col" className="px-6 py-3 text-left font-normal">
Asset
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Total Deposits
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Total Borrows
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Deposit Interest
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Borrow Interest
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Utilization
</Th>
</Tr>
</Thead>
<Tbody>
{latestStats.map((stat, index) => (
<Tr
key={stat.symbol}
className={`border-b border-th-bkg-2
${index % 2 === 0 ? `bg-th-bkg-3` : `bg-th-bkg-2`}
`}
>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
<div className="flex items-center">
<img
src={icons[stat.symbol]}
alt={icons[stat.symbol]}
width="20"
height="20"
className="mr-2.5"
/>
{stat.symbol}
</div>
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{formatBalanceDisplay(
stat.totalDeposits,
tokenPrecision[stat.symbol]
).toLocaleString(undefined, {
maximumFractionDigits: tokenPrecision[stat.symbol],
})}
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{formatBalanceDisplay(
stat.totalBorrows,
tokenPrecision[stat.symbol]
).toLocaleString(undefined, {
maximumFractionDigits: tokenPrecision[stat.symbol],
})}
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{stat.depositInterest.toFixed(2)}%
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{stat.borrowInterest.toFixed(2)}%
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{(parseFloat(stat.utilization) * 100).toFixed(2)}%
</Td>
</Tr>
))}
</Tbody>
</Table>
</div>
</>
)
}

View File

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react'
const useHistoricPrices = () => {
const [prices, setPrices] = useState<any>(null)
useEffect(() => {
const fetchPrices = async () => {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/stats/prices/2oogpTYm1sp6LPZAWD3bp2wsFpnV2kXL1s52yyFhW5vp`
)
const prices = await response.json()
setPrices(prices)
}
fetchPrices()
}, [])
return { prices }
}
export default useHistoricPrices

72
hooks/useMangoStats.tsx Normal file
View File

@ -0,0 +1,72 @@
import { useEffect, useState } from 'react'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useConnection from './useConnection'
import useMangoStore from '../stores/useMangoStore'
import useMangoGroupConfig from './useMangoGroupConfig'
const useMangoStats = () => {
const [stats, setStats] = useState([
{
symbol: '',
hourly: '',
depositInterest: 0,
borrowInterest: 0,
totalDeposits: 0,
totalBorrows: 0,
utilization: '0',
},
])
const [latestStats, setLatestStats] = useState<any[]>([])
const { cluster } = useConnection()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
const connection = useMangoStore((s) => s.connection.current);
const config = useMangoGroupConfig();
useEffect(() => {
const fetchStats = async () => {
const response = await fetch(
`http://localhost:8000/v3?mangoGroup=${mangoGroupName}`
)
const stats = await response.json()
setStats(stats)
}
fetchStats()
}, [])
useEffect(() => {
const getLatestStats = async () => {
if (mangoGroup) {
const rootBanks = await mangoGroup.loadRootBanks(connection);
const latestStats = config.tokens.map((token) => {
const rootBank = rootBanks.find((bank) => {
if (!bank) {
return false;
}
return bank.publicKey.toBase58() == token.rootKey.toBase58();
})
const totalDeposits = rootBank.getUiTotalDeposit(mangoGroup);
const totalBorrows = rootBank.getUiTotalBorrow(mangoGroup);
return {
time: new Date(),
symbol: token.symbol,
totalDeposits: totalDeposits,
totalBorrows: totalBorrows,
depositInterest: rootBank.getDepositRate(mangoGroup).mul(I80F48.fromNumber(100)),
borrowInterest: rootBank.getBorrowRate(mangoGroup).mul(I80F48.fromNumber(100)),
utilization: totalDeposits > I80F48.fromNumber(0) ? totalBorrows.div(totalDeposits) : I80F48.fromNumber(0),
}
});
setLatestStats(latestStats)
}
}
getLatestStats()
}, [cluster])
return { latestStats, stats }
}
export default useMangoStats

View File

@ -15,7 +15,7 @@ export const formatTokenMints = (symbols: { [name: string]: string }) => {
const useMarket = () => {
const market = useMangoStore((state) => state.selectedMarket.current)
const selectedMarketName = useMangoStore((state) => state.selectedMarket.name)
const selectedMarketName = useMangoStore((state) => state.selectedMarket.config.name)
const { programId } = useConnection()
const marketAddress = useMemo(

View File

@ -3,13 +3,18 @@ import useConnection from './useConnection'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import { IDS } from '@blockworks-foundation/mango-client'
import useMangoGroupConfig from './useMangoGroupConfig'
const useMarketList = () => {
const config = useMangoGroupConfig()
const mangoGroupName = useMangoStore((state) => state.selectedMangoGroup.name)
const { cluster, programId, dexProgramId } = useConnection()
const spotMarkets = useMemo(
() => IDS[cluster]?.mango_groups[mangoGroupName]?.spot_market_symbols || {},
() => config.spotMarkets.reduce((acc, market) => {
acc[market.name] = market.publicKey.toBase58()
return acc;
}, {}) || {},
[cluster, mangoGroupName]
)

View File

@ -4,13 +4,14 @@ import useConnection from './useConnection'
import useInterval from './useInterval'
import useMarket from './useMarket'
import useMarketList from './useMarketList'
import { nativeToUi } from '@blockworks-foundation/mango-client'
const SECONDS = 1000
export default function useOraclePrice() {
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const { connection } = useConnection()
const { marketAddress } = useMarket()
const { marketAddress, marketName } = useMarket()
const { getMarketIndex } = useMarketList()
const [oraclePrice, setOraclePrice] = useState(null)
@ -18,8 +19,13 @@ export default function useOraclePrice() {
if (selectedMangoGroup) {
setOraclePrice(null)
const marketIndex = getMarketIndex(marketAddress)
selectedMangoGroup.getPrices(connection).then((prices) => {
const oraclePriceForMarket = prices[marketIndex]
selectedMangoGroup.loadCache(connection).then((cache) => {
console.log(marketName)
console.log(cache.priceCache, marketIndex)
const oraclePriceForMarket = nativeToUi(
cache.priceCache[marketIndex].price.toNumber(),
3
)
setOraclePrice(oraclePriceForMarket)
})
}

View File

@ -1,348 +1,69 @@
import { useEffect, useState } from 'react'
import { LineChart, Line, ReferenceLine, XAxis, YAxis, Tooltip } from 'recharts'
import useDimensions from 'react-cool-dimensions'
import { I80F48, IDS, MangoClient } from '@blockworks-foundation/mango-client'
import { PublicKey, Connection } from '@solana/web3.js'
// import { DEFAULT_MANGO_GROUP } from '../utils/mango'
import useConnection from '../hooks/useConnection'
import { useState } from 'react'
import TopBar from '../components/TopBar'
import { formatBalanceDisplay, tokenPrecision } from '../utils/index'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import useMangoStore from '../stores/useMangoStore'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import PageBodyContainer from '../components/PageBodyContainer'
import StatsTotals from '../components/stats-page/StatsTotals'
import StatsAssets from '../components/stats-page/StatsAssets'
const DECIMALS = {
BTC: 5,
ETH: 4,
SOL: 2,
SRM: 2,
USDC: 2,
USDT: 2,
WUSDT: 2,
}
const icons = {
BTC: '/assets/icons/btc.svg',
ETH: '/assets/icons/eth.svg',
SOL: '/assets/icons/sol.svg',
SRM: '/assets/icons/srm.svg',
USDT: '/assets/icons/usdt.svg',
USDC: '/assets/icons/usdc.svg',
WUSDT: '/assets/icons/usdt.svg',
}
const useMangoStats = () => {
const [stats, setStats] = useState([
{
symbol: '',
depositInterest: 0,
borrowInterest: 0,
totalDeposits: 0,
totalBorrows: 0,
utilization: '0',
},
])
const [latestStats, setLatestStats] = useState<any[]>([])
const { cluster } = useConnection()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
const connection = useMangoStore((s) => s.connection.current);
const config = useMangoGroupConfig();
useEffect(() => {
const fetchStats = async () => {
const response = await fetch(
`http://localhost:8000/v3?mangoGroup=${mangoGroupName}`
)
const stats = await response.json()
setStats(stats)
}
fetchStats()
}, [])
useEffect(() => {
const getLatestStats = async () => {
if (mangoGroup) {
const rootBanks = await mangoGroup.loadRootBanks(connection);
const latestStats = config.tokens.map((token) => {
const rootBank = rootBanks.find((bank) => {
if (!bank) {
return false;
}
return bank.publicKey.toBase58() == token.rootKey.toBase58();
})
const totalDeposits = rootBank.getUiTotalDeposit(mangoGroup).toNumber();
const totalBorrows = rootBank.getUiTotalBorrow(mangoGroup).toNumber();
return {
time: new Date(),
symbol: token.symbol,
totalDeposits: totalDeposits.toFixed(tokenPrecision[token.symbol]),
totalBorrows: totalBorrows.toFixed(tokenPrecision[token.symbol]),
depositInterest: rootBank.getDepositRate(mangoGroup).mul(I80F48.fromNumber(100)),
borrowInterest: rootBank.getBorrowRate(mangoGroup).mul(I80F48.fromNumber(100)),
utilization: totalDeposits > 0.0 ? totalBorrows / totalDeposits : 0.0,
}
})
setLatestStats(latestStats)
}
}
getLatestStats()
}, [cluster])
return { latestStats, stats }
}
const StatsChart = ({ title, xAxis, yAxis, data, labelFormat }) => {
const [mouseData, setMouseData] = useState<string | null>(null)
// @ts-ignore
const { observe, width, height } = useDimensions()
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
return (
<div className="h-full w-full" ref={observe}>
<div className="absolute -top-4 left-0 h-full w-full pb-4">
<div className="text-center text-th-fgd-1 text-base font-semibold">
{title}
</div>
{mouseData ? (
<div className="text-center pt-1">
<div className="text-sm font-normal text-th-fgd-3">
{labelFormat(mouseData[yAxis])}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{new Date(mouseData[xAxis]).toDateString()}
</div>
</div>
) : null}
</div>
{width > 0 ? (
<LineChart
width={width}
height={height}
margin={{ top: 50, right: 50 }}
data={data}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
stroke: '#f7f7f7',
strokeWidth: 1.5,
strokeOpacity: 0.3,
strokeDasharray: '6 5',
}}
content={<></>}
/>
<Line
isAnimationActive={false}
type="linear"
dot={false}
dataKey={yAxis}
stroke="#f2c94c"
strokeWidth={2}
/>
{mouseData ? (
<ReferenceLine
y={mouseData[yAxis]}
strokeDasharray="6 5"
strokeWidth={1.5}
strokeOpacity={0.3}
/>
) : null}
<XAxis dataKey={xAxis} hide />
<YAxis dataKey={yAxis} hide />
</LineChart>
) : null}
</div>
)
}
const TABS = [
'Totals',
'Assets',
'Perps',
// 'Markets',
// 'Liquidations',
]
export default function StatsPage() {
const [selectedAsset, setSelectedAsset] = useState<string>('BTC')
const { latestStats, stats } = useMangoStats()
const [activeTab, setActiveTab] = useState(TABS[0])
const selectedStatsData = stats.filter(
(stat) => stat.symbol === selectedAsset
)
const handleTabChange = (tabName) => {
setActiveTab(tabName)
}
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all `}>
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<div className="min-h-screen w-full xl:w-3/4 mx-auto px-4 sm:px-6 sm:py-1 md:px-8 md:py-1 lg:px-12">
<div className="text-center pt-8 pb-6 md:pt-10">
<h1 className={`text-th-fgd-1 text-2xl font-semibold`}>
Mango Stats
</h1>
<PageBodyContainer>
<div className="flex flex-col sm:flex-row pt-8 pb-3 sm:pb-6 md:pt-10">
<h1 className={`text-th-fgd-1 text-2xl font-semibold`}>Stats</h1>
</div>
<div className="md:flex md:flex-col min-w-full">
<Table className="min-w-full divide-y divide-th-bkg-2">
<Thead>
<Tr className="text-th-fgd-3">
<Th scope="col" className="px-6 py-3 text-left font-normal">
Asset
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Total Deposits
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Total Borrows
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Deposit Interest
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Borrow Interest
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Utilization
</Th>
</Tr>
</Thead>
<Tbody>
{latestStats.map((stat, index) => (
<Tr
key={stat.symbol}
className={`border-b border-th-bkg-2
${index % 2 === 0 ? `bg-th-bkg-2` : `bg-th-bkg-1`}
<div className="bg-th-bkg-2 overflow-none p-6 rounded-lg">
<div className="border-b border-th-fgd-4 mb-4">
<nav className={`-mb-px flex space-x-6`} aria-label="Tabs">
{TABS.map((tabName) => (
<a
key={tabName}
onClick={() => handleTabChange(tabName)}
className={`whitespace-nowrap pb-4 px-1 border-b-2 font-semibold cursor-pointer default-transition hover:opacity-100
${
activeTab === tabName
? `border-th-primary text-th-primary`
: `border-transparent text-th-fgd-4 hover:text-th-primary`
}
`}
>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
<div className="flex items-center">
<img
src={icons[stat.symbol]}
alt={icons[stat.symbol]}
className="w-5 h-5 md:w-6 md:h-6"
/>
<button
onClick={() => setSelectedAsset(stat.symbol)}
className="underline cursor-pointer ml-3 hover:text-th-primary hover:no-underline"
>
{stat.symbol}
</button>
</div>
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{formatBalanceDisplay(
stat.totalDeposits,
DECIMALS[stat.symbol]
).toLocaleString(undefined, {
maximumFractionDigits: DECIMALS[stat.symbol],
})}
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{formatBalanceDisplay(
stat.totalBorrows,
DECIMALS[stat.symbol]
).toLocaleString(undefined, {
maximumFractionDigits: DECIMALS[stat.symbol],
})}
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{stat.depositInterest.toFixed(2)}%
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{stat.borrowInterest.toFixed(2)}%
</Td>
<Td className="px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1">
{(parseFloat(stat.utilization) * 100).toFixed(2)}%
</Td>
</Tr>
{tabName}
</a>
))}
</Tbody>
</Table>
</div>
{selectedAsset ? (
<div className="py-10 md:py-14">
<div className="flex flex-col items-center pb-12">
<h2 className="text-th-fgd-1 text-center text-2xl font-semibold mb-4">
Historical Stats
</h2>
<div className="flex self-center">
{latestStats.map((stat) => (
<div
className={`px-2 py-1 mr-2 rounded-md cursor-pointer default-transition bg-th-bkg-3
${
selectedAsset === stat.symbol
? `text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => setSelectedAsset(stat.symbol)}
key={stat.symbol as string}
>
{stat.symbol}
</div>
))}
</div>
</div>
<div className="flex flex-col md:flex-row pb-14">
<div
className="relative my-2 pb-14 md:pb-0 md:w-1/2"
style={{ height: '300px' }}
>
<StatsChart
title="Total Deposits"
xAxis="time"
yAxis="totalDeposits"
data={selectedStatsData}
labelFormat={(x) => x.toFixed(DECIMALS[selectedAsset])}
/>
</div>
<div
className="relative my-2 md:w-1/2"
style={{ height: '300px' }}
>
<StatsChart
title="Total Borrows"
xAxis="time"
yAxis="totalBorrows"
data={selectedStatsData}
labelFormat={(x) => x.toFixed(DECIMALS[selectedAsset])}
/>
</div>
</div>
<div className="flex flex-col md:flex-row">
<div
className="relative my-2 pb-14 md:pb-0 md:w-1/2"
style={{ height: '300px' }}
>
<StatsChart
title="Deposit Interest"
xAxis="time"
yAxis="depositInterest"
data={selectedStatsData}
labelFormat={(x) => `${(x * 100).toFixed(5)}%`}
/>
</div>
<div
className="relative my-2 md:w-1/2"
style={{ height: '300px' }}
>
<StatsChart
title="Borrow Interest"
xAxis="time"
yAxis="borrowInterest"
data={selectedStatsData}
labelFormat={(x) => `${(x * 100).toFixed(5)}%`}
/>
</div>
</div>
</nav>
</div>
) : null}
</div>
<TabContent activeTab={activeTab} />
</div>
</PageBodyContainer>
</div>
)
}
const TabContent = ({ activeTab }) => {
switch (activeTab) {
case 'Totals':
return <StatsTotals />
case 'Assets':
return <StatsAssets />
case 'Markets':
return <div>Markets</div>
case 'Liquidations':
return <div>Liquidations</div>
default:
return <StatsAssets />
}
}