import { HealthType, toUiDecimalsForQuote, } from '@blockworks-foundation/mango-v4' import { useTranslation } from 'next-i18next' import { useEffect, useMemo, useState } from 'react' import AccountActions from './AccountActions' import mangoStore from '@store/mangoStore' import { formatCurrencyValue } from '../../utils/numbers' import FlipNumbers from 'react-flip-numbers' import SimpleAreaChart from '@components/shared/SimpleAreaChart' import { COLORS } from '../../styles/colors' import { useTheme } from 'next-themes' import { IconButton } from '../shared/Button' import { ArrowsPointingOutIcon, CalendarIcon, ChartBarIcon, } from '@heroicons/react/20/solid' import { Transition } from '@headlessui/react' import AccountTabs from './AccountTabs' import SheenLoader from '../shared/SheenLoader' import AccountChart from './AccountChart' import useMangoAccount from '../../hooks/useMangoAccount' import Change from '../shared/Change' import Tooltip from '@components/shared/Tooltip' import { ANIMATION_SETTINGS_KEY, MANGO_DATA_API_URL, // IS_ONBOARDED_KEY } from 'utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' // import AccountOnboardingTour from '@components/tours/AccountOnboardingTour' import dayjs from 'dayjs' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useViewport } from 'hooks/useViewport' import { breakpoints } from 'utils/theme' import useMangoGroup from 'hooks/useMangoGroup' import PnlHistoryModal from '@components/modals/PnlHistoryModal' import FormatNumericValue from '@components/shared/FormatNumericValue' import HealthBar from './HealthBar' import AssetsLiabilities from './AssetsLiabilities' import { AccountPerformanceData, AccountVolumeTotalData, EmptyObject, FormattedHourlyAccountVolumeData, HourlyAccountVolumeData, PerformanceDataItem, TotalAccountFundingItem, } from 'types' import { useQuery } from '@tanstack/react-query' import FundingChart from './FundingChart' import VolumeChart from './VolumeChart' const TABS = ['account-value', 'account:assets-liabilities'] const fetchAccountPerformance = async ( mangoAccountPk: string, range: number ) => { try { const response = await fetch( `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() .subtract(range, 'day') .format('YYYY-MM-DD')}` ) const parsedResponse: null | EmptyObject | AccountPerformanceData[] = await response.json() if (parsedResponse && Object.keys(parsedResponse)?.length) { const entries = Object.entries(parsedResponse).sort((a, b) => b[0].localeCompare(a[0]) ) const stats = entries.map(([key, value]) => { return { ...value, time: key } as PerformanceDataItem }) return stats.reverse() } else return [] } catch (e) { console.error('Failed to load account performance data', e) return [] } } const fetchFundingTotals = async (mangoAccountPk: string) => { try { const data = await fetch( `${MANGO_DATA_API_URL}/stats/funding-account-total?mango-account=${mangoAccountPk}` ) const res = await data.json() if (res) { const entries: [string, Omit][] = Object.entries(res) const stats: TotalAccountFundingItem[] = entries .map(([key, value]) => { return { long_funding: value.long_funding * -1, short_funding: value.short_funding * -1, market: key, } }) .filter((x) => x) return stats } } catch (e) { console.log('Failed to fetch account funding', e) } } const fetchVolumeTotals = async (mangoAccountPk: string) => { try { const [perpTotal, spotTotal] = await Promise.all([ fetch( `${MANGO_DATA_API_URL}/stats/perp-volume-total?mango-account=${mangoAccountPk}` ), fetch( `${MANGO_DATA_API_URL}/stats/spot-volume-total?mango-account=${mangoAccountPk}` ), ]) const [perpTotalData, spotTotalData] = await Promise.all([ perpTotal.json(), spotTotal.json(), ]) const combinedData = [perpTotalData, spotTotalData] if (combinedData.length) { return combinedData.reduce((a, c) => { const entries: AccountVolumeTotalData[] = Object.entries(c) const marketVol = entries.reduce((a, c) => { return a + c[1].volume_usd }, 0) return a + marketVol }, 0) } return 0 } catch (e) { console.log('Failed to fetch spot volume', e) return 0 } } const formatHourlyVolumeData = (data: HourlyAccountVolumeData[]) => { if (!data || !data.length) return [] const formattedData: FormattedHourlyAccountVolumeData[] = [] // Loop through each object in the original data array for (const obj of data) { // Loop through the keys (markets) in each object for (const market in obj) { // Loop through the timestamps in each market for (const timestamp in obj[market]) { // Find the corresponding entry in the formatted data array based on the timestamp let entry = formattedData.find((item) => item.time === timestamp) // If the entry doesn't exist, create a new entry if (!entry) { entry = { time: timestamp, total_volume_usd: 0, markets: {} } formattedData.push(entry) } // Increment the total_volume_usd by the volume_usd value entry.total_volume_usd += obj[market][timestamp].volume_usd // Add or update the market entry in the markets object entry.markets[market] = obj[market][timestamp].volume_usd } } } return formattedData } const fetchHourlyVolume = async (mangoAccountPk: string) => { try { const [perpHourly, spotHourly] = await Promise.all([ fetch( `${MANGO_DATA_API_URL}/stats/perp-volume-hourly?mango-account=${mangoAccountPk}` ), fetch( `${MANGO_DATA_API_URL}/stats/spot-volume-hourly?mango-account=${mangoAccountPk}` ), ]) const [perpHourlyData, spotHourlyData] = await Promise.all([ perpHourly.json(), spotHourly.json(), ]) const hourlyVolume = [perpHourlyData, spotHourlyData] return formatHourlyVolumeData(hourlyVolume) } catch (e) { console.log('Failed to fetch spot volume', e) } } export type ChartToShow = | '' | 'account-value' | 'cumulative-interest-value' | 'pnl' | 'hourly-funding' | 'hourly-volume' const AccountPage = () => { const { t } = useTranslation(['common', 'account']) const { group } = useMangoGroup() const { mangoAccount, mangoAccountAddress } = useMangoAccount() const totalInterestData = mangoStore( (s) => s.mangoAccount.interestTotals.data ) const [chartToShow, setChartToShow] = useState('') const [showExpandChart, setShowExpandChart] = useState(false) const [showPnlHistory, setShowPnlHistory] = useState(false) const { theme } = useTheme() const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false // const tourSettings = mangoStore((s) => s.settings.tours) // const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY) const [animationSettings] = useLocalStorageState( ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS ) const [activeTab, setActiveTab] = useLocalStorageState( 'accountHeroKey-0.1', 'account-value' ) useEffect(() => { if (mangoAccountAddress) { const actions = mangoStore.getState().actions actions.fetchAccountInterestTotals(mangoAccountAddress) } }, [mangoAccountAddress]) const { data: performanceData, isLoading: loadingPerformanceData, isFetching: fetchingPerformanceData, } = useQuery( ['performance', mangoAccountAddress], () => fetchAccountPerformance(mangoAccountAddress, 31), { cacheTime: 1000 * 60 * 10, staleTime: 1000 * 60, retry: 3, refetchOnWindowFocus: false, enabled: !!mangoAccountAddress, } ) const { data: fundingData, isLoading: loadingFunding, isFetching: fetchingFunding, } = useQuery( ['funding', mangoAccountAddress], () => fetchFundingTotals(mangoAccountAddress), { cacheTime: 1000 * 60 * 10, staleTime: 1000 * 60, retry: 3, refetchOnWindowFocus: false, enabled: !!mangoAccountAddress, } ) const { data: volumeTotalData, isLoading: loadingVolumeTotalData, isFetching: fetchingVolumeTotalData, } = useQuery( ['total-volume', mangoAccountAddress], () => fetchVolumeTotals(mangoAccountAddress), { cacheTime: 1000 * 60 * 10, staleTime: 1000 * 60, retry: 3, refetchOnWindowFocus: false, enabled: !!mangoAccountAddress, } ) const { data: hourlyVolumeData, isLoading: loadingHourlyVolumeData, isFetching: fetchingHourlyVolumeData, } = useQuery( ['hourly-volume', mangoAccountAddress], () => fetchHourlyVolume(mangoAccountAddress), { cacheTime: 1000 * 60 * 10, staleTime: 1000 * 60, retry: 3, refetchOnWindowFocus: false, enabled: !!mangoAccountAddress, } ) const dailyVolume = useMemo(() => { if (!hourlyVolumeData || !hourlyVolumeData.length) return 0 // Calculate the current time in milliseconds const currentTime = new Date().getTime() // Calculate the start time for the last 24 hours in milliseconds const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000 // Filter the formatted data based on the timestamp const last24HoursData = hourlyVolumeData.filter((entry) => { const timestampMs = new Date(entry.time).getTime() return timestampMs >= last24HoursStartTime && timestampMs <= currentTime }) const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0) return volume }, [hourlyVolumeData]) const oneDayPerformanceData: PerformanceDataItem[] | [] = useMemo(() => { if (!performanceData || !performanceData.length) return [] const nowDate = new Date() return performanceData.filter((d) => { const dataTime = new Date(d.time).getTime() return dataTime >= nowDate.getTime() - 86400000 }) }, [performanceData]) const onHoverMenu = (open: boolean, action: string) => { if ( (!open && action === 'onMouseEnter') || (open && action === 'onMouseLeave') ) { setShowExpandChart(!open) } } const handleShowAccountValueChart = () => { setChartToShow('account-value') setShowExpandChart(false) } const handleHideChart = () => { setChartToShow('') } const handleCloseDailyPnlModal = () => { setShowPnlHistory(false) } const [accountPnl, accountValue] = useMemo(() => { if (!group || !mangoAccount) return [0, 0] return [ toUiDecimalsForQuote(mangoAccount.getPnl(group).toNumber()), toUiDecimalsForQuote(mangoAccount.getEquity(group).toNumber()), ] }, [group, mangoAccount]) const leverage = useMemo(() => { if (!group || !mangoAccount) return 0 const assetsValue = toUiDecimalsForQuote( mangoAccount.getAssetsValue(group).toNumber() ) if (isNaN(assetsValue / accountValue)) { return 0 } else { return Math.abs(1 - assetsValue / accountValue) } }, [mangoAccount, group, accountValue]) const [accountValueChange, oneDayPnlChange] = useMemo(() => { if ( accountValue && oneDayPerformanceData.length && performanceData && performanceData.length ) { const accountValueChange = accountValue - oneDayPerformanceData[0].account_equity const startDayPnl = oneDayPerformanceData[0].pnl const endDayPnl = oneDayPerformanceData[oneDayPerformanceData.length - 1].pnl const oneDayPnlChange = endDayPnl - startDayPnl return [accountValueChange, oneDayPnlChange] } return [0, 0] }, [accountValue, oneDayPerformanceData, performanceData]) const interestTotalValue = useMemo(() => { if (totalInterestData.length) { return totalInterestData.reduce( (a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd), 0 ) } return 0.0 }, [totalInterestData]) const fundingTotalValue = useMemo(() => { if (fundingData?.length && mangoAccountAddress) { return fundingData.reduce( (a, c) => a + c.long_funding + c.short_funding, 0 ) } return 0.0 }, [fundingData, mangoAccountAddress]) const oneDayInterestChange = useMemo(() => { if (oneDayPerformanceData.length) { const first = oneDayPerformanceData[0] const latest = oneDayPerformanceData[oneDayPerformanceData.length - 1] const startDayInterest = first.borrow_interest_cumulative_usd + first.deposit_interest_cumulative_usd const endDayInterest = latest.borrow_interest_cumulative_usd + latest.deposit_interest_cumulative_usd return endDayInterest - startDayInterest } return 0.0 }, [oneDayPerformanceData]) const maintHealth = useMemo(() => { return group && mangoAccount ? mangoAccount.getHealthRatioUi(group, HealthType.maint) : 0 }, [mangoAccount, group]) const handleChartToShow = ( chartName: | 'pnl' | 'cumulative-interest-value' | 'hourly-funding' | 'hourly-volume' ) => { setChartToShow(chartName) } const latestAccountData = useMemo(() => { if (!accountValue || !performanceData || !performanceData.length) return [] const latestDataItem = performanceData[performanceData.length - 1] return [ { account_equity: accountValue, time: dayjs(Date.now()).toISOString(), borrow_interest_cumulative_usd: latestDataItem.borrow_interest_cumulative_usd, deposit_interest_cumulative_usd: latestDataItem.deposit_interest_cumulative_usd, pnl: accountPnl, spot_value: latestDataItem.spot_value, transfer_balance: latestDataItem.transfer_balance, }, ] }, [accountPnl, accountValue, performanceData]) const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData const loadingHourlyVolume = fetchingHourlyVolumeData || loadingHourlyVolumeData const performanceLoading = loadingPerformanceData || fetchingPerformanceData return !chartToShow ? ( <>
{TABS.map((tab) => ( ))}
{activeTab === 'account-value' ? (
{animationSettings['number-scroll'] ? ( group && mangoAccount ? ( ) : ( ) ) : ( )}

{t('rolling-change')}

{!performanceLoading ? ( oneDayPerformanceData.length ? (
onHoverMenu(showExpandChart, 'onMouseEnter') } onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave') } > = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } data={oneDayPerformanceData.concat(latestAccountData)} name="accountValue" xKey="time" yKey="account_equity" /> handleShowAccountValueChart()} >
) : null ) : mangoAccountAddress ? (
) : null}
) : null} {activeTab === 'account:assets-liabilities' ? ( ) : null}

