add health view

This commit is contained in:
saml33 2023-07-10 21:56:38 +10:00
commit 0ec955cf25
61 changed files with 2919 additions and 1442 deletions

View File

@ -8,10 +8,12 @@ import { DEFAULT_DELEGATE } from './modals/DelegateModal'
import MangoAccountsListModal from './modals/MangoAccountsListModal'
import SheenLoader from './shared/SheenLoader'
import Tooltip from './shared/Tooltip'
import useUnownedAccount from 'hooks/useUnownedAccount'
const AccountsButton = () => {
const { t } = useTranslation('common')
const { mangoAccount, initialLoad } = useMangoAccount()
const { isDelegatedAccount } = useUnownedAccount()
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false)
@ -37,7 +39,9 @@ const AccountsButton = () => {
{mangoAccount.delegate.toString() !== DEFAULT_DELEGATE ? (
<Tooltip
content={t('delegate-account-info', {
address: abbreviateAddress(mangoAccount.delegate),
delegate: isDelegatedAccount
? t('you')
: abbreviateAddress(mangoAccount.delegate),
})}
>
<UserPlusIcon className="ml-1.5 h-4 w-4 text-th-fgd-3" />

View File

@ -1,31 +0,0 @@
import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
import ThemesIcon from './icons/ThemesIcon'
import { THEMES } from './settings/DisplaySettings'
import { LinkButton } from './shared/Button'
import IconDropMenu from './shared/IconDropMenu'
const ThemeSwitcher = () => {
const { t } = useTranslation('settings')
const { theme, setTheme } = useTheme()
return (
<IconDropMenu
icon={<ThemesIcon className="h-5 w-5" />}
panelClassName="rounded-t-none"
>
{THEMES.map((value) => (
<LinkButton
className={`whitespace-nowrap font-normal ${
t(value) === theme ? 'text-th-active' : ''
}`}
onClick={() => setTheme(t(value))}
key={value}
>
{t(value)}
</LinkButton>
))}
</IconDropMenu>
)
}
export default ThemeSwitcher

View File

@ -1,19 +1,18 @@
import { Bank, HealthType, MangoAccount } from '@blockworks-foundation/mango-v4'
import { Disclosure, Transition } from '@headlessui/react'
import { Bank, MangoAccount } from '@blockworks-foundation/mango-v4'
import { Disclosure, Popover, Transition } from '@headlessui/react'
import {
ChevronDownIcon,
EllipsisHorizontalIcon,
XMarkIcon,
} from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import Image from 'next/legacy/image'
import { useRouter } from 'next/router'
import { useCallback, useMemo, useState } from 'react'
import { Fragment, useCallback, useMemo, useState } from 'react'
import { useViewport } from '../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { breakpoints } from '../utils/theme'
import Switch from './forms/Switch'
import ContentBox from './shared/ContentBox'
import IconDropMenu from './shared/IconDropMenu'
import Tooltip from './shared/Tooltip'
import { formatTokenSymbol } from 'utils/tokens'
import useMangoAccount from 'hooks/useMangoAccount'
@ -33,7 +32,6 @@ import useBanksWithBalances, {
import useUnownedAccount from 'hooks/useUnownedAccount'
import useLocalStorageState from 'hooks/useLocalStorageState'
import TokenLogo from './shared/TokenLogo'
import useMangoGroup from 'hooks/useMangoGroup'
const TokenList = () => {
const { t } = useTranslation(['common', 'token', 'trade'])
@ -42,7 +40,6 @@ const TokenList = () => {
true
)
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { group } = useMangoGroup()
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
const totalInterestData = mangoStore(
(s) => s.mangoAccount.interestTotals.data
@ -85,24 +82,6 @@ const TokenList = () => {
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-init-health')}>
<span className="tooltip-underline">
{t('account:init-health')}
</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')}
</span>
</Tooltip>
</div>
</Th>
<Th className="text-right">{t('trade:in-orders')}</Th>
<Th className="text-right">{t('trade:unsettled')}</Th>
<Th className="flex justify-end" id="account-step-nine">
@ -129,9 +108,12 @@ const TokenList = () => {
const bank = b.bank
const tokenBalance = b.balance
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const hasInterestEarned = totalInterestData.find(
(d) => d.symbol === bank.name
(d) =>
d.symbol.toLowerCase() === symbol.toLowerCase() ||
(symbol === 'ETH (Portal)' && d.symbol === 'ETH')
)
const interestAmount = hasInterestEarned
@ -149,41 +131,6 @@ const TokenList = () => {
const unsettled =
spotBalances[bank.mint.toString()]?.unsettled || 0
let initHealth = 0
let maintHealth = 0
if (mangoAccount && group) {
const initHealthContributions =
mangoAccount.getHealthContributionPerAssetUi(
group,
HealthType.init
)
const maintHealthContributions =
mangoAccount.getHealthContributionPerAssetUi(
group,
HealthType.maint
)
initHealth =
initHealthContributions.find(
(cont) => cont.asset === bank.name
)?.contribution || 0
maintHealth =
maintHealthContributions.find(
(cont) => cont.asset === bank.name
)?.contribution || 0
}
const initAssetWeight = bank
.scaledInitAssetWeight(bank.price)
.toFixed(2)
const initLiabWeight = bank
.scaledInitLiabWeight(bank.price)
.toFixed(2)
const maintAssetWeight = bank.maintAssetWeight
.toNumber()
.toFixed(2)
const maintLiabWeight = bank.maintLiabWeight.toNumber().toFixed(2)
return (
<TrBody key={bank.name}>
<Td>
@ -191,7 +138,7 @@ const TokenList = () => {
<div className="mr-2.5 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} />
</div>
<p className="font-body">{bank.name}</p>
<p className="font-body">{symbol}</p>
</div>
</Td>
<Td className="text-right">
@ -201,44 +148,6 @@ const TokenList = () => {
stacked
/>
</Td>
<Td>
<div className="text-right">
<p>
<FormatNumericValue
value={initHealth}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{initHealth > 0
? initAssetWeight
: initHealth < 0
? initLiabWeight
: 0}
x
</p>
</div>
</Td>
<Td>
<div className="text-right">
<p>
<FormatNumericValue
value={maintHealth}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{maintHealth > 0
? maintAssetWeight
: maintHealth < 0
? maintLiabWeight
: 0}
x
</p>
</div>
</Td>
<Td className="text-right">
<BankAmountWithValue
amount={inOrders}
@ -314,9 +223,13 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
)
const tokenBank = bank.bank
const mint = tokenBank.mint
const symbol = tokenBank.name
const symbol = tokenBank.name === 'MSOL' ? 'mSOL' : tokenBank.name
const hasInterestEarned = totalInterestData.find((d) => d.symbol === symbol)
const hasInterestEarned = totalInterestData.find(
(d) =>
d.symbol.toLowerCase() === symbol.toLowerCase() ||
(symbol === 'ETH (Portal)' && d.symbol === 'ETH')
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest * -1 +
@ -342,31 +255,32 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
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-start">
<div className="mr-2.5 mt-0.5 flex flex-shrink-0 items-center">
<div className="flex items-center">
<div className="mr-2.5">
<TokenLogo bank={tokenBank} />
</div>
<div>
<p className="mb-0.5 leading-none text-th-fgd-1">{symbol}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="text-right">
<p className="font-mono text-sm text-th-fgd-2">
<FormatNumericValue
value={tokenBalance}
decimals={tokenBank.mintDecimals}
/>
<span className="mt-0.5 block text-sm leading-none text-th-fgd-4">
<FormatNumericValue
value={
mangoAccount ? tokenBalance * tokenBank.uiPrice : 0
}
decimals={2}
isUsd
/>
</span>
</p>
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue
value={
mangoAccount ? tokenBalance * tokenBank.uiPrice : 0
}
decimals={2}
isUsd
/>
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<ActionsMenu bank={tokenBank} mangoAccount={mangoAccount} />
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
@ -424,6 +338,9 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
</span>
</p>
</div>
<div className="col-span-1">
<ActionsMenu bank={tokenBank} mangoAccount={mangoAccount} />
</div>
</div>
</Disclosure.Panel>
</Transition>
@ -519,65 +436,93 @@ const ActionsMenu = ({
router.push(`/trade?name=${spotMarket?.name}`, undefined, { shallow: true })
}, [spotMarket, router])
const logoURI = useMemo(() => {
if (!bank || !mangoTokens?.length) return ''
return mangoTokens.find((t) => t.address === bank.mint.toString())?.logoURI
}, [bank, mangoTokens])
return (
<>
{isUnownedAccount ? null : (
<IconDropMenu
icon={<EllipsisHorizontalIcon className="h-5 w-5" />}
panelClassName="w-40 shadow-md"
postion="leftBottom"
>
<div className="flex items-center justify-center border-b border-th-bkg-3 pb-2">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image alt="" width="20" height="20" src={logoURI || ''} />
<Popover>
{({ open }) => (
<div className="relative">
<Popover.Button
className={`flex h-10 w-28 items-center justify-center rounded-full border border-th-button text-th-fgd-1 md:h-8 md:w-8 ${
!open ? 'focus-visible:border-th-fgd-2' : ''
} md:hover:border-th-button-hover md:hover:text-th-fgd-1`}
>
{open ? (
<XMarkIcon className="h-5 w-5" />
) : (
<EllipsisHorizontalIcon className="h-5 w-5" />
)}
<span className="ml-2 md:hidden">{t('actions')}</span>
</Popover.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition ease-in duration-100"
enterFrom="scale-90"
enterTo="scale-100"
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel
className={`thin-scroll absolute bottom-12 left-0 z-40 max-h-60 w-32 space-y-2 overflow-auto rounded-md bg-th-bkg-2 p-4 pt-2 md:bottom-0 md:right-12 md:left-auto md:pt-4`}
>
<div className="hidden items-center justify-center border-b border-th-bkg-3 pb-2 md:flex">
<div className="mr-2 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} size={20} />
</div>
<p className="font-body">{formatTokenSymbol(bank.name)}</p>
</div>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'deposit')}
>
{t('deposit')}
</ActionsLinkButton>
{balance < 0 ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'repay')}
>
{t('repay')}
</ActionsLinkButton>
) : null}
{balance && balance > 0 ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() =>
handleShowActionModals(bank.name, 'withdraw')
}
>
{t('withdraw')}
</ActionsLinkButton>
) : null}
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'borrow')}
>
{t('borrow')}
</ActionsLinkButton>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={handleSwap}
>
{t('swap')}
</ActionsLinkButton>
{spotMarket ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={handleTrade}
>
{t('trade')}
</ActionsLinkButton>
) : null}
</Popover.Panel>
</Transition>
</div>
<p className="font-body">{formatTokenSymbol(bank.name)}</p>
</div>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'deposit')}
>
{t('deposit')}
</ActionsLinkButton>
{balance < 0 ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'repay')}
>
{t('repay')}
</ActionsLinkButton>
) : null}
{balance && balance > 0 ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'withdraw')}
>
{t('withdraw')}
</ActionsLinkButton>
) : null}
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'borrow')}
>
{t('borrow')}
</ActionsLinkButton>
<ActionsLinkButton mangoAccount={mangoAccount!} onClick={handleSwap}>
{t('swap')}
</ActionsLinkButton>
{spotMarket ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={handleTrade}
>
{t('trade')}
</ActionsLinkButton>
) : null}
</IconDropMenu>
)}
</Popover>
)}
{showDepositModal ? (
<DepositWithdrawModal

View File

@ -48,7 +48,7 @@ const AccountActions = () => {
const [showDelegateModal, setShowDelegateModal] = useState(false)
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const { connected } = useWallet()
const { isUnownedAccount } = useUnownedAccount()
const { isDelegatedAccount, isUnownedAccount } = useUnownedAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
@ -129,6 +129,7 @@ const AccountActions = () => {
<span className="ml-2">{t('copy-address')}</span>
</ActionsLinkButton>
<ActionsLinkButton
disabled={isDelegatedAccount}
mangoAccount={mangoAccount!}
onClick={() => setShowEditAccountModal(true)}
>
@ -136,6 +137,7 @@ const AccountActions = () => {
<span className="ml-2">{t('edit-account')}</span>
</ActionsLinkButton>
<ActionsLinkButton
disabled={isDelegatedAccount}
mangoAccount={mangoAccount!}
onClick={() => setShowDelegateModal(true)}
>
@ -143,6 +145,7 @@ const AccountActions = () => {
<span className="ml-2">{t('delegate-account')}</span>
</ActionsLinkButton>
<ActionsLinkButton
disabled={isDelegatedAccount}
mangoAccount={mangoAccount!}
onClick={() => setShowCloseAccountModal(true)}
>

View File

@ -26,7 +26,7 @@ const AccountChart = ({
chartToShow: ChartToShow
setChartToShow: (chart: ChartToShow) => void
customTooltip?: ContentType<number, string>
data: PerformanceDataItem[] | HourlyFundingChartData[]
data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined
hideChart: () => void
loading?: boolean
yDecimals?: number
@ -36,7 +36,7 @@ const AccountChart = ({
const [daysToShow, setDaysToShow] = useState<string>('1')
const chartData = useMemo(() => {
if (!data.length) return []
if (!data || !data.length) return []
if (chartToShow === 'cumulative-interest-value') {
return data.map((d) => ({
interest_value:

View File

@ -0,0 +1,509 @@
import mangoStore from '@store/mangoStore'
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 { useQuery } from '@tanstack/react-query'
import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account'
import Tooltip from '@components/shared/Tooltip'
import {
HealthType,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import HealthBar from './HealthBar'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { IconButton } from '@components/shared/Button'
import { CalendarIcon, ChartBarIcon } from '@heroicons/react/20/solid'
import Change from '@components/shared/Change'
import SheenLoader from '@components/shared/SheenLoader'
import { PerformanceDataItem } from 'types'
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats'
const AccountHeroStats = ({
accountPnl,
accountValue,
rollingDailyData,
setShowPnlHistory,
setChartToShow,
}: {
accountPnl: number
accountValue: number
rollingDailyData: PerformanceDataItem[]
setShowPnlHistory: (show: boolean) => void
setChartToShow: (view: ChartToShow) => void
}) => {
const { t } = useTranslation(['common', 'account'])
const { group } = useMangoGroup()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { hourlyVolumeData, loadingHourlyVolume } =
useAccountHourlyVolumeStats()
const totalInterestData = mangoStore(
(s) => s.mangoAccount.interestTotals.data
)
useEffect(() => {
if (mangoAccountAddress) {
const actions = mangoStore.getState().actions
actions.fetchAccountInterestTotals(mangoAccountAddress)
}
}, [mangoAccountAddress])
const {
data: fundingData,
isLoading: loadingFunding,
isFetching: fetchingFunding,
} = useQuery(
['funding', mangoAccountAddress],
() => fetchFundingTotals(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const {
data: volumeTotalData,
isLoading: loadingVolumeTotalData,
isFetching: fetchingVolumeTotalData,
} = useQuery(
['total-volume', mangoAccountAddress],
() => fetchVolumeTotals(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const maintHealth = useMemo(() => {
return group && mangoAccount
? mangoAccount.getHealthRatioUi(group, HealthType.maint)
: 0
}, [mangoAccount, group])
const leverage = useMemo(() => {
if (!group || !mangoAccount) return 0
const assetsValue = toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group).toNumber()
)
if (isNaN(assetsValue / accountValue)) {
return 0
} else {
return Math.abs(1 - assetsValue / accountValue)
}
}, [mangoAccount, group, accountValue])
const rollingDailyPnlChange = useMemo(() => {
if (!accountPnl || !rollingDailyData.length) return 0
return accountPnl - rollingDailyData[0].pnl
}, [accountPnl, rollingDailyData])
const interestTotalValue = useMemo(() => {
if (totalInterestData.length) {
return totalInterestData.reduce(
(a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd),
0
)
}
return 0.0
}, [totalInterestData])
const fundingTotalValue = useMemo(() => {
if (fundingData?.length && mangoAccountAddress) {
return fundingData.reduce(
(a, c) => a + c.long_funding + c.short_funding,
0
)
}
return 0.0
}, [fundingData, mangoAccountAddress])
const oneDayInterestChange = useMemo(() => {
if (rollingDailyData.length) {
const first = rollingDailyData[0]
const latest = rollingDailyData[rollingDailyData.length - 1]
const startDayInterest =
first.borrow_interest_cumulative_usd +
first.deposit_interest_cumulative_usd
const endDayInterest =
latest.borrow_interest_cumulative_usd +
latest.deposit_interest_cumulative_usd
return endDayInterest - startDayInterest
}
return 0.0
}, [rollingDailyData])
const dailyVolume = useMemo(() => {
if (!hourlyVolumeData || !hourlyVolumeData.length) return 0
// Calculate the current time in milliseconds
const currentTime = new Date().getTime()
// Calculate the start time for the last 24 hours in milliseconds
const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000
// Filter the formatted data based on the timestamp
const last24HoursData = hourlyVolumeData.filter((entry) => {
const timestampMs = new Date(entry.time).getTime()
return timestampMs >= last24HoursStartTime && timestampMs <= currentTime
})
const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0)
return volume
}, [hourlyVolumeData])
const handleChartToShow = (viewName: ChartToShow) => {
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 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">
<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>
<Tooltip
className="hidden md:block"
content={t('account:health-breakdown')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('health-contributions')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
</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}%
</p>
<HealthBar health={maintHealth} />
</div>
<span className="flex text-xs font-normal text-th-fgd-4">
<Tooltip
content={t('account:tooltip-leverage')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<span className="tooltip-underline">{t('leverage')}</span>:
</Tooltip>
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue value={leverage} decimals={2} roundUp />x
</span>
</span>
</div>
</div>
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-3 md:border-l lg:col-span-2 lg:border-t-0 xl:col-span-1">
<div id="account-step-five">
<Tooltip
content={t('account:tooltip-free-collateral')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('free-collateral')}
</p>
</Tooltip>
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={
group && mangoAccount
? toUiDecimalsForQuote(
mangoAccount.getCollateralValue(group)
)
: 0
}
decimals={2}
isUsd={true}
/>
</p>
<span className="text-xs font-normal text-th-fgd-4">
<Tooltip
content={t('account:tooltip-total-collateral')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<span className="tooltip-underline">{t('total')}</span>:
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue
value={
group && mangoAccount
? toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group, HealthType.init)
)
: 0
}
decimals={2}
isUsd={true}
/>
</span>
</Tooltip>
</span>
</div>
</div>
<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"
>
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-pnl')}
placement="top-start"
delay={100}
>
<p className="tooltip-underline inline text-sm text-th-fgd-3 xl:text-base">
{t('pnl')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<div className="flex items-center space-x-3">
<Tooltip
className="hidden md:block"
content={t('account:pnl-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('pnl')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
<Tooltip
className="hidden md:block"
content={t('account:pnl-history')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => setShowPnlHistory(true)}
>
<CalendarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
</div>
) : null}
</div>
<p className="mt-1 mb-0.5 text-left text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={accountPnl}
decimals={2}
isUsd={true}
/>
</p>
<div className="flex space-x-1.5">
<Change change={rollingDailyPnlChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 md:border-l lg:col-span-2 lg:border-l-0 xl:col-span-1 xl:border-l xl:border-t-0">
<div id="account-step-six">
<div className="flex w-full items-center justify-between">
<p className="text-sm text-th-fgd-3 xl:text-base">
{t('account:lifetime-volume')}
</p>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:volume-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('hourly-volume')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
{loadingTotalVolume && mangoAccountAddress ? (
<SheenLoader className="mt-1">
<div className="h-7 w-16 bg-th-bkg-2" />
</SheenLoader>
) : (
<p className="mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue value={volumeTotalData || 0} isUsd />
</p>
)}
<span className="flex items-center text-xs font-normal text-th-fgd-4">
<span>{t('account:daily-volume')}</span>:
{loadingHourlyVolume && mangoAccountAddress ? (
<SheenLoader className="ml-1">
<div className="h-3.5 w-10 bg-th-bkg-2" />
</SheenLoader>
) : (
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue
value={dailyVolume}
decimals={2}
isUsd={true}
/>
</span>
)}
</span>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 lg:col-span-2 lg:border-l xl:col-span-1 xl:border-t-0">
<div id="account-step-eight">
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-total-interest')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('total-interest-earned')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:cumulative-interest-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() =>
handleChartToShow('cumulative-interest-value')
}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={interestTotalValue}
decimals={2}
isUsd={true}
/>
</p>
<div className="flex space-x-1.5">
<Change change={oneDayInterestChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 md:border-l lg:col-span-2 xl:col-span-1 xl:border-t-0">
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-total-funding')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('account:total-funding-earned')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:funding-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('hourly-funding')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
{(loadingFunding || fetchingFunding) && mangoAccountAddress ? (
<SheenLoader className="mt-2">
<div className="h-7 w-16 bg-th-bkg-2" />
</SheenLoader>
) : (
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={fundingTotalValue}
decimals={2}
isUsd={true}
/>
</p>
)}
</div>
</div>
</>
)
}
export default AccountHeroStats

View File

@ -1,172 +1,28 @@
import {
HealthType,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useEffect, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import AccountActions from './AccountActions'
import mangoStore from '@store/mangoStore'
import { formatCurrencyValue } from '../../utils/numbers'
import FlipNumbers from 'react-flip-numbers'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { COLORS } from '../../styles/colors'
import { useTheme } from 'next-themes'
import { IconButton } from '../shared/Button'
import {
ArrowsPointingOutIcon,
CalendarIcon,
ChartBarIcon,
} from '@heroicons/react/20/solid'
import { Transition } from '@headlessui/react'
import AccountTabs from './AccountTabs'
import SheenLoader from '../shared/SheenLoader'
import AccountChart from './AccountChart'
import useMangoAccount from '../../hooks/useMangoAccount'
import Change from '../shared/Change'
import Tooltip from '@components/shared/Tooltip'
import {
ANIMATION_SETTINGS_KEY,
MANGO_DATA_API_URL,
// IS_ONBOARDED_KEY
} from 'utils/constants'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from 'hooks/useLocalStorageState'
// import AccountOnboardingTour from '@components/tours/AccountOnboardingTour'
import dayjs from 'dayjs'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import useMangoGroup from 'hooks/useMangoGroup'
import PnlHistoryModal from '@components/modals/PnlHistoryModal'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import HealthBar from './HealthBar'
import AssetsLiabilities from './AssetsLiabilities'
import {
AccountVolumeTotalData,
FormattedHourlyAccountVolumeData,
HourlyAccountVolumeData,
PerformanceDataItem,
TotalAccountFundingItem,
} from 'types'
import { useQuery } from '@tanstack/react-query'
import FundingChart from './FundingChart'
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'
const TABS = ['account-value', 'account:assets-liabilities']
const fetchFundingTotals = async (mangoAccountPk: string) => {
try {
const data = await fetch(
`${MANGO_DATA_API_URL}/stats/funding-account-total?mango-account=${mangoAccountPk}`
)
const res = await data.json()
if (res) {
const entries: [string, Omit<TotalAccountFundingItem, 'market'>][] =
Object.entries(res)
const stats: TotalAccountFundingItem[] = entries
.map(([key, value]) => {
return {
long_funding: value.long_funding * -1,
short_funding: value.short_funding * -1,
market: key,
}
})
.filter((x) => x)
return stats
}
} catch (e) {
console.log('Failed to fetch account funding', e)
}
}
const fetchVolumeTotals = async (mangoAccountPk: string) => {
try {
const [perpTotal, spotTotal] = await Promise.all([
fetch(
`${MANGO_DATA_API_URL}/stats/perp-volume-total?mango-account=${mangoAccountPk}`
),
fetch(
`${MANGO_DATA_API_URL}/stats/spot-volume-total?mango-account=${mangoAccountPk}`
),
])
const [perpTotalData, spotTotalData] = await Promise.all([
perpTotal.json(),
spotTotal.json(),
])
const combinedData = [perpTotalData, spotTotalData]
if (combinedData.length) {
return combinedData.reduce((a, c) => {
const entries: AccountVolumeTotalData[] = Object.entries(c)
const marketVol = entries.reduce((a, c) => {
return a + c[1].volume_usd
}, 0)
return a + marketVol
}, 0)
}
return 0
} catch (e) {
console.log('Failed to fetch spot volume', e)
return 0
}
}
const formatHourlyVolumeData = (data: HourlyAccountVolumeData[]) => {
if (!data || !data.length) return []
const formattedData: FormattedHourlyAccountVolumeData[] = []
// Loop through each object in the original data array
for (const obj of data) {
// Loop through the keys (markets) in each object
for (const market in obj) {
// Loop through the timestamps in each market
for (const timestamp in obj[market]) {
// Find the corresponding entry in the formatted data array based on the timestamp
let entry = formattedData.find((item) => item.time === timestamp)
// If the entry doesn't exist, create a new entry
if (!entry) {
entry = { time: timestamp, total_volume_usd: 0, markets: {} }
formattedData.push(entry)
}
// Increment the total_volume_usd by the volume_usd value
entry.total_volume_usd += obj[market][timestamp].volume_usd
// Add or update the market entry in the markets object
entry.markets[market] = obj[market][timestamp].volume_usd
}
}
}
return formattedData
}
const fetchHourlyVolume = async (mangoAccountPk: string) => {
try {
const [perpHourly, spotHourly] = await Promise.all([
fetch(
`${MANGO_DATA_API_URL}/stats/perp-volume-hourly?mango-account=${mangoAccountPk}`
),
fetch(
`${MANGO_DATA_API_URL}/stats/spot-volume-hourly?mango-account=${mangoAccountPk}`
),
])
const [perpHourlyData, spotHourlyData] = await Promise.all([
perpHourly.json(),
spotHourly.json(),
])
const hourlyVolume = [perpHourlyData, spotHourlyData]
return formatHourlyVolumeData(hourlyVolume)
} catch (e) {
console.log('Failed to fetch spot volume', e)
}
}
export type ChartToShow =
| ''
| 'account-value'
@ -174,143 +30,31 @@ export type ChartToShow =
| 'pnl'
| 'hourly-funding'
| 'hourly-volume'
| 'health-contributions'
const AccountPage = () => {
const { t } = useTranslation(['common', 'account'])
const { connected } = useWallet()
const { group } = useMangoGroup()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const actions = mangoStore.getState().actions
const performanceLoading = mangoStore(
(s) => s.mangoAccount.performance.loading
)
const performanceData = mangoStore((s) => s.mangoAccount.performance.data)
const totalInterestData = mangoStore(
(s) => s.mangoAccount.interestTotals.data
)
const { mangoAccount } = useMangoAccount()
const [chartToShow, setChartToShow] = useState<ChartToShow>('')
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
const { theme } = useTheme()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
// const tourSettings = mangoStore((s) => s.settings.tours)
// const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY)
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS
)
const [activeTab, setActiveTab] = useLocalStorageState(
'accountHeroKey-0.1',
'account-value'
)
useEffect(() => {
if (mangoAccountAddress || (!mangoAccountAddress && connected)) {
actions.fetchAccountPerformance(mangoAccountAddress, 31)
actions.fetchAccountInterestTotals(mangoAccountAddress)
}
}, [actions, mangoAccountAddress, connected])
const {
data: fundingData,
isLoading: loadingFunding,
isFetching: fetchingFunding,
} = useQuery(
['funding', mangoAccountAddress],
() => fetchFundingTotals(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const {
data: volumeTotalData,
isLoading: loadingVolumeTotalData,
isFetching: fetchingVolumeTotalData,
} = useQuery(
['total-volume', mangoAccountAddress],
() => fetchVolumeTotals(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const {
data: hourlyVolumeData,
isLoading: loadingHourlyVolumeData,
isFetching: fetchingHourlyVolumeData,
} = useQuery(
['hourly-volume', mangoAccountAddress],
() => fetchHourlyVolume(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const dailyVolume = useMemo(() => {
if (!hourlyVolumeData || !hourlyVolumeData.length) return 0
// Calculate the current time in milliseconds
const currentTime = new Date().getTime()
// Calculate the start time for the last 24 hours in milliseconds
const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000
// Filter the formatted data based on the timestamp
const last24HoursData = hourlyVolumeData.filter((entry) => {
const timestampMs = new Date(entry.time).getTime()
return timestampMs >= last24HoursStartTime && timestampMs <= currentTime
})
const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0)
return volume
}, [hourlyVolumeData])
const oneDayPerformanceData: PerformanceDataItem[] | [] = useMemo(() => {
if (!performanceData || !performanceData.length) return []
const nowDate = new Date()
return performanceData.filter((d) => {
const dataTime = new Date(d.time).getTime()
return dataTime >= nowDate.getTime() - 86400000
})
}, [performanceData])
const onHoverMenu = (open: boolean, action: string) => {
if (
(!open && action === 'onMouseEnter') ||
(open && action === 'onMouseLeave')
) {
setShowExpandChart(!open)
}
}
const handleShowAccountValueChart = () => {
setChartToShow('account-value')
setShowExpandChart(false)
}
const { performanceData, rollingDailyData } = useAccountPerformanceData()
const { hourlyVolumeData, loadingHourlyVolume } =
useAccountHourlyVolumeStats()
const handleHideChart = () => {
setChartToShow('')
}
const handleCloseDailyPnlModal = () => {
const set = mangoStore.getState().set
set((s) => {
s.mangoAccount.performance.data = oneDayPerformanceData
})
setShowPnlHistory(false)
}
@ -322,104 +66,20 @@ const AccountPage = () => {
]
}, [group, mangoAccount])
const leverage = useMemo(() => {
if (!group || !mangoAccount) return 0
const assetsValue = toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group).toNumber()
)
const pnlChangeToday = useMemo(() => {
if (!accountPnl || !rollingDailyData.length) return 0
const startHour = rollingDailyData.find((item) => {
const itemHour = new Date(item.time).getHours()
return itemHour === 0
})
const startDayPnl = startHour?.pnl
const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0
if (isNaN(assetsValue / accountValue)) {
return 0
} else {
return Math.abs(1 - assetsValue / accountValue)
}
}, [mangoAccount, group, accountValue])
const [accountValueChange, oneDayPnlChange] = useMemo(() => {
if (
accountValue &&
oneDayPerformanceData.length &&
performanceData.length
) {
const accountValueChange =
accountValue - oneDayPerformanceData[0].account_equity
const startDayPnl = oneDayPerformanceData[0].pnl
const endDayPnl =
oneDayPerformanceData[oneDayPerformanceData.length - 1].pnl
const oneDayPnlChange = endDayPnl - startDayPnl
return [accountValueChange, oneDayPnlChange]
}
return [0, 0]
}, [accountValue, oneDayPerformanceData, performanceData])
const interestTotalValue = useMemo(() => {
if (totalInterestData.length) {
return totalInterestData.reduce(
(a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd),
0
)
}
return 0.0
}, [totalInterestData])
const fundingTotalValue = useMemo(() => {
if (fundingData?.length && mangoAccountAddress) {
return fundingData.reduce(
(a, c) => a + c.long_funding + c.short_funding,
0
)
}
return 0.0
}, [fundingData, mangoAccountAddress])
const oneDayInterestChange = useMemo(() => {
if (oneDayPerformanceData.length) {
const first = oneDayPerformanceData[0]
const latest = oneDayPerformanceData[oneDayPerformanceData.length - 1]
const startDayInterest =
first.borrow_interest_cumulative_usd +
first.deposit_interest_cumulative_usd
const endDayInterest =
latest.borrow_interest_cumulative_usd +
latest.deposit_interest_cumulative_usd
return endDayInterest - startDayInterest
}
return 0.0
}, [oneDayPerformanceData])
const maintHealth = useMemo(() => {
return group && mangoAccount
? mangoAccount.getHealthRatioUi(group, HealthType.maint)
: 0
}, [mangoAccount, group])
const handleChartToShow = (
chartName:
| 'pnl'
| 'cumulative-interest-value'
| 'hourly-funding'
| 'hourly-volume'
) => {
if (chartName === 'cumulative-interest-value' || interestTotalValue < -1) {
setChartToShow(chartName)
}
if (chartName === 'pnl') {
setChartToShow(chartName)
}
if (chartName === 'hourly-funding') {
setChartToShow(chartName)
}
if (chartName === 'hourly-volume') {
setChartToShow(chartName)
}
}
return pnlChangeToday
}, [accountPnl, rollingDailyData])
const latestAccountData = useMemo(() => {
if (!accountValue || !performanceData.length) return []
if (!accountValue || !performanceData || !performanceData.length) return []
const latestDataItem = performanceData[performanceData.length - 1]
return [
{
@ -436,10 +96,6 @@ const AccountPage = () => {
]
}, [accountPnl, accountValue, performanceData])
const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData
const loadingHourlyVolume =
fetchingHourlyVolumeData || loadingHourlyVolumeData
return !chartToShow ? (
<>
<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">
@ -459,427 +115,38 @@ const AccountPage = () => {
</button>
))}
</div>
{activeTab === 'account-value' ? (
<div className="flex flex-col md:flex-row md:items-end md:space-x-6">
<div className="mx-auto mt-4 md:mx-0">
<div className="mb-2 flex justify-start font-display text-5xl text-th-fgd-1">
{animationSettings['number-scroll'] ? (
group && mangoAccount ? (
<FlipNumbers
height={48}
width={35}
play
delay={0.05}
duration={1}
numbers={formatCurrencyValue(accountValue, 2)}
/>
) : (
<FlipNumbers
height={48}
width={36}
play
delay={0.05}
duration={1}
numbers={'$0.00'}
/>
)
) : (
<FormatNumericValue
value={accountValue}
isUsd
decimals={2}
/>
)}
</div>
<div className="flex items-center justify-center space-x-1.5 md:justify-start">
<Change change={accountValueChange} prefix="$" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
{!performanceLoading ? (
oneDayPerformanceData.length ? (
<div
className="relative mt-4 flex h-40 items-end md:mt-0 md:h-20 md:w-52 lg:w-60"
onMouseEnter={() =>
onHoverMenu(showExpandChart, 'onMouseEnter')
}
onMouseLeave={() =>
onHoverMenu(showExpandChart, 'onMouseLeave')
}
>
<SimpleAreaChart
color={
accountValueChange >= 0
? COLORS.UP[theme]
: COLORS.DOWN[theme]
}
data={oneDayPerformanceData.concat(latestAccountData)}
name="accountValue"
xKey="time"
yKey="account_equity"
/>
<Transition
appear={true}
className="absolute right-2 bottom-2"
show={showExpandChart || isMobile}
enter="transition ease-in duration-300"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleShowAccountValueChart()}
>
<ArrowsPointingOutIcon className="h-5 w-5" />
</IconButton>
</Transition>
</div>
) : null
) : mangoAccountAddress ? (
<SheenLoader className="mt-4 flex flex-1 md:mt-0">
<div className="h-40 w-full rounded-md bg-th-bkg-2 md:h-20 md:w-52 lg:w-60" />
</SheenLoader>
) : null}
</div>
) : null}
{activeTab === 'account:assets-liabilities' ? (
<div className="w-full">
<div className="md:h-24">
{activeTab === 'account-value' ? (
<AccountValue
accountValue={accountValue}
latestAccountData={latestAccountData}
rollingDailyData={rollingDailyData}
setChartToShow={setChartToShow}
/>
) : null}
{activeTab === 'account:assets-liabilities' ? (
<AssetsLiabilities isMobile={isMobile} />
</div>
) : null}
) : null}
</div>
</div>
<div className="mt-6 mb-1 lg:mt-0">
<AccountActions />
</div>
</div>
<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 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="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}%
</p>
<HealthBar health={maintHealth} />
</div>
<span className="flex text-xs font-normal text-th-fgd-4">
<Tooltip
content={t('account:tooltip-leverage')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<span className="tooltip-underline">{t('leverage')}</span>:
</Tooltip>
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue value={leverage} decimals={2} roundUp />x
</span>
</span>
</div>
</div>
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-3 md:border-l lg:col-span-2 lg:border-t-0 xl:col-span-1">
<div id="account-step-five">
<Tooltip
content={t('account:tooltip-free-collateral')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('free-collateral')}
</p>
</Tooltip>
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={
group && mangoAccount
? toUiDecimalsForQuote(
mangoAccount.getCollateralValue(group)
)
: 0
}
decimals={2}
isUsd={true}
/>
</p>
<span className="text-xs font-normal text-th-fgd-4">
<Tooltip
content={t('account:tooltip-total-collateral')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<span className="tooltip-underline">{t('total')}</span>:
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue
value={
group && mangoAccount
? toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group, HealthType.init)
)
: 0
}
decimals={2}
isUsd={true}
/>
</span>
</Tooltip>
</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
id="account-step-seven"
className="flex w-full flex-col items-start"
>
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-pnl')}
placement="top-start"
delay={100}
>
<p className="tooltip-underline inline text-sm text-th-fgd-3 xl:text-base">
{t('pnl')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<div className="flex items-center space-x-3">
<Tooltip
className="hidden md:block"
content={t('account:pnl-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('pnl')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
<Tooltip
className="hidden md:block"
content={t('account:pnl-history')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => setShowPnlHistory(true)}
>
<CalendarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
</div>
) : null}
</div>
<p className="mt-1 mb-0.5 text-left text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={accountPnl}
decimals={2}
isUsd={true}
/>
</p>
<div className="flex space-x-1.5">
<Change change={oneDayPnlChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 md:border-l lg:col-span-2 lg:border-l-0 xl:col-span-1 xl:border-l xl:border-t-0">
<div id="account-step-six">
<div className="flex w-full items-center justify-between">
<p className="text-sm text-th-fgd-3 xl:text-base">
{t('account:lifetime-volume')}
</p>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:volume-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('hourly-volume')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
{loadingTotalVolume && mangoAccountAddress ? (
<SheenLoader className="mt-1">
<div className="h-7 w-16 bg-th-bkg-2" />
</SheenLoader>
) : (
<p className="mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue value={volumeTotalData || 0} isUsd />
</p>
)}
<span className="flex items-center text-xs font-normal text-th-fgd-4">
<span>{t('account:daily-volume')}</span>:
{loadingHourlyVolume && mangoAccountAddress ? (
<SheenLoader className="ml-1">
<div className="h-3.5 w-10 bg-th-bkg-2" />
</SheenLoader>
) : (
<span className="ml-1 font-mono text-th-fgd-2">
<FormatNumericValue
value={dailyVolume}
decimals={2}
isUsd={true}
/>
</span>
)}
</span>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 lg:col-span-2 lg:border-l xl:col-span-1 xl:border-t-0">
<div id="account-step-eight">
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-total-interest')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('total-interest-earned')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:cumulative-interest-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() =>
handleChartToShow('cumulative-interest-value')
}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={interestTotalValue}
decimals={2}
isUsd={true}
/>
</p>
<div className="flex space-x-1.5">
<Change change={oneDayInterestChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 md:border-l lg:col-span-2 xl:col-span-1 xl:border-t-0">
<div className="flex w-full items-center justify-between">
<Tooltip
content={t('account:tooltip-total-funding')}
maxWidth="20rem"
placement="top-start"
delay={100}
>
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
{t('account:total-funding-earned')}
</p>
</Tooltip>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:funding-chart')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleChartToShow('hourly-funding')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
{(loadingFunding || fetchingFunding) && mangoAccountAddress ? (
<SheenLoader className="mt-2">
<div className="h-7 w-16 bg-th-bkg-2" />
</SheenLoader>
) : (
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
<FormatNumericValue
value={fundingTotalValue}
decimals={2}
isUsd={true}
/>
</p>
)}
</div>
</div>
<AccountHeroStats
accountPnl={accountPnl}
accountValue={accountValue}
rollingDailyData={rollingDailyData}
setChartToShow={setChartToShow}
setShowPnlHistory={setShowPnlHistory}
/>
<AccountTabs />
{/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? (
<AccountOnboardingTour />
) : null} */}
{showPnlHistory ? (
<PnlHistoryModal
pnlChangeToday={oneDayPnlChange}
pnlChangeToday={pnlChangeToday}
isOpen={showPnlHistory}
onClose={handleCloseDailyPnlModal}
/>
@ -891,7 +158,7 @@ const AccountPage = () => {
<AccountChart
chartToShow="account-value"
setChartToShow={setChartToShow}
data={performanceData.concat(latestAccountData)}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="account_equity"
/>
@ -899,7 +166,7 @@ const AccountPage = () => {
<AccountChart
chartToShow="pnl"
setChartToShow={setChartToShow}
data={performanceData.concat(latestAccountData)}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="pnl"
/>
@ -911,6 +178,8 @@ const AccountPage = () => {
hideChart={handleHideChart}
loading={loadingHourlyVolume}
/>
) : chartToShow === 'health-contributions' ? (
<HealthContributions hideView={handleHideChart} />
) : (
<AccountChart
chartToShow="cumulative-interest-value"

View File

@ -0,0 +1,148 @@
import { formatCurrencyValue } from '../../utils/numbers'
import FlipNumbers from 'react-flip-numbers'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { COLORS } from '../../styles/colors'
import { IconButton } from '../shared/Button'
import { ArrowsPointingOutIcon } from '@heroicons/react/20/solid'
import { Transition } from '@headlessui/react'
import SheenLoader from '../shared/SheenLoader'
import Change from '../shared/Change'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { useTheme } from 'next-themes'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { ANIMATION_SETTINGS_KEY } from 'utils/constants'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { PerformanceDataItem } from 'types'
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 useAccountPerformanceData from 'hooks/useAccountPerformanceData'
const AccountValue = ({
accountValue,
latestAccountData,
rollingDailyData,
setChartToShow,
}: {
accountValue: number
latestAccountData: PerformanceDataItem[]
rollingDailyData: PerformanceDataItem[]
setChartToShow: (chart: ChartToShow) => void
}) => {
const { t } = useTranslation('common')
const { theme } = useTheme()
const { group } = useMangoGroup()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS
)
const { width } = useViewport()
const { performanceLoading: loading } = useAccountPerformanceData()
const isMobile = width ? width < breakpoints.md : false
const accountValueChange = useMemo(() => {
if (!accountValue || !rollingDailyData.length) return 0
const accountValueChange = accountValue - rollingDailyData[0].account_equity
return accountValueChange
}, [accountValue, rollingDailyData])
const onHoverMenu = (open: boolean, action: string) => {
if (
(!open && action === 'onMouseEnter') ||
(open && action === 'onMouseLeave')
) {
setShowExpandChart(!open)
}
}
const handleShowAccountValueChart = () => {
setChartToShow('account-value')
setShowExpandChart(false)
}
return (
<div className="flex flex-col md:flex-row md:items-end md:space-x-6">
<div className="mx-auto mt-4 md:mx-0">
<div className="mb-2 flex justify-start font-display text-5xl text-th-fgd-1">
{animationSettings['number-scroll'] ? (
group && mangoAccount ? (
<FlipNumbers
height={48}
width={35}
play
delay={0.05}
duration={1}
numbers={formatCurrencyValue(accountValue, 2)}
/>
) : (
<FlipNumbers
height={48}
width={36}
play
delay={0.05}
duration={1}
numbers={'$0.00'}
/>
)
) : (
<FormatNumericValue value={accountValue} isUsd decimals={2} />
)}
</div>
<div className="flex items-center justify-center space-x-1.5 md:justify-start">
<Change change={accountValueChange} prefix="$" />
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
{!loading ? (
rollingDailyData.length ? (
<div
className="relative mt-4 flex h-40 items-end md:mt-0 md:h-20 md:w-52 lg:w-60"
onMouseEnter={() => onHoverMenu(showExpandChart, 'onMouseEnter')}
onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave')}
>
<SimpleAreaChart
color={
accountValueChange >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={rollingDailyData.concat(latestAccountData)}
name="accountValue"
xKey="time"
yKey="account_equity"
/>
<Transition
appear={true}
className="absolute right-2 bottom-2"
show={showExpandChart || isMobile}
enter="transition ease-in duration-300"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleShowAccountValueChart()}
>
<ArrowsPointingOutIcon className="h-5 w-5" />
</IconButton>
</Transition>
</div>
) : null
) : mangoAccountAddress ? (
<SheenLoader className="mt-4 flex flex-1 md:mt-0">
<div className="h-40 w-full rounded-md bg-th-bkg-2 md:h-20 md:w-52 lg:w-60" />
</SheenLoader>
) : null}
</div>
)
}
export default AccountValue

View File

@ -4,17 +4,19 @@ import { ReactNode } from 'react'
const ActionsLinkButton = ({
children,
disabled,
mangoAccount,
onClick,
}: {
children: ReactNode
disabled?: boolean
mangoAccount: MangoAccount
onClick: () => void
}) => {
return (
<LinkButton
className="w-full whitespace-nowrap text-left font-normal"
disabled={!mangoAccount}
disabled={!mangoAccount || disabled}
onClick={onClick}
>
{children}

View File

@ -5,6 +5,13 @@ import useMangoAccount from 'hooks/useMangoAccount'
import { useMemo } from 'react'
import { HealthType } from '@blockworks-foundation/mango-v4'
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import TokenLogo from '@components/shared/TokenLogo'
import { useTranslation } from 'next-i18next'
import MarketLogos from '@components/trade/MarketLogos'
import BankAmountWithValue from '@components/shared/BankAmountWithValue'
import FormatNumericValue from '@components/shared/FormatNumericValue'
export interface HealthContribution {
asset: string
@ -13,7 +20,7 @@ export interface HealthContribution {
}
const HealthContributions = ({ hideView }: { hideView: () => void }) => {
// const { t } = useTranslation('account')
const { t } = useTranslation(['common', 'account'])
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
@ -21,7 +28,7 @@ const HealthContributions = ({ hideView }: { hideView: () => void }) => {
if (!group || !mangoAccount) return [[], []]
const init = mangoAccount
.getHealthContributionPerAssetUi(group, HealthType.init)
.filter((asset) => Math.abs(asset.contribution) > 0.01)
// .filter((asset) => Math.abs(asset.contribution) > 0.01)
.map((item) => ({
...item,
contribution: Math.abs(item.contribution),
@ -29,7 +36,7 @@ const HealthContributions = ({ hideView }: { hideView: () => void }) => {
}))
const maint = mangoAccount
.getHealthContributionPerAssetUi(group, HealthType.maint)
.filter((asset) => Math.abs(asset.contribution) > 0.01)
// .filter((asset) => Math.abs(asset.contribution) > 0.01)
.map((item) => ({
...item,
contribution: Math.abs(item.contribution),
@ -38,36 +45,314 @@ const HealthContributions = ({ hideView }: { hideView: () => void }) => {
return [init, maint]
}, [group, mangoAccount])
console.log(initHealthContributions)
const [initHealthMarkets, initHealthTokens] = useMemo(() => {
if (!initHealthContributions.length) return [[], []]
const splitData = initHealthContributions.reduce(
(
acc: { market: HealthContribution[]; token: HealthContribution[] },
obj: HealthContribution
) => {
if (obj.asset.includes('/')) {
acc.market.push(obj)
} else {
acc.token.push(obj)
}
return acc
},
{ market: [], token: [] }
)
return [splitData.market, splitData.token]
}, [initHealthContributions])
return (
const [maintHealthMarkets, maintHealthTokens] = useMemo(() => {
if (!maintHealthContributions.length) return [[], []]
const splitData = maintHealthContributions.reduce(
(
acc: { market: HealthContribution[]; token: HealthContribution[] },
obj: HealthContribution
) => {
if (obj.asset.includes('/')) {
acc.market.push(obj)
} else {
acc.token.push(obj)
}
return acc
},
{ market: [], token: [] }
)
return [splitData.market, splitData.token]
}, [maintHealthContributions])
return group && mangoAccount ? (
<>
<div className="hide-scroll mb-3 flex h-14 items-center space-x-4 overflow-x-auto border-b border-th-bkg-3">
<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>
{/* <div className="flex space-x-2">
{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
? '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)}
key={tab}
>
{t(tab)}
</button>
))}
</div> */}
<h2 className="text-lg">{t('account:health-breakdown')}</h2>
</div>
<HealthContributionsChart data={maintHealthContributions} />
<div className="mx-auto grid max-w-[1140px] grid-cols-2 gap-8 p-6">
<div className="col-span-1 flex h-full flex-col items-center">
<Tooltip content={t('account:tooltip-init-health')}>
<h3 className="tooltip-underline mb-3 text-base">
{t('account:init-health')}
</h3>
</Tooltip>
<HealthContributionsChart data={initHealthContributions} />
</div>
<div className="col-span-1 flex flex-col items-center">
<Tooltip content={t('account:tooltip-maint-health')}>
<h3 className="tooltip-underline mb-3 text-base">
{t('account:maint-health')}
</h3>
</Tooltip>
<HealthContributionsChart data={maintHealthContributions} />
</div>
</div>
{maintHealthTokens.length ? (
<div className="border-t border-th-bkg-3 py-6">
<h2 className="mb-1 px-6 text-lg">{t('tokens')}</h2>
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th>
<div className="flex justify-end">
<span>{t('balance')}</span>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-init-health')}>
<span className="tooltip-underline">
{t('account:init-health')}
</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')}
</span>
</Tooltip>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{maintHealthTokens
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const { asset, contribution } = 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 initContribution =
initHealthTokens.find((cont) => cont.asset === asset)
?.contribution || 0
return (
<TrBody key={asset}>
<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 ? (
<BankAmountWithValue
amount={balance}
bank={bank}
stacked
/>
) : (
''
)}
</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={contribution}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{contribution > 0
? maintAssetWeight.toFixed(2)
: contribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
) : null}
{maintHealthMarkets.length ? (
<>
<h2 className="mb-1 px-6 text-lg">{t('markets')}</h2>
<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')}
</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')}
</span>
</Tooltip>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{maintHealthMarkets
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const { asset, contribution } = 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 initContribution =
initHealthMarkets.find((cont) => cont.asset === asset)
?.contribution || 0
return (
<TrBody key={asset}>
<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={contribution}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{contribution > 0
? maintAssetWeight.toFixed(2)
: contribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</>
) : null}
</>
)
) : null
}
export default HealthContributions

View File

@ -1,126 +1,188 @@
import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
import {
Cell,
Pie,
PieChart,
ResponsiveContainer,
Sector,
SectorProps,
} from 'recharts'
import { COLORS } from 'styles/colors'
import { formatCurrencyValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import { HealthContribution } from './HealthContributions'
interface CustomTooltipProps {
active?: boolean
contributions: HealthContribution[]
payload?: { payload: HealthContribution }[]
label?: string | number
}
import { useMemo, useState } from 'react'
import { formatCurrencyValue } from 'utils/numbers'
import mangoStore from '@store/mangoStore'
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import TokenLogo from '@components/shared/TokenLogo'
import MarketLogos from '@components/trade/MarketLogos'
const HealthContributionsChart = ({ data }: { data: HealthContribution[] }) => {
const { t } = useTranslation('account')
const { t } = useTranslation(['common', 'account'])
const { theme } = useTheme()
const [activeIndex, setActiveIndex] = useState<number | undefined>(undefined)
const pieSizes = { size: 160, outerRadius: 80, innerRadius: 64 }
const { size, outerRadius, innerRadius } = pieSizes
const CustomTooltip = ({
active,
contributions,
payload,
}: CustomTooltipProps) => {
if (active && payload) {
const isActivePayload = payload[0].payload.asset
const total = contributions.reduce((a, c) => {
const assetOrLiability = c.isAsset ? 1 : -1
return a + c.contribution * assetOrLiability
}, 0)
return (
<div className="rounded-md bg-th-bkg-2 p-4 focus:outline-none">
<div className="space-y-1">
{contributions
.sort((a, b) => b.contribution - a.contribution)
.map((asset) => {
const assetOrLiability = asset.isAsset ? 1 : -1
return (
<div
className="flex items-center justify-between"
key={asset.asset + asset.contribution}
>
<p
className={`whitespace-nowrap ${
isActivePayload === asset.asset ? 'text-th-active' : ''
}`}
>
{formatTokenSymbol(asset.asset)}
</p>
<p
className={`pl-4 font-mono ${
asset.isAsset ? 'text-th-up' : 'text-th-down'
}`}
>
{formatCurrencyValue(
asset.contribution * assetOrLiability
)}
</p>
</div>
)
})}
</div>
<div className="mt-3 flex justify-between border-t border-th-bkg-4 pt-3">
<p>{t('total')}</p>
<p className="pl-4 font-mono text-th-fgd-2">
{formatCurrencyValue(total)}
</p>
</div>
</div>
)
}
return null
const handleLegendClick = (entry: HealthContribution, index: number) => {
setActiveIndex(index)
}
return (
<div className="flex flex-col items-center pt-4 md:flex-row md:space-x-4">
{data.length ? (
<PieChart width={size} height={size}>
<Tooltip
cursor={{
fill: 'var(--bkg-2)',
opacity: 0.5,
}}
content={<CustomTooltip contributions={data} />}
position={{ x: 88, y: 0 }}
/>
<Pie
cursor="pointer"
data={data}
dataKey="contribution"
cx="50%"
cy="50%"
outerRadius={outerRadius}
innerRadius={innerRadius}
minAngle={2}
startAngle={90}
endAngle={450}
>
{data.map((entry, index) => {
const fillColor = entry.isAsset
? COLORS.UP[theme]
: COLORS.DOWN[theme]
return (
<Cell
key={`cell-${index}`}
fill={fillColor}
stroke={COLORS.BKG1[theme]}
strokeWidth={1}
/>
)
})}
</Pie>
</PieChart>
const handleMouseEnter = (data: HealthContribution, index: number) => {
setActiveIndex(index)
}
const handleMouseLeave = () => {
setActiveIndex(undefined)
}
const pieSizes = { size: 240, outerRadius: 120, innerRadius: 96 }
const { size, outerRadius, innerRadius } = pieSizes
const filteredData = useMemo(() => {
if (!data.length) return []
return data
.filter((cont) => cont.contribution > 0.01)
.sort((a, b) => {
const aMultiplier = a.isAsset ? 1 : -1
const bMultiplier = b.isAsset ? 1 : -1
return b.contribution * bMultiplier - a.contribution * aMultiplier
})
}, [data])
const [chartHeroAsset, chartHeroValue] = useMemo(() => {
if (!filteredData.length) return [undefined, undefined]
if (activeIndex === undefined) {
const value = filteredData.reduce((a, c) => {
const assetOrLiabMultiplier = c.isAsset ? 1 : -1
return a + c.contribution * assetOrLiabMultiplier
}, 0)
return [t('total'), value]
} else {
const asset = filteredData[activeIndex]
const assetOrLiabMultiplier = asset.isAsset ? 1 : -1
const value = asset.contribution * assetOrLiabMultiplier
return [asset.asset, value]
}
}, [activeIndex, filteredData])
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>
)
}
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('/')
if (isSpotMarket) {
const market = group.getSerum3MarketByName(asset)
return market ? (
<MarketLogos market={market} size="small" />
) : (
<div className="h-20 w-20 rounded-full ring-[8px] ring-inset ring-th-bkg-3" />
)}
<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" />
)
}
}
return filteredData.length ? (
<div className="flex h-full w-full flex-col items-center">
<div className="relative h-[248px] w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart width={size} height={size}>
<Pie
cursor="pointer"
data={filteredData}
dataKey="contribution"
cx="50%"
cy="50%"
outerRadius={outerRadius}
innerRadius={innerRadius}
minAngle={2}
startAngle={90}
endAngle={450}
activeIndex={activeIndex}
activeShape={renderActiveShape}
onClick={handleLegendClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{filteredData.map((entry, index) => {
const fillColor = entry.isAsset
? COLORS.UP[theme]
: COLORS.DOWN[theme]
return (
<Cell
key={`cell-${index}`}
fill={fillColor}
opacity={1 / ((index + 1) / 2.5)}
stroke="none"
/>
)
})}
</Pie>
</PieChart>
</ResponsiveContainer>
{chartHeroValue ? (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<p>{chartHeroAsset}</p>
<span className="text-xl font-bold">
{formatCurrencyValue(chartHeroValue)}
</span>
</div>
) : null}
</div>
<div className="mt-6 flex max-w-[420px] flex-wrap justify-center space-x-4">
{filteredData.map((d, i) => (
<div
key={d.asset + i}
className={`default-transition flex h-7 cursor-pointer items-center ${
activeIndex !== undefined && activeIndex !== i ? 'opacity-60' : ''
}`}
onClick={() => handleLegendClick(d, i)}
onMouseEnter={() => handleMouseEnter(d, i)}
onMouseLeave={handleMouseLeave}
>
{renderLegendLogo(d.asset)}
<p
className={`default-transition ${
activeIndex === i ? 'text-th-fgd-1' : ''
}`}
>
{d.asset}
</p>
</div>
))}
</div>
</div>
)
) : null
}
export default HealthContributionsChart

View File

@ -1,5 +1,4 @@
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import Tooltip from '@components/shared/Tooltip'
import useLocalStorageState from 'hooks/useLocalStorageState'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
@ -94,16 +93,9 @@ const BorrowPage = () => {
<div className="flex flex-col border-b border-th-bkg-3 px-4 py-4 md:px-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col md:flex-row">
<div className="pb-4 md:pr-6 md:pb-0">
<Tooltip
maxWidth="20rem"
placement="bottom-start"
content="The value of your assets (deposits) minus the value of your liabilities (borrows)."
delay={100}
>
<p className="mb-0.5 text-base">
{t('borrow:current-borrow-value')}
</p>
</Tooltip>
<p className="mb-0.5 text-base">
{t('borrow:current-borrow-value')}
</p>
<div className="flex items-center font-display text-5xl text-th-fgd-1">
{animationSettings['number-scroll'] ? (
group && mangoAccount ? (

View File

@ -51,7 +51,7 @@ const DelegateModal = ({ isOpen, onClose }: ModalProps) => {
title:
address !== DEFAULT_DELEGATE
? t('delegate-account-info', {
address: abbreviateAddress(new PublicKey(address)),
delegate: abbreviateAddress(new PublicKey(address)),
})
: 'Account delegation removed',
type: 'success',

View File

@ -29,6 +29,7 @@ import { DEFAULT_DELEGATE } from './DelegateModal'
import Tooltip from '@components/shared/Tooltip'
import { abbreviateAddress } from 'utils/formatting'
import { handleCopyAddress } from '@components/account/AccountActions'
import useUnownedAccount from 'hooks/useUnownedAccount'
const MangoAccountsListModal = ({
isOpen,
@ -38,6 +39,7 @@ const MangoAccountsListModal = ({
onClose: () => void
}) => {
const { t } = useTranslation('common')
const { isDelegatedAccount } = useUnownedAccount()
const { mangoAccount, initialLoad: loading } = useMangoAccount()
const mangoAccounts = mangoStore((s) => s.mangoAccounts)
const actions = mangoStore.getState().actions
@ -147,7 +149,9 @@ const MangoAccountsListModal = ({
<div className="mr-2">
<Tooltip
content={t('delegate-account-info', {
address: abbreviateAddress(acc.delegate),
delegate: isDelegatedAccount
? t('you')
: abbreviateAddress(acc.delegate),
})}
>
<UserPlusIcon className="ml-1.5 h-4 w-4 text-th-fgd-3" />

View File

@ -1,14 +1,13 @@
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import { useEffect, useMemo } from 'react'
import useMangoAccount from 'hooks/useMangoAccount'
import { useMemo } from 'react'
import dayjs from 'dayjs'
import Change from '@components/shared/Change'
import SheenLoader from '@components/shared/SheenLoader'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
import { PerformanceDataItem } from 'types'
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
interface PnlChange {
time: string
@ -27,19 +26,11 @@ const PnlHistoryModal = ({
pnlChangeToday,
}: ModalCombinedProps) => {
const { t } = useTranslation('account')
const { mangoAccountAddress } = useMangoAccount()
const actions = mangoStore.getState().actions
const loading = mangoStore((s) => s.mangoAccount.performance.loading)
const performanceData = mangoStore((s) => s.mangoAccount.performance.data)
useEffect(() => {
if (mangoAccountAddress) {
actions.fetchAccountPerformance(mangoAccountAddress, 31)
}
}, [actions, mangoAccountAddress])
const { performanceData, performanceLoading: loading } =
useAccountPerformanceData()
const dailyValues: PnlChange[] = useMemo(() => {
if (!performanceData.length) return []
if (!performanceData || !performanceData.length) return []
const dailyPnl = performanceData.filter((d: PerformanceDataItem) => {
const startTime = new Date().getTime() - 30 * 86400000

View File

@ -0,0 +1,494 @@
import ButtonGroup from '@components/forms/ButtonGroup'
import Checkbox from '@components/forms/Checkbox'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import Button, { IconButton } from '@components/shared/Button'
import InlineNotification from '@components/shared/InlineNotification'
import Modal from '@components/shared/Modal'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import { KeyIcon, TrashIcon } from '@heroicons/react/20/solid'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
import { useState } from 'react'
import { ModalProps } from 'types/modal'
import { HOT_KEYS_KEY } from 'utils/constants'
export type HotKey = {
ioc: boolean
keySequence: string
margin: boolean
orderSide: 'buy' | 'sell'
orderSizeType: 'percentage' | 'notional'
orderSize: string
orderType: 'limit' | 'market'
orderPrice: string
postOnly: boolean
reduceOnly: boolean
}
const HotKeysSettings = () => {
const { t } = useTranslation(['common', 'settings', 'trade'])
const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [showHotKeyModal, setShowHotKeyModal] = useState(false)
const handleDeleteKey = (key: string) => {
const newKeys = hotKeys.filter((hk: HotKey) => hk.keySequence !== key)
setHotKeys([...newKeys])
}
return (
<>
<div className="mb-4 flex items-center justify-between">
<div className="pr-6">
<h2 className="mb-1 text-base">{t('settings:hot-keys')}</h2>
<p>{t('settings:hot-keys-desc')}</p>
</div>
{hotKeys.length ? (
<Button
className="whitespace-nowrap"
disabled={hotKeys.length >= 20}
onClick={() => setShowHotKeyModal(true)}
secondary
>
{t('settings:new-hot-key')}
</Button>
) : null}
</div>
{hotKeys.length === 20 ? (
<div className="mb-4">
<InlineNotification
type="warning"
desc={t('settings:error-key-limit-reached')}
/>
</div>
) : null}
{hotKeys.length ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('settings:key-sequence')}</Th>
<Th className="text-right">{t('trade:order-type')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('settings:options')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{hotKeys.map((hk: HotKey) => {
const {
keySequence,
orderSide,
orderPrice,
orderSize,
orderSizeType,
orderType,
ioc,
margin,
reduceOnly,
postOnly,
} = hk
const size =
orderSizeType === 'percentage'
? t('settings:percentage-of-max', { size: orderSize })
: `$${orderSize}`
const price = orderPrice
? `${orderPrice}% ${
orderSide === 'buy'
? t('settings:below')
: t('settings:above')
} oracle`
: t('trade:market')
const options = {
margin: margin,
IOC: ioc,
post: postOnly,
reduce: reduceOnly,
}
return (
<TrBody key={keySequence} className="text-right">
<Td className="text-left">{keySequence}</Td>
<Td className="text-right">{t(`trade:${orderType}`)}</Td>
<Td className="text-right">{t(orderSide)}</Td>
<Td className="text-right">{size}</Td>
<Td className="text-right">{price}</Td>
<Td className="text-right">
{Object.entries(options).map((e) => {
return e[1]
? `${e[0] !== 'margin' ? ', ' : ''}${t(
`trade:${e[0]}`
)}`
: ''
})}
</Td>
<Td>
<div className="flex justify-end">
<IconButton
onClick={() => handleDeleteKey(keySequence)}
size="small"
>
<TrashIcon className="h-4 w-4" />
</IconButton>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="rounded-lg border border-th-bkg-3 p-6">
<div className="flex flex-col items-center">
<KeyIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p className="mb-4">{t('settings:no-hot-keys')}</p>
<Button onClick={() => setShowHotKeyModal(true)}>
<div className="flex items-center">
{t('settings:new-hot-key')}
</div>
</Button>
</div>
</div>
)}
{showHotKeyModal ? (
<HotKeyModal
isOpen={showHotKeyModal}
onClose={() => setShowHotKeyModal(false)}
/>
) : null}
</>
)
}
export default HotKeysSettings
type FormErrors = Partial<Record<keyof HotKeyForm, string>>
type HotKeyForm = {
baseKey: string
triggerKey: string
price: string
side: 'buy' | 'sell'
size: string
sizeType: 'percentage' | 'notional'
orderType: 'limit' | 'market'
ioc: boolean
post: boolean
margin: boolean
reduce: boolean
}
const DEFAULT_FORM_VALUES: HotKeyForm = {
baseKey: 'shift',
triggerKey: '',
price: '',
side: 'buy',
size: '',
sizeType: 'percentage',
orderType: 'limit',
ioc: false,
post: false,
margin: false,
reduce: false,
}
const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
const { t } = useTranslation(['common', 'settings', 'trade'])
const [hotKeys, setHotKeys] = useLocalStorageState<HotKey[]>(HOT_KEYS_KEY, [])
const [hotKeyForm, setHotKeyForm] = useState<HotKeyForm>({
...DEFAULT_FORM_VALUES,
})
const [formErrors, setFormErrors] = useState<FormErrors>({})
const handleSetForm = (propertyName: string, value: string | boolean) => {
setFormErrors({})
setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value }))
}
const handlePostOnlyChange = (postOnly: boolean) => {
if (postOnly) {
handleSetForm('ioc', !postOnly)
}
handleSetForm('post', postOnly)
}
const handleIocChange = (ioc: boolean) => {
if (ioc) {
handleSetForm('post', !ioc)
}
handleSetForm('ioc', ioc)
}
const isFormValid = (form: HotKeyForm) => {
const invalidFields: FormErrors = {}
setFormErrors({})
const triggerKey: (keyof HotKeyForm)[] = ['triggerKey']
const requiredFields: (keyof HotKeyForm)[] = ['size', 'price', 'triggerKey']
const numberFields: (keyof HotKeyForm)[] = ['size', 'price']
const alphanumericRegex = /^[a-zA-Z0-9]+$/
for (const key of triggerKey) {
const value = form[key] as string
if (value.length > 1) {
invalidFields[key] = t('settings:error-too-many-characters')
}
if (!alphanumericRegex.test(value)) {
invalidFields[key] = t('settings:error-alphanumeric-only')
}
}
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
if (hotKeyForm.orderType === 'market') {
if (key !== 'price') {
invalidFields[key] = t('settings:error-required-field')
}
} else {
invalidFields[key] = t('settings:error-required-field')
}
}
}
for (const key of numberFields) {
const value = form[key] as string
if (value) {
if (isNaN(parseFloat(value))) {
invalidFields[key] = t('settings:error-must-be-number')
}
if (parseFloat(value) < 0) {
invalidFields[key] = t('settings:error-must-be-above-zero')
}
if (parseFloat(value) > 100) {
if (key === 'price') {
invalidFields[key] = t('settings:error-must-be-below-100')
} else {
if (hotKeyForm.sizeType === 'percentage') {
invalidFields[key] = t('settings:error-must-be-below-100')
}
}
}
}
}
const newKeySequence = `${form.baseKey}+${form.triggerKey}`
const keyExists = hotKeys.find((k) => k.keySequence === newKeySequence)
if (keyExists) {
invalidFields.triggerKey = t('settings:error-key-in-use')
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
}
const handleSave = () => {
const invalidFields = isFormValid(hotKeyForm)
if (Object.keys(invalidFields).length) {
return
}
const newHotKey = {
keySequence: `${hotKeyForm.baseKey}+${hotKeyForm.triggerKey}`,
orderSide: hotKeyForm.side,
orderSizeType: hotKeyForm.sizeType,
orderSize: hotKeyForm.size,
orderType: hotKeyForm.orderType,
orderPrice: hotKeyForm.price,
ioc: hotKeyForm.ioc,
margin: hotKeyForm.margin,
postOnly: hotKeyForm.post,
reduceOnly: hotKeyForm.reduce,
}
setHotKeys([...hotKeys, newHotKey])
onClose()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
<h2 className="mb-4 text-center">{t('settings:new-hot-key')}</h2>
<div className="mb-4">
<Label text={t('settings:base-key')} />
<ButtonGroup
activeValue={hotKeyForm.baseKey}
onChange={(key) => handleSetForm('baseKey', key)}
values={['shift', 'ctrl', 'option']}
/>
</div>
<div className="mb-4">
<Label text={t('settings:trigger-key')} />
<Input
hasError={formErrors.triggerKey !== undefined}
type="text"
value={hotKeyForm.triggerKey}
onChange={(e) =>
handleSetForm('triggerKey', e.target.value.toLowerCase())
}
/>
{formErrors.triggerKey ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.triggerKey}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
<div className="mb-4">
<Label text={t('settings:order-side')} />
<ButtonGroup
activeValue={hotKeyForm.side}
names={[t('buy'), t('sell')]}
onChange={(side) => handleSetForm('side', side)}
values={['buy', 'sell']}
/>
</div>
<div className="mb-4">
<Label text={t('trade:order-type')} />
<ButtonGroup
activeValue={hotKeyForm.orderType}
names={[t('trade:limit'), t('market')]}
onChange={(type) => handleSetForm('orderType', type)}
values={['limit', 'market']}
/>
</div>
<div className="mb-4">
<Label text={t('settings:order-size-type')} />
<ButtonGroup
activeValue={hotKeyForm.sizeType}
names={[t('settings:percentage'), t('settings:notional')]}
onChange={(type) => handleSetForm('sizeType', type)}
values={['percentage', 'notional']}
/>
</div>
<div className="flex items-start space-x-4">
<div className="w-full">
<Tooltip
content={
hotKeyForm.sizeType === 'notional'
? t('settings:tooltip-hot-key-notional-size')
: t('settings:tooltip-hot-key-percentage-size')
}
>
<Label className="tooltip-underline" text={t('trade:size')} />
</Tooltip>
<Input
hasError={formErrors.size !== undefined}
type="text"
value={hotKeyForm.size}
onChange={(e) => handleSetForm('size', e.target.value)}
suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'}
/>
{formErrors.size ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.size}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
{hotKeyForm.orderType === 'limit' ? (
<div className="w-full">
<Tooltip content={t('settings:tooltip-hot-key-price')}>
<Label className="tooltip-underline" text={t('price')} />
</Tooltip>
<Input
hasError={formErrors.price !== undefined}
type="text"
value={hotKeyForm.price}
onChange={(e) => handleSetForm('price', e.target.value)}
placeholder="e.g. 1%"
suffix="%"
/>
{formErrors.price ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.price}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
) : null}
</div>
<div className="flex flex-wrap md:flex-nowrap">
{hotKeyForm.orderType === 'limit' ? (
<div className="flex">
<div className="mr-3 mt-4" id="trade-step-six">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-post')}
>
<Checkbox
checked={hotKeyForm.post}
onChange={(e) => handlePostOnlyChange(e.target.checked)}
>
{t('trade:post')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4" id="trade-step-seven">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-ioc')}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={hotKeyForm.ioc}
onChange={(e) => handleIocChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</div>
) : null}
<div className="mt-4 mr-3" id="trade-step-eight">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-enable-margin')}
>
<Checkbox
checked={hotKeyForm.margin}
onChange={(e) => handleSetForm('margin', e.target.checked)}
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4">
<Tooltip
className="hidden md:block"
delay={100}
content={
'Reduce will only decrease the size of an open position. This is often used for closing a position.'
}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={hotKeyForm.reduce}
onChange={(e) => handleSetForm('reduce', e.target.checked)}
>
{t('trade:reduce-only')}
</Checkbox>
</div>
</Tooltip>
</div>
</div>
<Button className="mt-6 w-full" onClick={handleSave}>
{t('settings:save-hot-key')}
</Button>
</>
</Modal>
)
}

View File

@ -42,7 +42,7 @@ const NotificationSettings = () => {
<h2 className="text-base">{t('settings:notifications')}</h2>
</div>
{isAuth ? (
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<div className="flex items-center justify-between border-y border-th-bkg-3 p-4">
<p>{t('settings:limit-order-filled')}</p>
<Switch
checked={!!data?.fillsNotifications}
@ -55,7 +55,7 @@ const NotificationSettings = () => {
/>
</div>
) : (
<div className="mb-8 rounded-lg border border-th-bkg-3 p-6">
<div className="rounded-lg border border-th-bkg-3 p-6">
{connected ? (
<div className="flex flex-col items-center">
<BellIcon className="mb-2 h-6 w-6 text-th-fgd-4" />

View File

@ -1,11 +1,16 @@
import { useViewport } from 'hooks/useViewport'
import AnimationSettings from './AnimationSettings'
import DisplaySettings from './DisplaySettings'
import HotKeysSettings from './HotKeysSettings'
import NotificationSettings from './NotificationSettings'
import PreferredExplorerSettings from './PreferredExplorerSettings'
import RpcSettings from './RpcSettings'
import SoundSettings from './SoundSettings'
import { breakpoints } from 'utils/theme'
const SettingsPage = () => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
return (
<div className="grid grid-cols-12">
<div className="col-span-12 border-b border-th-bkg-3 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
@ -14,9 +19,14 @@ const SettingsPage = () => {
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<DisplaySettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<NotificationSettings />
</div>
{!isMobile ? (
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<HotKeysSettings />
</div>
) : null}
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<AnimationSettings />
</div>

View File

@ -66,6 +66,7 @@ const BalancesTable = () => {
<tbody>
{filteredBanks.map((b) => {
const bank = b.bank
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
@ -77,7 +78,7 @@ const BalancesTable = () => {
<div className="mr-2.5 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} />
</div>
<span>{bank.name}</span>
<span>{symbol}</span>
</div>
</Td>
<Td className="text-right">
@ -104,6 +105,7 @@ const BalancesTable = () => {
<div className="border-b border-th-bkg-3">
{filteredBanks.map((b, i) => {
const bank = b.bank
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
@ -119,29 +121,29 @@ const BalancesTable = () => {
>
<div className="flex items-center justify-between">
<div className="flex items-start">
<div className="mr-2.5 mt-0.5 flex flex-shrink-0 items-center">
<div className="mr-2.5">
<TokenLogo bank={bank} />
</div>
<div>
<p className="mb-0.5 leading-none text-th-fgd-1">
{bank.name}
</p>
<p className="text-th-fgd-2">{symbol}</p>
</div>
<div className="flex items-center space-x-2">
<div className="text-right">
<Balance bank={b} />
<p className="mt-0.5 text-sm leading-none text-th-fgd-4">
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue
value={
mangoAccount ? b.balance * bank.uiPrice : 0
}
isUsd
/>
</p>
</span>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</div>
</Disclosure.Button>
<Transition
@ -289,7 +291,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
if (!balance) return <p className="md:flex md:justify-end">0</p>
return (
<p className="text-th-fgd-2 md:flex md:justify-end">
<p className="font-mono text-th-fgd-2 md:flex md:justify-end">
{!isUnownedAccount && !isMobile ? (
asPath.includes('/trade') && isBaseOrQuote ? (
<LinkButton

View File

@ -1,81 +0,0 @@
import { Popover, Transition } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/20/solid'
import { Fragment, ReactNode } from 'react'
const IconDropMenu = ({
icon,
children,
disabled,
size,
postion = 'bottomRight',
panelClassName,
}: {
icon: ReactNode
children: ReactNode
disabled?: boolean
size?: 'small' | 'medium' | 'large'
postion?:
| 'bottomLeft'
| 'bottomRight'
| 'topLeft'
| 'topRight'
| 'leftBottom'
| 'leftTop'
| 'rightBottom'
| 'rightTop'
panelClassName?: string
}) => {
const panelPosition = {
bottomLeft: size === 'large' ? 'left-0 top-14' : 'left-0 top-12',
bottomRight: size === 'large' ? 'right-0 top-14' : 'right-0 top-12',
topLeft: size === 'large' ? 'left-0 bottom-14' : 'left-0 bottom-12',
topRight: size === 'large' ? 'right-0 bottom-14' : 'right-0 bottom-12',
leftBottom: size === 'large' ? 'right-14 bottom-0' : 'right-12 bottom-0',
leftTop: size === 'large' ? 'right-14 top-0' : 'right-12 top-0',
rightBottom: size === 'large' ? 'left-14 bottom-0' : 'left-12 bottom-0',
rightTop: size === 'large' ? 'left-14 top-0' : 'left-12 top-0',
}
return (
<Popover>
{({ open }) => (
<div className="relative">
<Popover.Button
className={`flex ${
size === 'large'
? 'h-12 w-12'
: size === 'medium'
? 'h-10 w-10'
: 'h-8 w-8'
} items-center justify-center rounded-full border border-th-button text-th-fgd-1 ${
!open ? 'focus:border-th-fgd-2' : ''
} md:hover:border-th-button-hover md:hover:text-th-fgd-1 ${
disabled ? 'cursor-not-allowed opacity-60' : ''
}`}
disabled={disabled}
>
{open ? <XMarkIcon className="h-6 w-6" /> : icon}
</Popover.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition ease-in duration-100"
enterFrom="scale-90"
enterTo="scale-100"
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel
className={`absolute ${panelPosition[postion]} thin-scroll z-40 max-h-60 space-y-2 overflow-auto rounded-md bg-th-bkg-2 p-4 ${panelClassName}`}
>
{children}
</Popover.Panel>
</Transition>
</div>
)}
</Popover>
)
}
export default IconDropMenu

View File

@ -59,7 +59,7 @@ import InlineNotification from '@components/shared/InlineNotification'
const set = mangoStore.getState().set
const successSound = new Howl({
export const successSound = new Howl({
src: ['/sounds/swap-success.mp3'],
volume: 0.5,
})

View File

@ -455,11 +455,9 @@ const OpenOrders = () => {
</span>
) : (
<Link href={`/trade?name=${market.name}`}>
<div className="flex items-center underline underline-offset-2 md:hover:text-th-fgd-3 md:hover:no-underline">
<span className="whitespace-nowrap">
{market.name}
</span>
</div>
<span className="whitespace-nowrap">
{market.name}
</span>
</Link>
)}
{o instanceof PerpOrder ? (
@ -482,7 +480,10 @@ const OpenOrders = () => {
<FormatNumericValue
value={o.price}
decimals={getDecimalCount(tickSize)}
isUsd={quoteBank?.name === 'USDC'}
isUsd={
quoteBank?.name === 'USDC' ||
o instanceof PerpOrder
}
/>{' '}
{quoteBank && quoteBank.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
@ -491,6 +492,9 @@ const OpenOrders = () => {
) : null}
</span>
</p>
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue value={o.price * o.size} isUsd />
</span>
</div>
</div>
) : (

View File

@ -321,6 +321,7 @@ const PerpPositions = () => {
mangoAccount
)
const unsettledPnl = position.getUnsettledPnlUi(market)
const notional = Math.abs(floorBasePosition) * market._uiPrice
return (
<Disclosure key={position.marketIndex}>
{({ open }) => (
@ -348,33 +349,33 @@ const PerpPositions = () => {
decimals={getDecimalCount(
market.minOrderSize
)}
/>{' '}
<span className="font-body text-th-fgd-3">
{market.name.split('-')[0]}
</span>
/>
</span>
<span className="text-th-fgd-4">|</span>
<span
className={`font-mono ${
unrealizedPnl > 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={unrealizedPnl}
isUsd
decimals={2}
/>
<span className="font-mono">
<FormatNumericValue value={notional} isUsd />
</span>
</div>
</div>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} ml-3 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
<div className="flex items-center space-x-2">
<span
className={`font-mono ${
unrealizedPnl > 0 ? 'text-th-up' : 'text-th-down'
}`}
>
<FormatNumericValue
value={unrealizedPnl}
isUsd
decimals={2}
/>
</span>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} ml-3 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</div>
</Disclosure.Button>
<Transition
enter="transition ease-in duration-200"
@ -407,10 +408,7 @@ const PerpPositions = () => {
</LinkButton>
<FormatNumericValue
classNames="text-xs text-th-fgd-3"
value={
Math.abs(floorBasePosition) *
market._uiPrice
}
value={notional}
isUsd
/>
</div>
@ -488,33 +486,31 @@ const PerpPositions = () => {
<p className="text-xs text-th-fgd-3">
{t('trade:unrealized-pnl')}
</p>
<p className="font-mono text-th-fgd-2">
<Tooltip
content={
<PnlTooltipContent
unrealizedPnl={unrealizedPnl}
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
/>
}
delay={100}
<Tooltip
content={
<PnlTooltipContent
unrealizedPnl={unrealizedPnl}
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
/>
}
delay={100}
>
<span
className={`tooltip-underline mb-1 font-mono ${
unrealizedPnl >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<span
className={`tooltip-underline mb-1 ${
unrealizedPnl >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={unrealizedPnl}
isUsd
decimals={2}
/>
</span>
</Tooltip>
</p>
<FormatNumericValue
value={unrealizedPnl}
isUsd
decimals={2}
/>
</span>
</Tooltip>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">ROE</p>

View File

@ -16,6 +16,7 @@ import OrderbookAndTrades from './OrderbookAndTrades'
import FavoriteMarketsBar from './FavoriteMarketsBar'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { TRADE_LAYOUT_KEY } from 'utils/constants'
import TradeHotKeys from './TradeHotKeys'
export type TradeLayout =
| 'chartLeft'
@ -206,7 +207,7 @@ const TradeAdvancedPage = () => {
return showMobileView ? (
<MobileTradeAdvancedPage />
) : (
<>
<TradeHotKeys>
<FavoriteMarketsBar />
<ResponsiveGridLayout
onBreakpointChange={(bp) => console.log('bp: ', bp)}
@ -262,7 +263,7 @@ const TradeAdvancedPage = () => {
{/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? (
<TradeOnboardingTour />
) : null} */}
</>
</TradeHotKeys>
)
}

View File

@ -0,0 +1,393 @@
import {
Group,
MangoAccount,
PerpMarket,
PerpOrderSide,
PerpOrderType,
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '@blockworks-foundation/mango-v4'
import { HotKey } from '@components/settings/HotKeysSettings'
import mangoStore from '@store/mangoStore'
import { ReactNode, useCallback } from 'react'
import Hotkeys from 'react-hot-keys'
import { GenericMarket, isMangoError } from 'types'
import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants'
import { notify } from 'utils/notifications'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import { successSound } from './AdvancedTradeForm'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useMangoAccount from 'hooks/useMangoAccount'
import { Market } from '@project-serum/serum'
import { useRouter } from 'next/router'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { useTranslation } from 'next-i18next'
const set = mangoStore.getState().set
const calcBaseSize = (
orderDetails: HotKey,
maxSize: number,
market: PerpMarket | Market,
oraclePrice: number,
quoteTokenIndex: number,
group: Group,
limitPrice?: number
) => {
const { orderSize, orderSide, orderSizeType, orderType } = orderDetails
let baseSize: number
let quoteSize: number
if (orderSide === 'buy') {
// assumes USDC = $1 as tokenIndex is 0
if (!quoteTokenIndex) {
quoteSize =
orderSizeType === 'percentage'
? (Number(orderSize) / 100) * maxSize
: Number(orderSize)
} else {
// required for non USDC quote tokens
const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex)
const quotePrice = quoteBank.uiPrice
const orderSizeInQuote = Number(orderSize) / quotePrice
quoteSize =
orderSizeType === 'percentage'
? (orderSizeInQuote / 100) * maxSize
: orderSizeInQuote
}
if (orderType === 'market') {
baseSize = floorToDecimal(
quoteSize / oraclePrice,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
const price = limitPrice ? limitPrice : 0
baseSize = floorToDecimal(
quoteSize / price,
getDecimalCount(market.minOrderSize)
).toNumber()
}
} else {
if (orderSizeType === 'percentage') {
baseSize = floorToDecimal(
(Number(orderSize) / 100) * maxSize,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
if (orderType === 'market') {
baseSize = floorToDecimal(
Number(orderSize) / oraclePrice,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
const price = limitPrice ? limitPrice : 0
baseSize = floorToDecimal(
Number(orderSize) / price,
getDecimalCount(market.minOrderSize)
).toNumber()
}
}
}
return baseSize
}
const calcSpotMarketMax = (
mangoAccount: MangoAccount | undefined,
selectedMarket: GenericMarket | undefined,
side: string,
useMargin: boolean
) => {
const spotBalances = mangoStore.getState().mangoAccount.spotBalances
const group = mangoStore.getState().group
if (!mangoAccount || !group || !selectedMarket) return 0
if (!(selectedMarket instanceof Serum3Market)) return 0
let leverageMax = 0
let spotMax = 0
try {
if (side === 'buy') {
leverageMax = mangoAccount.getMaxQuoteForSerum3BidUi(
group,
selectedMarket.serumMarketExternal
)
const bank = group.getFirstBankByTokenIndex(
selectedMarket.quoteTokenIndex
)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
} else {
leverageMax = mangoAccount.getMaxBaseForSerum3AskUi(
group,
selectedMarket.serumMarketExternal
)
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
}
return useMargin ? leverageMax : Math.max(spotMax, 0)
} catch (e) {
console.error('Error calculating max size: ', e)
return 0
}
}
const calcPerpMax = (
mangoAccount: MangoAccount,
selectedMarket: GenericMarket,
side: string
) => {
const group = mangoStore.getState().group
if (
!mangoAccount ||
!group ||
!selectedMarket ||
selectedMarket instanceof Serum3Market
)
return 0
try {
if (side === 'buy') {
return mangoAccount.getMaxQuoteForPerpBidUi(
group,
selectedMarket.perpMarketIndex
)
} else {
return mangoAccount.getMaxBaseForPerpAskUi(
group,
selectedMarket.perpMarketIndex
)
}
} catch (e) {
console.error('Error calculating max leverage: ', e)
return 0
}
}
const TradeHotKeys = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation(['common', 'settings'])
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const { mangoAccountAddress } = useMangoAccount()
const { isUnownedAccount } = useUnownedAccount()
const { asPath } = useRouter()
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [soundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS
)
const handlePlaceOrder = useCallback(
async (hkOrder: HotKey) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
const selectedMarket = mangoStore.getState().selectedMarket.current
const {
ioc,
orderPrice,
orderSide,
orderType,
postOnly,
reduceOnly,
margin,
} = hkOrder
if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket)
return
try {
const orderMax =
serumOrPerpMarket instanceof PerpMarket
? calcPerpMax(mangoAccount, selectedMarket, orderSide)
: calcSpotMarketMax(mangoAccount, selectedMarket, orderSide, margin)
const quoteTokenIndex =
selectedMarket instanceof PerpMarket
? 0
: selectedMarket.quoteTokenIndex
let baseSize: number
let price: number
if (orderType === 'market') {
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
group
)
const orderbook = mangoStore.getState().selectedMarket.orderbook
price = calculateLimitPriceForMarketOrder(
orderbook,
baseSize,
orderSide
)
} else {
// change in price from oracle for limit order
const priceChange = (Number(orderPrice) / 100) * oraclePrice
// subtract price change for buy limit, add for sell limit
const rawPrice =
orderSide === 'buy'
? oraclePrice - priceChange
: oraclePrice + priceChange
price = floorToDecimal(
rawPrice,
getDecimalCount(serumOrPerpMarket.tickSize)
).toNumber()
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
group,
price
)
}
// check if size < max
if (orderSide === 'buy') {
if (baseSize * price > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
} else {
console.log(baseSize, orderMax)
if (baseSize > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
}
notify({
type: 'info',
title: t('settings:placing-order'),
description: `${t(orderSide)} ${baseSize} ${selectedMarket.name} ${
orderType === 'limit'
? `${t('settings:at')} ${price}`
: `${t('settings:at')} ${t('market')}`
}`,
})
if (selectedMarket instanceof Serum3Market) {
const spotOrderType = ioc
? Serum3OrderType.immediateOrCancel
: postOnly && orderType !== 'market'
? Serum3OrderType.postOnly
: Serum3OrderType.limit
const tx = await client.serum3PlaceOrder(
group,
mangoAccount,
selectedMarket.serumMarketExternal,
orderSide === 'buy' ? Serum3Side.bid : Serum3Side.ask,
price,
baseSize,
Serum3SelfTradeBehavior.decrementTake,
spotOrderType,
Date.now(),
10
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
} else if (selectedMarket instanceof PerpMarket) {
const perpOrderType =
orderType === 'market'
? PerpOrderType.market
: ioc
? PerpOrderType.immediateOrCancel
: postOnly
? PerpOrderType.postOnly
: PerpOrderType.limit
console.log('perpOrderType', perpOrderType)
const tx = await client.perpPlaceOrder(
group,
mangoAccount,
selectedMarket.perpMarketIndex,
orderSide === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
price,
Math.abs(baseSize),
undefined, // maxQuoteQuantity
Date.now(),
perpOrderType,
selectedMarket.reduceOnly || reduceOnly,
undefined,
undefined
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
}
} catch (e) {
console.error('Place trade error:', e)
if (!isMangoError(e)) return
notify({
title: 'There was an issue.',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
},
[serumOrPerpMarket]
)
const onKeyDown = useCallback(
(keyName: string) => {
const orderDetails = hotKeys.find(
(hk: HotKey) => hk.keySequence === keyName
)
if (orderDetails) {
handlePlaceOrder(orderDetails)
}
},
[handlePlaceOrder, hotKeys]
)
const showHotKeys =
hotKeys.length &&
asPath.includes('/trade') &&
mangoAccountAddress &&
!isUnownedAccount
return showHotKeys ? (
<Hotkeys
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
onKeyDown={onKeyDown}
>
{children}
</Hotkeys>
) : (
<>{children}</>
)
}
export default TradeHotKeys

View File

@ -46,10 +46,6 @@ const ConnectedMenu = () => {
state.mangoAccount.initialLoad = true
state.mangoAccount.openOrders = {}
state.mangoAccount.interestTotals = { data: [], loading: false }
state.mangoAccount.performance = {
data: [],
loading: true,
}
})
disconnect()
notify({

View File

@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query'
import { fetchHourlyVolume } from 'utils/account'
import useMangoAccount from './useMangoAccount'
export default function useAccountHourlyVolumeStats() {
const { mangoAccountAddress } = useMangoAccount()
const {
data: hourlyVolumeData,
isLoading: loadingHourlyVolumeData,
isFetching: fetchingHourlyVolumeData,
} = useQuery(
['hourly-volume', mangoAccountAddress],
() => fetchHourlyVolume(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const loadingHourlyVolume =
fetchingHourlyVolumeData || loadingHourlyVolumeData
return {
hourlyVolumeData,
loadingHourlyVolumeData,
fetchingHourlyVolumeData,
loadingHourlyVolume,
}
}

View File

@ -0,0 +1,44 @@
import { useQuery } from '@tanstack/react-query'
import { fetchAccountPerformance } from 'utils/account'
import useMangoAccount from './useMangoAccount'
import { useMemo } from 'react'
import { PerformanceDataItem } from 'types'
export default function useAccountPerformanceData() {
const { mangoAccountAddress } = useMangoAccount()
const {
data: performanceData,
isLoading: loadingPerformanceData,
isFetching: fetchingPerformanceData,
} = useQuery(
['performance', mangoAccountAddress],
() => fetchAccountPerformance(mangoAccountAddress, 31),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
}
)
const rollingDailyData: PerformanceDataItem[] | [] = useMemo(() => {
if (!performanceData || !performanceData.length) return []
const nowDate = new Date()
return performanceData.filter((d) => {
const dataTime = new Date(d.time).getTime()
return dataTime >= nowDate.getTime() - 86400000
})
}, [performanceData])
const performanceLoading = loadingPerformanceData || fetchingPerformanceData
return {
performanceData,
rollingDailyData,
loadingPerformanceData,
fetchingPerformanceData,
performanceLoading,
}
}

View File

@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useJupiterMints from './useJupiterMints'
import useMangoGroup from './useMangoGroup'
import { CUSTOM_TOKEN_ICONS } from 'utils/constants'
export default function useSelectedMarket() {
const { group } = useMangoGroup()
@ -53,13 +54,22 @@ export default function useSelectedMarket() {
const baseLogoURI = useMemo(() => {
if (!baseSymbol || !mangoTokens.length) return ''
const token =
mangoTokens.find((t) => t.symbol.toUpperCase() === baseSymbol) ||
mangoTokens.find((t) => t.symbol.toUpperCase()?.includes(baseSymbol))
if (token) {
return token.logoURI
const lowerCaseBaseSymbol = baseSymbol.toLowerCase()
const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseBaseSymbol]
if (hasCustomIcon) {
return `/icons/${lowerCaseBaseSymbol}.svg`
} else {
const token =
mangoTokens.find(
(t) => t.symbol.toLowerCase() === lowerCaseBaseSymbol
) ||
mangoTokens.find((t) =>
t.symbol.toLowerCase()?.includes(lowerCaseBaseSymbol)
)
if (token) {
return token.logoURI
}
}
return ''
}, [baseSymbol, mangoTokens])
const quoteBank = useMemo(() => {
@ -78,13 +88,18 @@ export default function useSelectedMarket() {
const quoteLogoURI = useMemo(() => {
if (!quoteSymbol || !mangoTokens.length) return ''
const token = mangoTokens.find(
(t) => t.symbol.toUpperCase() === quoteSymbol
)
if (token) {
return token.logoURI
const lowerCaseQuoteSymbol = quoteSymbol.toLowerCase()
const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseQuoteSymbol]
if (hasCustomIcon) {
return `/icons/${lowerCaseQuoteSymbol}.svg`
} else {
const token = mangoTokens.find(
(t) => t.symbol.toLowerCase() === lowerCaseQuoteSymbol
)
if (token) {
return token.logoURI
}
}
return ''
}, [quoteSymbol, mangoTokens])
return {

View File

@ -5,12 +5,7 @@ const webpack = require('webpack')
const nextConfig = {
i18n,
images: {
domains: [
'raw.githubusercontent.com',
'arweave.net',
'www.dual.finance',
'storage.googleapis.com',
],
domains: ['raw.githubusercontent.com', 'arweave.net', 'www.dual.finance'],
},
reactStrictMode: true,
//proxy for openserum api cors

View File

@ -65,6 +65,7 @@
"react-dom": "18.2.0",
"react-flip-numbers": "3.0.5",
"react-grid-layout": "1.3.4",
"react-hot-keys": "2.7.2",
"react-nice-dates": "3.1.0",
"react-number-format": "4.9.2",
"react-tsparticles": "2.2.4",

View File

@ -468,7 +468,7 @@ const Dashboard: NextPage = () => {
/>
<KeyValuePair
label="Maint Asset/Liab Weight"
value={`${formattedBankValues.maintAssetWeight}/
value={`${formattedBankValues.maintAssetWeight} /
${formattedBankValues.maintLiabWeight}`}
proposedValue={
(suggestedFields.maintAssetWeight ||
@ -476,7 +476,7 @@ const Dashboard: NextPage = () => {
`${
suggestedFields.maintAssetWeight ||
formattedBankValues.maintAssetWeight
}/
} /
${
suggestedFields.maintLiabWeight ||
formattedBankValues.maintLiabWeight
@ -485,7 +485,7 @@ const Dashboard: NextPage = () => {
/>
<KeyValuePair
label="Init Asset/Liab Weight"
value={`${formattedBankValues.initAssetWeight}/
value={`${formattedBankValues.initAssetWeight} /
${formattedBankValues.initLiabWeight}`}
proposedValue={
(suggestedFields.initAssetWeight ||
@ -493,13 +493,17 @@ const Dashboard: NextPage = () => {
`${
suggestedFields.initAssetWeight ||
formattedBankValues.initAssetWeight
}/
} /
${
suggestedFields.initLiabWeight ||
formattedBankValues.initLiabWeight
}`
}
/>
<KeyValuePair
label="Scaled Init Asset/Liab Weight"
value={`${formattedBankValues.scaledInitAssetWeight} / ${formattedBankValues.scaledInitLiabWeight}`}
/>
<KeyValuePair
label="Deposit weight scale start quote"
value={`$${formattedBankValues.depositWeightScale}`}

View File

@ -17,6 +17,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
'profile',
'search',
'settings',
'trade',
])),
},
}

36
public/icons/ethpo.svg Normal file
View File

@ -0,0 +1,36 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3_228)">
<g clip-path="url(#clip1_3_228)">
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F3F3F3"/>
<path d="M15.9979 3V12.4613L23.9947 16.0346L15.9979 3Z" fill="#343434"/>
<path d="M15.9978 3L8 16.0346L15.9978 12.4613V3Z" fill="#8C8C8C"/>
<path d="M15.9979 22.166V28.5948L24 17.5239L15.9979 22.166Z" fill="#3C3C3B"/>
<path d="M15.9978 28.5948V22.165L8 17.5239L15.9978 28.5948Z" fill="#8C8C8C"/>
<path d="M15.9979 20.6779L23.9947 16.0347L15.9979 12.4635V20.6779Z" fill="#141414"/>
<path d="M8 16.0347L15.9978 20.6779V12.4635L8 16.0347Z" fill="#393939"/>
</g>
<circle cx="27" cy="5" r="5" fill="url(#paint0_radial_3_228)"/>
<circle cx="27" cy="5" r="3.75" fill="url(#paint1_radial_3_228)"/>
<circle cx="27" cy="5" r="2.5" fill="url(#paint2_radial_3_228)"/>
</g>
<defs>
<radialGradient id="paint0_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(5)">
<stop offset="0.739583" stop-color="#49266B"/>
<stop offset="1" stop-color="#976EC0"/>
</radialGradient>
<radialGradient id="paint1_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(3.75)">
<stop offset="0.713542" stop-color="#3E2755"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<radialGradient id="paint2_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(2.5)">
<stop offset="0.494792"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<clipPath id="clip0_3_228">
<rect width="32" height="32" fill="white"/>
</clipPath>
<clipPath id="clip1_3_228">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

19
public/icons/orca.svg Normal file
View File

@ -0,0 +1,19 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_15_367)">
<path d="M32 16C32 24.8365 24.8365 32 16 32C7.16344 32 0 24.8365 0 16C0 7.16344 7.16344 0 16 0C24.8365 0 32 7.16344 32 16Z" fill="#FFD15C"/>
<path d="M8.25031 22.5564C8.38837 22.4627 8.51724 22.3221 8.58139 22.1228C8.65268 21.9012 8.61063 21.7063 8.57724 21.5983C8.57615 21.5947 8.57497 21.5911 8.57381 21.5875L8.60041 21.4321C8.67389 21.4735 8.75845 21.5291 8.85488 21.5968C8.87813 21.6132 8.91065 21.6364 8.94447 21.6604C8.98887 21.6921 9.03567 21.7256 9.06685 21.7473C9.12044 21.7845 9.19653 21.8365 9.27716 21.8793C11.4108 23.2055 13.1753 23.7521 14.5768 23.6844C16.0259 23.6144 17.042 22.8855 17.5661 21.8217C18.0727 20.7935 18.0904 19.5193 17.7353 18.3391C17.378 17.1512 16.6265 15.9967 15.5064 15.1977C13.6189 13.8515 11.8893 11.8045 11.0148 10.064C10.5706 9.18003 10.3939 8.46713 10.4406 8.00164C10.4623 7.785 10.5287 7.65395 10.608 7.5696C10.6875 7.4852 10.8323 7.39211 11.1111 7.34619C11.7019 7.24887 12.3851 7.03612 13.0785 6.82023C13.3476 6.7364 13.6183 6.65211 13.8857 6.57391C14.8851 6.28168 15.9431 6.03617 17.0552 6.03827C19.212 6.04232 21.7433 6.97876 24.4085 10.8743C27.8323 15.8787 25.9525 21.4707 22.0084 24.4084C20.0415 25.8735 17.5872 26.6512 15.0653 26.3555C12.7946 26.0893 10.4163 24.9465 8.25031 22.5564Z" fill="white"/>
<path d="M8.64744 21.2612C8.64744 21.2613 8.64671 21.2628 8.64496 21.2653M8.43305 21.3589C8.43299 21.3588 8.43517 21.3591 8.43991 21.3603M8.25031 22.5564C8.38837 22.4627 8.51724 22.3221 8.58139 22.1228C8.65268 21.9012 8.61063 21.7063 8.57724 21.5983C8.57615 21.5947 8.57497 21.5911 8.57381 21.5875L8.60041 21.4321C8.67389 21.4735 8.75845 21.5291 8.85488 21.5968C8.87813 21.6132 8.91065 21.6364 8.94447 21.6604C8.98887 21.6921 9.03567 21.7256 9.06685 21.7473C9.12044 21.7845 9.19653 21.8365 9.27716 21.8793C11.4108 23.2055 13.1753 23.7521 14.5768 23.6844C16.0259 23.6144 17.042 22.8855 17.5661 21.8217C18.0727 20.7935 18.0904 19.5193 17.7353 18.3391C17.378 17.1512 16.6265 15.9967 15.5064 15.1977C13.6189 13.8515 11.8893 11.8045 11.0148 10.064C10.5706 9.18003 10.3939 8.46713 10.4406 8.00164C10.4623 7.785 10.5287 7.65395 10.608 7.5696C10.6875 7.4852 10.8323 7.39211 11.1111 7.34619C11.7019 7.24887 12.3851 7.03612 13.0785 6.82023C13.3476 6.7364 13.6183 6.65211 13.8857 6.57391C14.8851 6.28168 15.9431 6.03617 17.0552 6.03827C19.212 6.04232 21.7433 6.97876 24.4085 10.8743C27.8323 15.8787 25.9525 21.4707 22.0084 24.4084C20.0415 25.8735 17.5872 26.6512 15.0653 26.3555C12.7946 26.0893 10.4163 24.9465 8.25031 22.5564Z" stroke="black" stroke-width="1.65708"/>
<path d="M10.2103 7.6018C10.2103 7.6018 16.3265 5.97315 17.6657 5.97315C19.0048 5.97315 24.3557 8.57157 25.9173 13.32C28.1312 20.0516 22.1096 24.4655 21.4296 24.0328C27.9079 18.6764 17.3037 8.77553 12.0922 9.51991C11.4408 9.613 11.8027 10.1714 11.8027 10.1714L11.6579 11.619L10.5722 9.80944L10.2103 7.6018Z" fill="black"/>
<path d="M25.0263 11.2951C26.9576 14.6952 26.5563 12.7485 26.1275 16.6353C26.921 15.3517 28.1616 16.3128 28.674 16.8036C28.7659 16.8916 28.9174 16.8371 28.9131 16.71C28.8936 16.1335 28.7284 14.8876 27.774 13.5511C26.4654 11.7184 25.0263 11.2951 25.0263 11.2951Z" fill="black"/>
<path d="M26.1275 16.6353C26.2014 16.4528 26.3167 16.168 26.3167 16.168M26.1275 16.6353C26.5563 12.7485 26.9576 14.6952 25.0263 11.2951C25.0263 11.2951 26.4654 11.7184 27.774 13.5511C28.7284 14.8876 28.8936 16.1335 28.9131 16.71C28.9174 16.8371 28.7659 16.8916 28.674 16.8036C28.1616 16.3128 26.921 15.3517 26.1275 16.6353Z" stroke="black" stroke-width="0.0930942"/>
<path d="M9.48682 16.1168C8.67187 17.4965 6.99987 17.3931 7.03189 19.0387C8.52529 22.3567 8.53411 22.1187 8.53411 22.1187C11.3005 20.5647 10.1636 17.1332 9.75317 16.124C9.70674 16.0099 9.54943 16.0108 9.48682 16.1168Z" fill="black"/>
<path d="M3.21598 19.1673C4.80489 19.3747 5.75273 17.9933 7.02974 19.0316C8.72495 22.2512 8.53194 22.1117 8.53194 22.1117C5.60426 23.3348 3.60018 20.3264 3.05762 19.3816C2.99629 19.2748 3.09386 19.1515 3.21598 19.1673Z" fill="black"/>
<path d="M16.5438 17.1203C16.5438 17.1203 18.1363 18.7491 17.3039 19.0023C16.2659 18.3635 14.4059 18.8599 13.4942 19.1673C13.2697 19.2431 13.0497 19.0439 13.1306 18.8212C13.4406 17.9681 14.2062 16.3891 15.6752 16.1793C16.5438 15.962 16.5438 17.1203 16.5438 17.1203Z" fill="black"/>
<path d="M17.5209 9.15793C17.34 8.90463 16.9781 8.21696 18.1 8.21696C19.222 8.21696 20.9362 9.48733 21.2848 9.95728C21.1762 10.2437 20.4525 10.3077 20.0905 10.2799C19.7286 10.2521 19.1133 10.1925 18.6429 9.95728C18.1724 9.72205 17.7018 9.41131 17.5209 9.15793Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_15_367">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

26
public/icons/wbtcpo.svg Normal file
View File

@ -0,0 +1,26 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3_219)">
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F7931A"/>
<path d="M23.189 14.02C23.503 11.924 21.906 10.797 19.724 10.045L20.432 7.205L18.704 6.775L18.014 9.54C17.56 9.426 17.094 9.32 16.629 9.214L17.324 6.431L15.596 6L14.888 8.839C14.512 8.753 14.142 8.669 13.784 8.579L13.786 8.57L11.402 7.975L10.942 9.821C10.942 9.821 12.225 10.115 12.198 10.133C12.898 10.308 13.024 10.771 13.003 11.139L12.197 14.374C12.245 14.386 12.307 14.404 12.377 14.431L12.194 14.386L11.064 18.918C10.978 19.13 10.761 19.449 10.271 19.328C10.289 19.353 9.015 19.015 9.015 19.015L8.157 20.993L10.407 21.554C10.825 21.659 11.235 21.769 11.638 21.872L10.923 24.744L12.65 25.174L13.358 22.334C13.83 22.461 14.288 22.579 14.736 22.691L14.03 25.519L15.758 25.949L16.473 23.083C19.421 23.641 21.637 23.416 22.57 20.75C23.322 18.604 22.533 17.365 20.982 16.558C22.112 16.298 22.962 15.555 23.189 14.02V14.02ZM19.239 19.558C18.706 21.705 15.091 20.544 13.919 20.253L14.869 16.448C16.041 16.741 19.798 17.32 19.239 19.558ZM19.774 13.989C19.287 15.942 16.279 14.949 15.304 14.706L16.164 11.256C17.139 11.499 20.282 11.952 19.774 13.989Z" fill="white"/>
<circle cx="27" cy="5" r="5" fill="url(#paint0_radial_3_219)"/>
<circle cx="27" cy="5" r="3.75" fill="url(#paint1_radial_3_219)"/>
<circle cx="27" cy="5" r="2.5" fill="url(#paint2_radial_3_219)"/>
</g>
<defs>
<radialGradient id="paint0_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(5)">
<stop offset="0.739583" stop-color="#49266B"/>
<stop offset="1" stop-color="#976EC0"/>
</radialGradient>
<radialGradient id="paint1_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(3.75)">
<stop offset="0.713542" stop-color="#3E2755"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<radialGradient id="paint2_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(2.5)">
<stop offset="0.494792"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<clipPath id="clip0_3_219">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -5,6 +5,7 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"health-breakdown": "Health Breakdown",
"init-health": "Init Health",
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
@ -16,7 +17,7 @@
"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. The sum of these values equals your account's total collateral.",
"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",

View File

@ -59,7 +59,7 @@
"date-to": "Date To",
"delegate": "Delegate",
"delegate-account": "Delegate Account",
"delegate-account-info": "Account delegated to {{address}}",
"delegate-account-info": "Account delegated to: {{delegate}}",
"delegate-desc": "Delegate your Mango account to another wallet address",
"delegate-placeholder": "Enter a wallet address to delegate to",
"delete": "Delete",
@ -100,6 +100,7 @@
"mango": "Mango",
"mango-stats": "Mango Stats",
"market": "Market",
"markets": "Markets",
"max": "Max",
"max-borrow": "Max Borrow",
"more": "More",
@ -179,6 +180,7 @@
"withdraw-amount": "Withdraw Amount",
"list-market-token": "List Market/Token",
"vote": "Vote",
"yes": "Yes"
"yes": "Yes",
"you": "You"
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -5,6 +5,7 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"health-breakdown": "Health Breakdown",
"init-health": "Init Health",
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
@ -16,7 +17,7 @@
"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. The sum of these values equals your account's total collateral.",
"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",

View File

@ -59,7 +59,7 @@
"date-to": "Date To",
"delegate": "Delegate",
"delegate-account": "Delegate Account",
"delegate-account-info": "Account delegated to {{address}}",
"delegate-account-info": "Account delegated to: {{delegate}}",
"delegate-desc": "Delegate your Mango account to another wallet address",
"delegate-placeholder": "Enter a wallet address to delegate to",
"delete": "Delete",
@ -100,6 +100,7 @@
"mango": "Mango",
"mango-stats": "Mango Stats",
"market": "Market",
"markets": "Markets",
"max": "Max",
"max-borrow": "Max Borrow",
"more": "More",
@ -179,6 +180,7 @@
"withdraw-amount": "Withdraw Amount",
"list-market-token": "List Market/Token",
"vote": "Vote",
"yes": "Yes"
"yes": "Yes",
"you": "You"
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -5,6 +5,7 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"health-breakdown": "Health Breakdown",
"init-health": "Init Health",
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
@ -16,7 +17,7 @@
"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. The sum of these values equals your account's total collateral.",
"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",

View File

@ -59,7 +59,7 @@
"date-to": "Date To",
"delegate": "Delegate",
"delegate-account": "Delegate Account",
"delegate-account-info": "Account delegated to {{address}}",
"delegate-account-info": "Account delegated to: {{delegate}}",
"delegate-desc": "Delegate your Mango account to another wallet address",
"delegate-placeholder": "Enter a wallet address to delegate to",
"delete": "Delete",
@ -100,6 +100,7 @@
"mango": "Mango",
"mango-stats": "Mango Stats",
"market": "Market",
"markets": "Markets",
"max": "Max",
"max-borrow": "Max Borrow",
"more": "More",
@ -179,6 +180,7 @@
"withdraw-amount": "Withdraw Amount",
"list-market-token": "List Market/Token",
"vote": "Vote",
"yes": "Yes"
"yes": "Yes",
"you": "You"
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -5,6 +5,7 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"health-breakdown": "Health Breakdown",
"init-health": "Init Health",
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
@ -16,7 +17,7 @@
"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. The sum of these values equals your account's total collateral.",
"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",

View File

@ -59,7 +59,7 @@
"date-to": "至",
"delegate": "委托",
"delegate-account": "委托帐户",
"delegate-account-info": "帐户委托给 {{address}}",
"delegate-account-info": "帐户委托给: {{delegate}}",
"delegate-desc": "以Mango帐户委托给别的钱包地址",
"delegate-placeholder": "输入受委钱包地执",
"delete": "删除",
@ -100,6 +100,7 @@
"mango": "Mango",
"mango-stats": "Mango统计",
"market": "市场",
"markets": "Markets",
"max": "最多",
"max-borrow": "最多借贷",
"more": "更多",
@ -179,6 +180,7 @@
"withdraw-amount": "取款额",
"list-market-token": "List Market/Token",
"vote": "投票",
"yes": "是"
"yes": "是",
"you": "You"
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "动画",
"at": "at",
"avocado": "酪梨",
"banana": "香蕉",
"base-key": "Base Key",
"below": "Below",
"blueberry": "蓝莓",
"bottom-left": "左下",
"bottom-right": "右下",
@ -16,8 +20,20 @@
"custom": "自定",
"dark": "暗",
"display": "显示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "高对比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "语言",
"light": "光",
"limit-order-filled": "限价单成交",
@ -25,11 +41,21 @@
"mango": "芒果",
"mango-classic": "芒果经典",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "通知位置",
"notional": "Notional",
"notifications": "通知",
"number-scroll": "数字滑动",
"olive": "橄榄",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "挂单薄闪光",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "首选探索器",
"recent-trades": "最近交易",
"rpc": "RPC",
@ -37,6 +63,7 @@
"rpc-url": "输入RPC URL",
"russian": "Русский",
"save": "存",
"save-hot-key": "Save Hot Key",
"sign-to-notifications": "登录通知中心以更改设置",
"slider": "滑块",
"solana-beach": "Solana Beach",
@ -48,11 +75,15 @@
"swap-success": "换币/交易成功",
"swap-trade-size-selector": "换币/交易大小选择器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易图表",
"trade-layout": "交易布局",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"transaction-fail": "交易失败",
"transaction-success": "交易成功",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -31,14 +31,15 @@
"insured": "{{token}} Insured",
"last-updated": "Last updated",
"limit": "Limit",
"limit-price": "Limit Price",
"long": "Long",
"maker": "Maker",
"limit-price": "限价价格",
"long": "做多",
"maker": "挂单者",
"margin": "保证金",
"market": "Market",
"market-details": "{{market}}市场细节",
"max-leverage": "最多杠杆",
"min-order-size": "最小订单量",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
"more-details": "More Details",
"no-balances": "No balances",
@ -60,25 +61,25 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"short": "Short",
"show-asks": "Show Asks",
"show-bids": "Show Bids",
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"taker": "Taker",
"preview-sound": "声音预览",
"price-expect": "您收到的价格可能与您预期有差异,并且无法保证完全执行。为了您的安全,最大滑点保持为 2.5%。超过 2.5%滑点的部分不会被平仓。",
"price-provided-by": "语言机来自",
"quote": "计价",
"reduce": "Reduce",
"reduce-only": "限减少",
"sells": "卖单",
"settle-funds": "借清资金",
"settle-funds-error": "借清出错",
"short": "做空",
"show-asks": "显示要价",
"show-bids": "显示出价",
"side": "方向",
"size": "數量",
"spread": "差價",
"stable-price": "穩定價格",
"taker": "吃單者",
"tick-size": "波動單位",
"taker-fee": "Taker Fee",
"tick-size": "Tick Size",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-enable-margin": "Enable spot margin for this trade",

View File

@ -5,6 +5,7 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"health-breakdown": "Health Breakdown",
"init-health": "Init Health",
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
@ -16,7 +17,7 @@
"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. The sum of these values equals your account's total collateral.",
"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",

View File

@ -59,7 +59,7 @@
"date-to": "至",
"delegate": "委託",
"delegate-account": "委託帳戶",
"delegate-account-info": "帳戶委託給 {{address}}",
"delegate-account-info": "帳戶委託給: {{delegate}}",
"delegate-desc": "以Mango帳戶委託給別的錢包地址",
"delegate-placeholder": "輸入受委錢包地執",
"delete": "刪除",
@ -100,6 +100,7 @@
"mango": "Mango",
"mango-stats": "Mango統計",
"market": "市場",
"markets": "Markets",
"max": "最多",
"max-borrow": "最多借貸",
"more": "更多",
@ -179,5 +180,6 @@
"withdraw-amount": "取款額",
"list-market-token": "List Market/Token",
"vote": "投票",
"yes": "是"
"yes": "是",
"you": "You"
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "動畫",
"at": "at",
"avocado": "酪梨",
"banana": "香蕉",
"base-key": "Base Key",
"below": "Below",
"blueberry": "藍莓",
"bottom-left": "左下",
"bottom-right": "右下",
@ -16,8 +20,20 @@
"custom": "自定",
"dark": "暗",
"display": "顯示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "高對比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "語言",
"light": "光",
"limit-order-filled": "限价单成交",
@ -25,11 +41,21 @@
"mango": "芒果",
"mango-classic": "芒果經典",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "通知位置",
"notional": "Notional",
"notifications": "通知",
"number-scroll": "數字滑動",
"olive": "橄欖",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "掛單薄閃光",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "首選探索器",
"recent-trades": "最近交易",
"rpc": "RPC",
@ -37,6 +63,7 @@
"rpc-url": "輸入RPC URL",
"russian": "Русский",
"save": "存",
"save-hot-key": "Save Hot Key",
"sign-to-notifications": "登录通知中心以更改设置",
"slider": "滑塊",
"solana-beach": "Solana Beach",
@ -48,11 +75,15 @@
"swap-success": "換幣/交易成功",
"swap-trade-size-selector": "換幣/交易大小選擇器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易圖表",
"trade-layout": "交易佈局",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"transaction-fail": "交易失敗",
"transaction-success": "交易成功",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -31,14 +31,15 @@
"insured": "{{token}} Insured",
"last-updated": "Last updated",
"limit": "Limit",
"limit-price": "Limit Price",
"long": "Long",
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
"limit-price": "限價價格",
"long": "做多",
"maker": "掛單者",
"maker-fee": "掛單者 Fee",
"margin": "保證金",
"market": "Market",
"market-details": "{{market}}市場細節",
"max-leverage": "最多槓桿",
"min-order-size": "最小訂單量",
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
"more-details": "More Details",
"no-balances": "No balances",
@ -60,25 +61,26 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
"price-provided-by": "Oracle by",
"quote": "Quote",
"preview-sound": "聲音預覽",
"price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。",
"price-provided-by": "語言機來自",
"quote": "計價",
"reduce": "Reduce",
"reduce-only": "限減少",
"sells": "賣單",
"settle-funds": "借清資金",
"settle-funds-error": "借清出錯",
"short": "做空",
"show-asks": "顯示要價",
"show-bids": "顯示出價",
"side": "方向",
"size": "數量",
"spread": "差價",
"stable-price": "穩定價格",
"taker": "吃單者",
"taker-fee": "吃單者 Fee",
"tick-size": "波動單位",
"realized-pnl": "Realized PnL",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"short": "Short",
"show-asks": "Show Asks",
"show-bids": "Show Bids",
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"taker": "Taker",
"taker-fee": "Taker Fee",
"tick-size": "Tick Size",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-enable-margin": "Enable spot margin for this trade",

View File

@ -150,10 +150,6 @@ export type MangoStore = {
perpPositions: PerpPosition[]
spotBalances: SpotBalances
interestTotals: { data: TotalInterestDataItem[]; loading: boolean }
performance: {
data: PerformanceDataItem[]
loading: boolean
}
swapHistory: {
data: SwapHistoryItem[]
initialLoad: boolean
@ -245,10 +241,6 @@ export type MangoStore = {
params?: string,
limit?: number
) => Promise<void>
fetchAccountPerformance: (
mangoAccountPk: string,
range: number
) => Promise<void>
fetchGroup: () => Promise<void>
reloadMangoAccount: () => Promise<void>
fetchMangoAccounts: (ownerPk: PublicKey) => Promise<void>
@ -313,7 +305,6 @@ const mangoStore = create<MangoStore>()(
perpPositions: [],
spotBalances: {},
interestTotals: { data: [], loading: false },
performance: { data: [], loading: true },
swapHistory: { data: [], loading: true, initialLoad: true },
tradeHistory: { data: [], loading: true },
},
@ -438,43 +429,6 @@ const mangoStore = create<MangoStore>()(
})
}
},
fetchAccountPerformance: async (
mangoAccountPk: string,
range: number
) => {
const set = get().set
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs()
.subtract(range, 'day')
.format('YYYY-MM-DD')}`
)
const parsedResponse:
| null
| EmptyObject
| AccountPerformanceData[] = await response.json()
if (parsedResponse && Object.keys(parsedResponse)?.length) {
const entries = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0])
)
const stats = entries.map(([key, value]) => {
return { ...value, time: key } as PerformanceDataItem
})
set((state) => {
state.mangoAccount.performance.data = stats.reverse()
})
}
} catch (e) {
console.error('Failed to load account performance data', e)
} finally {
set((state) => {
state.mangoAccount.performance.loading = false
})
}
},
fetchActivityFeed: async (
mangoAccountPk: string,
offset = 0,

152
utils/account.ts Normal file
View File

@ -0,0 +1,152 @@
import {
AccountPerformanceData,
AccountVolumeTotalData,
EmptyObject,
FormattedHourlyAccountVolumeData,
HourlyAccountVolumeData,
PerformanceDataItem,
TotalAccountFundingItem,
} from 'types'
import { MANGO_DATA_API_URL } from './constants'
import dayjs from 'dayjs'
export const fetchAccountPerformance = async (
mangoAccountPk: string,
range: number
) => {
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs()
.subtract(range, 'day')
.format('YYYY-MM-DD')}`
)
const parsedResponse: null | EmptyObject | AccountPerformanceData[] =
await response.json()
if (parsedResponse && Object.keys(parsedResponse)?.length) {
const entries = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0])
)
const stats = entries.map(([key, value]) => {
return { ...value, time: key } as PerformanceDataItem
})
return stats.reverse()
} else return []
} catch (e) {
console.error('Failed to load account performance data', e)
return []
}
}
export const fetchFundingTotals = async (mangoAccountPk: string) => {
try {
const data = await fetch(
`${MANGO_DATA_API_URL}/stats/funding-account-total?mango-account=${mangoAccountPk}`
)
const res = await data.json()
if (res) {
const entries: [string, Omit<TotalAccountFundingItem, 'market'>][] =
Object.entries(res)
const stats: TotalAccountFundingItem[] = entries
.map(([key, value]) => {
return {
long_funding: value.long_funding * -1,
short_funding: value.short_funding * -1,
market: key,
}
})
.filter((x) => x)
return stats
}
} catch (e) {
console.log('Failed to fetch account funding', e)
}
}
export const fetchVolumeTotals = async (mangoAccountPk: string) => {
try {
const [perpTotal, spotTotal] = await Promise.all([
fetch(
`${MANGO_DATA_API_URL}/stats/perp-volume-total?mango-account=${mangoAccountPk}`
),
fetch(
`${MANGO_DATA_API_URL}/stats/spot-volume-total?mango-account=${mangoAccountPk}`
),
])
const [perpTotalData, spotTotalData] = await Promise.all([
perpTotal.json(),
spotTotal.json(),
])
const combinedData = [perpTotalData, spotTotalData]
if (combinedData.length) {
return combinedData.reduce((a, c) => {
const entries: AccountVolumeTotalData[] = Object.entries(c)
const marketVol = entries.reduce((a, c) => {
return a + c[1].volume_usd
}, 0)
return a + marketVol
}, 0)
}
return 0
} catch (e) {
console.log('Failed to fetch spot volume', e)
return 0
}
}
const formatHourlyVolumeData = (data: HourlyAccountVolumeData[]) => {
if (!data || !data.length) return []
const formattedData: FormattedHourlyAccountVolumeData[] = []
// Loop through each object in the original data array
for (const obj of data) {
// Loop through the keys (markets) in each object
for (const market in obj) {
// Loop through the timestamps in each market
for (const timestamp in obj[market]) {
// Find the corresponding entry in the formatted data array based on the timestamp
let entry = formattedData.find((item) => item.time === timestamp)
// If the entry doesn't exist, create a new entry
if (!entry) {
entry = { time: timestamp, total_volume_usd: 0, markets: {} }
formattedData.push(entry)
}
// Increment the total_volume_usd by the volume_usd value
entry.total_volume_usd += obj[market][timestamp].volume_usd
// Add or update the market entry in the markets object
entry.markets[market] = obj[market][timestamp].volume_usd
}
}
}
return formattedData
}
export const fetchHourlyVolume = async (mangoAccountPk: string) => {
try {
const [perpHourly, spotHourly] = await Promise.all([
fetch(
`${MANGO_DATA_API_URL}/stats/perp-volume-hourly?mango-account=${mangoAccountPk}`
),
fetch(
`${MANGO_DATA_API_URL}/stats/spot-volume-hourly?mango-account=${mangoAccountPk}`
),
])
const [perpHourlyData, spotHourlyData] = await Promise.all([
perpHourly.json(),
spotHourly.json(),
])
const hourlyVolume = [perpHourlyData, spotHourlyData]
return formatHourlyVolumeData(hourlyVolume)
} catch (e) {
console.log('Failed to fetch spot volume', e)
}
}

View File

@ -59,6 +59,8 @@ export const STATS_TAB_KEY = 'activeStatsTab-0.1'
export const USE_ORDERBOOK_FEED_KEY = 'useOrderbookFeed-0.1'
export const HOT_KEYS_KEY = 'hotKeys-0.1'
// Unused
export const PROFILE_CATEGORIES = [
'borrower',
@ -109,17 +111,20 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
dai: true,
dual: true,
eth: true,
ethpo: true,
'eth (portal)': true,
hnt: true,
jitosol: true,
ldo: true,
mngo: true,
msol: true,
orca: true,
ray: true,
rndr: true,
sol: true,
stsol: true,
usdc: true,
usdt: true,
wbtcpo: true,
'wbtc (portal)': true,
}

View File

@ -495,6 +495,8 @@ export const getFormattedBankValues = (group: Group, bank: Bank) => {
maintLiabWeight: bank.maintLiabWeight.toFixed(2),
initAssetWeight: bank.initAssetWeight.toFixed(2),
initLiabWeight: bank.initLiabWeight.toFixed(2),
scaledInitAssetWeight: bank.scaledInitAssetWeight(bank.price).toFixed(2),
scaledInitLiabWeight: bank.scaledInitLiabWeight(bank.price).toFixed(2),
depositWeightScale: toUiDecimalsForQuote(bank.depositWeightScaleStartQuote),
borrowWeightScale: toUiDecimalsForQuote(bank.borrowWeightScaleStartQuote),
rate0: (100 * bank.rate0.toNumber()).toFixed(2),

View File

@ -5379,6 +5379,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"
hotkeys-js@^3.8.1:
version "3.10.2"
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.10.2.tgz#cf52661904f5a13a973565cb97085fea2f5ae257"
integrity sha512-Z6vLmJTYzkbZZXlBkhrYB962Q/rZGc/WHQiyEGu9ZZVF7bAeFDjjDa31grWREuw9Ygb4zmlov2bTkPYqj0aFnQ==
howler@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da"
@ -7139,6 +7144,14 @@ react-grid-layout@1.3.4:
react-draggable "^4.0.0"
react-resizable "^3.0.4"
react-hot-keys@2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/react-hot-keys/-/react-hot-keys-2.7.2.tgz#7d2b02b7e2cf69182ea71ca01885446ebfae01d2"
integrity sha512-Z7eSh7SU6s52+zP+vkfFoNk0x4kgEmnwqDiyACKv53crK2AZ7FUaBLnf+vxLor3dvtId9murLmKOsrJeYgeHWw==
dependencies:
hotkeys-js "^3.8.1"
prop-types "^15.7.2"
react-i18next@^11.18.0:
version "11.18.6"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"