diff --git a/components/AccountsButton.tsx b/components/AccountsButton.tsx index a0cb4d68..d3ba9d07 100644 --- a/components/AccountsButton.tsx +++ b/components/AccountsButton.tsx @@ -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 ? ( diff --git a/components/ThemeSwitcher.tsx b/components/ThemeSwitcher.tsx deleted file mode 100644 index 5994d6d8..00000000 --- a/components/ThemeSwitcher.tsx +++ /dev/null @@ -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 ( - } - panelClassName="rounded-t-none" - > - {THEMES.map((value) => ( - setTheme(t(value))} - key={value} - > - {t(value)} - - ))} - - ) -} - -export default ThemeSwitcher diff --git a/components/TokenList.tsx b/components/TokenList.tsx index 9f915f2e..ad7d3bab 100644 --- a/components/TokenList.tsx +++ b/components/TokenList.tsx @@ -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 = () => { - -
- - - {t('account:init-health')} - - -
- - -
- - - {t('account:maint-health')} - - -
- {t('trade:in-orders')} {t('trade:unsettled')} @@ -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 ( @@ -191,7 +138,7 @@ const TokenList = () => {
-

{bank.name}

+

{symbol}

@@ -201,44 +148,6 @@ const TokenList = () => { stacked /> - -
-

- -

-

- {initHealth > 0 - ? initAssetWeight - : initHealth < 0 - ? initLiabWeight - : 0} - x -

-
- - -
-

- -

-

- {maintHealth > 0 - ? maintAssetWeight - : maintHealth < 0 - ? maintLiabWeight - : 0} - x -

-
- { ) 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`} >
-
-
+
+

{symbol}

+
+
+
+

- - -

+ + +
-
-
- {

+
+ +
@@ -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 : ( - } - panelClassName="w-40 shadow-md" - postion="leftBottom" - > -
-
- + + {({ open }) => ( +
+ + {open ? ( + + ) : ( + + )} + {t('actions')} + + + +
+
+ +
+

{formatTokenSymbol(bank.name)}

+
+ handleShowActionModals(bank.name, 'deposit')} + > + {t('deposit')} + + {balance < 0 ? ( + handleShowActionModals(bank.name, 'repay')} + > + {t('repay')} + + ) : null} + {balance && balance > 0 ? ( + + handleShowActionModals(bank.name, 'withdraw') + } + > + {t('withdraw')} + + ) : null} + handleShowActionModals(bank.name, 'borrow')} + > + {t('borrow')} + + + {t('swap')} + + {spotMarket ? ( + + {t('trade')} + + ) : null} +
+
-

{formatTokenSymbol(bank.name)}

-
- handleShowActionModals(bank.name, 'deposit')} - > - {t('deposit')} - - {balance < 0 ? ( - handleShowActionModals(bank.name, 'repay')} - > - {t('repay')} - - ) : null} - {balance && balance > 0 ? ( - handleShowActionModals(bank.name, 'withdraw')} - > - {t('withdraw')} - - ) : null} - handleShowActionModals(bank.name, 'borrow')} - > - {t('borrow')} - - - {t('swap')} - - {spotMarket ? ( - - {t('trade')} - - ) : null} - + )} + )} {showDepositModal ? ( { 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 = () => { {t('copy-address')} setShowEditAccountModal(true)} > @@ -136,6 +137,7 @@ const AccountActions = () => { {t('edit-account')} setShowDelegateModal(true)} > @@ -143,6 +145,7 @@ const AccountActions = () => { {t('delegate-account')} setShowCloseAccountModal(true)} > diff --git a/components/account/AccountChart.tsx b/components/account/AccountChart.tsx index ba0728ee..488548b4 100644 --- a/components/account/AccountChart.tsx +++ b/components/account/AccountChart.tsx @@ -26,7 +26,7 @@ const AccountChart = ({ chartToShow: ChartToShow setChartToShow: (chart: ChartToShow) => void customTooltip?: ContentType - data: PerformanceDataItem[] | HourlyFundingChartData[] + data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined hideChart: () => void loading?: boolean yDecimals?: number @@ -36,7 +36,7 @@ const AccountChart = ({ const [daysToShow, setDaysToShow] = useState('1') const chartData = useMemo(() => { - if (!data.length) return [] + if (!data || !data.length) return [] if (chartToShow === 'cumulative-interest-value') { return data.map((d) => ({ interest_value: diff --git a/components/account/AccountHeroStats.tsx b/components/account/AccountHeroStats.tsx new file mode 100644 index 00000000..7594b6ae --- /dev/null +++ b/components/account/AccountHeroStats.tsx @@ -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 ( + <> +
+
+
+
+ +

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

+ {maintHealth < 100 && mangoAccountAddress ? ( + <> +

+ Your account health is {maintHealth}% +

+

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

+

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

+

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

+ + ) : null} +
+ } + > +

+ {t('health')} +

+ + + handleChartToShow('health-contributions')} + > + + + +
+
+

+ {maintHealth}% +

+ +
+ + + {t('leverage')}: + + + x + + +
+
+
+
+ +

+ {t('free-collateral')} +

+
+

+ +

+ + + {t('total')}: + + + + + +
+
+
+
+
+ +

+ {t('pnl')} +

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

+ +

+
+ +

{t('rolling-change')}

+
+
+
+
+
+
+

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

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

+ +

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

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

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

+ +

+
+ +

{t('rolling-change')}

+
+
+
+
+
+ +

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

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

+ +

+ )} +
+
+ + ) +} + +export default AccountHeroStats diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 2059c0aa..7c00b943 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -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][] = - 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('') - const [showExpandChart, setShowExpandChart] = useState(false) const [showPnlHistory, setShowPnlHistory] = useState(false) - const { theme } = useTheme() const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false // const tourSettings = mangoStore((s) => s.settings.tours) // const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY) - const [animationSettings] = useLocalStorageState( - ANIMATION_SETTINGS_KEY, - INITIAL_ANIMATION_SETTINGS - ) const [activeTab, setActiveTab] = useLocalStorageState( 'accountHeroKey-0.1', 'account-value' ) - - useEffect(() => { - if (mangoAccountAddress || (!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 ? ( <>
@@ -459,427 +115,38 @@ const AccountPage = () => { ))}
- {activeTab === 'account-value' ? ( -
-
-
- {animationSettings['number-scroll'] ? ( - group && mangoAccount ? ( - - ) : ( - - ) - ) : ( - - )} -
-
- -

{t('rolling-change')}

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

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

- {maintHealth < 100 && mangoAccountAddress ? ( - <> -

- Your account health is {maintHealth}% -

-

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

-

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

-

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

- - ) : null} -
- } - > -

- {t('health')} -

- -
-

- {maintHealth}% -

- -
- - - {t('leverage')}: - - - x - - -
-
-
-
- -

- {t('free-collateral')} -

-
-

- -

- - - {t('total')}: - - - - - -
-
-
-
-
- -

- {t('pnl')} -

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

- -

-
- -

{t('rolling-change')}

-
-
-
-
-
-
-

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

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

- -

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

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

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

- -

-
- -

{t('rolling-change')}

-
-
-
-
-
- -

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

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

- -

- )} -
-
+ {/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? ( ) : null} */} {showPnlHistory ? ( @@ -891,7 +158,7 @@ const AccountPage = () => { @@ -899,7 +166,7 @@ const AccountPage = () => { @@ -911,6 +178,8 @@ const AccountPage = () => { hideChart={handleHideChart} loading={loadingHourlyVolume} /> + ) : chartToShow === 'health-contributions' ? ( + ) : ( void +}) => { + const { t } = useTranslation('common') + const { theme } = useTheme() + const { group } = useMangoGroup() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const [showExpandChart, setShowExpandChart] = useState(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 ( +
+
+
+ {animationSettings['number-scroll'] ? ( + group && mangoAccount ? ( + + ) : ( + + ) + ) : ( + + )} +
+
+ +

{t('rolling-change')}

+
+
+ {!loading ? ( + rollingDailyData.length ? ( +
onHoverMenu(showExpandChart, 'onMouseEnter')} + onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave')} + > + = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] + } + data={rollingDailyData.concat(latestAccountData)} + name="accountValue" + xKey="time" + yKey="account_equity" + /> + + handleShowAccountValueChart()} + > + + + +
+ ) : null + ) : mangoAccountAddress ? ( + +
+ + ) : null} +
+ ) +} + +export default AccountValue diff --git a/components/account/ActionsLinkButton.tsx b/components/account/ActionsLinkButton.tsx index 9523f20b..72f5ad37 100644 --- a/components/account/ActionsLinkButton.tsx +++ b/components/account/ActionsLinkButton.tsx @@ -4,17 +4,19 @@ import { ReactNode } from 'react' const ActionsLinkButton = ({ children, + disabled, mangoAccount, onClick, }: { children: ReactNode + disabled?: boolean mangoAccount: MangoAccount onClick: () => void }) => { return ( {children} diff --git a/components/account/HealthContributions.tsx b/components/account/HealthContributions.tsx index c6993721..350dda41 100644 --- a/components/account/HealthContributions.tsx +++ b/components/account/HealthContributions.tsx @@ -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 ? ( <> -
+
- {/*
- {CHART_TABS.map((tab) => ( - - ))} -
*/} +

{t('account:health-breakdown')}

- +
+
+ +

+ {t('account:init-health')} +

+
+ +
+
+ +

+ {t('account:maint-health')} +

+
+ +
+
+ {maintHealthTokens.length ? ( +
+

{t('tokens')}

+ + + + + + + + + + + {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 ( + + + + + + + ) + })} + +
{t('token')} +
+ {t('balance')} +
+
+
+ + + {t('account:init-health')} + + +
+
+
+ + + {t('account:maint-health')} + + +
+
+
+
+ +
+

{asset}

+
+
+ {bank ? ( + + ) : ( + '–' + )} + +
+

+ +

+

+ {initContribution > 0 + ? initAssetWeight.toFixed(2) + : initContribution < 0 + ? initLiabWeight.toFixed(2) + : 0} + x +

+
+
+
+

+ +

+

+ {contribution > 0 + ? maintAssetWeight.toFixed(2) + : contribution < 0 + ? maintLiabWeight.toFixed(2) + : 0} + x +

+
+
+
+ ) : null} + {maintHealthMarkets.length ? ( + <> +

{t('markets')}

+ + + + + + + + + + {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 ( + + + + + + ) + })} + +
{t('market')} +
+ + + {t('account:init-health')} + + +
+
+
+ + + {t('account:maint-health')} + + +
+
+
+ +

{asset}

+
+
+
+

+ +

+

+ {initContribution > 0 + ? initAssetWeight.toFixed(2) + : initContribution < 0 + ? initLiabWeight.toFixed(2) + : 0} + x +

+
+
+
+

+ +

+

+ {contribution > 0 + ? maintAssetWeight.toFixed(2) + : contribution < 0 + ? maintLiabWeight.toFixed(2) + : 0} + x +

+
+
+ + ) : null} - ) + ) : null } export default HealthContributions diff --git a/components/account/HealthContributionsChart.tsx b/components/account/HealthContributionsChart.tsx index 66d82e12..429c18bb 100644 --- a/components/account/HealthContributionsChart.tsx +++ b/components/account/HealthContributionsChart.tsx @@ -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(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 ( -
-
- {contributions - .sort((a, b) => b.contribution - a.contribution) - .map((asset) => { - const assetOrLiability = asset.isAsset ? 1 : -1 - return ( -
-

- {formatTokenSymbol(asset.asset)} -

-

- {formatCurrencyValue( - asset.contribution * assetOrLiability - )} -

-
- ) - })} -
-
-

{t('total')}

-

- {formatCurrencyValue(total)} -

-
-
- ) - } - - return null + const handleLegendClick = (entry: HealthContribution, index: number) => { + setActiveIndex(index) } - return ( -
- {data.length ? ( - - } - position={{ x: 88, y: 0 }} - /> - - {data.map((entry, index) => { - const fillColor = entry.isAsset - ? COLORS.UP[theme] - : COLORS.DOWN[theme] - return ( - - ) - })} - - + 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 ( + + + + ) + } + + const renderLegendLogo = (asset: string) => { + const group = mangoStore.getState().group + if (!group) + return + const isSpotMarket = asset.includes('/') + if (isSpotMarket) { + const market = group.getSerum3MarketByName(asset) + return market ? ( + ) : ( -
- )} + + ) + } else { + const bank = group.banksMapByName.get(asset)?.[0] + return bank ? ( +
+ +
+ ) : ( + + ) + } + } + + return filteredData.length ? ( +
+
+ + + + {filteredData.map((entry, index) => { + const fillColor = entry.isAsset + ? COLORS.UP[theme] + : COLORS.DOWN[theme] + return ( + + ) + })} + + + + {chartHeroValue ? ( +
+

{chartHeroAsset}

+ + {formatCurrencyValue(chartHeroValue)} + +
+ ) : null} +
+
+ {filteredData.map((d, i) => ( +
handleLegendClick(d, i)} + onMouseEnter={() => handleMouseEnter(d, i)} + onMouseLeave={handleMouseLeave} + > + {renderLegendLogo(d.asset)} +

+ {d.asset} +

+
+ ))} +
- ) + ) : null } export default HealthContributionsChart diff --git a/components/borrow/BorrowPage.tsx b/components/borrow/BorrowPage.tsx index 7a332736..6de51e7c 100644 --- a/components/borrow/BorrowPage.tsx +++ b/components/borrow/BorrowPage.tsx @@ -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 = () => {
- -

- {t('borrow:current-borrow-value')} -

-
+

+ {t('borrow:current-borrow-value')} +

{animationSettings['number-scroll'] ? ( group && mangoAccount ? ( diff --git a/components/modals/DelegateModal.tsx b/components/modals/DelegateModal.tsx index 03147a06..2ae4d27d 100644 --- a/components/modals/DelegateModal.tsx +++ b/components/modals/DelegateModal.tsx @@ -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', diff --git a/components/modals/MangoAccountsListModal.tsx b/components/modals/MangoAccountsListModal.tsx index 096e7f20..88341ead 100644 --- a/components/modals/MangoAccountsListModal.tsx +++ b/components/modals/MangoAccountsListModal.tsx @@ -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 = ({
diff --git a/components/modals/PnlHistoryModal.tsx b/components/modals/PnlHistoryModal.tsx index 96a19f68..81cf8a69 100644 --- a/components/modals/PnlHistoryModal.tsx +++ b/components/modals/PnlHistoryModal.tsx @@ -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 diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx new file mode 100644 index 00000000..be9060f2 --- /dev/null +++ b/components/settings/HotKeysSettings.tsx @@ -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 ( + <> +
+
+

{t('settings:hot-keys')}

+

{t('settings:hot-keys-desc')}

+
+ {hotKeys.length ? ( + + ) : null} +
+ {hotKeys.length === 20 ? ( +
+ +
+ ) : null} + {hotKeys.length ? ( + + + + + + + + + + + + {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 ( + + + + + + + + + + ) + })} + +
{t('settings:key-sequence')}{t('trade:order-type')}{t('trade:side')}{t('trade:size')}{t('price')}{t('settings:options')} + +
{keySequence}{t(`trade:${orderType}`)}{t(orderSide)}{size}{price} + {Object.entries(options).map((e) => { + return e[1] + ? `${e[0] !== 'margin' ? ', ' : ''}${t( + `trade:${e[0]}` + )}` + : '' + })} + +
+ handleDeleteKey(keySequence)} + size="small" + > + + +
+
+ ) : ( +
+
+ +

{t('settings:no-hot-keys')}

+ +
+
+ )} + {showHotKeyModal ? ( + setShowHotKeyModal(false)} + /> + ) : null} + + ) +} + +export default HotKeysSettings + +type FormErrors = Partial> + +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(HOT_KEYS_KEY, []) + const [hotKeyForm, setHotKeyForm] = useState({ + ...DEFAULT_FORM_VALUES, + }) + const [formErrors, setFormErrors] = useState({}) + + 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 ( + + <> +

{t('settings:new-hot-key')}

+
+
+
+
+
+
+
+
+
+
+
+
+ + + handleSetForm('size', e.target.value)} + suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'} + /> + {formErrors.size ? ( +
+ +
+ ) : null} +
+ {hotKeyForm.orderType === 'limit' ? ( +
+ + + handleSetForm('price', e.target.value)} + placeholder="e.g. 1%" + suffix="%" + /> + {formErrors.price ? ( +
+ +
+ ) : null} +
+ ) : null} +
+
+ {hotKeyForm.orderType === 'limit' ? ( +
+
+ + handlePostOnlyChange(e.target.checked)} + > + {t('trade:post')} + + +
+
+ +
+ handleIocChange(e.target.checked)} + > + IOC + +
+
+
+
+ ) : null} +
+ + handleSetForm('margin', e.target.checked)} + > + {t('trade:margin')} + + +
+
+ +
+ handleSetForm('reduce', e.target.checked)} + > + {t('trade:reduce-only')} + +
+
+
+
+ + +
+ ) +} diff --git a/components/settings/NotificationSettings.tsx b/components/settings/NotificationSettings.tsx index 50ad70a2..adca0257 100644 --- a/components/settings/NotificationSettings.tsx +++ b/components/settings/NotificationSettings.tsx @@ -42,7 +42,7 @@ const NotificationSettings = () => {

{t('settings:notifications')}

{isAuth ? ( -
+

{t('settings:limit-order-filled')}

{ />
) : ( -
+
{connected ? (
diff --git a/components/settings/SettingsPage.tsx b/components/settings/SettingsPage.tsx index c0bb54d3..fc812e27 100644 --- a/components/settings/SettingsPage.tsx +++ b/components/settings/SettingsPage.tsx @@ -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 (
@@ -14,9 +19,14 @@ const SettingsPage = () => {
-
+
+ {!isMobile ? ( +
+ +
+ ) : null}
diff --git a/components/shared/BalancesTable.tsx b/components/shared/BalancesTable.tsx index 4066de94..969e8a85 100644 --- a/components/shared/BalancesTable.tsx +++ b/components/shared/BalancesTable.tsx @@ -66,6 +66,7 @@ const BalancesTable = () => { {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 = () => {
- {bank.name} + {symbol}
@@ -104,6 +105,7 @@ const BalancesTable = () => {
{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 = () => { >
-
+
-
-

- {bank.name} -

+

{symbol}

+
+
+
-

+ -

+
+
-
{ if (!balance) return

0

return ( -

+

{!isUnownedAccount && !isMobile ? ( asPath.includes('/trade') && isBaseOrQuote ? ( { - 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 ( - - {({ open }) => ( -

- - {open ? : icon} - - - - {children} - - -
- )} - - ) -} - -export default IconDropMenu diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index 15cd99de..a67a7dca 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -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, }) diff --git a/components/trade/OpenOrders.tsx b/components/trade/OpenOrders.tsx index d3d5fb8c..316a78e6 100644 --- a/components/trade/OpenOrders.tsx +++ b/components/trade/OpenOrders.tsx @@ -455,11 +455,9 @@ const OpenOrders = () => { ) : ( -
- - {market.name} - -
+ + {market.name} + )} {o instanceof PerpOrder ? ( @@ -482,7 +480,10 @@ const OpenOrders = () => { {' '} {quoteBank && quoteBank.name !== 'USDC' ? ( @@ -491,6 +492,9 @@ const OpenOrders = () => { ) : null}

+ + +
) : ( diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 1ec4d700..8e3c0390 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -321,6 +321,7 @@ const PerpPositions = () => { mangoAccount ) const unsettledPnl = position.getUnsettledPnlUi(market) + const notional = Math.abs(floorBasePosition) * market._uiPrice return ( {({ open }) => ( @@ -348,33 +349,33 @@ const PerpPositions = () => { decimals={getDecimalCount( market.minOrderSize )} - />{' '} - - {market.name.split('-')[0]} - + /> | - 0 - ? 'text-th-up' - : 'text-th-down' - }`} - > - + +
- +
+ 0 ? 'text-th-up' : 'text-th-down' + }`} + > + + + +
{
@@ -488,33 +486,31 @@ const PerpPositions = () => {

{t('trade:unrealized-pnl')}

-

- - } - delay={100} + + } + delay={100} + > + = 0 + ? 'text-th-up' + : 'text-th-down' + }`} > - = 0 - ? 'text-th-up' - : 'text-th-down' - }`} - > - - - -

+ + +

ROE

diff --git a/components/trade/TradeAdvancedPage.tsx b/components/trade/TradeAdvancedPage.tsx index 61352fb2..a4c5adb6 100644 --- a/components/trade/TradeAdvancedPage.tsx +++ b/components/trade/TradeAdvancedPage.tsx @@ -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 ? ( ) : ( - <> + console.log('bp: ', bp)} @@ -262,7 +263,7 @@ const TradeAdvancedPage = () => { {/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? ( ) : null} */} - + ) } diff --git a/components/trade/TradeHotKeys.tsx b/components/trade/TradeHotKeys.tsx new file mode 100644 index 00000000..48f9690c --- /dev/null +++ b/components/trade/TradeHotKeys.tsx @@ -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 ? ( + k.keySequence).toString()} + onKeyDown={onKeyDown} + > + {children} + + ) : ( + <>{children} + ) +} + +export default TradeHotKeys diff --git a/components/wallet/ConnectedMenu.tsx b/components/wallet/ConnectedMenu.tsx index 7e891282..0352d716 100644 --- a/components/wallet/ConnectedMenu.tsx +++ b/components/wallet/ConnectedMenu.tsx @@ -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({ diff --git a/hooks/useAccountHourlyVolumeStats.ts b/hooks/useAccountHourlyVolumeStats.ts new file mode 100644 index 00000000..1b56478a --- /dev/null +++ b/hooks/useAccountHourlyVolumeStats.ts @@ -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, + } +} diff --git a/hooks/useAccountPerformanceData.ts b/hooks/useAccountPerformanceData.ts new file mode 100644 index 00000000..f8b6f0e8 --- /dev/null +++ b/hooks/useAccountPerformanceData.ts @@ -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, + } +} diff --git a/hooks/useSelectedMarket.ts b/hooks/useSelectedMarket.ts index 8adc93ff..5c994a61 100644 --- a/hooks/useSelectedMarket.ts +++ b/hooks/useSelectedMarket.ts @@ -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 { diff --git a/next.config.js b/next.config.js index 26d9b600..7e1bf3c5 100644 --- a/next.config.js +++ b/next.config.js @@ -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 diff --git a/package.json b/package.json index 538451e9..6b74d823 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index bf482d83..213e3e29 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -468,7 +468,7 @@ const Dashboard: NextPage = () => { /> { `${ suggestedFields.maintAssetWeight || formattedBankValues.maintAssetWeight - }/ + } / ${ suggestedFields.maintLiabWeight || formattedBankValues.maintLiabWeight @@ -485,7 +485,7 @@ const Dashboard: NextPage = () => { /> { `${ suggestedFields.initAssetWeight || formattedBankValues.initAssetWeight - }/ + } / ${ suggestedFields.initLiabWeight || formattedBankValues.initLiabWeight }` } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/orca.svg b/public/icons/orca.svg new file mode 100644 index 00000000..4d7c6d5e --- /dev/null +++ b/public/icons/orca.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/wbtcpo.svg b/public/icons/wbtcpo.svg new file mode 100644 index 00000000..cffdb69d --- /dev/null +++ b/public/icons/wbtcpo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 9b754391..95647b1a 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -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", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5195b442..cbacbdcb 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 4ff063a5..91c6aee4 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -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", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index cbcc9b54..40f3314e 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -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", diff --git a/public/locales/es/account.json b/public/locales/es/account.json index 9b754391..95647b1a 100644 --- a/public/locales/es/account.json +++ b/public/locales/es/account.json @@ -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", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 5195b442..cbacbdcb 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index 4ff063a5..91c6aee4 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -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", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index cbcc9b54..40f3314e 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -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", diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index 9b754391..95647b1a 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -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", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 5195b442..cbacbdcb 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index 4ff063a5..91c6aee4 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -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", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index cbcc9b54..40f3314e 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -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", diff --git a/public/locales/zh/account.json b/public/locales/zh/account.json index 9b754391..95647b1a 100644 --- a/public/locales/zh/account.json +++ b/public/locales/zh/account.json @@ -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", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 6a0e5838..9c1d2eaf 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index 37a9621f..59a2f40d 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -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", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index cbcc9b54..9cab4139 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -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", diff --git a/public/locales/zh_tw/account.json b/public/locales/zh_tw/account.json index 9b754391..95647b1a 100644 --- a/public/locales/zh_tw/account.json +++ b/public/locales/zh_tw/account.json @@ -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", diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index 5061696d..f96c5910 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -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" } diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index 9d33405c..c2fa321c 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -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", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index cbcc9b54..60dc6b37 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -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", diff --git a/store/mangoStore.ts b/store/mangoStore.ts index dc4f0efd..f5fce086 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -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 - fetchAccountPerformance: ( - mangoAccountPk: string, - range: number - ) => Promise fetchGroup: () => Promise reloadMangoAccount: () => Promise fetchMangoAccounts: (ownerPk: PublicKey) => Promise @@ -313,7 +305,6 @@ const mangoStore = create()( 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()( }) } }, - 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, diff --git a/utils/account.ts b/utils/account.ts new file mode 100644 index 00000000..3c6a7c84 --- /dev/null +++ b/utils/account.ts @@ -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][] = + 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) + } +} diff --git a/utils/constants.ts b/utils/constants.ts index 86ddda77..bcb6ad30 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -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, } diff --git a/utils/governance/listingTools.ts b/utils/governance/listingTools.ts index 418e1147..f84e089d 100644 --- a/utils/governance/listingTools.ts +++ b/utils/governance/listingTools.ts @@ -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), diff --git a/yarn.lock b/yarn.lock index 65dac0a0..aef37c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"