Health describes how close your account is to liquidation. The lower your account health is the more likely you are to get liquidated when prices fluctuate.

{maintHealth < 100 && mangoAccountAddress ? ( <>

Your account health is {maintHealth}%

Scenario: {' '} If the prices of all your liabilities increase by{' '} {maintHealth}%, even for just a moment, some of your liabilities will be liquidated.

Scenario: {' '} If the value of your total collateral decreases by{' '} {( (1 - 1 / ((maintHealth || 0) / 100 + 1)) * 100 ).toFixed(2)} % , some of your liabilities will be liquidated.

These are examples. A combination of events can also lead to liquidation.

) : null}
} >

{t('health')}

{maintHealth}%

{t('leverage')}: x

{t('free-collateral')}

{t('total')}:

{t('pnl')}

{mangoAccountAddress ? (
handleChartToShow('pnl')} > setShowPnlHistory(true)} >
) : null}

{t('rolling-change')}

{t('account:lifetime-volume')}

{mangoAccountAddress ? ( handleChartToShow('hourly-volume')} > ) : null}
{loadingTotalVolume && mangoAccountAddress ? (
) : (

)} {t('account:daily-volume')}: {loadingHourlyVolume && mangoAccountAddress ? (
) : ( )}

{t('total-interest-earned')}

{mangoAccountAddress ? ( handleChartToShow('cumulative-interest-value') } > ) : null}

{t('rolling-change')}

{t('account:total-funding-earned')}

{mangoAccountAddress ? ( handleChartToShow('hourly-funding')} > ) : null}
{(loadingFunding || fetchingFunding) && mangoAccountAddress ? (
) : (

)}
{/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? ( ) : null} */} {showPnlHistory ? ( ) : null} ) : ( <> {chartToShow === 'account-value' ? ( ) : chartToShow === 'pnl' ? ( ) : chartToShow === 'hourly-funding' ? ( ) : chartToShow === 'hourly-volume' ? ( ) : ( )} ) } export default AccountPage