add performance chart to account page

This commit is contained in:
tjs 2022-03-01 14:06:02 -05:00
parent 46e90570ce
commit b6999cd220
8 changed files with 650 additions and 214 deletions

View File

@ -70,7 +70,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
description?.includes('Timed out awaiting') ||
description?.includes('was not confirmed')
) {
parsedTitle = 'Unable to confirm transaction'
parsedTitle = 'Transaction status unknown'
}
}

View File

@ -57,9 +57,9 @@ const TopBar = () => {
className={`hidden md:flex md:items-center md:space-x-2 lg:space-x-3 md:ml-4`}
>
<TradeNavMenu />
<MenuItem href="/swap">{t('swap')}</MenuItem>
<MenuItem href="/account">{t('account')}</MenuItem>
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
<MenuItem href="/swap">{t('swap')}</MenuItem>
<MenuItem href="/stats">{t('stats')}</MenuItem>
<div className="relative">
<MenuItem href="/referral">

View File

@ -115,7 +115,7 @@ export default function AccountHistory() {
return (
<>
<div className="bg-th-bkg-3 flex mb-4 md:mb-6 md:-mt-6 md:-mx-6 px-3 md:px-4 py-2 rounded-md md:rounded-none md:rounded-t-md">
<div className="bg-th-bkg-3 flex mb-4 md:mb-6 md:-mx-6 px-3 md:px-4 py-2">
{historyViews.map(({ label, key }, index) => (
<div
className={`font-bold md:px-2 py-1 text-xs md:text-sm ${

View File

@ -1,39 +1,77 @@
import { useMemo } from 'react'
import {
ScaleIcon,
CurrencyDollarIcon,
GiftIcon,
HeartIcon,
} from '@heroicons/react/outline'
import { nativeToUi, ZERO_BN } from '@blockworks-foundation/mango-client'
import useMangoStore, { MNGO_INDEX } from '../../stores/useMangoStore'
import { formatUsdValue } from '../../utils'
import { notify } from '../../utils/notifications'
import { LinkButton } from '../Button'
import BalancesTable from '../BalancesTable'
import PositionsTable from '../PerpPositionsTable'
import Switch from '../Switch'
import useLocalStorageState from '../../hooks/useLocalStorageState'
import { useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { ExclamationIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import useMangoStore from '../../stores/useMangoStore'
import { formatUsdValue } from '../../utils'
import BalancesTable from '../BalancesTable'
import Switch from '../Switch'
import useLocalStorageState from '../../hooks/useLocalStorageState'
import ButtonGroup from '../ButtonGroup'
import PerformanceChart from './PerformanceChart'
import PositionsTable from '../PerpPositionsTable'
dayjs.extend(utc)
const SHOW_ZERO_BALANCE_KEY = 'showZeroAccountBalances-0.2'
const performanceRangePresets = [
{ label: '24h', value: 1 },
{ label: '7d', value: 7 },
{ label: '30d', value: 30 },
{ label: '3m', value: 90 },
]
const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label)
const fetchHourlyPerformanceStats = async (mangoAccountPk: string) => {
const range =
performanceRangePresets[performanceRangePresets.length - 1].value
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/account-performance-detailed?mango-account=${mangoAccountPk}&start-date=${dayjs()
.subtract(range, 'day')
.format('YYYY-MM-DD')}`
)
const parsedResponse = await response.json()
const entries: any = Object.entries(parsedResponse)
const stats = entries
.map(([key, value]) => {
return { ...value, time: key }
})
.filter((x) => x)
.reverse()
return stats
}
export default function AccountOverview() {
const { t } = useTranslation('common')
const actions = useMangoStore((s) => s.actions)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const mangoClient = useMangoStore((s) => s.connection.client)
const router = useRouter()
const { pubkey } = router.query
const [showZeroBalances, setShowZeroBalances] = useLocalStorageState(
SHOW_ZERO_BALANCE_KEY,
true
)
const [pnl, setPnl] = useState(0)
const [performanceRange, setPerformanceRange] = useState('30d')
const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState([])
useEffect(() => {
const fetchData = async () => {
const stats = await fetchHourlyPerformanceStats(
mangoAccount.publicKey.toString()
)
setPnl(stats?.length ? stats?.[0]?.['pnl'] : 0)
setHourlyPerformanceStats(stats)
}
fetchData()
}, [mangoAccount.publicKey])
const maintHealthRatio = useMemo(() => {
return mangoAccount
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
@ -46,93 +84,63 @@ export default function AccountOverview() {
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const mngoAccrued = useMemo(() => {
return mangoAccount
? mangoAccount.perpAccounts.reduce((acc, perpAcct) => {
return perpAcct.mngoAccrued.add(acc)
}, ZERO_BN)
: ZERO_BN
const mangoAccountValue = useMemo(() => {
return +mangoAccount.computeValue(mangoGroup, mangoCache)
}, [mangoAccount])
const handleRedeemMngo = async () => {
const wallet = useMangoStore.getState().wallet.current
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
try {
const txid = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
actions.reloadMangoAccount()
notify({
title: t('redeem-success'),
description: '',
txid,
})
} catch (e) {
notify({
title: t('redeem-failure'),
description: e.message,
txid: e.txid,
type: 'error',
})
}
}
return mangoAccount ? (
<>
<div className="grid grid-flow-col grid-cols-2 grid-rows-2 lg:grid-cols-4 lg:grid-rows-1 gap-2 sm:gap-4 pb-8">
<div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs sm:text-sm">
{t('account-value')}
</div>
<div className="flex items-center pb-1 sm:pb-3">
<CurrencyDollarIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
{formatUsdValue(
+mangoAccount.computeValue(mangoGroup, mangoCache)
)}
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-4">
<h2 className="mb-4 sm:mb-0">{t('summary')}</h2>
<div className="w-full sm:w-56">
<ButtonGroup
activeValue={performanceRange}
onChange={(p) => setPerformanceRange(p)}
values={performanceRangePresetLabels}
/>
</div>
{/* <div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs sm:text-sm">PNL</div>
<div className="flex items-center pb-1 sm:pb-3">
<ChartBarIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
{formatUsdValue(
+mangoAccount.computeValue(mangoGroup, mangoCache)
)}
</div>
<div className="flex flex-col lg:flex-row lg:space-x-6 pb-8 lg:pb-12">
<div className="border-t border-th-bkg-4 pb-6 lg:pb-0 w-full lg:w-1/4">
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3 text-xs sm:text-sm">
{t('account-value')}
</div>
<div className="font-bold text-th-fgd-3 text-xl sm:text-2xl">
{formatUsdValue(mangoAccountValue)}
</div>
</div>
</div> */}
<div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs sm:text-sm">
{t('leverage')}
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3 text-xs sm:text-sm">
{t('pnl')} ({t('all-time')})
</div>
<div className="font-bold text-th-fgd-3 text-xl sm:text-2xl">
{formatUsdValue(pnl)}
</div>
</div>
<div className="flex items-center pb-1 sm:pb-3">
<ScaleIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3 text-xs sm:text-sm">
{t('leverage')}
</div>
<div className="font-bold text-th-fgd-3 text-xl sm:text-2xl">
{mangoAccount.getLeverage(mangoGroup, mangoCache).toFixed(2)}x
</div>
</div>
</div>
<div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs sm:text-sm">
{t('health-ratio')}
</div>
<div className="flex items-center pb-3 sm:pb-4">
<HeartIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
<div className="p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3 text-xs sm:text-sm">
{t('health-ratio')}
</div>
<div className={`font-bold text-th-fgd-3 text-xl sm:text-2xl`}>
{maintHealthRatio < 1000 ? maintHealthRatio.toFixed(2) : '>100'}%
</div>
{mangoAccount.beingLiquidated ? (
<div className="pt-0.5 sm:pt-2 text-xs sm:text-sm flex items-center">
<ExclamationIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-red" />
<span className="text-th-red">{t('being-liquidated')}</span>
</div>
) : null}
</div>
<div className="h-1.5 flex rounded bg-th-bkg-3">
<div className="h-1 flex rounded bg-th-bkg-3">
<div
style={{
width: `${maintHealthRatio}%`,
@ -146,37 +154,13 @@ export default function AccountOverview() {
}`}
></div>
</div>
{mangoAccount.beingLiquidated ? (
<div className="pt-0.5 sm:pt-2 text-xs sm:text-sm flex items-center">
<ExclamationIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-red" />
<span className="text-th-red">{t('being-liquidated')}</span>
</div>
) : null}
</div>
<div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs sm:text-sm">
{t('mngo-rewards')}
</div>
<div className="flex items-center pb-1 sm:pb-2">
<GiftIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
{mangoGroup
? nativeToUi(
mngoAccrued.toNumber(),
mangoGroup.tokens[MNGO_INDEX].decimals
)
: 0}
</div>
</div>
{!pubkey ? (
<LinkButton
onClick={handleRedeemMngo}
disabled={mngoAccrued.eq(ZERO_BN)}
className="text-th-primary text-xs"
>
{t('claim-reward')}
</LinkButton>
) : null}
<div className="border-t border-th-bkg-4 h-80 lg:h-auto w-full lg:w-3/4">
<PerformanceChart
hourlyPerformanceStats={hourlyPerformanceStats}
performanceRange={performanceRange}
accountValue={mangoAccountValue}
/>
</div>
</div>
<div className="pb-8">
@ -185,7 +169,7 @@ export default function AccountOverview() {
</div>
<h2 className="mb-4">{t('assets-liabilities')}</h2>
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 md:grid-cols-2 md:grid-rows-1 gap-2 sm:gap-4 pb-8">
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 md:grid-cols-2 md:grid-rows-1 gap-2 sm:gap-4 pb-8 lg:pb-12">
<div className="border border-th-bkg-4 p-3 sm:p-4 rounded-md sm:rounded-lg">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('total-assets')}

View File

@ -0,0 +1,383 @@
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { cloneDeep } from 'lodash'
import dayjs from 'dayjs'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as ChartTooltip,
} from 'recharts'
import { InformationCircleIcon } from '@heroicons/react/outline'
import useDimensions from 'react-cool-dimensions'
import { useTranslation } from 'next-i18next'
import ButtonGroup from '../ButtonGroup'
import { formatUsdValue } from '../../utils'
import { numberCompacter } from '../SwapTokenInfo'
import Checkbox from '../Checkbox'
import Tooltip from '../Tooltip'
type PerformanceChart = {
hourlyPerformanceStats: any[]
performanceRange: '24hr' | '7d' | '30d' | '3m'
}
const defaultData = [
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
]
const PerformanceChart = ({
hourlyPerformanceStats,
performanceRange,
accountValue,
}) => {
const { theme } = useTheme()
const { t } = useTranslation('common')
const { observe, width, height } = useDimensions()
const [chartData, setChartData] = useState([])
const [mouseData, setMouseData] = useState<string | null>(null)
const [chartToShow, setChartToShow] = useState('Value')
const [showSpotPnl, setShowSpotPnl] = useState(true)
const [showPerpPnl, setShowPerpPnl] = useState(true)
useEffect(() => {
if (hourlyPerformanceStats.length > 0) {
if (performanceRange === '3m') {
setChartData(hourlyPerformanceStats.slice().reverse())
}
if (performanceRange === '30d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '7d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '24h') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
} else {
setChartData([])
}
}, [hourlyPerformanceStats, performanceRange])
useEffect(() => {
if (chartData.length > 0) {
for (const stat of chartData) {
stat.spot_pnl = stat.pnl - stat.perp_pnl
}
}
}, [chartData])
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const renderPnlChartTitle = () => {
if (showPerpPnl && showSpotPnl) {
return t('pnl')
}
if (!showSpotPnl) {
return `${t('perp')} PnL`
}
if (!showPerpPnl) {
return `${t('spot')} PnL`
}
}
const formatDateAxis = (date) => {
if (['3m', '30d'].includes(performanceRange)) {
return dayjs(date).format('D MMM')
} else if (performanceRange === '7d') {
return dayjs(date).format('ddd, h:mma')
} else {
return dayjs(date).format('h:mma')
}
}
const pnlChartDataKey = () => {
if (!showPerpPnl && showSpotPnl) {
return 'spot_pnl'
} else if (!showSpotPnl && showPerpPnl) {
return 'perp_pnl'
} else {
return 'pnl'
}
}
const pnlChartColor =
chartToShow === 'PnL' &&
chartData.length > 0 &&
chartData[chartData.length - 1][pnlChartDataKey()] > 0
? theme === 'Mango'
? '#AFD803'
: '#5EBF4D'
: theme === 'Mango'
? '#F84638'
: '#CC2929'
console.log('chartData', chartData)
return (
<div className="h-64 mt-4 w-full" ref={observe}>
<div className="flex justify-between pb-9">
<div>
<div className="flex items-center pb-0.5">
<div className="text-sm text-th-fgd-3">
{chartToShow === 'Value'
? t('account-value')
: renderPnlChartTitle()}{' '}
<span className="text-th-fgd-4">
{`(${t('timeframe-desc', {
timeframe: performanceRange,
})})`}
</span>
</div>
<Tooltip content={t('delayed-info')}>
<InformationCircleIcon className="cursor-help h-5 ml-1.5 text-th-fgd-3 w-5" />
</Tooltip>
</div>
{mouseData ? (
<>
<div className="font-bold pb-1 text-xl text-th-fgd-3">
{formatUsdValue(
mouseData[
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
]
)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length === 0 ? (
<>
<div className="font-bold pb-1 text-xl text-th-fgd-3">
{formatUsdValue(0.0)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length > 0 ? (
<>
<div className="font-bold pb-1 text-xl text-th-fgd-3">
{chartToShow === 'PnL'
? formatUsdValue(
chartData[chartData.length - 1][pnlChartDataKey()]
)
: formatUsdValue(accountValue)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(chartData[chartData.length - 1]['time']).format(
'ddd MMM D YYYY, h:mma'
)}
</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>
<div className="flex flex-col items-end">
<div className="w-36">
<ButtonGroup
activeValue={chartToShow}
className="pb-2 pt-2 text-sm"
onChange={(v) => setChartToShow(v)}
values={[t('value'), 'PnL']}
/>
</div>
{chartToShow === 'PnL' ? (
<div className="flex pt-4 space-x-3">
<Checkbox
checked={showSpotPnl}
disabled={!showPerpPnl}
onChange={(e) => setShowSpotPnl(e.target.checked)}
>
{t('include-spot')}
</Checkbox>
<Checkbox
checked={showPerpPnl}
disabled={!showSpotPnl}
onChange={(e) => setShowPerpPnl(e.target.checked)}
>
{t('include-perp')}
</Checkbox>
</div>
) : null}
</div>
</div>
{chartData.length > 0 ? (
<AreaChart
width={width}
height={height}
data={chartData?.length ? chartData : defaultData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<ChartTooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient
id="defaultGradientArea"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
<linearGradient id="greenGradientArea" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient id="redGradientArea" x1="0" y1="1" x2="0" y2="0">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={true}
type="monotone"
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
fill={
chartToShow === 'PnL'
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
? 'url(#greenGradientArea)'
: 'url(#redGradientArea)'
: 'url(#defaultGradientArea)'
}
fillOpacity={0.3}
/>
<YAxis
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
type="number"
domain={['dataMin', 'dataMax']}
axisLine={false}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => numberCompacter.format(v)}
/>
<XAxis
dataKey="time"
axisLine={false}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
</AreaChart>
) : null}
</div>
)
}
export default PerformanceChart

View File

@ -1,15 +1,20 @@
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
BellIcon,
CurrencyDollarIcon,
DuplicateIcon,
ExclamationCircleIcon,
ExternalLinkIcon,
GiftIcon,
LinkIcon,
PencilIcon,
TrashIcon,
UsersIcon,
} from '@heroicons/react/outline'
import useMangoStore, { serumProgramId } from '../stores/useMangoStore'
import { nativeToUi, ZERO_BN } from '@blockworks-foundation/mango-client'
import useMangoStore, {
serumProgramId,
MNGO_INDEX,
} from '../stores/useMangoStore'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import AccountOrders from '../components/account_page/AccountOrders'
@ -18,9 +23,8 @@ import AccountsModal from '../components/AccountsModal'
import AccountOverview from '../components/account_page/AccountOverview'
import AccountInterest from '../components/account_page/AccountInterest'
import AccountFunding from '../components/account_page/AccountFunding'
import AccountPerformance from '../components/account_page/AccountPerformance'
import AccountNameModal from '../components/AccountNameModal'
import Button, { IconButton } from '../components/Button'
import Button, { IconButton, LinkButton } from '../components/Button'
import EmptyState from '../components/EmptyState'
import Loading from '../components/Loading'
import Swipeable from '../components/mobile/Swipeable'
@ -33,6 +37,7 @@ import Select from '../components/Select'
import { useRouter } from 'next/router'
import { PublicKey } from '@solana/web3.js'
import CloseAccountModal from '../components/CloseAccountModal'
import { notify } from '../utils/notifications'
import {
actionsSelector,
mangoAccountSelector,
@ -40,6 +45,7 @@ import {
walletConnectedSelector,
} from '../stores/selectors'
import CreateAlertModal from '../components/CreateAlertModal'
import { copyToClipboard } from '../utils'
import DelegateModal from '../components/DelegateModal'
export async function getStaticProps({ locale }) {
@ -56,24 +62,13 @@ export async function getStaticProps({ locale }) {
}
}
const TABS = [
'Portfolio',
'Orders',
'History',
'Interest',
'Funding',
'Performance',
]
const TABS = ['Portfolio', 'Orders', 'History', 'Interest', 'Funding']
export default function Account() {
const { t } = useTranslation(['common', 'close-account', 'delegate'])
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showNameModal, setShowNameModal] = useState(false)
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
const [showAlertsModal, setShowAlertsModal] = useState(false)
const [showDelegateModal, setShowDelegateModal] = useState(false)
const [isCopied, setIsCopied] = useState(false)
const [resetOnLeave, setResetOnLeave] = useState(false)
const { width } = useViewport()
const router = useRouter()
const connected = useMangoStore(walletConnectedSelector)
const mangoAccount = useMangoStore(mangoAccountSelector)
const mangoClient = useMangoStore((s) => s.connection.client)
@ -82,14 +77,21 @@ export default function Account() {
const isLoading = useMangoStore((s) => s.selectedMangoAccount.initialLoad)
const actions = useMangoStore(actionsSelector)
const setMangoStore = useMangoStore((s) => s.set)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showNameModal, setShowNameModal] = useState(false)
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
const [showAlertsModal, setShowAlertsModal] = useState(false)
const [showDelegateModal, setShowDelegateModal] = useState(false)
const [isCopied, setIsCopied] = useState(false)
const [resetOnLeave, setResetOnLeave] = useState(false)
const [mngoAccrued, setMngoAccrued] = useState(ZERO_BN)
const [viewIndex, setViewIndex] = useState(0)
const [activeTab, setActiveTab] = useState(TABS[0])
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const router = useRouter()
const { pubkey } = router.query
const isDelegatedAccount = !mangoAccount?.owner.equals(wallet?.publicKey)
const buttonCols = isDelegatedAccount ? 2 : 4
const handleCloseAlertModal = useCallback(() => {
setShowAlertsModal(false)
@ -169,6 +171,11 @@ export default function Account() {
}
}, [isCopied])
const handleCopyAddress = (address) => {
setIsCopied(true)
copyToClipboard(address)
}
const handleChangeViewIndex = (index) => {
setViewIndex(index)
}
@ -177,6 +184,47 @@ export default function Account() {
setActiveTab(tabName)
}
useMemo(() => {
setMngoAccrued(
mangoAccount
? mangoAccount.perpAccounts.reduce((acc, perpAcct) => {
return perpAcct.mngoAccrued.add(acc)
}, ZERO_BN)
: ZERO_BN
)
}, [mangoAccount])
const handleRedeemMngo = async () => {
const wallet = useMangoStore.getState().wallet.current
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
try {
const txid = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
actions.reloadMangoAccount()
setMngoAccrued(ZERO_BN)
notify({
title: t('redeem-success'),
description: '',
txid,
})
} catch (e) {
notify({
title: t('redeem-failure'),
description: e.message,
txid: e.txid,
type: 'error',
})
}
}
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
@ -190,44 +238,69 @@ export default function Account() {
{mangoAccount?.name || t('account')}
</h1>
{!pubkey ? (
<IconButton onClick={() => setShowNameModal(true)}>
<PencilIcon className="h-4 w-4" />
<IconButton
className="h-7 w-7"
onClick={() => setShowNameModal(true)}
>
<PencilIcon className="h-3.5 w-3.5" />
</IconButton>
) : null}
</div>
<a
className="flex items-center text-th-fgd-3"
href={`https://explorer.solana.com/address/${mangoAccount?.publicKey}`}
target="_blank"
rel="noopener noreferrer"
>
<span className="text-xxs sm:text-xs">
{mangoAccount.publicKey.toString()}
</span>
<ExternalLinkIcon className="cursor-pointer default-transition h-4 w-4 ml-1.5 hover:text-th-fgd-1" />
</a>
<div className="flex items-center text-th-red text-xxs">
<div className="flex h-4 items-center">
<LinkButton
className="flex items-center no-underline text-th-fgd-4"
onClick={() =>
handleCopyAddress(mangoAccount.publicKey.toString())
}
>
<span className="text-xxs sm:text-xs font-normal">
{mangoAccount.publicKey.toBase58()}
</span>
<DuplicateIcon className="h-4 w-4 ml-1.5" />
</LinkButton>
{isCopied ? (
<span className="bg-th-bkg-3 ml-2 px-1.5 py-0.5 rounded text-xs">
Copied
</span>
) : null}
</div>
<div className="flex items-center text-th-fgd-4 text-xxs">
<ExclamationCircleIcon className="h-4 mr-1.5 w-4" />
{t('account-address-warning')}
</div>
</div>
{!pubkey ? (
<div
className={`grid grid-cols-${buttonCols} grid-rows-1 gap-2 auto-cols-min`}
>
{!isDelegatedAccount && (
<div className="flex flex-col sm:flex-row items-center pb-1.5 space-y-2 sm:space-y-0 sm:space-x-2">
<button
className="bg-th-primary flex items-center justify-center h-8 text-th-bkg-1 text-xs px-3 py-0 rounded-full w-full hover:brightness-[1.15] focus:outline-none disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:cursor-not-allowed disabled:hover:brightness-100"
disabled={mngoAccrued.eq(ZERO_BN)}
onClick={handleRedeemMngo}
>
<div className="flex items-center whitespace-nowrap">
<GiftIcon className="flex-shrink-0 h-4 w-4 mr-1.5" />
{!mngoAccrued.eq(ZERO_BN)
? `Claim ${nativeToUi(
mngoAccrued.toNumber(),
mangoGroup.tokens[MNGO_INDEX].decimals
).toLocaleString(undefined, {
minimumSignificantDigits: 1,
})} MNGO`
: '0 MNGO Rewards'}
</div>
</button>
{!isDelegatedAccount ? (
<Button
className="col-span-1 flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3 text-xs"
className="flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3 text-xs w-full"
onClick={() => setShowCloseAccountModal(true)}
>
<div className="flex items-center">
<TrashIcon className="h-4 w-4 mr-1.5" />
<div className="flex items-center whitespace-nowrap">
<TrashIcon className="flex-shrink-0 h-4 w-4 mr-1.5" />
{t('close-account:close-account')}
</div>
</Button>
)}
) : null}
<Button
className="col-span-1 flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3 text-xs"
className="flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3 text-xs w-full"
onClick={() => setShowAlertsModal(true)}
>
<div className="flex items-center">
@ -246,43 +319,34 @@ export default function Account() {
</div>
</Button>
)}
<Button
className="col-span-1 flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3 text-xs"
onClick={() => setShowAccountsModal(true)}
>
<div className="flex items-center">
<CurrencyDollarIcon className="h-4 w-4 mr-1.5" />
{t('accounts')}
</div>
</Button>
</div>
) : null}
</>
) : null}
</div>
{mangoAccount ? (
!isMobile ? (
<Tabs
activeTab={activeTab}
onChange={handleTabChange}
tabs={TABS}
/>
) : (
<div className="pb-2 pt-3">
<Select
value={t(TABS[viewIndex].toLowerCase())}
onChange={(e) => handleChangeViewIndex(e)}
>
{TABS.map((tab, index) => (
<Select.Option key={index + tab} value={index}>
{t(tab.toLowerCase())}
</Select.Option>
))}
</Select>
</div>
)
) : null}
<div className="bg-th-bkg-2 p-4 sm:p-6 rounded-lg">
{mangoAccount ? (
!isMobile ? (
<Tabs
activeTab={activeTab}
onChange={handleTabChange}
tabs={TABS}
/>
) : (
<div className="pb-2 pt-3">
<Select
value={t(TABS[viewIndex].toLowerCase())}
onChange={(e) => handleChangeViewIndex(e)}
>
{TABS.map((tab, index) => (
<Select.Option key={index + tab} value={index}>
{t(tab.toLowerCase())}
</Select.Option>
))}
</Select>
</div>
)
) : null}
{mangoAccount ? (
!isMobile ? (
<TabContent activeTab={activeTab} />
@ -380,8 +444,6 @@ const TabContent = ({ activeTab }) => {
return <AccountInterest />
case 'Funding':
return <AccountFunding />
case 'Performance':
return <AccountPerformance />
default:
return <AccountOverview />
}

View File

@ -14,8 +14,8 @@
"account-health-tip-title": "Account Health",
"account-name": "Account Name",
"account-performance": "Account Performance",
"account-pnl": "Account PNL",
"account-pnl-chart-title": "Account PNL",
"account-pnl": "Account PnL",
"account-pnl-chart-title": "Account PnL",
"account-risk": "Account Risk",
"account-value": "Account Value",
"accounts": "Accounts",
@ -23,6 +23,7 @@
"add-name": "Add Name",
"alerts": "Alerts",
"all-assets": "All Assets",
"all-time": "All Time",
"amount": "Amount",
"approximate-time": "Time",
"asset": "Asset",
@ -99,6 +100,7 @@
"default-market": "Default Market",
"default-spot-margin": "Trade with margin by default",
"delay-displaying-recent": "There may be a delay in displaying the latest activity.",
"delayed-info": "Data updates hourly",
"deposit": "Deposit",
"deposit-before": "You need more {{tokenSymbol}} in your wallet to fully repay your borrow",
"deposit-failed": "Deposit failed",
@ -151,6 +153,8 @@
"hourly-deposit-interest": "Hourly Deposit Interest",
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"include-perp": "Include Perp",
"include-spot": "Include Spot",
"includes-borrow": "Includes borrow of",
"init-error": "Could not perform init mango account and deposit operation",
"init-health": "Init Health",
@ -172,7 +176,7 @@
"languages-tip-title": "Multilingual?",
"layout-tip-desc": "Unlock to re-arrange and re-size the trading panels to your liking.",
"layout-tip-title": "Customize Layout",
"learn": "Learn",
"learn": "Documentation",
"learn-more": "Learn more",
"lets-go": "Let's Go",
"leverage": "Leverage",
@ -262,13 +266,14 @@
"perp-desc": "Perpetual swaps settled in USDC",
"perp-fees": "Mango Perp Fees",
"perp-positions": "Perp Positions",
"perp-positions-tip-desc": "Perp positions accrue Unsettled PnL as price moves. Settling PnL adds or removes that amount from your USDC balance.",
"perp-positions-tip-desc": "Perp positions accrue Unsettled PnL as price moves. Redeeming adds or removes that amount from your USDC balance.",
"perp-positions-tip-title": "Perp Position Details",
"perpetual-futures": "Perpetual Futures",
"perps": "Perps",
"pnl-error": "Error settling PNL",
"pnl": "PnL",
"pnl-error": "Error redeeming",
"pnl-help": "Redeeming will update your USDC balance to reflect the redeemed PnL amount.",
"pnl-success": "Successfully settled PNL",
"pnl-success": "Successfully redeemed",
"portfolio": "Portfolio",
"position": "Position",
"position-size": "Position Size",
@ -334,6 +339,7 @@
"stop-loss": "Stop Loss",
"stop-price": "Stop Price",
"successfully-placed": "Successfully placed order",
"summary": "Summary",
"supported-assets": "Please fund wallet with one of the supported assets.",
"swap": "Swap",
"take-profit": "Take Profit",
@ -344,6 +350,7 @@
"themes-tip-desc": "Mango, Dark or Light (if you're that way inclined).",
"themes-tip-title": "Color Themes",
"time": "Time",
"timeframe-desc": "Last {{timeframe}}",
"token": "Token",
"too-large": "Size Too Large",
"tooltip-account-liquidated": "Account will be liquidated if Health Ratio reaches 0% and will continue until Init Health is above 0.",

View File

@ -521,4 +521,4 @@ body::-webkit-scrollbar-corner {
/* CJK characters */
.keep-break {
word-break: keep-all;
}
}