Stats page alignment (#28)
* total deposit/borrow value charts, new chart styles * responsive and bar chart * reduce icon size to match trade page * round large numbers * use historic prices for total deposits/borrows * fix lint errors * use current price if no match from prices api * fix data passed to borrow chart * update prices endpoint url Co-authored-by: Maximilian Schneider <mail@maximilianschneider.net>
This commit is contained in:
parent
1157d55307
commit
8a59b00347
|
@ -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
|
|
@ -61,17 +61,16 @@ export default function MarginInfo() {
|
|||
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const tradeHistory = useTradeHistory()
|
||||
const tradeHistoryLength = useMemo(() => tradeHistory.length, [tradeHistory])
|
||||
const [mAccountInfo, setMAccountInfo] =
|
||||
useState<
|
||||
| {
|
||||
label: string
|
||||
value: string
|
||||
unit: string
|
||||
desc: string
|
||||
currency: string
|
||||
}[]
|
||||
| null
|
||||
>(null)
|
||||
const [mAccountInfo, setMAccountInfo] = useState<
|
||||
| {
|
||||
label: string
|
||||
value: string
|
||||
unit: string
|
||||
desc: string
|
||||
currency: string
|
||||
}[]
|
||||
| null
|
||||
>(null)
|
||||
const [openAlertModal, setOpenAlertModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
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 backupPrices = useMangoStore((s) => s.selectedMangoGroup.prices)
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,69 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { IDS, MangoClient } from '@blockworks-foundation/mango-client'
|
||||
import { PublicKey, Connection } from '@solana/web3.js'
|
||||
import useConnection from './useConnection'
|
||||
import { DEFAULT_MANGO_GROUP } from '../utils/mango'
|
||||
|
||||
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()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-stats.herokuapp.com?mangoGroup=BTC_ETH_SOL_SRM_USDC`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setStats(stats)
|
||||
}
|
||||
fetchStats()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const getLatestStats = async () => {
|
||||
const client = new MangoClient()
|
||||
const connection = new Connection(
|
||||
IDS.cluster_urls[cluster],
|
||||
'singleGossip'
|
||||
)
|
||||
const assets = IDS[cluster].mango_groups?.[DEFAULT_MANGO_GROUP]?.symbols
|
||||
const mangoGroupId =
|
||||
IDS[cluster].mango_groups?.[DEFAULT_MANGO_GROUP]?.mango_group_pk
|
||||
if (!mangoGroupId) return
|
||||
const mangoGroupPk = new PublicKey(mangoGroupId)
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
const latestStats = Object.keys(assets).map((symbol, index) => {
|
||||
const totalDeposits = mangoGroup.getUiTotalDeposit(index)
|
||||
const totalBorrows = mangoGroup.getUiTotalBorrow(index)
|
||||
|
||||
return {
|
||||
time: new Date(),
|
||||
symbol,
|
||||
totalDeposits,
|
||||
totalBorrows,
|
||||
depositInterest: mangoGroup.getDepositRate(index) * 100,
|
||||
borrowInterest: mangoGroup.getBorrowRate(index) * 100,
|
||||
utilization: totalDeposits > 0.0 ? totalBorrows / totalDeposits : 0.0,
|
||||
}
|
||||
})
|
||||
setLatestStats(latestStats)
|
||||
}
|
||||
|
||||
getLatestStats()
|
||||
}, [cluster])
|
||||
|
||||
return { latestStats, stats }
|
||||
}
|
||||
|
||||
export default useMangoStats
|
|
@ -62,18 +62,17 @@ const useMarginInfo = () => {
|
|||
)
|
||||
const tradeHistory = useTradeHistory()
|
||||
const tradeHistoryLength = useMemo(() => tradeHistory.length, [tradeHistory])
|
||||
const [mAccountInfo, setMAccountInfo] =
|
||||
useState<
|
||||
| {
|
||||
label: string
|
||||
value: string
|
||||
unit: string
|
||||
desc: string
|
||||
currency: string
|
||||
icon: ReactNode
|
||||
}[]
|
||||
| null
|
||||
>(null)
|
||||
const [mAccountInfo, setMAccountInfo] = useState<
|
||||
| {
|
||||
label: string
|
||||
value: string
|
||||
unit: string
|
||||
desc: string
|
||||
currency: string
|
||||
icon: ReactNode
|
||||
}[]
|
||||
| null
|
||||
>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMangoGroup) {
|
||||
|
|
378
pages/stats.tsx
378
pages/stats.tsx
|
@ -1,342 +1,68 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { LineChart, Line, ReferenceLine, XAxis, YAxis, Tooltip } from 'recharts'
|
||||
import useDimensions from 'react-cool-dimensions'
|
||||
import { 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 } from '../utils/index'
|
||||
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
|
||||
import PageBodyContainer from '../components/PageBodyContainer'
|
||||
import StatsTotals from '../components/stats-page/StatsTotals'
|
||||
import StatsAssets from '../components/stats-page/StatsAssets'
|
||||
|
||||
const DECIMALS = {
|
||||
BTC: 4,
|
||||
ETH: 3,
|
||||
SOL: 2,
|
||||
SRM: 2,
|
||||
USDT: 2,
|
||||
USDC: 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()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-stats.herokuapp.com?mangoGroup=BTC_ETH_SOL_SRM_USDC`
|
||||
)
|
||||
const stats = await response.json()
|
||||
|
||||
setStats(stats)
|
||||
}
|
||||
fetchStats()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const getLatestStats = async () => {
|
||||
const client = new MangoClient()
|
||||
const connection = new Connection(
|
||||
IDS.cluster_urls[cluster],
|
||||
'singleGossip'
|
||||
)
|
||||
const assets = IDS[cluster].mango_groups?.[DEFAULT_MANGO_GROUP]?.symbols
|
||||
const mangoGroupId =
|
||||
IDS[cluster].mango_groups?.[DEFAULT_MANGO_GROUP]?.mango_group_pk
|
||||
if (!mangoGroupId) return
|
||||
const mangoGroupPk = new PublicKey(mangoGroupId)
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
const latestStats = Object.keys(assets).map((symbol, index) => {
|
||||
const totalDeposits = mangoGroup.getUiTotalDeposit(index)
|
||||
const totalBorrows = mangoGroup.getUiTotalBorrow(index)
|
||||
|
||||
return {
|
||||
time: new Date(),
|
||||
symbol,
|
||||
totalDeposits,
|
||||
totalBorrows,
|
||||
depositInterest: mangoGroup.getDepositRate(index) * 100,
|
||||
borrowInterest: mangoGroup.getBorrowRate(index) * 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',
|
||||
// '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 <StatsTotals />
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue