Merge branch 'main' into ts/trade-pg-jup
This commit is contained in:
commit
e5945b60e2
|
@ -0,0 +1,14 @@
|
|||
import { WHITE_LIST_API } from 'utils/constants'
|
||||
|
||||
export type WhiteListedResp = {
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export const fetchIsWhiteListed = async (wallet: string) => {
|
||||
const data = await fetch(`${WHITE_LIST_API}isWhiteListed?wallet=${wallet}`)
|
||||
const body = await data.json()
|
||||
if (body.error) {
|
||||
throw { error: body.error, status: data.status }
|
||||
}
|
||||
return body.found
|
||||
}
|
|
@ -25,22 +25,20 @@ import { Transition } from '@headlessui/react'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import TermsOfUseModal from './modals/TermsOfUseModal'
|
||||
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
|
||||
import PromoBanner from './rewards/PromoBanner'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const sideBarAnimationDuration = 300
|
||||
const termsLastUpdated = 1679441610978
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
const { connected } = useWallet()
|
||||
const loadingMangoAccount = mangoStore((s) => s.mangoAccount.initialLoad)
|
||||
const [isCollapsed, setIsCollapsed] = useLocalStorageState(
|
||||
SIDEBAR_COLLAPSE_KEY,
|
||||
false
|
||||
)
|
||||
const [acceptTerms, setAcceptTerms] = useLocalStorageState(
|
||||
ACCEPT_TERMS_KEY,
|
||||
''
|
||||
)
|
||||
|
||||
const { width } = useViewport()
|
||||
const { asPath } = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (width < breakpoints.xl) {
|
||||
|
@ -70,10 +68,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
particlesInit()
|
||||
}, [])
|
||||
|
||||
const showTermsOfUse = useMemo(() => {
|
||||
return (!acceptTerms || acceptTerms < termsLastUpdated) && connected
|
||||
}, [acceptTerms, connected])
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`${ttCommons.variable} ${ttCommonsExpanded.variable} ${ttCommonsMono.variable} font-sans`}
|
||||
|
@ -81,11 +75,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
<div className="fixed z-30">
|
||||
<SuccessParticles />
|
||||
</div>
|
||||
{connected && loadingMangoAccount ? (
|
||||
<div className="fixed z-30 flex h-screen w-full items-center justify-center bg-[rgba(0,0,0,0.7)]">
|
||||
<BounceLoader />
|
||||
</div>
|
||||
) : null}
|
||||
<MangoAccountLoadingOverlay />
|
||||
<div className="flex-grow bg-th-bkg-1 text-th-fgd-2 transition-all">
|
||||
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
|
||||
<BottomBar />
|
||||
|
@ -117,25 +107,60 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
}`}
|
||||
>
|
||||
<TopBar />
|
||||
{asPath !== '/rewards' ? <PromoBanner /> : null}
|
||||
{children}
|
||||
</div>
|
||||
<DeployRefreshManager />
|
||||
<TermsOfUse />
|
||||
</div>
|
||||
{showTermsOfUse ? (
|
||||
<TermsOfUseModal
|
||||
isOpen={showTermsOfUse}
|
||||
onClose={() => setAcceptTerms(Date.now())}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
||||
const MangoAccountLoadingOverlay = () => {
|
||||
const { connected } = useWallet()
|
||||
const loadingMangoAccount = mangoStore((s) => s.mangoAccount.initialLoad)
|
||||
|
||||
return (
|
||||
<>
|
||||
{connected && loadingMangoAccount ? (
|
||||
<div className="fixed z-30 flex h-screen w-full items-center justify-center bg-[rgba(0,0,0,0.7)]">
|
||||
<BounceLoader />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TermsOfUse = () => {
|
||||
const { connected } = useWallet()
|
||||
const [acceptTerms, setAcceptTerms] = useLocalStorageState(
|
||||
ACCEPT_TERMS_KEY,
|
||||
''
|
||||
)
|
||||
|
||||
const showTermsOfUse = useMemo(() => {
|
||||
return (!acceptTerms || acceptTerms < termsLastUpdated) && connected
|
||||
}, [acceptTerms, connected])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTermsOfUse ? (
|
||||
<TermsOfUseModal
|
||||
isOpen={showTermsOfUse}
|
||||
onClose={() => setAcceptTerms(Date.now())}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DeployRefreshManager(): JSX.Element | null {
|
||||
const { t } = useTranslation('common')
|
||||
const [newBuildAvailable, setNewBuildAvailable] = useState(false)
|
||||
|
||||
useInterval(async () => {
|
||||
const response = await fetch('/api/build-id')
|
||||
const { buildId } = await response.json()
|
||||
|
@ -144,7 +169,7 @@ function DeployRefreshManager(): JSX.Element | null {
|
|||
// There's a new version deployed that we need to load
|
||||
setNewBuildAvailable(true)
|
||||
}
|
||||
}, 30000)
|
||||
}, 300000)
|
||||
|
||||
return (
|
||||
<Transition
|
||||
|
|
|
@ -48,7 +48,7 @@ const HydrateStore = () => {
|
|||
useInterval(async () => {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.loadMarketFills()
|
||||
}, 6000)
|
||||
}, 30000)
|
||||
|
||||
// The websocket library solana/web3.js uses closes its websocket connection when the subscription list
|
||||
// is empty after opening its first time, preventing subsequent subscriptions from receiving responses.
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
NewspaperIcon,
|
||||
PlusCircleIcon,
|
||||
ArchiveBoxArrowDownIcon,
|
||||
// ClipboardDocumentIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -112,6 +113,13 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
title={t('stats')}
|
||||
pagePath="/stats"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/leaderboard'}
|
||||
collapsed={collapsed}
|
||||
icon={<LeaderboardIcon className="h-5 w-5" />}
|
||||
title={t('leaderboard')}
|
||||
pagePath="/leaderboard"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/settings'}
|
||||
collapsed={collapsed}
|
||||
|
@ -124,15 +132,6 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
icon={<EllipsisHorizontalIcon className="h-5 w-5" />}
|
||||
title={t('more')}
|
||||
>
|
||||
<MenuItem
|
||||
active={pathname === '/leaderboard'}
|
||||
collapsed={false}
|
||||
icon={<LeaderboardIcon className="h-5 w-5" />}
|
||||
title={t('leaderboard')}
|
||||
pagePath="/leaderboard"
|
||||
hideIconBg
|
||||
showTooltip={false}
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/search'}
|
||||
collapsed={false}
|
||||
|
@ -178,6 +177,15 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
isExternal
|
||||
showTooltip={false}
|
||||
/>
|
||||
{/* <MenuItem
|
||||
collapsed={false}
|
||||
icon={<ClipboardDocumentIcon className="h-5 w-5" />}
|
||||
title={t('feedback-survey')}
|
||||
pagePath="https://forms.gle/JgV4w7SJ2kPH89mq7"
|
||||
hideIconBg
|
||||
isExternal
|
||||
showTooltip={false}
|
||||
/> */}
|
||||
<MenuItem
|
||||
collapsed={false}
|
||||
icon={<NewspaperIcon className="h-5 w-5" />}
|
||||
|
|
|
@ -4,18 +4,18 @@ import { formatYAxis } from 'utils/formatting'
|
|||
import { HourlyFundingChartData, PerformanceDataItem } from 'types'
|
||||
import { ContentType } from 'recharts/types/component/Tooltip'
|
||||
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
||||
import { ChartToShow } from './AccountPage'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const CHART_TABS: ChartToShow[] = [
|
||||
const CHART_TABS: ViewToShow[] = [
|
||||
'account-value',
|
||||
'pnl',
|
||||
'cumulative-interest-value',
|
||||
]
|
||||
|
||||
const AccountChart = ({
|
||||
chartToShow,
|
||||
setChartToShow,
|
||||
chartName,
|
||||
handleViewChange,
|
||||
customTooltip,
|
||||
data,
|
||||
hideChart,
|
||||
|
@ -23,8 +23,8 @@ const AccountChart = ({
|
|||
yDecimals,
|
||||
yKey,
|
||||
}: {
|
||||
chartToShow: ChartToShow
|
||||
setChartToShow: (chart: ChartToShow) => void
|
||||
chartName: ViewToShow
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
customTooltip?: ContentType<number, string>
|
||||
data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined
|
||||
hideChart: () => void
|
||||
|
@ -37,7 +37,7 @@ const AccountChart = ({
|
|||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data || !data.length) return []
|
||||
if (chartToShow === 'cumulative-interest-value') {
|
||||
if (chartName === 'cumulative-interest-value') {
|
||||
return data.map((d) => ({
|
||||
interest_value:
|
||||
d.borrow_interest_cumulative_usd * -1 +
|
||||
|
@ -46,7 +46,7 @@ const AccountChart = ({
|
|||
}))
|
||||
}
|
||||
return data
|
||||
}, [data, chartToShow])
|
||||
}, [data, chartName])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -61,11 +61,11 @@ const AccountChart = ({
|
|||
{CHART_TABS.map((tab) => (
|
||||
<button
|
||||
className={`whitespace-nowrap rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
|
||||
chartToShow === tab
|
||||
chartName === tab
|
||||
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
|
||||
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
|
||||
}`}
|
||||
onClick={() => setChartToShow(tab)}
|
||||
onClick={() => handleViewChange(tab)}
|
||||
key={tab}
|
||||
>
|
||||
{t(tab)}
|
||||
|
|
|
@ -3,7 +3,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
|
|||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { ChartToShow } from './AccountPage'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
|
@ -25,13 +25,13 @@ const AccountHeroStats = ({
|
|||
accountValue,
|
||||
rollingDailyData,
|
||||
setShowPnlHistory,
|
||||
setChartToShow,
|
||||
handleViewChange,
|
||||
}: {
|
||||
accountPnl: number
|
||||
accountValue: number
|
||||
rollingDailyData: PerformanceDataItem[]
|
||||
setShowPnlHistory: (show: boolean) => void
|
||||
setChartToShow: (view: ChartToShow) => void
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { group } = useMangoGroup()
|
||||
|
@ -162,71 +162,78 @@ const AccountHeroStats = ({
|
|||
return volume
|
||||
}, [hourlyVolumeData])
|
||||
|
||||
const handleChartToShow = (
|
||||
viewName:
|
||||
| 'pnl'
|
||||
| 'cumulative-interest-value'
|
||||
| 'hourly-funding'
|
||||
| 'hourly-volume'
|
||||
) => {
|
||||
setChartToShow(viewName)
|
||||
}
|
||||
|
||||
const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-6 border-b border-th-bkg-3">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 px-6 md:col-span-3 lg:col-span-2 lg:border-t-0 xl:col-span-1">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-t-0 xl:col-span-1">
|
||||
<div id="account-step-four">
|
||||
<Tooltip
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
content={
|
||||
<div className="flex-col space-y-2 text-sm">
|
||||
<p className="text-xs">
|
||||
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.
|
||||
</p>
|
||||
{maintHealth < 100 && mangoAccountAddress ? (
|
||||
<>
|
||||
<p className="text-xs font-bold text-th-fgd-1">
|
||||
Your account health is {maintHealth}%
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the prices of all your liabilities increase by{' '}
|
||||
{maintHealth}%, even for just a moment, some of your
|
||||
liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
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.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
These are examples. A combination of events can also
|
||||
lead to liquidation.
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="tooltip-underline text-sm font-normal text-th-fgd-3 xl:text-base">
|
||||
{t('health')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
<div className="flex justify-between">
|
||||
<Tooltip
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
content={
|
||||
<div className="flex-col space-y-2 text-sm">
|
||||
<p className="text-xs">
|
||||
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.
|
||||
</p>
|
||||
{maintHealth < 100 && mangoAccountAddress ? (
|
||||
<>
|
||||
<p className="text-xs font-bold text-th-fgd-1">
|
||||
Your account health is {maintHealth}%
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the prices of all your liabilities increase by{' '}
|
||||
{maintHealth}%, even for just a moment, some of your
|
||||
liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
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.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
These are examples. A combination of events can also
|
||||
lead to liquidation.
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="tooltip-underline text-sm font-normal text-th-fgd-3 xl:text-base">
|
||||
{t('health')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:health-contributions')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleViewChange('health-contributions')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1 mb-0.5 flex items-center space-x-3">
|
||||
<p className="text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
{maintHealth}%
|
||||
|
@ -298,7 +305,7 @@ const AccountHeroStats = ({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 px-6 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 xl:col-span-1">
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 xl:col-span-1">
|
||||
<div
|
||||
id="account-step-seven"
|
||||
className="flex w-full flex-col items-start"
|
||||
|
@ -323,7 +330,7 @@ const AccountHeroStats = ({
|
|||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleChartToShow('pnl')}
|
||||
onClick={() => handleViewChange('pnl')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
|
@ -372,7 +379,7 @@ const AccountHeroStats = ({
|
|||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleChartToShow('hourly-volume')}
|
||||
onClick={() => handleViewChange('hourly-volume')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
|
@ -429,7 +436,7 @@ const AccountHeroStats = ({
|
|||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() =>
|
||||
handleChartToShow('cumulative-interest-value')
|
||||
handleViewChange('cumulative-interest-value')
|
||||
}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
|
@ -471,7 +478,7 @@ const AccountHeroStats = ({
|
|||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleChartToShow('hourly-funding')}
|
||||
onClick={() => handleViewChange('hourly-funding')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import AccountActions from './AccountActions'
|
||||
import AccountTabs from './AccountTabs'
|
||||
import AccountChart from './AccountChart'
|
||||
import useMangoAccount from '../../hooks/useMangoAccount'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
// import AccountOnboardingTour from '@components/tours/AccountOnboardingTour'
|
||||
import dayjs from 'dayjs'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
|
@ -18,39 +17,44 @@ import VolumeChart from './VolumeChart'
|
|||
import AccountHeroStats from './AccountHeroStats'
|
||||
import AccountValue from './AccountValue'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats'
|
||||
import HealthContributions from './HealthContributions'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
|
||||
const TABS = ['account-value', 'account:assets-liabilities']
|
||||
|
||||
export type ChartToShow =
|
||||
export type ViewToShow =
|
||||
| ''
|
||||
| 'account-value'
|
||||
| 'cumulative-interest-value'
|
||||
| 'pnl'
|
||||
| 'hourly-funding'
|
||||
| 'hourly-volume'
|
||||
| 'health-contributions'
|
||||
|
||||
const AccountPage = () => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const [chartToShow, setChartToShow] = useState<ChartToShow>('')
|
||||
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
// const tourSettings = mangoStore((s) => s.settings.tours)
|
||||
// const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY)
|
||||
const [activeTab, setActiveTab] = useLocalStorageState(
|
||||
'accountHeroKey-0.1',
|
||||
'account-value'
|
||||
)
|
||||
const { performanceData, rollingDailyData } = useAccountPerformanceData()
|
||||
const { hourlyVolumeData, loadingHourlyVolume } =
|
||||
useAccountHourlyVolumeStats()
|
||||
const router = useRouter()
|
||||
const { view } = router.query
|
||||
|
||||
const handleHideChart = () => {
|
||||
setChartToShow('')
|
||||
}
|
||||
const handleViewChange = useCallback(
|
||||
(view: ViewToShow) => {
|
||||
const query = { ...router.query, ['view']: view }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const handleCloseDailyPnlModal = () => {
|
||||
setShowPnlHistory(false)
|
||||
|
@ -94,7 +98,7 @@ const AccountPage = () => {
|
|||
]
|
||||
}, [accountPnl, accountValue, performanceData])
|
||||
|
||||
return !chartToShow ? (
|
||||
return !view ? (
|
||||
<>
|
||||
<div className="flex flex-col border-b-0 border-th-bkg-3 px-6 py-4 lg:flex-row lg:items-center lg:justify-between lg:border-b">
|
||||
<div>
|
||||
|
@ -119,7 +123,7 @@ const AccountPage = () => {
|
|||
accountValue={accountValue}
|
||||
latestAccountData={latestAccountData}
|
||||
rollingDailyData={rollingDailyData}
|
||||
setChartToShow={setChartToShow}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'account:assets-liabilities' ? (
|
||||
|
@ -135,13 +139,10 @@ const AccountPage = () => {
|
|||
accountPnl={accountPnl}
|
||||
accountValue={accountValue}
|
||||
rollingDailyData={rollingDailyData}
|
||||
setChartToShow={setChartToShow}
|
||||
setShowPnlHistory={setShowPnlHistory}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
<AccountTabs />
|
||||
{/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? (
|
||||
<AccountOnboardingTour />
|
||||
) : null} */}
|
||||
{showPnlHistory ? (
|
||||
<PnlHistoryModal
|
||||
pnlChangeToday={pnlChangeToday}
|
||||
|
@ -151,42 +152,76 @@ const AccountPage = () => {
|
|||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{chartToShow === 'account-value' ? (
|
||||
<AccountChart
|
||||
chartToShow="account-value"
|
||||
setChartToShow={setChartToShow}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="account_equity"
|
||||
/>
|
||||
) : chartToShow === 'pnl' ? (
|
||||
<AccountChart
|
||||
chartToShow="pnl"
|
||||
setChartToShow={setChartToShow}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="pnl"
|
||||
/>
|
||||
) : chartToShow === 'hourly-funding' ? (
|
||||
<FundingChart hideChart={handleHideChart} />
|
||||
) : chartToShow === 'hourly-volume' ? (
|
||||
<VolumeChart
|
||||
chartData={hourlyVolumeData}
|
||||
hideChart={handleHideChart}
|
||||
loading={loadingHourlyVolume}
|
||||
/>
|
||||
) : (
|
||||
<AccountChart
|
||||
chartToShow="cumulative-interest-value"
|
||||
setChartToShow={setChartToShow}
|
||||
data={performanceData}
|
||||
hideChart={handleHideChart}
|
||||
yKey="interest_value"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<AccountView
|
||||
view={view as ViewToShow}
|
||||
latestAccountData={latestAccountData}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
|
||||
const AccountView = ({
|
||||
view,
|
||||
handleViewChange,
|
||||
latestAccountData,
|
||||
}: {
|
||||
view: ViewToShow
|
||||
latestAccountData: PerformanceDataItem[]
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { connected } = useWallet()
|
||||
const { address } = router.query
|
||||
const { performanceData } = useAccountPerformanceData()
|
||||
|
||||
const handleHideChart = useCallback(() => {
|
||||
if (address && !connected) {
|
||||
router.push(`/?address=${address}`, undefined, { shallow: true })
|
||||
} else {
|
||||
router.push('/', undefined, { shallow: true })
|
||||
}
|
||||
}, [address, router, connected])
|
||||
|
||||
switch (view) {
|
||||
case 'account-value':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="account-value"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="account_equity"
|
||||
/>
|
||||
)
|
||||
case 'pnl':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="pnl"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="pnl"
|
||||
/>
|
||||
)
|
||||
case 'cumulative-interest-value':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="cumulative-interest-value"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="interest_value"
|
||||
/>
|
||||
)
|
||||
case 'hourly-funding':
|
||||
return <FundingChart hideChart={handleHideChart} />
|
||||
case 'hourly-volume':
|
||||
return <VolumeChart hideChart={handleHideChart} />
|
||||
case 'health-contributions':
|
||||
return <HealthContributions hideView={handleHideChart} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,19 +19,19 @@ import { useMemo, useState } from 'react'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { ChartToShow } from './AccountPage'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
|
||||
const AccountValue = ({
|
||||
accountValue,
|
||||
latestAccountData,
|
||||
rollingDailyData,
|
||||
setChartToShow,
|
||||
handleViewChange,
|
||||
}: {
|
||||
accountValue: number
|
||||
latestAccountData: PerformanceDataItem[]
|
||||
rollingDailyData: PerformanceDataItem[]
|
||||
setChartToShow: (chart: ChartToShow) => void
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { theme } = useTheme()
|
||||
|
@ -62,7 +62,7 @@ const AccountValue = ({
|
|||
}
|
||||
|
||||
const handleShowAccountValueChart = () => {
|
||||
setChartToShow('account-value')
|
||||
handleViewChange('account-value')
|
||||
setShowExpandChart(false)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
HourlyFundingData,
|
||||
HourlyFundingStatsData,
|
||||
} from 'types'
|
||||
import { MANGO_DATA_API_URL } from 'utils/constants'
|
||||
import { DAILY_MILLISECONDS, MANGO_DATA_API_URL } from 'utils/constants'
|
||||
import { formatCurrencyValue } from 'utils/numbers'
|
||||
import { TooltipProps } from 'recharts/types/component/Tooltip'
|
||||
import {
|
||||
|
@ -191,7 +191,7 @@ const FundingChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
|
||||
const filteredData: HourlyFundingChartData[] = useMemo(() => {
|
||||
if (!chartData.length) return []
|
||||
const start = Number(daysToShow) * 86400000
|
||||
const start = Number(daysToShow) * DAILY_MILLISECONDS
|
||||
const filtered = chartData.filter((d: HourlyFundingChartData) => {
|
||||
const date = new Date()
|
||||
if (daysToShow === '30') {
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
import HealthContributionsChart from './HealthContributionsChart'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { HealthType } from '@blockworks-foundation/mango-v4'
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
NoSymbolIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import TokensHealthTable from './TokensHealthTable'
|
||||
import MarketsHealthTable from './MarketsHealthTable'
|
||||
import { HealthContribution, PerpMarketContribution } from 'types'
|
||||
|
||||
const HealthContributions = ({ hideView }: { hideView: () => void }) => {
|
||||
const { t } = useTranslation(['common', 'account', 'trade'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const [initActiveIndex, setInitActiveIndex] = useState<number | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [maintActiveIndex, setMaintActiveIndex] = useState<number | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [initHealthContributions, maintHealthContributions] = useMemo(() => {
|
||||
if (!group || !mangoAccount) return [[], []]
|
||||
const initAssets = mangoAccount.getHealthContributionPerAssetUi(
|
||||
group,
|
||||
HealthType.init
|
||||
)
|
||||
const initContributions = []
|
||||
for (const item of initAssets) {
|
||||
const contribution = item.contribution
|
||||
if (item.asset === 'USDC') {
|
||||
const hasPerp =
|
||||
!!item.contributionDetails?.perpMarketContributions.find(
|
||||
(perp: PerpMarketContribution) => Math.abs(perp.contributionUi) > 0
|
||||
)
|
||||
initContributions.push({
|
||||
...item,
|
||||
contribution: Math.abs(contribution),
|
||||
hasPerp: hasPerp,
|
||||
isAsset: contribution > 0 ? true : false,
|
||||
})
|
||||
if (item.contributionDetails) {
|
||||
for (const perpMarket of item.contributionDetails
|
||||
.perpMarketContributions) {
|
||||
const contribution = Math.abs(perpMarket.contributionUi)
|
||||
if (contribution > 0) {
|
||||
initContributions.push({
|
||||
asset: perpMarket.market,
|
||||
contribution: contribution,
|
||||
isAsset: perpMarket.contributionUi > 0 ? true : false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
initContributions.push({
|
||||
...item,
|
||||
isAsset: contribution > 0 ? true : false,
|
||||
contribution: Math.abs(contribution),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const maintAssets = mangoAccount.getHealthContributionPerAssetUi(
|
||||
group,
|
||||
HealthType.maint
|
||||
)
|
||||
const maintContributions = []
|
||||
for (const item of maintAssets) {
|
||||
const contribution = item.contribution
|
||||
if (item.asset === 'USDC') {
|
||||
const hasPerp =
|
||||
!!item.contributionDetails?.perpMarketContributions.find(
|
||||
(perp: PerpMarketContribution) => Math.abs(perp.contributionUi) > 0
|
||||
)
|
||||
maintContributions.push({
|
||||
...item,
|
||||
hasPerp: hasPerp,
|
||||
isAsset: contribution > 0 ? true : false,
|
||||
contribution: Math.abs(contribution),
|
||||
})
|
||||
if (item.contributionDetails) {
|
||||
for (const perpMarket of item.contributionDetails
|
||||
.perpMarketContributions) {
|
||||
const contribution = Math.abs(perpMarket.contributionUi)
|
||||
if (contribution > 0) {
|
||||
maintContributions.push({
|
||||
asset: perpMarket.market,
|
||||
contribution: contribution,
|
||||
isAsset: perpMarket.contributionUi > 0 ? true : false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
maintContributions.push({
|
||||
...item,
|
||||
isAsset: contribution > 0 ? true : false,
|
||||
contribution: Math.abs(contribution),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return [initContributions, maintContributions]
|
||||
}, [group, mangoAccount])
|
||||
|
||||
const [initHealthMarkets, initHealthTokens] = useMemo(() => {
|
||||
if (!initHealthContributions.length) return [[], []]
|
||||
const splitData = initHealthContributions.reduce(
|
||||
(
|
||||
acc: { market: HealthContribution[]; token: HealthContribution[] },
|
||||
obj: HealthContribution
|
||||
) => {
|
||||
const isPerp = obj.asset.includes('PERP')
|
||||
const isSpotMarket = obj.asset.includes('/')
|
||||
if (isSpotMarket) {
|
||||
acc.market.push(obj)
|
||||
}
|
||||
if (!isPerp && !isSpotMarket) {
|
||||
acc.token.push(obj)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ market: [], token: [] }
|
||||
)
|
||||
return [splitData.market, splitData.token]
|
||||
}, [initHealthContributions])
|
||||
|
||||
const [maintHealthMarkets, maintHealthTokens] = useMemo(() => {
|
||||
if (!maintHealthContributions.length) return [[], []]
|
||||
const splitData = maintHealthContributions.reduce(
|
||||
(
|
||||
acc: { market: HealthContribution[]; token: HealthContribution[] },
|
||||
obj: HealthContribution
|
||||
) => {
|
||||
const isPerp = obj.asset.includes('PERP')
|
||||
const isSpotMarket = obj.asset.includes('/')
|
||||
if (isSpotMarket) {
|
||||
acc.market.push(obj)
|
||||
}
|
||||
if (!isPerp && !isSpotMarket) {
|
||||
acc.token.push(obj)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ market: [], token: [] }
|
||||
)
|
||||
const markets = splitData.market.filter((d) => d.contribution > 0)
|
||||
const tokens = splitData.token
|
||||
return [markets, tokens]
|
||||
}, [maintHealthContributions])
|
||||
|
||||
const handleLegendClick = (item: HealthContribution) => {
|
||||
const maintIndex = maintChartData.findIndex((d) => d.asset === item.asset)
|
||||
const initIndex = initChartData.findIndex((d) => d.asset === item.asset)
|
||||
setMaintActiveIndex(maintIndex)
|
||||
setInitActiveIndex(initIndex)
|
||||
}
|
||||
|
||||
const handleLegendMouseEnter = (item: HealthContribution) => {
|
||||
const maintIndex = maintChartData.findIndex((d) => d.asset === item.asset)
|
||||
const initIndex = initChartData.findIndex((d) => d.asset === item.asset)
|
||||
setMaintActiveIndex(maintIndex)
|
||||
setInitActiveIndex(initIndex)
|
||||
}
|
||||
|
||||
const handleLegendMouseLeave = () => {
|
||||
setInitActiveIndex(undefined)
|
||||
setMaintActiveIndex(undefined)
|
||||
}
|
||||
|
||||
const renderLegendLogo = (asset: string) => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!group)
|
||||
return <QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
|
||||
const isSpotMarket = asset.includes('/')
|
||||
const isPerpMarket = asset.includes('PERP')
|
||||
const isMarket = isSpotMarket || isPerpMarket
|
||||
if (isMarket) {
|
||||
let market
|
||||
if (isSpotMarket) {
|
||||
market = group.getSerum3MarketByName(asset)
|
||||
} else {
|
||||
market = group.getPerpMarketByName(asset)
|
||||
}
|
||||
return market ? (
|
||||
<MarketLogos market={market} size="small" />
|
||||
) : (
|
||||
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
|
||||
)
|
||||
} else {
|
||||
const bank = group.banksMapByName.get(asset)?.[0]
|
||||
return bank ? (
|
||||
<div className="mr-1.5">
|
||||
<TokenLogo bank={bank} size={16} />
|
||||
</div>
|
||||
) : (
|
||||
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const initChartData = useMemo(() => {
|
||||
if (!initHealthContributions.length) return []
|
||||
return initHealthContributions
|
||||
.filter((cont) => {
|
||||
if (cont.asset.includes('PERP')) {
|
||||
return
|
||||
} else if (cont.asset.includes('/')) {
|
||||
return cont.contribution > 0.01
|
||||
} else return cont
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aMultiplier = a.isAsset ? 1 : -1
|
||||
const bMultiplier = b.isAsset ? 1 : -1
|
||||
return b.contribution * bMultiplier - a.contribution * aMultiplier
|
||||
})
|
||||
}, [initHealthContributions])
|
||||
|
||||
const maintChartData = useMemo(() => {
|
||||
if (!maintHealthContributions.length) return []
|
||||
return maintHealthContributions
|
||||
.filter((cont) => {
|
||||
if (cont.asset.includes('PERP')) {
|
||||
return
|
||||
} else if (cont.asset.includes('/')) {
|
||||
return cont.contribution > 0.01
|
||||
} else return cont
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aMultiplier = a.isAsset ? 1 : -1
|
||||
const bMultiplier = b.isAsset ? 1 : -1
|
||||
return b.contribution * bMultiplier - a.contribution * aMultiplier
|
||||
})
|
||||
}, [maintHealthContributions])
|
||||
|
||||
return group ? (
|
||||
<>
|
||||
<div className="hide-scroll flex h-14 items-center space-x-4 overflow-x-auto border-b border-th-bkg-3">
|
||||
<button
|
||||
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={hideView}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<h2 className="text-lg">{t('account:health-contributions')}</h2>
|
||||
</div>
|
||||
{mangoAccountAddress ? (
|
||||
<>
|
||||
<div className="mx-auto grid max-w-[1140px] grid-cols-2 gap-6 p-6 sm:gap-8">
|
||||
<div className="col-span-1 flex h-full flex-col items-center">
|
||||
<Tooltip content={t('account:tooltip-init-health')}>
|
||||
<h3 className="tooltip-underline text-xs sm:text-base">
|
||||
{t('account:init-health-contributions')}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
<HealthContributionsChart
|
||||
data={initChartData}
|
||||
activeIndex={initActiveIndex}
|
||||
setActiveIndex={setInitActiveIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center">
|
||||
<Tooltip content={t('account:tooltip-maint-health')}>
|
||||
<h3 className="tooltip-underline text-xs sm:text-base">
|
||||
{t('account:maint-health-contributions')}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
<HealthContributionsChart
|
||||
data={maintChartData}
|
||||
activeIndex={maintActiveIndex}
|
||||
setActiveIndex={setMaintActiveIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 mx-auto flex max-w-[600px] flex-wrap justify-center space-x-4">
|
||||
{[...maintChartData]
|
||||
.sort((a, b) => b.contribution - a.contribution)
|
||||
.map((d, i) => {
|
||||
return (
|
||||
<div
|
||||
key={d.asset + i}
|
||||
className={`default-transition flex h-7 cursor-pointer items-center md:hover:text-th-active`}
|
||||
onClick={() => handleLegendClick(d)}
|
||||
onMouseEnter={() => handleLegendMouseEnter(d)}
|
||||
onMouseLeave={handleLegendMouseLeave}
|
||||
>
|
||||
{renderLegendLogo(d.asset)}
|
||||
<span className={`default-transition`}>{d.asset}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{maintHealthTokens.length ? (
|
||||
<div className="border-t border-th-bkg-3 pt-6">
|
||||
<h2 className="mb-1 px-6 text-lg">{t('tokens')}</h2>
|
||||
<TokensHealthTable
|
||||
initTokens={initHealthTokens}
|
||||
maintTokens={maintHealthTokens}
|
||||
handleLegendClick={handleLegendClick}
|
||||
handleLegendMouseEnter={handleLegendMouseEnter}
|
||||
handleLegendMouseLeave={handleLegendMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{maintHealthMarkets.length ? (
|
||||
<div className="pt-6">
|
||||
<h2 className="mb-1 px-6 text-lg">{t('markets')}</h2>
|
||||
<MarketsHealthTable
|
||||
initMarkets={initHealthMarkets}
|
||||
maintMarkets={maintHealthMarkets}
|
||||
handleLegendClick={handleLegendClick}
|
||||
handleLegendMouseEnter={handleLegendMouseEnter}
|
||||
handleLegendMouseLeave={handleLegendMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-6 mt-6 flex flex-col items-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('account:no-data')}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default HealthContributions
|
|
@ -0,0 +1,148 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useTheme } from 'next-themes'
|
||||
import {
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
ResponsiveContainer,
|
||||
Sector,
|
||||
SectorProps,
|
||||
} from 'recharts'
|
||||
import { COLORS } from 'styles/colors'
|
||||
import { useMemo } from 'react'
|
||||
import { formatCurrencyValue } from 'utils/numbers'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { HealthContribution } from 'types'
|
||||
|
||||
const HealthContributionsChart = ({
|
||||
data,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
}: {
|
||||
data: HealthContribution[]
|
||||
activeIndex: number | undefined
|
||||
setActiveIndex: (i: number | undefined) => void
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { theme } = useTheme()
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
setActiveIndex(index)
|
||||
}
|
||||
|
||||
const handleMouseEnter = (data: HealthContribution, index: number) => {
|
||||
setActiveIndex(index)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setActiveIndex(undefined)
|
||||
}
|
||||
|
||||
const pieSizes = isMobile
|
||||
? { size: 160, outerRadius: 80, innerRadius: 64 }
|
||||
: { size: 240, outerRadius: 120, innerRadius: 96 }
|
||||
const { size, outerRadius, innerRadius } = pieSizes
|
||||
|
||||
const [chartHeroAsset, chartHeroValue] = useMemo(() => {
|
||||
if (!data.length) return [undefined, undefined]
|
||||
if (activeIndex === undefined) {
|
||||
const value = data.reduce((a, c) => {
|
||||
const assetOrLiabMultiplier = c.isAsset ? 1 : -1
|
||||
return a + c.contribution * assetOrLiabMultiplier
|
||||
}, 0)
|
||||
return [t('total'), value]
|
||||
} else {
|
||||
const asset = data[activeIndex]
|
||||
const assetOrLiabMultiplier = asset.isAsset ? 1 : -1
|
||||
const value = asset.contribution * assetOrLiabMultiplier
|
||||
return [asset.asset, value]
|
||||
}
|
||||
}, [activeIndex, data])
|
||||
|
||||
const renderActiveShape = ({
|
||||
cx,
|
||||
cy,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
}: SectorProps) => {
|
||||
return (
|
||||
<g>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius! + 4}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
return data.length ? (
|
||||
<div className="mt-4 flex h-full w-full flex-col items-center">
|
||||
<div className="relative h-[168px] w-[168px] sm:h-[248px] sm:w-[248px]">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<PieChart width={size} height={size}>
|
||||
<Pie
|
||||
cursor="pointer"
|
||||
data={data}
|
||||
dataKey="contribution"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={outerRadius}
|
||||
innerRadius={innerRadius}
|
||||
minAngle={2}
|
||||
startAngle={90}
|
||||
endAngle={450}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{data.map((entry, index) => {
|
||||
const fillColor = entry.isAsset
|
||||
? COLORS.UP[theme]
|
||||
: COLORS.DOWN[theme]
|
||||
|
||||
let opacity
|
||||
|
||||
if (entry.isAsset) {
|
||||
opacity = 1 - index * 0.1
|
||||
} else {
|
||||
opacity = 1 - Math.abs((index - (data.length - 1)) * 0.1)
|
||||
}
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={fillColor}
|
||||
opacity={opacity}
|
||||
stroke="none"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
{chartHeroValue !== undefined ? (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<p className="text-xs sm:text-sm">{chartHeroAsset}</p>
|
||||
<span className="text-base font-bold sm:text-xl">
|
||||
{formatCurrencyValue(chartHeroValue, 2)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default HealthContributionsChart
|
|
@ -0,0 +1,276 @@
|
|||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { MouseEventHandler } from 'react'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import { HealthContribution } from 'types'
|
||||
|
||||
const MarketsHealthTable = ({
|
||||
initMarkets,
|
||||
maintMarkets,
|
||||
handleLegendClick,
|
||||
handleLegendMouseEnter,
|
||||
handleLegendMouseLeave,
|
||||
}: {
|
||||
initMarkets: HealthContribution[]
|
||||
maintMarkets: HealthContribution[]
|
||||
handleLegendClick: (cont: HealthContribution) => void
|
||||
handleLegendMouseEnter: (cont: HealthContribution) => void
|
||||
handleLegendMouseLeave: MouseEventHandler
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'account', 'trade'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
return group && mangoAccount ? (
|
||||
!isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th className="text-left">{t('market')}</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<Tooltip content={t('account:tooltip-init-health')}>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:init-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<Tooltip content={t('account:tooltip-maint-health')}>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:maint-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{maintMarkets
|
||||
.sort((a, b) => b.contribution - a.contribution)
|
||||
.map((cont) => {
|
||||
const { asset, contribution, isAsset } = cont
|
||||
const market = group.getSerum3MarketByName(asset)
|
||||
const bank = group.banksMapByTokenIndex.get(
|
||||
market.baseTokenIndex
|
||||
)?.[0]
|
||||
|
||||
let initAssetWeight = 0
|
||||
let initLiabWeight = 0
|
||||
let maintAssetWeight = 0
|
||||
let maintLiabWeight = 0
|
||||
|
||||
if (bank) {
|
||||
initAssetWeight = bank
|
||||
.scaledInitAssetWeight(bank.price)
|
||||
.toNumber()
|
||||
initLiabWeight = bank
|
||||
.scaledInitLiabWeight(bank.price)
|
||||
.toNumber()
|
||||
maintAssetWeight = bank.maintAssetWeight.toNumber()
|
||||
maintLiabWeight = bank.maintLiabWeight.toNumber()
|
||||
}
|
||||
|
||||
const assetOrLiabMultiplier = isAsset ? 1 : -1
|
||||
|
||||
const initContribution =
|
||||
(initMarkets.find((cont) => cont.asset === asset)
|
||||
?.contribution || 0) * assetOrLiabMultiplier
|
||||
|
||||
const maintContribution = contribution * assetOrLiabMultiplier
|
||||
|
||||
return (
|
||||
<TrBody
|
||||
key={asset}
|
||||
className="cursor-pointer md:hover:bg-th-bkg-2"
|
||||
onClick={() => handleLegendClick(cont)}
|
||||
onMouseEnter={() => handleLegendMouseEnter(cont)}
|
||||
onMouseLeave={handleLegendMouseLeave}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<MarketLogos market={market} />
|
||||
<p className="font-body">{asset}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="text-right">
|
||||
<p>
|
||||
<FormatNumericValue
|
||||
value={initContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
<p className="text-th-fgd-3">
|
||||
{initContribution > 0
|
||||
? initAssetWeight.toFixed(2)
|
||||
: initContribution < 0
|
||||
? initLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="text-right">
|
||||
<p>
|
||||
<FormatNumericValue
|
||||
value={maintContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
<p className="text-th-fgd-3">
|
||||
{maintContribution > 0
|
||||
? maintAssetWeight.toFixed(2)
|
||||
: maintContribution < 0
|
||||
? maintLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="mt-3 border-y border-th-bkg-3">
|
||||
{maintMarkets
|
||||
.sort((a, b) => b.contribution - a.contribution)
|
||||
.map((cont) => {
|
||||
const { asset, contribution, isAsset } = cont
|
||||
const market = group.getSerum3MarketByName(asset)
|
||||
const bank = group.banksMapByTokenIndex.get(
|
||||
market.baseTokenIndex
|
||||
)?.[0]
|
||||
|
||||
let initAssetWeight = 0
|
||||
let initLiabWeight = 0
|
||||
let maintAssetWeight = 0
|
||||
let maintLiabWeight = 0
|
||||
|
||||
if (bank) {
|
||||
initAssetWeight = bank
|
||||
.scaledInitAssetWeight(bank.price)
|
||||
.toNumber()
|
||||
initLiabWeight = bank.scaledInitLiabWeight(bank.price).toNumber()
|
||||
maintAssetWeight = bank.maintAssetWeight.toNumber()
|
||||
maintLiabWeight = bank.maintLiabWeight.toNumber()
|
||||
}
|
||||
|
||||
const assetOrLiabMultiplier = isAsset ? 1 : -1
|
||||
|
||||
const initContribution =
|
||||
(initMarkets.find((cont) => cont.asset === asset)?.contribution ||
|
||||
0) * assetOrLiabMultiplier
|
||||
|
||||
const maintContribution = contribution * assetOrLiabMultiplier
|
||||
|
||||
return (
|
||||
<Disclosure key={asset}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={`w-full border-t border-th-bkg-3 p-4 text-left first:border-t-0 focus:outline-none`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<MarketLogos market={market} />
|
||||
<div>
|
||||
<p className="text-th-fgd-1">{asset}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
|
||||
/>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
enter="transition ease-in duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4 pb-4">
|
||||
<div className="col-span-1">
|
||||
<p className="text-xs text-th-fgd-3">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-init-health')}
|
||||
>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:init-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</p>
|
||||
<p className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={initContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
<p className="font-mono text-th-fgd-3">
|
||||
{initContribution > 0
|
||||
? initAssetWeight.toFixed(2)
|
||||
: initContribution < 0
|
||||
? initLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<p className="text-xs text-th-fgd-3">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-maint-health')}
|
||||
>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:maint-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</p>
|
||||
<p className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={maintContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
<p className="font-mono text-th-fgd-3">
|
||||
{maintContribution > 0
|
||||
? maintAssetWeight.toFixed(2)
|
||||
: maintContribution < 0
|
||||
? maintLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
export default MarketsHealthTable
|
|
@ -0,0 +1,406 @@
|
|||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { MouseEventHandler } from 'react'
|
||||
import { ContributionDetails, HealthContribution } from 'types'
|
||||
|
||||
const TokensHealthTable = ({
|
||||
initTokens,
|
||||
maintTokens,
|
||||
handleLegendClick,
|
||||
handleLegendMouseEnter,
|
||||
handleLegendMouseLeave,
|
||||
}: {
|
||||
initTokens: HealthContribution[]
|
||||
maintTokens: HealthContribution[]
|
||||
handleLegendClick: (cont: HealthContribution) => void
|
||||
handleLegendMouseEnter: (cont: HealthContribution) => void
|
||||
handleLegendMouseLeave: MouseEventHandler
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'account', 'trade'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
return group && mangoAccount ? (
|
||||
!isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th className="text-left">{t('token')}</Th>
|
||||
<Th className="text-right">{t('trade:notional')}</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<Tooltip content={t('account:tooltip-init-health')}>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:init-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<Tooltip content={t('account:tooltip-maint-health')}>
|
||||
<span className="tooltip-underline">
|
||||
{t('account:maint-health-contribution')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{maintTokens
|
||||
.sort((a, b) => b.contribution - a.contribution)
|
||||
.map((cont) => {
|
||||
const {
|
||||
asset,
|
||||
contribution,
|
||||
contributionDetails,
|
||||
isAsset,
|
||||
hasPerp,
|
||||
} = cont
|
||||
const bank = group.banksMapByName.get(asset)?.[0]
|
||||
|
||||
let initAssetWeight = 0
|
||||
let initLiabWeight = 0
|
||||
let maintAssetWeight = 0
|
||||
let maintLiabWeight = 0
|
||||
|
||||
let balance = 0
|
||||
|
||||
if (bank) {
|
||||
initAssetWeight = bank
|
||||
.scaledInitAssetWeight(bank.price)
|
||||
.toNumber()
|
||||
initLiabWeight = bank
|
||||
.scaledInitLiabWeight(bank.price)
|
||||
.toNumber()
|
||||
maintAssetWeight = bank.maintAssetWeight.toNumber()
|
||||
maintLiabWeight = bank.maintLiabWeight.toNumber()
|
||||
|
||||
balance = mangoAccount.getTokenBalanceUi(bank)
|
||||
}
|
||||
|
||||
const assetOrLiabMultiplier = isAsset ? 1 : -1
|
||||
const initToken = initTokens.find((cont) => cont.asset === asset)
|
||||
const initContribution =
|
||||
(initToken?.contribution || 0) * assetOrLiabMultiplier
|
||||
|
||||
const maintContribution = contribution * assetOrLiabMultiplier
|
||||
|
||||
return (
|
||||
<TrBody
|
||||
key={asset}
|
||||
className="cursor-pointer md:hover:bg-th-bkg-2"
|
||||
onClick={() => handleLegendClick(cont)}
|
||||
onMouseEnter={() => handleLegendMouseEnter(cont)}
|
||||
onMouseLeave={handleLegendMouseLeave}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2.5 flex flex-shrink-0 items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
</div>
|
||||
<p className="font-body">{asset}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
{bank ? (
|
||||
<p>
|
||||
<FormatNumericValue
|
||||
value={balance * bank.uiPrice}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>{' '}
|
||||
<span className={`block text-th-fgd-4`}>
|
||||
<FormatNumericValue
|
||||
value={balance}
|
||||
decimals={bank.mintDecimals}
|
||||
/>
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
'–'
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col items-end text-right">
|
||||
<Tooltip
|
||||
className={!hasPerp ? 'hidden' : ''}
|
||||
content={
|
||||
<UsdcTooltipContent
|
||||
contributions={initToken?.contributionDetails}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p className={hasPerp ? 'tooltip-underline' : ''}>
|
||||
<FormatNumericValue
|
||||
value={initContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
</Tooltip>
|
||||
<p className="text-th-fgd-3">
|
||||
{initContribution > 0
|
||||
? initAssetWeight.toFixed(2)
|
||||
: initContribution < 0
|
||||
? initLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col items-end text-right">
|
||||
<Tooltip
|
||||
className={!hasPerp ? 'hidden' : ''}
|
||||
content={
|
||||
<UsdcTooltipContent
|
||||
contributions={contributionDetails}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p className={hasPerp ? 'tooltip-underline' : ''}>
|
||||
<FormatNumericValue
|
||||
value={maintContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
</Tooltip>
|
||||
<p className="text-th-fgd-3">
|
||||
{maintContribution > 0
|
||||
? maintAssetWeight.toFixed(2)
|
||||
: maintContribution < 0
|
||||
? maintLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="mt-3 border-y border-th-bkg-3">
|
||||
{maintTokens
|
||||
.sort((a, b) => b.contribution - a.contribution)
|
||||
.map((cont) => {
|
||||
const {
|
||||
asset,
|
||||
contribution,
|
||||
contributionDetails,
|
||||
isAsset,
|
||||
hasPerp,
|
||||
} = cont
|
||||
const bank = group.banksMapByName.get(asset)?.[0]
|
||||
|
||||
let initAssetWeight = 0
|
||||
let initLiabWeight = 0
|
||||
let maintAssetWeight = 0
|
||||
let maintLiabWeight = 0
|
||||
|
||||
let balance = 0
|
||||
|
||||
if (bank) {
|
||||
initAssetWeight = bank
|
||||
.scaledInitAssetWeight(bank.price)
|
||||
.toNumber()
|
||||
initLiabWeight = bank.scaledInitLiabWeight(bank.price).toNumber()
|
||||
maintAssetWeight = bank.maintAssetWeight.toNumber()
|
||||
maintLiabWeight = bank.maintLiabWeight.toNumber()
|
||||
|
||||
balance = mangoAccount.getTokenBalanceUi(bank)
|
||||
}
|
||||
|
||||
const assetOrLiabMultiplier = isAsset ? 1 : -1
|
||||
|
||||
const initToken = initTokens.find((cont) => cont.asset === asset)
|
||||
const initContribution =
|
||||
(initToken?.contribution || 0) * assetOrLiabMultiplier
|
||||
|
||||
const maintContribution = contribution * assetOrLiabMultiplier
|
||||
|
||||
return (
|
||||
<Disclosure key={asset}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={`w-full border-t border-th-bkg-3 p-4 text-left first:border-t-0 focus:outline-none`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2.5">
|
||||
<TokenLogo bank={bank} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-th-fgd-1">{asset}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
|
||||
/>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
enter="transition ease-in duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4 pb-4">
|
||||
<div className="col-span-1">
|
||||
<p className="text-xs text-th-fgd-3">
|
||||
{t('trade:notional')}
|
||||
</p>
|
||||
<p>
|
||||
{bank ? (
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={balance * bank.uiPrice}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>{' '}
|
||||
<span className={`block text-th-fgd-4`}>
|
||||
<FormatNumericValue
|
||||
value={balance}
|
||||
decimals={bank.mintDecimals}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
'–'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<p className="text-xs text-th-fgd-3">
|
||||
{t('account:init-health-contribution')}
|
||||
</p>
|
||||
<Tooltip
|
||||
className={!hasPerp ? 'hidden' : ''}
|
||||
content={
|
||||
<UsdcTooltipContent
|
||||
contributions={initToken?.contributionDetails}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p
|
||||
className={`font-mono text-th-fgd-2 ${
|
||||
hasPerp ? 'tooltip-underline' : ''
|
||||
}`}
|
||||
>
|
||||
<FormatNumericValue
|
||||
value={initContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
</Tooltip>
|
||||
<p className="font-mono text-th-fgd-3">
|
||||
{initContribution > 0
|
||||
? initAssetWeight.toFixed(2)
|
||||
: initContribution < 0
|
||||
? initLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<p className="text-xs text-th-fgd-3">
|
||||
{t('account:maint-health-contribution')}
|
||||
</p>
|
||||
<Tooltip
|
||||
className={!hasPerp ? 'hidden' : ''}
|
||||
content={
|
||||
<UsdcTooltipContent
|
||||
contributions={contributionDetails}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p
|
||||
className={`font-mono text-th-fgd-2 ${
|
||||
hasPerp ? 'tooltip-underline' : ''
|
||||
}`}
|
||||
>
|
||||
<FormatNumericValue
|
||||
value={maintContribution}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</p>
|
||||
</Tooltip>
|
||||
<p className="font-mono text-th-fgd-3">
|
||||
{maintContribution > 0
|
||||
? maintAssetWeight.toFixed(2)
|
||||
: maintContribution < 0
|
||||
? maintLiabWeight.toFixed(2)
|
||||
: 0}
|
||||
x
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
export default TokensHealthTable
|
||||
|
||||
const UsdcTooltipContent = ({
|
||||
contributions,
|
||||
}: {
|
||||
contributions: ContributionDetails | undefined
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
if (!contributions) return null
|
||||
const { perpMarketContributions, spotUi } = contributions
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<p className="mr-3">{t('spot')}</p>
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue value={spotUi} decimals={2} isUsd />
|
||||
</span>
|
||||
</div>
|
||||
{perpMarketContributions
|
||||
.filter((cont) => Math.abs(cont.contributionUi) > 0.01)
|
||||
.map((perp) => (
|
||||
<div className="flex justify-between" key={perp.market}>
|
||||
<p className="mr-3">{perp.market}</p>
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={perp.contributionUi}
|
||||
decimals={2}
|
||||
isUsd
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -24,17 +24,15 @@ import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
|||
import { FadeInFadeOut } from '@components/shared/Transitions'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { DAILY_MILLISECONDS } from 'utils/constants'
|
||||
|
||||
const VolumeChart = ({
|
||||
chartData,
|
||||
hideChart,
|
||||
loading,
|
||||
}: {
|
||||
chartData: FormattedHourlyAccountVolumeData[] | undefined
|
||||
hideChart: () => void
|
||||
loading: boolean
|
||||
}) => {
|
||||
const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
|
||||
const { t } = useTranslation(['account', 'common', 'stats'])
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const { hourlyVolumeData: chartData, loadingHourlyVolume: loading } =
|
||||
useAccountHourlyVolumeStats()
|
||||
const [daysToShow, setDaysToShow] = useState('30')
|
||||
const { theme } = useTheme()
|
||||
|
||||
|
@ -123,7 +121,7 @@ const VolumeChart = ({
|
|||
|
||||
const filteredData: FormattedHourlyAccountVolumeData[] = useMemo(() => {
|
||||
if (!chartData || !chartData.length) return []
|
||||
const start = Number(daysToShow) * 86400000
|
||||
const start = Number(daysToShow) * DAILY_MILLISECONDS
|
||||
const filtered = chartData.filter((d: FormattedHourlyAccountVolumeData) => {
|
||||
const date = new Date()
|
||||
if (daysToShow === '30') {
|
||||
|
@ -159,8 +157,8 @@ const VolumeChart = ({
|
|||
onChange={(v) => setDaysToShow(v)}
|
||||
/>
|
||||
</div>
|
||||
{loading ? (
|
||||
<SheenLoader className="flex flex-1">
|
||||
{loading && mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-6 flex flex-1">
|
||||
<div
|
||||
className={`h-[calc(100vh-166px)] w-full rounded-lg bg-th-bkg-2`}
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,7 @@ interface SelectProps {
|
|||
children: ReactNode
|
||||
className?: string
|
||||
dropdownPanelClassName?: string
|
||||
icon?: ReactNode
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
@ -18,6 +19,7 @@ const Select = ({
|
|||
children,
|
||||
className,
|
||||
dropdownPanelClassName,
|
||||
icon,
|
||||
placeholder = 'Select',
|
||||
disabled = false,
|
||||
}: SelectProps) => {
|
||||
|
@ -32,13 +34,16 @@ const Select = ({
|
|||
<div
|
||||
className={`flex items-center justify-between space-x-2 px-3 text-th-fgd-1`}
|
||||
>
|
||||
{value ? (
|
||||
value
|
||||
) : (
|
||||
<span className="text-th-fgd-3">{placeholder}</span>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
{icon ? icon : null}
|
||||
{value ? (
|
||||
value
|
||||
) : (
|
||||
<span className="text-th-fgd-3">{placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`h-5 w-5 flex-shrink-0 text-th-fgd-3 ${
|
||||
className={`ml-1 h-5 w-5 flex-shrink-0 text-th-fgd-3 ${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
}`}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { Governance, Proposal } from '@solana/spl-governance'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { DAILY_SECONDS } from 'utils/constants'
|
||||
|
||||
interface CountdownState {
|
||||
days: number
|
||||
|
@ -53,8 +54,8 @@ export function VoteCountdown({
|
|||
return ZeroCountdown
|
||||
}
|
||||
|
||||
const days = Math.floor(timeToVoteEnd / 86400)
|
||||
timeToVoteEnd -= days * 86400
|
||||
const days = Math.floor(timeToVoteEnd / DAILY_SECONDS)
|
||||
timeToVoteEnd -= days * DAILY_SECONDS
|
||||
|
||||
const hours = Math.floor(timeToVoteEnd / 3600) % 24
|
||||
timeToVoteEnd -= hours * 3600
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
const AcornIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 40"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M31.9764 20.7091L31.47 16.9109C31.2226 15.0512 30.4484 13.36 29.3079 11.9846L31.6864 9.60615C32.034 9.2594 32.034 8.69894 31.6864 8.35219C31.3397 8.00544 30.7801 8.00544 30.4324 8.35219L28.0584 10.7262C26.674 9.56713 24.9686 8.7823 23.0903 8.53044L19.292 8.02407C18.6145 7.93273 17.9192 8.10122 17.3445 8.53221L16.5934 9.09623C16.1624 9.41814 16.1154 10.0496 16.4985 10.4327L29.5695 23.5035C29.9722 23.9061 30.5983 23.8183 30.906 23.4095L31.4691 22.6574C31.8886 22.0961 32.0686 21.4035 31.9764 20.7091ZM15.2427 11.6857L15.2108 11.6529L12.161 13.7875C9.21582 15.8493 7.64701 19.3478 8.06737 22.919L8.77062 28.8989C8.91518 30.1271 9.87385 31.0857 11.1021 31.2303L17.082 31.9335C20.6515 32.3503 24.1519 30.7851 26.2138 27.84L28.3484 24.7902L28.3156 24.7583L15.2427 11.6857Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default AcornIcon
|
|
@ -0,0 +1,14 @@
|
|||
const MangoIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 40"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M30.9524 16.6647C29.6789 12.8631 26.0028 10.0966 22.9101 9.51321C23.0607 7.74416 23.7758 6.92237 24.3655 6.55225C24.7544 6.30132 24.8799 5.79319 24.6352 5.39798C24.3906 5.00904 23.8699 4.8773 23.4872 5.12823C22.4835 5.79319 22.0883 6.9851 21.9566 8.13938C20.2628 8.08919 16.2668 8.30875 13.7575 11.3764C10.7087 15.1027 11.5367 16.2318 9.11528 19.2869C8.83298 19.6445 9.10273 20.1777 9.56068 20.1463C11.4426 20.0271 15.1564 19.4814 17.9982 17.1101C19.9805 15.4665 21.1724 13.2458 21.8625 11.4328L22.7595 12.562C21.9817 14.3812 20.7396 16.4639 18.8011 18.0762C17.1387 19.4625 15.2317 20.2781 13.494 20.7548C13.8076 21.7836 14.228 23.1073 14.3095 23.4272C14.9494 25.9491 15.5391 28.5775 14.9682 31.0555C14.4914 33.1068 16.6431 34.5747 18.5314 34.8884C21.0407 35.3087 23.6754 34.512 25.7268 33.0002C27.7844 31.4695 29.2711 29.3366 30.2686 26.9653C31.6361 23.7283 32.0627 20.002 30.9524 16.6647ZM27.9601 25.9867C26.649 29.1045 24.3844 31.4256 21.9064 32.2035C21.8437 32.2223 21.781 32.2348 21.7182 32.2348C21.4485 32.2348 21.2038 32.0592 21.1223 31.7957C21.0156 31.4632 21.1976 31.1119 21.53 31.0053C23.6629 30.3403 25.6327 28.2827 26.7996 25.5037C26.9376 25.1837 27.3014 25.0332 27.6213 25.1649C27.9412 25.3029 28.0918 25.6667 27.9601 25.9867Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default MangoIcon
|
|
@ -0,0 +1,21 @@
|
|||
const RobotIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 40"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M15.102 17.2186C14.3807 17.2186 13.7959 17.8104 13.7959 18.5404C13.7959 19.2705 14.3807 19.8623 15.102 19.8623C15.8233 19.8623 16.4082 19.2705 16.4082 18.5404C16.4082 17.8104 15.8233 17.2186 15.102 17.2186Z" />
|
||||
<path d="M23.5918 18.5404C23.5918 17.8104 24.1767 17.2186 24.898 17.2186C25.6193 17.2186 26.2041 17.8104 26.2041 18.5404C26.2041 19.2705 25.6193 19.8623 24.898 19.8623C24.1767 19.8623 23.5918 19.2705 23.5918 18.5404Z" />
|
||||
<path d="M24.2449 25.8107H15.7551V27.1325H24.2449V25.8107Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.4082 7.30465C16.4082 5.47955 17.8701 4 19.6735 4C21.4769 4 22.9388 5.47955 22.9388 7.30465C22.9388 8.78436 21.9778 10.0369 20.6531 10.458V11.6007H30.449C31.5309 11.6007 32.4082 12.4884 32.4082 13.5835V16.5577H34.3673C35.269 16.5577 36 17.2974 36 18.21V24.8193C36 25.7318 35.269 26.4716 34.3673 26.4716H32.4082V29.4458C32.4082 30.5409 31.5309 31.4286 30.449 31.4286H9.55102C8.46907 31.4286 7.59184 30.5409 7.59184 29.4458V26.4716H5.63265C4.73103 26.4716 4 25.7318 4 24.8193V18.21C4 17.2974 4.73103 16.5577 5.63265 16.5577H7.59184V13.5835C7.59184 12.4884 8.46907 11.6007 9.55102 11.6007H18.6939V10.458C17.3691 10.0369 16.4082 8.78436 16.4082 7.30465ZM19.6735 5.98279C18.9522 5.98279 18.3673 6.57462 18.3673 7.30465C18.3673 8.03468 18.9522 8.62651 19.6735 8.62651C20.3948 8.62651 20.9796 8.03468 20.9796 7.30465C20.9796 6.57462 20.3948 5.98279 19.6735 5.98279ZM7.59184 18.5404H5.95918V24.4888H7.59184V18.5404ZM32.4082 24.4888H34.0408V18.5404H32.4082V24.4888ZM11.8367 18.5404C11.8367 16.7153 13.2986 15.2358 15.102 15.2358C16.9055 15.2358 18.3673 16.7153 18.3673 18.5404C18.3673 20.3655 16.9055 21.8451 15.102 21.8451C13.2986 21.8451 11.8367 20.3655 11.8367 18.5404ZM24.898 15.2358C23.0945 15.2358 21.6327 16.7153 21.6327 18.5404C21.6327 20.3655 23.0945 21.8451 24.898 21.8451C26.7014 21.8451 28.1633 20.3655 28.1633 18.5404C28.1633 16.7153 26.7014 15.2358 24.898 15.2358ZM13.7959 25.4802C13.7959 24.5676 14.5269 23.8279 15.4286 23.8279H24.5714C25.4731 23.8279 26.2041 24.5676 26.2041 25.4802V27.463C26.2041 28.3756 25.4731 29.1153 24.5714 29.1153H15.4286C14.5269 29.1153 13.7959 28.3756 13.7959 27.463V25.4802Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default RobotIcon
|
|
@ -0,0 +1,20 @@
|
|||
const WhaleIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 40"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M37.4952 15.4842C34.6812 14.5986 31.5602 14.4129 27.1108 16.8341C24.0041 18.5267 20.1331 19.6908 16.605 18.8552C13.084 18.0196 10.3772 14.97 9.30594 11.1776C11.27 10.1778 12.1127 5.98546 10.3987 4C7.98469 4.78561 6.21349 6.20686 6.00637 8.18518C3.56383 6.82821 1.06415 7.57811 0 8.6994C2.24257 13.4059 6.3349 12.4132 6.3349 12.4132C6.3349 12.4132 5.98494 16.5127 6.73485 21.7834C6.7577 21.9605 6.79884 22.1376 6.83997 22.3148C6.85026 22.359 6.86054 22.4033 6.87054 22.4476C6.9991 22.4119 7.1348 22.3905 7.27049 22.3905C7.65616 22.3905 8.03468 22.5476 8.31322 22.8404C8.82029 23.3832 11.6199 26.1614 16.6621 27.6541C17.5335 27.9112 18.3762 28.1112 19.2047 28.2754C18.4405 31.6393 19.4046 35.8673 19.4046 35.8673C22.0043 35.3674 25.0325 32.9248 25.8467 29.3539C25.8661 29.2513 25.8844 29.1508 25.9025 29.0517C25.931 28.8955 25.9589 28.7426 25.9895 28.5897C28.0535 28.2754 29.8676 27.547 31.4888 26.3685C31.8745 26.09 32.2244 25.8258 32.5529 25.5758C34.4527 24.1403 35.9597 23.0047 38.7164 23.0047C38.8521 23.0047 38.995 23.0047 39.1378 23.0118C39.2664 23.0118 39.3878 23.0404 39.5092 23.0761C40.4019 19.7623 40.2662 16.277 37.4952 15.4771V15.4842ZM31.878 20.2015C31.0924 20.2015 30.4497 19.5587 30.4497 18.7731C30.4497 17.9875 31.0924 17.3447 31.878 17.3447C32.6637 17.3447 33.3064 17.9875 33.3064 18.7731C33.3064 19.5587 32.6637 20.2015 31.878 20.2015Z"
|
||||
/>
|
||||
<path d="M16.2621 29.0254C10.7628 27.397 7.72034 24.2903 7.27754 23.8189C8.7345 27.5827 12.6483 31.2679 17.5476 32.7892C17.4762 31.7464 17.4619 30.5609 17.5905 29.3825L17.4391 29.3428C17.0525 29.2416 16.6608 29.1391 16.2692 29.0254H16.2621Z" />
|
||||
<path d="M27.2036 29.8109C29.0319 29.411 30.7531 28.6754 32.3315 27.5255C32.7224 27.2405 33.0802 26.9702 33.4156 26.7169C35.3661 25.2436 36.5588 24.3427 39.0878 24.4402C38.5236 26.0257 37.7594 27.4755 36.9952 28.4397C34.167 31.9964 30.4889 33.439 25.3538 33.5962C26.1894 32.5249 26.8465 31.2536 27.2036 29.8109Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhaleIcon
|
|
@ -18,6 +18,8 @@ import {
|
|||
BanknotesIcon,
|
||||
PlusCircleIcon,
|
||||
ArchiveBoxArrowDownIcon,
|
||||
// ClipboardDocumentIcon,
|
||||
NewspaperIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import SolanaTps from '@components/SolanaTps'
|
||||
import LeaderboardIcon from '@components/icons/LeaderboardIcon'
|
||||
|
@ -108,7 +110,7 @@ const MoreMenuPanel = ({
|
|||
const { t } = useTranslation(['common', 'search'])
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-0 z-30 h-[calc(100%-32px)] w-full overflow-hidden rounded-t-3xl bg-th-bkg-2 px-4 transition duration-300 ease-in-out ${
|
||||
className={`fixed bottom-0 z-30 h-full w-full overflow-hidden rounded-t-3xl bg-th-bkg-2 px-4 transition duration-300 ease-in-out ${
|
||||
showPanel ? 'translate-y-0' : 'translate-y-full'
|
||||
}`}
|
||||
>
|
||||
|
@ -164,6 +166,18 @@ const MoreMenuPanel = ({
|
|||
icon={<BuildingLibraryIcon className="h-5 w-5" />}
|
||||
isExternal
|
||||
/>
|
||||
{/* <MoreMenuItem
|
||||
title={t('feedback-survey')}
|
||||
path="https://forms.gle/JgV4w7SJ2kPH89mq7"
|
||||
icon={<ClipboardDocumentIcon className="h-5 w-5" />}
|
||||
isExternal
|
||||
/> */}
|
||||
<MoreMenuItem
|
||||
title={t('terms-of-use')}
|
||||
path="https://docs.mango.markets/legal"
|
||||
icon={<NewspaperIcon className="h-5 w-5" />}
|
||||
isExternal
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ import SheenLoader from '@components/shared/SheenLoader'
|
|||
import { NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import { DAILY_MILLISECONDS } from 'utils/constants'
|
||||
|
||||
interface PnlChange {
|
||||
time: string
|
||||
|
@ -33,7 +34,7 @@ const PnlHistoryModal = ({
|
|||
if (!performanceData || !performanceData.length) return []
|
||||
|
||||
const dailyPnl = performanceData.filter((d: PerformanceDataItem) => {
|
||||
const startTime = new Date().getTime() - 30 * 86400000
|
||||
const startTime = new Date().getTime() - 30 * DAILY_MILLISECONDS
|
||||
const dataDate = new Date(d.time)
|
||||
const dataTime = dataDate.getTime()
|
||||
return dataTime >= startTime && dataDate.getHours() === 0
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import MedalIcon from '@components/icons/MedalIcon'
|
||||
import ProfileImage from '@components/profile/ProfileImage'
|
||||
import { ArrowLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import {
|
||||
Badge,
|
||||
RewardsLeaderboardItem,
|
||||
fetchLeaderboard,
|
||||
tiers,
|
||||
} from './RewardsPage'
|
||||
import { useState } from 'react'
|
||||
import Select from '@components/forms/Select'
|
||||
import { IconButton } from '@components/shared/Button'
|
||||
import AcornIcon from '@components/icons/AcornIcon'
|
||||
import WhaleIcon from '@components/icons/WhaleIcon'
|
||||
import RobotIcon from '@components/icons/RobotIcon'
|
||||
import MangoIcon from '@components/icons/MangoIcon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { abbreviateAddress } from 'utils/formatting'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { formatNumericValue } from 'utils/numbers'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const Leaderboards = ({
|
||||
goBack,
|
||||
leaderboard,
|
||||
}: {
|
||||
goBack: () => void
|
||||
leaderboard: string
|
||||
}) => {
|
||||
const { t } = useTranslation('rewards')
|
||||
const [topAccountsTier, setTopAccountsTier] = useState<string>(leaderboard)
|
||||
const renderTierIcon = (tier: string) => {
|
||||
if (tier === 'bot') {
|
||||
return <RobotIcon className="mr-2 h-5 w-5" />
|
||||
} else if (tier === 'mango') {
|
||||
return <MangoIcon className="mr-2 h-5 w-5" />
|
||||
} else if (tier === 'whale') {
|
||||
return <WhaleIcon className="mr-2 h-5 w-5" />
|
||||
} else return <AcornIcon className="mr-2 h-5 w-5" />
|
||||
}
|
||||
|
||||
const {
|
||||
data: rewardsLeaderboardData,
|
||||
isFetching: fetchingRewardsLeaderboardData,
|
||||
isLoading: loadingRewardsLeaderboardData,
|
||||
} = useQuery(
|
||||
['rewards-leaderboard-data', topAccountsTier],
|
||||
() => fetchLeaderboard(topAccountsTier),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
const isLoading =
|
||||
fetchingRewardsLeaderboardData || loadingRewardsLeaderboardData
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[1140px] flex-col items-center p-8 lg:p-10">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<IconButton className="mr-2" hideBg onClick={goBack} size="small">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<h2 className="mr-4">Leaderboard</h2>
|
||||
<Badge
|
||||
label="Season 1"
|
||||
borderColor="var(--active)"
|
||||
shadowColor="var(--active)"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
className="w-32"
|
||||
icon={renderTierIcon(topAccountsTier)}
|
||||
value={t(topAccountsTier)}
|
||||
onChange={(tier) => setTopAccountsTier(tier)}
|
||||
>
|
||||
{tiers.map((tier) => (
|
||||
<Select.Option key={tier} value={tier}>
|
||||
<div className="flex w-full items-center">
|
||||
{renderTierIcon(tier)}
|
||||
{t(tier)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{!isLoading ? (
|
||||
rewardsLeaderboardData && rewardsLeaderboardData.length ? (
|
||||
rewardsLeaderboardData.map(
|
||||
(wallet: RewardsLeaderboardItem, i: number) => (
|
||||
<LeaderboardCard rank={i + 1} key={i} wallet={wallet} />
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className="flex justify-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<span className="text-th-fgd-3">Leaderboard not available</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{[...Array(20)].map((x, i) => (
|
||||
<SheenLoader className="flex flex-1" key={i}>
|
||||
<div className="h-16 w-full bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Leaderboards
|
||||
|
||||
const LeaderboardCard = ({
|
||||
rank,
|
||||
wallet,
|
||||
}: {
|
||||
rank: number
|
||||
wallet: RewardsLeaderboardItem
|
||||
}) => {
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
return (
|
||||
<a
|
||||
className="flex w-full items-center justify-between rounded-md border border-th-bkg-3 px-3 py-3 md:px-4 md:hover:bg-th-bkg-2"
|
||||
href={`/?address=${'account'}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full ${
|
||||
rank < 4 ? '' : 'bg-th-bkg-3'
|
||||
} md:mr-2`}
|
||||
>
|
||||
<p
|
||||
className={`relative z-10 font-bold ${
|
||||
rank < 4 ? 'text-th-bkg-1' : 'text-th-fgd-3'
|
||||
}`}
|
||||
>
|
||||
{rank}
|
||||
</p>
|
||||
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
|
||||
</div>
|
||||
<ProfileImage
|
||||
imageSize={isMobile ? '32' : '40'}
|
||||
imageUrl={''}
|
||||
placeholderSize={isMobile ? '20' : '24'}
|
||||
/>
|
||||
<div className="text-left">
|
||||
<p className="capitalize text-th-fgd-2 md:text-base">
|
||||
{abbreviateAddress(new PublicKey(wallet.wallet_pk))}
|
||||
</p>
|
||||
{/* <p className="text-xs text-th-fgd-4">
|
||||
Acc: {'A1at5'.slice(0, 4) + '...' + 'tt45eU'.slice(-4)}
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-right font-mono md:text-base">
|
||||
{formatNumericValue(wallet.points)}
|
||||
</span>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { IconButton } from '@components/shared/Button'
|
||||
import { XMarkIcon } from '@heroicons/react/20/solid'
|
||||
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
const PromoBanner = () => {
|
||||
const [showBanner, setShowBanner] = useState(true)
|
||||
const { data: isWhiteListed } = useIsWhiteListed()
|
||||
return isWhiteListed && showBanner ? (
|
||||
<div className="relative">
|
||||
<div className="flex flex-wrap items-center justify-center bg-th-bkg-2 py-3 px-10">
|
||||
<p className="mr-2 text-center text-th-fgd-1 text-th-fgd-1 lg:text-base">
|
||||
Season 1 of Mango Mints is starting soon.
|
||||
</p>
|
||||
<Link
|
||||
className="bg-gradient-to-b from-mango-classic-theme-active to-mango-classic-theme-down bg-clip-text font-bold text-transparent lg:text-base"
|
||||
href="/rewards"
|
||||
>
|
||||
Get Ready
|
||||
</Link>
|
||||
</div>
|
||||
<IconButton
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 sm:right-2"
|
||||
hideBg
|
||||
onClick={() => setShowBanner(false)}
|
||||
size="medium"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default PromoBanner
|
|
@ -0,0 +1,786 @@
|
|||
import Select from '@components/forms/Select'
|
||||
import AcornIcon from '@components/icons/AcornIcon'
|
||||
import MangoIcon from '@components/icons/MangoIcon'
|
||||
import RobotIcon from '@components/icons/RobotIcon'
|
||||
import WhaleIcon from '@components/icons/WhaleIcon'
|
||||
import Button, { LinkButton } from '@components/shared/Button'
|
||||
import Modal from '@components/shared/Modal'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
// import { useTranslation } from 'next-i18next'
|
||||
import Image from 'next/image'
|
||||
import { ReactNode, RefObject, useEffect, useRef, useState } from 'react'
|
||||
import Particles from 'react-tsparticles'
|
||||
import { ModalProps } from 'types/modal'
|
||||
import Leaderboards from './Leaderboards'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { MANGO_DATA_API_URL } from 'utils/constants'
|
||||
import { formatNumericValue } from 'utils/numbers'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { abbreviateAddress } from 'utils/formatting'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
|
||||
import InlineNotification from '@components/shared/InlineNotification'
|
||||
|
||||
const FAQS = [
|
||||
{
|
||||
q: 'What is Mango Mints?',
|
||||
a: 'Mango Mints is a weekly rewards program with amazing prizes. Anyone can participate simply by performing actions on Mango.',
|
||||
},
|
||||
{
|
||||
q: 'How do I participate?',
|
||||
a: "Simply by using Mango. Points are allocated for transactions across the platform (swaps, trades, orders and more). You'll receive a notificaton when you earn points (make sure notifications are enabled for your wallet).",
|
||||
},
|
||||
{
|
||||
q: 'How do Seasons work?',
|
||||
a: 'Each weekly cycle is called a Season and each Season has two periods. The first period is about earning points and runs from midnight Sunday UTC to midnight Friday UTC. The second period is allocated to claim prizes and runs from midnight Friday UTC to midnight Sunday UTC.',
|
||||
},
|
||||
{
|
||||
q: 'What are the rewards tiers?',
|
||||
a: "There are 4 rewards tiers. Everyone starts in the Seed tier. After your first Season is completed you'll be promoted to either the Mango or Whale tier (depending on the average notional value of your swaps/trades). Bots are automatically assigned to the Bots tier and will remain there.",
|
||||
},
|
||||
{
|
||||
q: 'How do the prizes work?',
|
||||
a: "At the end of each Season loot boxes are distributed based on the amount of points earned relative to the other participants in your tier. Each box contains a prize. So you're guaranteed to get something.",
|
||||
},
|
||||
{
|
||||
q: 'What happens during the Season claim period?',
|
||||
a: "During the claim period you can come back to this page and often as you like and open your loot boxes. However, if you don't claim your prizes during this time window they will be lost.",
|
||||
},
|
||||
]
|
||||
|
||||
export type RewardsLeaderboardItem = {
|
||||
points: number
|
||||
tier: string
|
||||
wallet_pk: string
|
||||
}
|
||||
|
||||
export const tiers = ['seed', 'mango', 'whale', 'bot']
|
||||
|
||||
const fetchRewardsPoints = async (walletPk: string | undefined) => {
|
||||
try {
|
||||
const data = await fetch(
|
||||
`${MANGO_DATA_API_URL}/user-data/campaign-total-points-wallet?wallet-pk=${walletPk}`
|
||||
)
|
||||
const res = await data.json()
|
||||
return res
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch points', e)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchLeaderboard = async (tier: string | undefined) => {
|
||||
try {
|
||||
const data = await fetch(
|
||||
`${MANGO_DATA_API_URL}/user-data/campaign-leaderboard?tier=${tier}`
|
||||
)
|
||||
const res = await data.json()
|
||||
return res
|
||||
} catch (e) {
|
||||
console.log('Failed to top accounts leaderboard', e)
|
||||
}
|
||||
}
|
||||
|
||||
const RewardsPage = () => {
|
||||
// const { t } = useTranslation(['common', 'rewards'])
|
||||
const [showClaim] = useState(true)
|
||||
const { data: isWhiteListed, isLoading, isFetching } = useIsWhiteListed()
|
||||
const [showLeaderboards, setShowLeaderboards] = useState('')
|
||||
const [showWhitelistModal, setShowWhitelistModal] = useState(false)
|
||||
const faqRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const scrollToFaqs = () => {
|
||||
if (faqRef.current) {
|
||||
faqRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start', // or 'end' or 'center'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWhiteListed && !isLoading && !isFetching) {
|
||||
setShowWhitelistModal(true)
|
||||
}
|
||||
}, [isWhiteListed, isLoading, isFetching])
|
||||
|
||||
return !showLeaderboards ? (
|
||||
<>
|
||||
<div className="bg-[url('/images/rewards/madlad-tile.png')]">
|
||||
<div className="mx-auto flex max-w-[1140px] flex-col items-center p-8 lg:flex-row lg:p-10">
|
||||
<div className="mb-6 h-[180px] w-[180px] flex-shrink-0 lg:mr-10 lg:mb-0 lg:h-[220px] lg:w-[220px]">
|
||||
<Image
|
||||
className="rounded-lg shadow-lg"
|
||||
priority
|
||||
src="/images/rewards/madlad.png"
|
||||
width={260}
|
||||
height={260}
|
||||
alt="Top Prize"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center lg:items-start">
|
||||
<Badge
|
||||
label="Season 1"
|
||||
borderColor="var(--active)"
|
||||
shadowColor="var(--active)"
|
||||
/>
|
||||
<h1 className="my-2 text-center text-4xl lg:text-left">
|
||||
Win amazing prizes every week.
|
||||
</h1>
|
||||
<p className="mb-4 text-center text-lg leading-snug lg:text-left">
|
||||
Earn points by performing actions on Mango. More points equals
|
||||
more chances to win.
|
||||
</p>
|
||||
<Button size="large" onClick={scrollToFaqs}>
|
||||
How it Works
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!showClaim ? (
|
||||
<Claim />
|
||||
) : (
|
||||
<Season
|
||||
faqRef={faqRef}
|
||||
showLeaderboard={setShowLeaderboards}
|
||||
setShowWhitelistModal={() => setShowWhitelistModal(true)}
|
||||
/>
|
||||
)}
|
||||
{showWhitelistModal ? (
|
||||
<WhitelistWalletModal
|
||||
isOpen={showWhitelistModal}
|
||||
onClose={() => setShowWhitelistModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Leaderboards
|
||||
leaderboard={showLeaderboards}
|
||||
goBack={() => setShowLeaderboards('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RewardsPage
|
||||
|
||||
const Season = ({
|
||||
faqRef,
|
||||
showLeaderboard,
|
||||
setShowWhitelistModal,
|
||||
}: {
|
||||
faqRef: RefObject<HTMLDivElement>
|
||||
showLeaderboard: (x: string) => void
|
||||
setShowWhitelistModal: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'rewards'])
|
||||
const { wallet } = useWallet()
|
||||
const [topAccountsTier, setTopAccountsTier] = useState('seed')
|
||||
const { data: isWhiteListed } = useIsWhiteListed()
|
||||
const {
|
||||
data: walletRewardsData,
|
||||
isFetching: fetchingWalletRewardsData,
|
||||
isLoading: loadingWalletRewardsData,
|
||||
} = useQuery(
|
||||
['rewards-points', wallet?.adapter.publicKey],
|
||||
() => fetchRewardsPoints(wallet?.adapter.publicKey?.toString()),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!wallet?.adapter,
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
data: topAccountsLeaderboardData,
|
||||
isFetching: fetchingTopAccountsLeaderboardData,
|
||||
isLoading: loadingTopAccountsLeaderboardData,
|
||||
} = useQuery(
|
||||
['top-accounts-leaderboard-data', topAccountsTier],
|
||||
() => fetchLeaderboard(topAccountsTier),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (walletRewardsData?.tier) {
|
||||
setTopAccountsTier(walletRewardsData.tier)
|
||||
}
|
||||
}, [walletRewardsData])
|
||||
|
||||
const isLoadingWalletData =
|
||||
fetchingWalletRewardsData || loadingWalletRewardsData
|
||||
|
||||
const isLoadingLeaderboardData =
|
||||
fetchingTopAccountsLeaderboardData || loadingTopAccountsLeaderboardData
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-center bg-th-bkg-3 px-4 py-3">
|
||||
<ClockIcon className="mr-2 h-5 w-5 text-th-active" />
|
||||
<p className="text-base text-th-fgd-2">
|
||||
Season 1 starts in:{' '}
|
||||
<span className="mr-4 font-bold text-th-fgd-1">4 days</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto grid max-w-[1140px] grid-cols-12 gap-4 p-8 lg:gap-6 lg:p-10">
|
||||
{!isWhiteListed ? (
|
||||
<div className="col-span-12">
|
||||
<InlineNotification
|
||||
desc={
|
||||
<>
|
||||
<span>
|
||||
You need to whitelist your wallet to claim any rewards you
|
||||
win
|
||||
</span>
|
||||
<LinkButton className="mt-2" onClick={setShowWhitelistModal}>
|
||||
Get Whitelisted
|
||||
</LinkButton>
|
||||
</>
|
||||
}
|
||||
title="Wallet not whitelisted"
|
||||
type="warning"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="col-span-12 lg:col-span-8">
|
||||
<div className="mb-2 rounded-lg border border-th-bkg-3 p-4">
|
||||
<h2 className="mb-4">Rewards Tiers</h2>
|
||||
<div className="mb-6 space-y-2">
|
||||
<RewardsTierCard
|
||||
icon={<AcornIcon className="h-8 w-8 text-th-fgd-2" />}
|
||||
name="seed"
|
||||
desc="All new participants start here"
|
||||
showLeaderboard={showLeaderboard}
|
||||
status={walletRewardsData?.tier === 'seed' ? 'Qualified' : ''}
|
||||
/>
|
||||
<RewardsTierCard
|
||||
icon={<MangoIcon className="h-8 w-8 text-th-fgd-2" />}
|
||||
name="mango"
|
||||
desc="Average swap/trade value less than $1,000"
|
||||
showLeaderboard={showLeaderboard}
|
||||
status={walletRewardsData?.tier === 'mango' ? 'Qualified' : ''}
|
||||
/>
|
||||
<RewardsTierCard
|
||||
icon={<WhaleIcon className="h-8 w-8 text-th-fgd-2" />}
|
||||
name="whale"
|
||||
desc="Average swap/trade value greater than $1,000"
|
||||
showLeaderboard={showLeaderboard}
|
||||
status={walletRewardsData?.tier === 'whale' ? 'Qualified' : ''}
|
||||
/>
|
||||
<RewardsTierCard
|
||||
icon={<RobotIcon className="h-8 w-8 text-th-fgd-2" />}
|
||||
name="bot"
|
||||
desc="All bots"
|
||||
showLeaderboard={showLeaderboard}
|
||||
status={walletRewardsData?.tier === 'bot' ? 'Qualified' : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={faqRef}>
|
||||
<Faqs />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-4">
|
||||
<div className="mb-2 rounded-lg border border-th-bkg-3 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Your Points</h2>
|
||||
{isWhiteListed ? (
|
||||
<Badge
|
||||
label="Whitelisted"
|
||||
borderColor="var(--success)"
|
||||
shadowColor="var(--success)"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-4 flex h-14 w-full items-center rounded-md bg-th-bkg-2 px-3">
|
||||
<span className="w-full font-display text-3xl text-th-fgd-1">
|
||||
{!isLoadingWalletData ? (
|
||||
walletRewardsData?.points ? (
|
||||
formatNumericValue(walletRewardsData.points)
|
||||
) : wallet?.adapter.publicKey ? (
|
||||
0
|
||||
) : (
|
||||
<span className="flex items-center justify-center text-center font-body text-sm text-th-fgd-3">
|
||||
{t('connect-wallet')}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<SheenLoader>
|
||||
<div className="h-8 w-32 rounded-md bg-th-bkg-3" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<p>Points Earned</p>
|
||||
<p className="font-mono text-th-fgd-2">
|
||||
{!isLoadingWalletData ? (
|
||||
walletRewardsData?.points ? (
|
||||
formatNumericValue(walletRewardsData.points)
|
||||
) : wallet?.adapter.publicKey ? (
|
||||
0
|
||||
) : (
|
||||
'–'
|
||||
)
|
||||
) : (
|
||||
<SheenLoader>
|
||||
<div className="h-4 w-12 rounded-sm bg-th-bkg-3" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<p>Streak Bonus</p>
|
||||
<p className="font-mono text-th-fgd-2">0x</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<p>Rewards Tier</p>
|
||||
<p className="text-th-fgd-2">
|
||||
{!isLoadingWalletData ? (
|
||||
walletRewardsData?.tier ? (
|
||||
<span className="capitalize">
|
||||
{walletRewardsData.tier}
|
||||
</span>
|
||||
) : (
|
||||
'–'
|
||||
)
|
||||
) : (
|
||||
<SheenLoader>
|
||||
<div className="h-4 w-12 rounded-sm bg-th-bkg-3" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<p>Rank</p>
|
||||
<p className="text-th-fgd-2">–</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-th-bkg-3 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="">Top Accounts</h2>
|
||||
<Select
|
||||
value={t(`rewards:${topAccountsTier}`)}
|
||||
onChange={(tier) => setTopAccountsTier(tier)}
|
||||
>
|
||||
{tiers.map((tier) => (
|
||||
<Select.Option key={tier} value={tier}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{t(`rewards:${tier}`)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="border-b border-th-bkg-3">
|
||||
{!isLoadingLeaderboardData ? (
|
||||
topAccountsLeaderboardData &&
|
||||
topAccountsLeaderboardData.length ? (
|
||||
topAccountsLeaderboardData
|
||||
.slice(0, 5)
|
||||
.map((wallet: RewardsLeaderboardItem, i: number) => (
|
||||
<div
|
||||
className="flex items-center justify-between border-t border-th-bkg-3 p-3"
|
||||
key={i + wallet.wallet_pk}
|
||||
>
|
||||
<div className="flex items-center space-x-2 font-mono">
|
||||
<span>{i + 1}.</span>
|
||||
<span className="text-th-fgd-3">
|
||||
{abbreviateAddress(new PublicKey(wallet.wallet_pk))}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-th-fgd-1">
|
||||
{formatNumericValue(wallet.points, 0)}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center border-t border-th-bkg-3 py-4">
|
||||
<span className="text-th-fgd-3">
|
||||
Leaderboard not available
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{[...Array(5)].map((x, i) => (
|
||||
<SheenLoader className="flex flex-1" key={i}>
|
||||
<div className="h-10 w-full bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="mt-6 w-full"
|
||||
onClick={() => showLeaderboard(topAccountsTier)}
|
||||
secondary
|
||||
>
|
||||
Full Leaderboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Claim = () => {
|
||||
const [showWinModal, setShowWinModal] = useState(false)
|
||||
const [showLossModal, setShowLossModal] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-center bg-th-bkg-3 px-4 py-3">
|
||||
<ClockIcon className="mr-2 h-5 w-5 text-th-active" />
|
||||
<p className="text-base text-th-fgd-2">
|
||||
Season 1 claim ends in:{' '}
|
||||
<span className="font-bold text-th-fgd-1">24 hours</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto grid max-w-[1140px] grid-cols-12 gap-4 p-8 lg:gap-6 lg:p-10">
|
||||
<div className="col-span-12">
|
||||
<div className="mb-6 text-center md:mb-12">
|
||||
<h2 className="mb-2 text-5xl">Congratulations!</h2>
|
||||
<p className="text-lg">You earnt 3 boxes in Season 1</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:justify-center md:space-x-6 md:space-y-0">
|
||||
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
|
||||
<Image
|
||||
className="md:-mt-10"
|
||||
src="/images/rewards/cube.png"
|
||||
width={140}
|
||||
height={140}
|
||||
alt="Reward"
|
||||
style={{ width: 'auto', maxWidth: '140px' }}
|
||||
/>
|
||||
<Button className="mt-8" size="large">
|
||||
Open Box
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
|
||||
<Image
|
||||
className="md:-mt-10"
|
||||
src="/images/rewards/cube.png"
|
||||
width={140}
|
||||
height={140}
|
||||
alt="Reward"
|
||||
style={{ width: 'auto', maxWidth: '140px' }}
|
||||
/>
|
||||
<Button
|
||||
className="mt-8"
|
||||
size="large"
|
||||
onClick={() => setShowLossModal(true)}
|
||||
>
|
||||
Open Box
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
|
||||
<Image
|
||||
className="md:-mt-10"
|
||||
src="/images/rewards/cube.png"
|
||||
width={140}
|
||||
height={140}
|
||||
alt="Reward"
|
||||
style={{ width: 'auto', maxWidth: '140px' }}
|
||||
/>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => setShowWinModal(true)}
|
||||
size="large"
|
||||
>
|
||||
Open Box
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showWinModal ? (
|
||||
<ClaimWinModal
|
||||
isOpen={showWinModal}
|
||||
onClose={() => setShowWinModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
{showLossModal ? (
|
||||
<ClaimLossModal
|
||||
isOpen={showLossModal}
|
||||
onClose={() => setShowLossModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RewardsTierCard = ({
|
||||
desc,
|
||||
icon,
|
||||
name,
|
||||
showLeaderboard,
|
||||
status,
|
||||
}: {
|
||||
desc: string
|
||||
icon: ReactNode
|
||||
name: string
|
||||
showLeaderboard: (x: string) => void
|
||||
status?: string
|
||||
}) => {
|
||||
const { t } = useTranslation('rewards')
|
||||
return (
|
||||
<button
|
||||
className="w-full rounded-lg bg-th-bkg-2 p-4 text-left focus:outline-none md:hover:bg-th-bkg-3"
|
||||
onClick={() => showLeaderboard(name)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-14 w-14 items-center justify-center rounded-full bg-th-bkg-1">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(name)}</h3>
|
||||
<p>{desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center pl-4">
|
||||
{status ? (
|
||||
<Badge
|
||||
label={status}
|
||||
borderColor="var(--success)"
|
||||
shadowColor="var(--success)"
|
||||
/>
|
||||
) : null}
|
||||
<ChevronRightIcon className="ml-4 h-6 w-6 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const Badge = ({
|
||||
label,
|
||||
fillColor,
|
||||
shadowColor,
|
||||
borderColor,
|
||||
}: {
|
||||
label: string
|
||||
fillColor?: string
|
||||
shadowColor?: string
|
||||
borderColor: string
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="w-max rounded-full border px-3 py-1"
|
||||
style={{
|
||||
background: fillColor ? fillColor : 'transparent',
|
||||
borderColor: borderColor,
|
||||
boxShadow: shadowColor ? `0px 0px 8px 0px ${shadowColor}` : 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: fillColor ? 'var(--fgd-1)' : borderColor }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const particleOptions = {
|
||||
detectRetina: true,
|
||||
emitters: {
|
||||
life: {
|
||||
count: 60,
|
||||
delay: 0,
|
||||
duration: 0.1,
|
||||
},
|
||||
startCount: 0,
|
||||
particles: {
|
||||
shape: {
|
||||
type: ['character', 'character', 'character', 'character', 'character'],
|
||||
options: {
|
||||
character: [
|
||||
{
|
||||
fill: true,
|
||||
font: 'Verdana',
|
||||
value: ['🍀', '🦄', '⭐️', '🎉', '💸'],
|
||||
style: '',
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
opacity: {
|
||||
value: 1,
|
||||
},
|
||||
rotate: {
|
||||
value: {
|
||||
min: 0,
|
||||
max: 360,
|
||||
},
|
||||
direction: 'random',
|
||||
animation: {
|
||||
enable: true,
|
||||
speed: 30,
|
||||
},
|
||||
},
|
||||
tilt: {
|
||||
direction: 'random',
|
||||
enable: true,
|
||||
value: {
|
||||
min: 0,
|
||||
max: 360,
|
||||
},
|
||||
animation: {
|
||||
enable: true,
|
||||
speed: 30,
|
||||
},
|
||||
},
|
||||
size: {
|
||||
value: 16,
|
||||
},
|
||||
roll: {
|
||||
darken: {
|
||||
enable: true,
|
||||
value: 25,
|
||||
},
|
||||
enable: true,
|
||||
speed: {
|
||||
min: 5,
|
||||
max: 15,
|
||||
},
|
||||
},
|
||||
move: {
|
||||
angle: 10,
|
||||
attract: {
|
||||
rotate: {
|
||||
x: 600,
|
||||
y: 1200,
|
||||
},
|
||||
},
|
||||
direction: 'bottom',
|
||||
enable: true,
|
||||
speed: { min: 8, max: 16 },
|
||||
outMode: 'destroy',
|
||||
},
|
||||
},
|
||||
position: {
|
||||
x: { random: true },
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const ClaimWinModal = ({ isOpen, onClose }: ModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="mb-6">You're a winner!</h2>
|
||||
<div
|
||||
className="mx-auto mb-3 h-48 w-48 rounded-lg border border-th-success"
|
||||
style={{
|
||||
boxShadow: '0px 0px 8px 0px var(--success)',
|
||||
}}
|
||||
></div>
|
||||
<p className="text-lg">Prize name goes here</p>
|
||||
</div>
|
||||
<Button className="w-full" size="large">
|
||||
Claim Prize
|
||||
</Button>
|
||||
</Modal>
|
||||
<div className="relative z-50">
|
||||
<Particles id="tsparticles" options={particleOptions} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ClaimLossModal = ({ isOpen, onClose }: ModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="mb-2">Better luck next time</h2>
|
||||
<p className="text-lg">This box is empty</p>
|
||||
</div>
|
||||
<Button className="w-full" onClick={onClose} size="large">
|
||||
Close
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Faqs = () => {
|
||||
return (
|
||||
<div className="rounded-lg border border-th-bkg-3 p-4">
|
||||
<h2 className="mb-2">How it Works</h2>
|
||||
<p className="mb-4">
|
||||
Feel free to reach out to us on{' '}
|
||||
<a
|
||||
href="https://discord.gg/2uwjsBc5yw"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>{' '}
|
||||
with additional questions.
|
||||
</p>
|
||||
<div className="border-b border-th-bkg-3">
|
||||
{FAQS.map((faq, i) => (
|
||||
<Disclosure key={i}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={`w-full border-t border-th-bkg-3 p-4 text-left focus:outline-none md:hover:bg-th-bkg-2`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-th-fgd-2">{faq.q}</p>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
} h-5 w-5 flex-shrink-0`}
|
||||
/>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="p-4">
|
||||
<p>{faq.a}</p>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const WhitelistWalletModal = ({ isOpen, onClose }: ModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="mb-2">Whitelist Wallet</h2>
|
||||
<p className="text-lg">
|
||||
Wallets are required to be verified with your Discord account to
|
||||
participate in Mango Mints. We are doing this as a sybil prevention
|
||||
mechanism.
|
||||
</p>
|
||||
</div>
|
||||
<Button className="w-full" onClick={onClose} size="large">
|
||||
Whitelist Wallet
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -6,11 +6,7 @@ import { useViewport } from 'hooks/useViewport'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
floorToDecimal,
|
||||
formatNumericValue,
|
||||
getDecimalCount,
|
||||
} from 'utils/numbers'
|
||||
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
|
||||
import { LinkButton } from './Button'
|
||||
|
@ -26,6 +22,9 @@ import useBanksWithBalances, {
|
|||
import useUnownedAccount from 'hooks/useUnownedAccount'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import TokenLogo from './TokenLogo'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { USDC_MINT } from 'utils/constants'
|
||||
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
|
||||
|
||||
const BalancesTable = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
|
@ -257,12 +256,20 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
|
|||
const handleSwapFormBalanceClick = useCallback(
|
||||
(balance: number) => {
|
||||
const set = mangoStore.getState().set
|
||||
const group = mangoStore.getState().group
|
||||
const swap = mangoStore.getState().swap
|
||||
const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT))
|
||||
const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT)
|
||||
if (balance >= 0) {
|
||||
set((s) => {
|
||||
s.swap.inputBank = tokenBank
|
||||
s.swap.amountIn = balance.toString()
|
||||
s.swap.amountOut = ''
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
if (tokenBank.name === swap.outputBank?.name) {
|
||||
s.swap.outputBank =
|
||||
swap.outputBank.name === 'USDC' ? solBank : usdcBank
|
||||
}
|
||||
})
|
||||
} else {
|
||||
set((s) => {
|
||||
|
@ -270,6 +277,10 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
|
|||
s.swap.amountIn = ''
|
||||
s.swap.amountOut = Math.abs(balance).toString()
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
if (tokenBank.name === swap.inputBank?.name) {
|
||||
s.swap.inputBank =
|
||||
swap.inputBank.name === 'USDC' ? solBank : usdcBank
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -310,7 +321,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
|
|||
className="font-normal underline underline-offset-2 md:underline-offset-4 md:hover:no-underline"
|
||||
onClick={() =>
|
||||
handleSwapFormBalanceClick(
|
||||
Number(formatNumericValue(balance, tokenBank.mintDecimals))
|
||||
Number(floorToDecimal(balance, tokenBank.mintDecimals))
|
||||
)
|
||||
}
|
||||
>
|
||||
|
|
|
@ -25,7 +25,7 @@ import { FadeInFadeOut } from './Transitions'
|
|||
import ChartRangeButtons from './ChartRangeButtons'
|
||||
import Change from './Change'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { ANIMATION_SETTINGS_KEY } from 'utils/constants'
|
||||
import { ANIMATION_SETTINGS_KEY, DAILY_MILLISECONDS } from 'utils/constants'
|
||||
import { formatNumericValue } from 'utils/numbers'
|
||||
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
|
||||
import { AxisDomain } from 'recharts/types/util/types'
|
||||
|
@ -120,7 +120,7 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data.length) return []
|
||||
const start = Number(daysToShow) * 86400000
|
||||
const start = Number(daysToShow) * DAILY_MILLISECONDS
|
||||
const filtered = data.filter((d: any) => {
|
||||
const dataTime = new Date(d[xKey]).getTime()
|
||||
const now = new Date().getTime()
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { MinusSmallIcon } from '@heroicons/react/20/solid'
|
||||
import { DownTriangle, UpTriangle } from './DirectionTriangles'
|
||||
import FormatNumericValue from './FormatNumericValue'
|
||||
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import use24HourChange from 'hooks/use24HourChange'
|
||||
import { useMemo } from 'react'
|
||||
import SheenLoader from './SheenLoader'
|
||||
|
||||
const MarketChange = ({
|
||||
market,
|
||||
size,
|
||||
}: {
|
||||
market: PerpMarket | Serum3Market | undefined
|
||||
size?: 'small'
|
||||
}) => {
|
||||
const { loading, spotChange, perpChange } = use24HourChange(market)
|
||||
|
||||
const change = useMemo(() => {
|
||||
if (!market) return
|
||||
return market instanceof PerpMarket ? perpChange : spotChange
|
||||
}, [perpChange, spotChange])
|
||||
|
||||
return loading ? (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
) : change && !isNaN(change) ? (
|
||||
<div className="flex items-center space-x-1.5">
|
||||
{change > 0 ? (
|
||||
<div className="mt-[1px]">
|
||||
<UpTriangle size={size} />
|
||||
</div>
|
||||
) : change < 0 ? (
|
||||
<div className="mt-[1px]">
|
||||
<DownTriangle size={size} />
|
||||
</div>
|
||||
) : (
|
||||
<MinusSmallIcon
|
||||
className={`-mr-1 ${
|
||||
size === 'small' ? 'h-4 w-4' : 'h-6 w-6'
|
||||
} text-th-fgd-4`}
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
className={`font-mono font-normal ${
|
||||
size === 'small' ? 'text-xs' : 'text-sm'
|
||||
} ${
|
||||
change > 0
|
||||
? 'text-th-up'
|
||||
: change < 0
|
||||
? 'text-th-down'
|
||||
: 'text-th-fgd-4'
|
||||
}`}
|
||||
>
|
||||
<FormatNumericValue
|
||||
value={isNaN(change) ? '0.00' : Math.abs(change)}
|
||||
decimals={2}
|
||||
/>
|
||||
%
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>–</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketChange
|
|
@ -8,6 +8,7 @@ type ModalProps = {
|
|||
fullScreen?: boolean
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
panelClassNames?: string
|
||||
hideClose?: boolean
|
||||
}
|
||||
|
||||
|
@ -17,6 +18,7 @@ function Modal({
|
|||
fullScreen = false,
|
||||
isOpen,
|
||||
onClose,
|
||||
panelClassNames,
|
||||
hideClose,
|
||||
}: ModalProps) {
|
||||
const handleClose = () => {
|
||||
|
@ -48,7 +50,7 @@ function Modal({
|
|||
fullScreen
|
||||
? ''
|
||||
: 'p-4 pt-6 sm:h-auto sm:max-w-md sm:rounded-lg sm:border sm:border-th-bkg-3 sm:p-6'
|
||||
} relative `}
|
||||
} relative ${panelClassNames}`}
|
||||
>
|
||||
<div>{children}</div>
|
||||
{!hideClose ? (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import dayjs from 'dayjs'
|
||||
import { ReactNode, forwardRef } from 'react'
|
||||
import { MouseEventHandler, ReactNode, forwardRef } from 'react'
|
||||
|
||||
export const Table = ({
|
||||
children,
|
||||
|
@ -43,15 +44,19 @@ interface TrBodyProps {
|
|||
children: ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
onMouseEnter?: (x: any) => void
|
||||
onMouseLeave?: MouseEventHandler
|
||||
}
|
||||
|
||||
export const TrBody = forwardRef<HTMLTableRowElement, TrBodyProps>(
|
||||
(props, ref) => {
|
||||
const { children, className, onClick } = props
|
||||
const { children, className, onClick, onMouseEnter, onMouseLeave } = props
|
||||
return (
|
||||
<tr
|
||||
className={`border-y border-th-bkg-3 ${className}`}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -24,6 +24,7 @@ import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
|||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { LinkButton } from '@components/shared/Button'
|
||||
import SoonBadge from '@components/shared/SoonBadge'
|
||||
import { DAILY_SECONDS } from 'utils/constants'
|
||||
|
||||
export const getOneDayPerpStats = (
|
||||
stats: PerpStatsItem[] | null,
|
||||
|
@ -33,7 +34,7 @@ export const getOneDayPerpStats = (
|
|||
? stats
|
||||
.filter((s) => s.perp_market === marketName)
|
||||
.filter((f) => {
|
||||
const seconds = 86400
|
||||
const seconds = DAILY_SECONDS
|
||||
const dataTime = new Date(f.date_hour).getTime() / 1000
|
||||
const now = new Date().getTime() / 1000
|
||||
const limit = now - seconds
|
||||
|
|
|
@ -20,6 +20,7 @@ import { fetchSpotVolume } from '@components/trade/AdvancedMarketHeader'
|
|||
import { TickerData } from 'types'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import MarketChange from '@components/shared/MarketChange'
|
||||
|
||||
const SpotMarketsTable = () => {
|
||||
const { t } = useTranslation('common')
|
||||
|
@ -87,7 +88,7 @@ const SpotMarketsTable = () => {
|
|||
(m) => m.mint === mkt.serumMarketExternal.toString()
|
||||
)
|
||||
|
||||
const change =
|
||||
const birdeyeChange =
|
||||
birdeyeData && price
|
||||
? ((price - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
|
@ -131,7 +132,7 @@ const SpotMarketsTable = () => {
|
|||
<div className="h-10 w-24">
|
||||
<SimpleAreaChart
|
||||
color={
|
||||
change >= 0
|
||||
birdeyeChange >= 0
|
||||
? COLORS.UP[theme]
|
||||
: COLORS.DOWN[theme]
|
||||
}
|
||||
|
@ -153,7 +154,7 @@ const SpotMarketsTable = () => {
|
|||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col items-end">
|
||||
<Change change={change} suffix="%" />
|
||||
<MarketChange market={mkt} />
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
|
|
|
@ -49,6 +49,7 @@ import useUnownedAccount from 'hooks/useUnownedAccount'
|
|||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { formatCurrencyValue } from 'utils/numbers'
|
||||
import Switch from '@components/forms/Switch'
|
||||
import MaxAmountButton from '@components/shared/MaxAmountButton'
|
||||
|
||||
const MAX_DIGITS = 11
|
||||
export const withValueLimit = (values: NumberFormatValues): boolean => {
|
||||
|
@ -140,6 +141,18 @@ const SwapForm = () => {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const setBorrowAmountOut = useCallback(
|
||||
(borrowAmount: string) => {
|
||||
if (swapMode === 'ExactIn') {
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
})
|
||||
}
|
||||
setAmountOutFormValue(borrowAmount.toString())
|
||||
},
|
||||
[setAmountOutFormValue]
|
||||
)
|
||||
|
||||
/*
|
||||
Once a route is returned from the Jupiter API, use the inAmount or outAmount
|
||||
depending on the swapMode and set those values in state
|
||||
|
@ -275,6 +288,12 @@ const SwapForm = () => {
|
|||
amountOutAsDecimal,
|
||||
])
|
||||
|
||||
const outputTokenBalanceBorrow = useMemo(() => {
|
||||
if (!outputBank) return 0
|
||||
const balance = mangoAccount?.getTokenBalanceUi(outputBank)
|
||||
return balance && balance < 0 ? Math.abs(balance) : 0
|
||||
}, [outputBank])
|
||||
|
||||
const loadingSwapDetails: boolean = useMemo(() => {
|
||||
return (
|
||||
!!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) &&
|
||||
|
@ -406,7 +425,24 @@ const SwapForm = () => {
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-2 text-th-fgd-2 lg:text-base">{t('swap:receive')}</p>
|
||||
<div className="mb-2 flex items-end justify-between">
|
||||
<p className="text-th-fgd-2 lg:text-base">{t('swap:receive')}</p>
|
||||
{outputTokenBalanceBorrow ? (
|
||||
<MaxAmountButton
|
||||
className="mb-0.5 text-xs"
|
||||
decimals={outputBank?.mintDecimals || 9}
|
||||
label={t('repay')}
|
||||
onClick={() =>
|
||||
setBorrowAmountOut(
|
||||
outputTokenBalanceBorrow.toFixed(
|
||||
outputBank?.mintDecimals || 9
|
||||
)
|
||||
)
|
||||
}
|
||||
value={outputTokenBalanceBorrow}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div id="swap-step-three" className="mb-3 grid grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<TokenSelect
|
||||
|
|
|
@ -13,6 +13,7 @@ import parse from 'html-react-parser'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useMemo, useState } from 'react'
|
||||
import PriceChart from '@components/token/PriceChart'
|
||||
import { DAILY_SECONDS } from 'utils/constants'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const DEFAULT_COINGECKO_VALUES = {
|
||||
|
@ -44,7 +45,7 @@ const fetchBirdeyePrices = async (
|
|||
): Promise<BirdeyePriceResponse[] | []> => {
|
||||
const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H'
|
||||
const queryEnd = Math.floor(Date.now() / 1000)
|
||||
const queryStart = queryEnd - parseInt(daysToShow) * 86400
|
||||
const queryStart = queryEnd - parseInt(daysToShow) * DAILY_SECONDS
|
||||
const query = `defi/history_price?address=${mint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
const response: BirdeyeResponse = await makeApiRequest(query)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import { IconButton, LinkButton } from '@components/shared/Button'
|
||||
import Change from '@components/shared/Change'
|
||||
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
|
||||
import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/20/solid'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
|
@ -10,9 +9,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||
import { numberCompacter } from 'utils/numbers'
|
||||
import MarketSelectDropdown from './MarketSelectDropdown'
|
||||
import PerpFundingRate from './PerpFundingRate'
|
||||
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import usePrevious from '@components/shared/usePrevious'
|
||||
import PerpMarketDetailsModal from '@components/modals/PerpMarketDetailsModal'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import OraclePrice from './OraclePrice'
|
||||
|
@ -23,6 +20,7 @@ import { TickerData } from 'types'
|
|||
import ManualRefresh from '@components/shared/ManualRefresh'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import MarketChange from '@components/shared/MarketChange'
|
||||
|
||||
export const fetchSpotVolume = async () => {
|
||||
try {
|
||||
|
@ -43,17 +41,8 @@ const AdvancedMarketHeader = ({
|
|||
}) => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
|
||||
const {
|
||||
serumOrPerpMarket,
|
||||
price: stalePrice,
|
||||
selectedMarket,
|
||||
} = useSelectedMarket()
|
||||
const { serumOrPerpMarket, selectedMarket } = useSelectedMarket()
|
||||
const selectedMarketName = mangoStore((s) => s.selectedMarket.name)
|
||||
const [changePrice, setChangePrice] = useState(stalePrice)
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
const previousMarketName = usePrevious(selectedMarketName)
|
||||
const [showMarketDetails, setShowMarketDetails] = useState(false)
|
||||
const { group } = useMangoGroup()
|
||||
const { width } = useViewport()
|
||||
|
@ -85,18 +74,6 @@ const AdvancedMarketHeader = ({
|
|||
}
|
||||
}, [group])
|
||||
|
||||
const birdeyeData = useMemo(() => {
|
||||
if (
|
||||
!birdeyePrices?.length ||
|
||||
!selectedMarket ||
|
||||
selectedMarket instanceof PerpMarket
|
||||
)
|
||||
return
|
||||
return birdeyePrices.find(
|
||||
(m) => m.mint === selectedMarket.serumMarketExternal.toString()
|
||||
)
|
||||
}, [birdeyePrices, selectedMarket])
|
||||
|
||||
const oneDayPerpStats = useMemo(() => {
|
||||
if (
|
||||
!perpStats ||
|
||||
|
@ -108,36 +85,6 @@ const AdvancedMarketHeader = ({
|
|||
return getOneDayPerpStats(perpStats, selectedMarketName)
|
||||
}, [perpStats, selectedMarketName])
|
||||
|
||||
const change = useMemo(() => {
|
||||
if (
|
||||
!changePrice ||
|
||||
!serumOrPerpMarket ||
|
||||
selectedMarketName !== previousMarketName
|
||||
)
|
||||
return 0
|
||||
if (serumOrPerpMarket instanceof PerpMarket) {
|
||||
return oneDayPerpStats.length
|
||||
? ((changePrice - oneDayPerpStats[0].price) /
|
||||
oneDayPerpStats[0].price) *
|
||||
100
|
||||
: 0
|
||||
} else {
|
||||
if (!birdeyeData) return 0
|
||||
return (
|
||||
((changePrice - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
100
|
||||
)
|
||||
}
|
||||
}, [
|
||||
birdeyeData,
|
||||
changePrice,
|
||||
serumOrPerpMarket,
|
||||
oneDayPerpStats,
|
||||
previousMarketName,
|
||||
selectedMarketName,
|
||||
])
|
||||
|
||||
const perpVolume = useMemo(() => {
|
||||
if (!oneDayPerpStats.length) return
|
||||
return (
|
||||
|
@ -155,19 +102,13 @@ const AdvancedMarketHeader = ({
|
|||
<div className="hide-scroll flex w-full items-center justify-between overflow-x-auto border-t border-th-bkg-3 py-2 px-5 md:border-t-0 md:py-0 md:px-0 md:pr-6">
|
||||
<div className="flex items-center">
|
||||
<>
|
||||
<OraclePrice setChangePrice={setChangePrice} />
|
||||
<OraclePrice />
|
||||
</>
|
||||
<div className="ml-6 flex-col whitespace-nowrap">
|
||||
<div className="mb-0.5 text-xs text-th-fgd-4">
|
||||
{t('rolling-change')}
|
||||
</div>
|
||||
{!loadingPrices && !loadingPerpStats ? (
|
||||
<Change change={change} size="small" suffix="%" />
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
<MarketChange market={selectedMarket} size="small" />
|
||||
</div>
|
||||
{serumOrPerpMarket instanceof PerpMarket ? (
|
||||
<>
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import {
|
||||
|
@ -17,6 +23,7 @@ import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
|
|||
import { Howl } from 'howler'
|
||||
import { isMangoError } from 'types'
|
||||
import { decodeBook, decodeBookL2 } from './Orderbook'
|
||||
import InlineNotification from '@components/shared/InlineNotification'
|
||||
|
||||
interface MarketCloseModalProps {
|
||||
onClose: () => void
|
||||
|
@ -42,7 +49,6 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|||
const [submitting, setSubmitting] = useState(false)
|
||||
const connection = mangoStore((s) => s.connection)
|
||||
const group = mangoStore((s) => s.group)
|
||||
const perpMarket = group?.getPerpMarketByMarketIndex(position.marketIndex)
|
||||
const [soundSettings] = useLocalStorageState(
|
||||
SOUND_SETTINGS_KEY,
|
||||
INITIAL_SOUND_SETTINGS
|
||||
|
@ -50,6 +56,10 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|||
const [asks, setAsks] = useState<BidsAndAsks>(null)
|
||||
const [bids, setBids] = useState<BidsAndAsks>(null)
|
||||
|
||||
const perpMarket = useMemo(() => {
|
||||
return group?.getPerpMarketByMarketIndex(position.marketIndex)
|
||||
}, [group, position?.marketIndex])
|
||||
|
||||
// subscribe to the bids and asks orderbook accounts
|
||||
useEffect(() => {
|
||||
console.log('setting up orderbook websockets')
|
||||
|
@ -113,6 +123,21 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|||
}
|
||||
}, [connection, perpMarket, group])
|
||||
|
||||
const insufficientLiquidity = useMemo(() => {
|
||||
if (!perpMarket) return true
|
||||
const baseSize = position.getBasePositionUi(perpMarket)
|
||||
const isBids = baseSize < 0
|
||||
if (isBids) {
|
||||
if (!bids || !bids.length) return true
|
||||
const liquidityMax = bids.reduce((a, c) => a + c[1], 0)
|
||||
return liquidityMax < baseSize
|
||||
} else {
|
||||
if (!asks || !asks.length) return true
|
||||
const liquidityMax = asks.reduce((a, c) => a + c[1], 0)
|
||||
return liquidityMax < baseSize
|
||||
}
|
||||
}, [perpMarket, position, bids, asks])
|
||||
|
||||
const handleMarketClose = useCallback(
|
||||
async (bids: BidsAndAsks, asks: BidsAndAsks) => {
|
||||
const client = mangoStore.getState().client
|
||||
|
@ -138,14 +163,6 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|||
)
|
||||
|
||||
const maxSlippage = 0.025
|
||||
// const perpOrderType =
|
||||
// tradeForm.tradeType === 'Market'
|
||||
// ? PerpOrderType.market
|
||||
// : tradeForm.ioc
|
||||
// ? PerpOrderType.immediateOrCancel
|
||||
// : tradeForm.postOnly
|
||||
// ? PerpOrderType.postOnly
|
||||
// : PerpOrderType.limit
|
||||
const tx = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -196,8 +213,17 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|||
{t('trade:close-confirm', { config_name: perpMarket?.name })}
|
||||
</h3>
|
||||
<div className="pb-6 text-th-fgd-3">{t('trade:price-expect')}</div>
|
||||
{insufficientLiquidity ? (
|
||||
<div className="mb-3">
|
||||
<InlineNotification
|
||||
type="error"
|
||||
desc={t('trade:insufficient-perp-liquidity')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Button
|
||||
className="mb-4 flex w-full items-center justify-center"
|
||||
disabled={insufficientLiquidity}
|
||||
onClick={() => handleMarketClose(bids, asks)}
|
||||
size="large"
|
||||
>
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
// import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import Change from '@components/shared/Change'
|
||||
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -23,6 +18,7 @@ import SoonBadge from '@components/shared/SoonBadge'
|
|||
import TabButtons from '@components/shared/TabButtons'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import Loading from '@components/shared/Loading'
|
||||
import MarketChange from '@components/shared/MarketChange'
|
||||
|
||||
const MARKET_LINK_WRAPPER_CLASSES =
|
||||
'flex items-center justify-between px-4 md:pl-6 md:pr-4'
|
||||
|
@ -41,11 +37,7 @@ const MarketSelectDropdown = () => {
|
|||
)
|
||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const allPerpMarkets = mangoStore((s) => s.perpMarkets)
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
|
||||
const { group } = useMangoGroup()
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
||||
|
||||
const perpMarkets = useMemo(() => {
|
||||
|
@ -130,14 +122,7 @@ const MarketSelectDropdown = () => {
|
|||
<div className="py-3">
|
||||
{spotOrPerp === 'perp' && perpMarkets?.length
|
||||
? perpMarkets.map((m) => {
|
||||
const changeData = getOneDayPerpStats(perpStats, m.name)
|
||||
const isComingSoon = m.oracleLastUpdatedSlot == 0
|
||||
|
||||
const change = changeData.length
|
||||
? ((m.uiPrice - changeData[0].price) /
|
||||
changeData[0].price) *
|
||||
100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className={MARKET_LINK_WRAPPER_CLASSES}
|
||||
|
@ -167,17 +152,7 @@ const MarketSelectDropdown = () => {
|
|||
getDecimalCount(m.tickSize)
|
||||
)}
|
||||
</span>
|
||||
{!loadingPerpStats ? (
|
||||
<Change
|
||||
change={change}
|
||||
suffix="%"
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
<MarketChange market={m} size="small" />
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
|
@ -216,12 +191,6 @@ const MarketSelectDropdown = () => {
|
|||
.map((x) => x)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((m) => {
|
||||
const birdeyeData = birdeyePrices?.length
|
||||
? birdeyePrices.find(
|
||||
(market) =>
|
||||
market.mint === m.serumMarketExternal.toString()
|
||||
)
|
||||
: null
|
||||
const baseBank = group?.getFirstBankByTokenIndex(
|
||||
m.baseTokenIndex
|
||||
)
|
||||
|
@ -238,12 +207,6 @@ const MarketSelectDropdown = () => {
|
|||
getDecimalCount(market.tickSize)
|
||||
).toNumber()
|
||||
}
|
||||
const change =
|
||||
birdeyeData && price
|
||||
? ((price - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className={MARKET_LINK_WRAPPER_CLASSES}
|
||||
|
@ -281,21 +244,7 @@ const MarketSelectDropdown = () => {
|
|||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
{!loadingPrices ? (
|
||||
change ? (
|
||||
<Change
|
||||
change={change}
|
||||
suffix="%"
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-th-fgd-3">–</span>
|
||||
)
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
<MarketChange market={m} size="small" />
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
|
|
|
@ -17,11 +17,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
|||
import useOracleProvider from 'hooks/useOracleProvider'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const OraclePrice = ({
|
||||
setChangePrice,
|
||||
}: {
|
||||
setChangePrice: (price: number) => void
|
||||
}) => {
|
||||
const OraclePrice = () => {
|
||||
const {
|
||||
serumOrPerpMarket,
|
||||
price: stalePrice,
|
||||
|
@ -96,7 +92,6 @@ const OraclePrice = ({
|
|||
|
||||
if (selectedMarket instanceof PerpMarket) {
|
||||
setPrice(uiPrice)
|
||||
setChangePrice(uiPrice)
|
||||
} else {
|
||||
let price
|
||||
if (quoteBank && serumOrPerpMarket) {
|
||||
|
@ -108,7 +103,6 @@ const OraclePrice = ({
|
|||
price = 0
|
||||
}
|
||||
setPrice(price)
|
||||
setChangePrice(price)
|
||||
}
|
||||
},
|
||||
'processed'
|
||||
|
@ -118,14 +112,7 @@ const OraclePrice = ({
|
|||
connection.removeAccountChangeListener(subId)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
connection,
|
||||
selectedMarket,
|
||||
serumOrPerpMarket,
|
||||
setChangePrice,
|
||||
quoteBank,
|
||||
stalePrice,
|
||||
])
|
||||
}, [connection, selectedMarket, serumOrPerpMarket, quoteBank, stalePrice])
|
||||
|
||||
const oracleDecimals = getDecimalCount(serumOrPerpMarket?.tickSize || 0.01)
|
||||
|
||||
|
|
|
@ -119,12 +119,14 @@ const TradeSummary = ({
|
|||
? mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize) || 0
|
||||
parseFloat(tradeForm.baseSize) || 0,
|
||||
HealthType.maint
|
||||
)
|
||||
: mangoAccount.simHealthRatioWithPerpBidUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize) || 0
|
||||
parseFloat(tradeForm.baseSize) || 0,
|
||||
HealthType.maint
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import NotificationCookieStore from '@store/notificationCookieStore'
|
|||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { fetchNotificationSettings } from 'apis/notifications/notificationSettings'
|
||||
import { useIsAuthorized } from './useIsAuthorized'
|
||||
import { DAILY_MILLISECONDS } from 'utils/constants'
|
||||
|
||||
export function useNotificationSettings() {
|
||||
const { publicKey } = useWallet()
|
||||
|
@ -18,7 +19,7 @@ export function useNotificationSettings() {
|
|||
{
|
||||
enabled: !!isAuth,
|
||||
retry: 1,
|
||||
staleTime: 86400000,
|
||||
staleTime: DAILY_MILLISECONDS,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { useMemo } from 'react'
|
||||
import { DAILY_SECONDS, MANGO_DATA_API_URL } from 'utils/constants'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
const fetchPrices = async (market: Serum3Market | PerpMarket | undefined) => {
|
||||
if (!market || market instanceof PerpMarket) return
|
||||
const { baseTokenIndex, quoteTokenIndex } = market
|
||||
const nowTimestamp = Date.now() / 1000
|
||||
const changePriceTimestamp = nowTimestamp - DAILY_SECONDS
|
||||
const changePriceTime = dayjs
|
||||
.unix(changePriceTimestamp)
|
||||
.utc()
|
||||
.format('YYYY-MM-DDTHH:mm:ss[Z]')
|
||||
const promises = [
|
||||
fetch(
|
||||
`${MANGO_DATA_API_URL}/stats/token-price?token-index=${baseTokenIndex}&price-time=${changePriceTime}`
|
||||
),
|
||||
fetch(
|
||||
`${MANGO_DATA_API_URL}/stats/token-price?token-index=${quoteTokenIndex}&price-time=${changePriceTime}`
|
||||
),
|
||||
]
|
||||
try {
|
||||
const data = await Promise.all(promises)
|
||||
const baseTokenPriceData = await data[0].json()
|
||||
const quoteTokenPriceData = await data[1].json()
|
||||
const baseTokenPrice = baseTokenPriceData ? baseTokenPriceData.price : 1
|
||||
const quoteTokenPrice = quoteTokenPriceData ? quoteTokenPriceData.price : 1
|
||||
return { baseTokenPrice, quoteTokenPrice }
|
||||
} catch (e) {
|
||||
console.log('failed to fetch 24hr price data', e)
|
||||
return { baseTokenPrice: 1, quoteTokenPrice: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
export default function use24HourChange(
|
||||
market: Serum3Market | PerpMarket | undefined
|
||||
) {
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
|
||||
const {
|
||||
data: priceData,
|
||||
isLoading: loadingPriceData,
|
||||
isFetching: fetchingPriceData,
|
||||
} = useQuery(['token-prices', market?.name], () => fetchPrices(market), {
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: market && market instanceof Serum3Market,
|
||||
})
|
||||
|
||||
const [currentBasePrice, currentQuotePrice] = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!group || !market || market instanceof PerpMarket)
|
||||
return [undefined, undefined]
|
||||
const baseBank = group.getFirstBankByTokenIndex(market.baseTokenIndex)
|
||||
const quoteBank = group.getFirstBankByTokenIndex(market.quoteTokenIndex)
|
||||
return [baseBank?.uiPrice, quoteBank?.uiPrice]
|
||||
}, [market])
|
||||
|
||||
const perpChange = useMemo(() => {
|
||||
if (
|
||||
!market ||
|
||||
market instanceof Serum3Market ||
|
||||
!perpStats ||
|
||||
!perpStats.length
|
||||
)
|
||||
return
|
||||
const oneDayStats = getOneDayPerpStats(perpStats, market.name)
|
||||
const currentPrice = market.uiPrice
|
||||
const change = oneDayStats.length
|
||||
? ((currentPrice - oneDayStats[0].price) / oneDayStats[0].price) * 100
|
||||
: undefined
|
||||
return change
|
||||
}, [market, perpStats])
|
||||
|
||||
const spotChange = useMemo(() => {
|
||||
if (!market) return
|
||||
if (!currentBasePrice || !currentQuotePrice || !priceData) return
|
||||
const currentPrice = currentBasePrice / currentQuotePrice
|
||||
const oneDayPrice = priceData.baseTokenPrice / priceData.quoteTokenPrice
|
||||
const change = ((currentPrice - oneDayPrice) / oneDayPrice) * 100
|
||||
return change
|
||||
}, [market, priceData])
|
||||
|
||||
const loading = useMemo(() => {
|
||||
if (!market) return false
|
||||
if (market instanceof PerpMarket) return loadingPerpStats
|
||||
return loadingPriceData || fetchingPriceData
|
||||
}, [market, loadingPerpStats, loadingPriceData, fetchingPriceData])
|
||||
|
||||
return { loading, perpChange, spotChange }
|
||||
}
|
|
@ -26,8 +26,6 @@ export default function useAccountHourlyVolumeStats() {
|
|||
|
||||
return {
|
||||
hourlyVolumeData,
|
||||
loadingHourlyVolumeData,
|
||||
fetchingHourlyVolumeData,
|
||||
loadingHourlyVolume,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { fetchAccountPerformance } from 'utils/account'
|
|||
import useMangoAccount from './useMangoAccount'
|
||||
import { useMemo } from 'react'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import { DAILY_MILLISECONDS } from 'utils/constants'
|
||||
|
||||
export default function useAccountPerformanceData() {
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
|
@ -28,7 +29,7 @@ export default function useAccountPerformanceData() {
|
|||
const nowDate = new Date()
|
||||
return performanceData.filter((d) => {
|
||||
const dataTime = new Date(d.time).getTime()
|
||||
return dataTime >= nowDate.getTime() - 86400000
|
||||
return dataTime >= nowDate.getTime() - DAILY_MILLISECONDS
|
||||
})
|
||||
}, [performanceData])
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Serum3Market } from '@blockworks-foundation/mango-v4'
|
|||
import mangoStore from '@store/mangoStore'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { makeApiRequest } from 'apis/birdeye/helpers'
|
||||
import { DAILY_SECONDS } from 'utils/constants'
|
||||
|
||||
export interface BirdeyePriceResponse {
|
||||
address: string
|
||||
|
@ -18,7 +19,7 @@ const fetchBirdeyePrices = async (
|
|||
|
||||
const promises = []
|
||||
const queryEnd = Math.floor(Date.now() / 1000)
|
||||
const queryStart = queryEnd - 86400
|
||||
const queryStart = queryEnd - DAILY_SECONDS
|
||||
for (const mint of mints) {
|
||||
const query = `defi/history_price?address=${mint}&address_type=pair&type=30m&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
promises.push(makeApiRequest(query))
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { fetchIsWhiteListed } from 'apis/whitelist'
|
||||
|
||||
const refetchMs = 24 * 60 * 60 * 1000
|
||||
|
||||
export function useIsWhiteListed() {
|
||||
const { publicKey } = useWallet()
|
||||
const walletPubKey = publicKey?.toBase58()
|
||||
const criteria = walletPubKey
|
||||
|
||||
return useQuery(
|
||||
['isWhiteListed', criteria],
|
||||
() => fetchIsWhiteListed(walletPubKey!),
|
||||
{
|
||||
enabled: !!walletPubKey,
|
||||
staleTime: refetchMs,
|
||||
retry: 1,
|
||||
refetchInterval: refetchMs,
|
||||
}
|
||||
)
|
||||
}
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-feeds": "0.1.7",
|
||||
"@blockworks-foundation/mango-v4": "^0.17.19",
|
||||
"@blockworks-foundation/mango-v4": "^0.17.25",
|
||||
"@headlessui/react": "1.6.6",
|
||||
"@heroicons/react": "2.0.10",
|
||||
"@metaplex-foundation/js": "0.18.3",
|
||||
|
|
|
@ -277,6 +277,15 @@ const MangoAccountDashboard: NextPage = () => {
|
|||
label="Break even price"
|
||||
value={`$${perp.getBreakEvenPriceUi(market).toFixed(6)}`}
|
||||
/>
|
||||
<KeyValuePair
|
||||
label="Max Settle"
|
||||
value={`$${toUiDecimalsForQuote(
|
||||
mangoAccount.perpMaxSettle(
|
||||
group,
|
||||
market.settleTokenIndex
|
||||
)
|
||||
).toFixed(6)}`}
|
||||
/>
|
||||
<KeyValuePair
|
||||
label="Quote Running"
|
||||
value={`$${toUiDecimalsForQuote(
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import RewardsPage from '@components/rewards/RewardsPage'
|
||||
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
|
||||
import type { NextPage } from 'next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, [
|
||||
'common',
|
||||
'notifications',
|
||||
'onboarding',
|
||||
'profile',
|
||||
'rewards',
|
||||
'search',
|
||||
])),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const Rewards: NextPage = () => {
|
||||
const { data: isWhiteListed } = useIsWhiteListed()
|
||||
return <div className="pb-20 md:pb-0">{isWhiteListed && <RewardsPage />}</div>
|
||||
}
|
||||
|
||||
export default Rewards
|
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 412 KiB |
|
@ -5,14 +5,21 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
"liabilities": "Liabilities",
|
||||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
"pnl-history": "PnL History",
|
||||
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
|
||||
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
|
||||
"tooltip-leverage": "Total assets value divided by account equity value",
|
||||
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
|
||||
"tooltip-pnl": "The amount your account has profited or lost",
|
||||
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
|
||||
"tooltip-total-funding": "The sum of perp position funding earned and paid",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"edit-profile-image": "Edit Profile Image",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
"fees": "Fees",
|
||||
"fetching-route": "Finding Route",
|
||||
"free-collateral": "Free Collateral",
|
||||
|
@ -101,6 +102,7 @@
|
|||
"mango": "Mango",
|
||||
"mango-stats": "Mango Stats",
|
||||
"market": "Market",
|
||||
"markets": "Markets",
|
||||
"max": "Max",
|
||||
"max-borrow": "Max Borrow",
|
||||
"more": "More",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"bot": "Bot",
|
||||
"connect-wallet": "Connect Wallet",
|
||||
"mango": "Mango",
|
||||
"seed": "Seed",
|
||||
"whale": "Whale"
|
||||
}
|
|
@ -27,6 +27,7 @@
|
|||
"in-orders": "In Orders",
|
||||
"init-leverage": "Init Leverage",
|
||||
"instantaneous-funding": "Instantaneous Funding Snapshot",
|
||||
"insufficient-perp-liquidity": "Not enough liquidity to close your position. Set a limit order instead.",
|
||||
"interval-seconds": "Interval (seconds)",
|
||||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
"liabilities": "Liabilities",
|
||||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
"pnl-history": "PnL History",
|
||||
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
|
||||
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
|
||||
"tooltip-leverage": "Total assets value divided by account equity value",
|
||||
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
|
||||
"tooltip-pnl": "The amount your account has profited or lost",
|
||||
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
|
||||
"tooltip-total-funding": "The sum of perp position funding earned and paid",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"edit-profile-image": "Edit Profile Image",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
"fees": "Fees",
|
||||
"free-collateral": "Free Collateral",
|
||||
"get-started": "Get Started",
|
||||
|
@ -100,6 +101,7 @@
|
|||
"mango": "Mango",
|
||||
"mango-stats": "Mango Stats",
|
||||
"market": "Market",
|
||||
"markets": "Markets",
|
||||
"max": "Max",
|
||||
"max-borrow": "Max Borrow",
|
||||
"more": "More",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"in-orders": "In Orders",
|
||||
"init-leverage": "Init Leverage",
|
||||
"instantaneous-funding": "Instantaneous Funding Snapshot",
|
||||
"insufficient-perp-liquidity": "Not enough liquidity to close your position. Set a limit order instead.",
|
||||
"interval-seconds": "Interval (seconds)",
|
||||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
"liabilities": "Liabilities",
|
||||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
"pnl-history": "PnL History",
|
||||
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
|
||||
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
|
||||
"tooltip-leverage": "Total assets value divided by account equity value",
|
||||
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
|
||||
"tooltip-pnl": "The amount your account has profited or lost",
|
||||
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
|
||||
"tooltip-total-funding": "The sum of perp position funding earned and paid",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"edit-profile-image": "Edit Profile Image",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
"fees": "Fees",
|
||||
"free-collateral": "Free Collateral",
|
||||
"get-started": "Get Started",
|
||||
|
@ -100,6 +101,7 @@
|
|||
"mango": "Mango",
|
||||
"mango-stats": "Mango Stats",
|
||||
"market": "Market",
|
||||
"markets": "Markets",
|
||||
"max": "Max",
|
||||
"max-borrow": "Max Borrow",
|
||||
"more": "More",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"in-orders": "In Orders",
|
||||
"init-leverage": "Init Leverage",
|
||||
"instantaneous-funding": "Instantaneous Funding Snapshot",
|
||||
"insufficient-perp-liquidity": "Not enough liquidity to close your position. Set a limit order instead.",
|
||||
"interval-seconds": "Interval (seconds)",
|
||||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
"liabilities": "Liabilities",
|
||||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
"pnl-history": "PnL History",
|
||||
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
|
||||
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
|
||||
"tooltip-leverage": "Total assets value divided by account equity value",
|
||||
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
|
||||
"tooltip-pnl": "The amount your account has profited or lost",
|
||||
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
|
||||
"tooltip-total-funding": "The sum of perp position funding earned and paid",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"edit-profile-image": "切换头像",
|
||||
"explorer": "浏览器",
|
||||
"fee": "费用",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
"fees": "费用",
|
||||
"free-collateral": "可用的质押品",
|
||||
"funding": "资金费",
|
||||
|
@ -100,6 +101,7 @@
|
|||
"mango": "Mango",
|
||||
"mango-stats": "Mango统计",
|
||||
"market": "市场",
|
||||
"markets": "Markets",
|
||||
"max": "最多",
|
||||
"max-borrow": "最多借贷",
|
||||
"more": "更多",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"in-orders": "In Orders",
|
||||
"init-leverage": "Init Leverage",
|
||||
"instantaneous-funding": "Instantaneous Funding Snapshot",
|
||||
"insufficient-perp-liquidity": "Not enough liquidity to close your position. Set a limit order instead.",
|
||||
"interval-seconds": "Interval (seconds)",
|
||||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
"liabilities": "Liabilities",
|
||||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
"pnl-history": "PnL History",
|
||||
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
|
||||
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
|
||||
"tooltip-leverage": "Total assets value divided by account equity value",
|
||||
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
|
||||
"tooltip-pnl": "The amount your account has profited or lost",
|
||||
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
|
||||
"tooltip-total-funding": "The sum of perp position funding earned and paid",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"edit-profile-image": "切換頭像",
|
||||
"explorer": "瀏覽器",
|
||||
"fee": "費用",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
"fees": "費用",
|
||||
"free-collateral": "可用的質押品",
|
||||
"funding": "資金費",
|
||||
|
@ -100,6 +101,7 @@
|
|||
"mango": "Mango",
|
||||
"mango-stats": "Mango統計",
|
||||
"market": "市場",
|
||||
"markets": "Markets",
|
||||
"max": "最多",
|
||||
"max-borrow": "最多借貸",
|
||||
"more": "更多",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"init-leverage": "初始槓桿率",
|
||||
"instantaneous-funding": "瞬時資金費率",
|
||||
"insured": "{{token}} 受保險",
|
||||
"insufficient-perp-liquidity": "Not enough liquidity to close your position. Set a limit order instead.",
|
||||
"interval-seconds": "時間間隔 (秒)",
|
||||
"last-updated": "最近更新",
|
||||
"limit": "限價",
|
||||
|
|
|
@ -413,3 +413,21 @@ export type TickerData = {
|
|||
target_volume: string
|
||||
ticker_id: string
|
||||
}
|
||||
|
||||
export interface HealthContribution {
|
||||
asset: string
|
||||
contribution: number
|
||||
contributionDetails?: ContributionDetails
|
||||
hasPerp?: boolean
|
||||
isAsset: boolean
|
||||
}
|
||||
|
||||
export interface PerpMarketContribution {
|
||||
market: string
|
||||
contributionUi: number
|
||||
}
|
||||
|
||||
export interface ContributionDetails {
|
||||
perpMarketContributions: PerpMarketContribution[]
|
||||
spotUi: number
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@ export const JUPITER_API_DEVNET = 'https://api.jup.ag/api/tokens/devnet'
|
|||
export const JUPITER_PRICE_API_MAINNET = 'https://price.jup.ag/v4/'
|
||||
|
||||
export const NOTIFICATION_API = 'https://notifications-api.herokuapp.com/'
|
||||
|
||||
export const NOTIFICATION_API_WEBSOCKET =
|
||||
'wss://notifications-api.herokuapp.com/ws'
|
||||
|
||||
|
@ -128,3 +129,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
|
|||
wbtcpo: true,
|
||||
'wbtc (portal)': true,
|
||||
}
|
||||
|
||||
export const WHITE_LIST_API = 'https://api.mngo.cloud/whitelist/v1/'
|
||||
export const DAILY_SECONDS = 86400
|
||||
export const DAILY_MILLISECONDS = 86400000
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
dependencies:
|
||||
ws "^8.13.0"
|
||||
|
||||
"@blockworks-foundation/mango-v4@^0.17.19":
|
||||
version "0.17.19"
|
||||
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.17.19.tgz#63a7bb62022b71a80eb2c654983af9a0347fac5a"
|
||||
integrity sha512-L9BEsiXOK/dYAC+1MZ1T+7bmf3y3zK8yST2+EQqP7UEhvcy5dk/oHDjEQ7iSm9LcofpJD1OgE3tfVbn7yPttPQ==
|
||||
"@blockworks-foundation/mango-v4@^0.17.25":
|
||||
version "0.17.25"
|
||||
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.17.25.tgz#00c082d5ec0a12aaf1a70d88930bc468e97467a1"
|
||||
integrity sha512-CJOoLuEVY2szMmZq+V/TZx3fSmkmy16OP3FPkA85nPfSXrgWsVhb2WaugN4VyT/1le3g1ppzNvosCfYV+n/XRQ==
|
||||
dependencies:
|
||||
"@coral-xyz/anchor" "^0.27.0"
|
||||
"@project-serum/serum" "0.13.65"
|
||||
|
|
Loading…
Reference in New Issue