From a9124d5148b0bead561c13b8136d8706412b8b6a Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 8 Feb 2024 11:24:17 +1100 Subject: [PATCH] update token page layout --- components/TokenList.tsx | 40 +----- components/token/ActionPanel.tsx | 209 +++++++++++++++++---------- components/token/ChartTabs.tsx | 16 ++- components/token/CoingeckoStats.tsx | 82 +---------- components/token/PriceChart.tsx | 95 ++++++++++++ components/token/TokenPage.tsx | 214 +++++++++++++++++++--------- hooks/useAccountInterest.ts | 44 ++++++ package.json | 1 + public/locales/en/token.json | 2 + public/locales/es/token.json | 2 + public/locales/pt/token.json | 2 + public/locales/ru/token.json | 2 + public/locales/zh/token.json | 2 + public/locales/zh_tw/token.json | 2 + utils/contentful.ts | 117 +++++++++++++++ yarn.lock | 85 ++++++++++- 16 files changed, 641 insertions(+), 274 deletions(-) create mode 100644 components/token/PriceChart.tsx create mode 100644 hooks/useAccountInterest.ts create mode 100644 utils/contentful.ts diff --git a/components/TokenList.tsx b/components/TokenList.tsx index 780f74f0..5865b51e 100644 --- a/components/TokenList.tsx +++ b/components/TokenList.tsx @@ -28,7 +28,6 @@ import DepositWithdrawModal from './modals/DepositWithdrawModal' import BorrowRepayModal from './modals/BorrowRepayModal' import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions' import { - MANGO_DATA_API_URL, SHOW_ZERO_BALANCES_KEY, TOKEN_REDUCE_ONLY_OPTIONS, USDC_MINT, @@ -48,9 +47,8 @@ import { useSortableData } from 'hooks/useSortableData' import TableTokenName from './shared/TableTokenName' import CloseBorrowModal from './modals/CloseBorrowModal' import { floorToDecimal } from 'utils/numbers' -import { useQuery } from '@tanstack/react-query' -import { TotalInterestDataItem } from 'types' import SheenLoader from './shared/SheenLoader' +import useAccountInterest from 'hooks/useAccountInterest' export const handleOpenCloseBorrowModal = (borrowBank: Bank) => { const group = mangoStore.getState().group @@ -100,30 +98,6 @@ export const handleCloseBorrowModal = () => { }) } -export const fetchInterestData = async (mangoAccountPk: string) => { - try { - const response = await fetch( - `${MANGO_DATA_API_URL}/stats/interest-account-total?mango-account=${mangoAccountPk}`, - ) - const parsedResponse: Omit[] | null = - await response.json() - if (parsedResponse) { - const entries: [string, Omit][] = - Object.entries(parsedResponse).sort((a, b) => b[0].localeCompare(a[0])) - - const stats: TotalInterestDataItem[] = entries - .map(([key, value]) => { - return { ...value, symbol: key } - }) - .filter((x) => x) - return stats - } else return [] - } catch (e) { - console.log('Failed to fetch account funding', e) - return [] - } -} - type TableData = { bank: Bank balance: number @@ -159,17 +133,7 @@ const TokenList = () => { const { data: totalInterestData, isInitialLoading: loadingTotalInterestData, - } = useQuery( - ['account-interest-data', mangoAccountAddress], - () => fetchInterestData(mangoAccountAddress), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - }, - ) + } = useAccountInterest() const formattedTableData = useCallback( (banks: BankWithBalance[]) => { diff --git a/components/token/ActionPanel.tsx b/components/token/ActionPanel.tsx index 1d632362..703555d7 100644 --- a/components/token/ActionPanel.tsx +++ b/components/token/ActionPanel.tsx @@ -1,112 +1,167 @@ import { Bank } from '@blockworks-foundation/mango-v4' -import BorrowRepayModal from '@components/modals/BorrowRepayModal' import DepositWithdrawModal from '@components/modals/DepositWithdrawModal' import Button from '@components/shared/Button' import FormatNumericValue from '@components/shared/FormatNumericValue' +import Tooltip from '@components/shared/Tooltip' +import { ArrowDownTrayIcon, ArrowUpTrayIcon } from '@heroicons/react/20/solid' import mangoStore from '@store/mangoStore' +import useAccountInterest from 'hooks/useAccountInterest' +import useHealthContributions from 'hooks/useHealthContributions' import useMangoAccount from 'hooks/useMangoAccount' -import useMangoGroup from 'hooks/useMangoGroup' import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' import { useMemo, useState } from 'react' const ActionPanel = ({ bank }: { bank: Bank }) => { - const { t } = useTranslation('common') - const { group } = useMangoGroup() + const { t } = useTranslation(['common', 'trade']) const { mangoAccount } = useMangoAccount() - const router = useRouter() - const [showDepositModal, setShowDepositModal] = useState(false) - const [showBorrowModal, setShowBorrowModal] = useState(false) - const spotMarkets = mangoStore((s) => s.serumMarkets) + const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances) + const { initContributions } = useHealthContributions() + const [showDepositModal, setShowDepositModal] = useState< + 'deposit' | 'withdraw' | '' + >('') + const { data: totalInterestData } = useAccountInterest() - const serumMarkets = useMemo(() => { - if (group) { - return Array.from(group.serum3MarketsMapByExternal.values()) - } - return [] - }, [group]) + const [depositRate, borrowRate] = useMemo(() => { + const depositRate = bank.getDepositRateUi() + const borrowRate = bank.getBorrowRateUi() + return [depositRate, borrowRate] + }, [bank]) - const handleTrade = () => { - const markets = spotMarkets.filter( - (m) => m.baseTokenIndex === bank?.tokenIndex, - ) - if (markets) { - if (markets.length === 1) { - router.push(`/trade?name=${markets[0].name}`) - } - if (markets.length > 1) { - const market = markets.find((mkt) => !mkt.reduceOnly) - if (market) { - router.push(`/trade?name=${market.name}`) - } - } - } - } + const collateralValue = + initContributions.find((val) => val.asset === bank.name)?.contribution || 0 + const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0 + const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 + + const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name + const hasInterestEarned = totalInterestData?.find( + (d) => + d.symbol.toLowerCase() === symbol.toLowerCase() || + (symbol === 'ETH (Portal)' && d.symbol === 'ETH'), + ) + + const interestAmount = hasInterestEarned + ? hasInterestEarned.borrow_interest * -1 + + hasInterestEarned.deposit_interest + : 0 return ( <> -
-
-

- {bank.name} {t('balance')}: -

-

- {mangoAccount ? ( - - ) : ( - 0 - )} -

+
+

Your {bank?.name}

+
+
+

+ {bank.name} {t('balance')} +

+

+ {mangoAccount ? ( + + ) : ( + 0 + )} +

+
+
+

{t('collateral-value')}

+

+ $ + {mangoAccount ? ( + + ) : ( + '0.00' + )} +

+
+
+

{t('trade:in-orders')}

+

+ {inOrders ? ( + + ) : ( + 0 + )} +

+
+
+

{t('trade:unsettled')}

+

+ {unsettled ? ( + + ) : ( + 0 + )} +

+
+
+

{t('interest-earned')}

+

+ {interestAmount ? ( + + ) : ( + 0 + )} +

+
+
+

{t('rates')}

+
+ +

+ % +

+
+ | + +

+ % +

+
+
+
-
+
-
{showDepositModal ? ( setShowDepositModal(false)} - token={bank!.name} - /> - ) : null} - {showBorrowModal ? ( - setShowBorrowModal(false)} - token={bank!.name} + action={showDepositModal} + isOpen={!!showDepositModal} + onClose={() => setShowDepositModal('')} + token={bank?.name} /> ) : null} diff --git a/components/token/ChartTabs.tsx b/components/token/ChartTabs.tsx index 3f8b5c0b..f08bc61a 100644 --- a/components/token/ChartTabs.tsx +++ b/components/token/ChartTabs.tsx @@ -61,8 +61,12 @@ const ChartTabs = ({ bank }: { bank: Bank }) => { const [showBorrowsRelativeChange, setShowBorrowsRelativeChange] = useState(true) const [showBorrowsNotional, setShowBorrowsNotional] = useState(false) - const [activeDepositsTab, setActiveDepositsTab] = useState('token:deposits') - const [activeBorrowsTab, setActiveBorrowsTab] = useState('token:borrows') + const [activeDepositsTab, setActiveDepositsTab] = useState( + 'token:total-deposits', + ) + const [activeBorrowsTab, setActiveBorrowsTab] = useState( + 'token:total-borrows', + ) const [depositDaysToShow, setDepositDaysToShow] = useState('30') const [borrowDaysToShow, setBorrowDaysToShow] = useState('30') const [depositRateDaysToShow, setDepositRateDaysToShow] = useState('30') @@ -163,12 +167,12 @@ const ChartTabs = ({ bank }: { bank: Bank }) => { onChange={(v) => setActiveDepositsTab(v)} showBorders values={[ - ['token:deposits', 0], + ['token:total-deposits', 0], ['token:deposit-rates', 0], ]} />
- {activeDepositsTab === 'token:deposits' ? ( + {activeDepositsTab === 'token:total-deposits' ? ( <>
{ onChange={(v) => setActiveBorrowsTab(v)} showBorders values={[ - ['token:borrows', 0], + ['token:total-borrows', 0], ['token:borrow-rates', 0], ]} />
- {activeBorrowsTab === 'token:borrows' ? ( + {activeBorrowsTab === 'token:total-borrows' ? ( <>
=> { - const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H' - const queryEnd = Math.floor(Date.now() / 1000) - const queryStart = queryEnd - parseInt(daysToShow) * DAILY_SECONDS - const query = `defi/history_price?address=${mint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}` - const response: BirdeyeResponse = await makeApiRequest(query) - - if (response.success && response?.data?.items) { - return response.data.items - } - return [] -} - const CoingeckoStats = ({ bank, coingeckoData, @@ -65,32 +38,6 @@ const CoingeckoStats = ({ }) => { const { t } = useTranslation(['common', 'token']) const [showFullDesc, setShowFullDesc] = useState(false) - const [daysToShow, setDaysToShow] = useState('1') - - const { data: birdeyePrices, isLoading: loadingBirdeyePrices } = useQuery( - ['birdeye-token-prices', daysToShow, bank.mint], - () => fetchBirdeyePrices(daysToShow, bank.mint.toString()), - { - cacheTime: 1000 * 60 * 15, - staleTime: 1000 * 60 * 10, - retry: 3, - enabled: !!bank, - refetchOnWindowFocus: false, - }, - ) - - const chartData = useMemo(() => { - if (!birdeyePrices || !birdeyePrices.length) return [] - return birdeyePrices.map((item) => { - const decimals = countLeadingZeros(item.value) + 3 - const floatPrice = parseFloat(item.value.toString()) - const roundedPrice = +floatPrice.toFixed(decimals) - return { - unixTime: item.unixTime * 1000, - value: roundedPrice, - } - }) - }, [birdeyePrices]) const { ath, @@ -142,35 +89,8 @@ const CoingeckoStats = ({
) : null} -
- - x < 0.00001 ? x.toExponential() : formatCurrencyValue(x) - } - title={`${bank.name} Price Chart`} - xKey="unixTime" - yKey="value" - yDecimals={countLeadingZeros(bank.uiPrice) + 3} - domain={['dataMin', 'dataMax']} - /> -
-
+

{bank.name} Stats

diff --git a/components/token/PriceChart.tsx b/components/token/PriceChart.tsx new file mode 100644 index 00000000..991e7a95 --- /dev/null +++ b/components/token/PriceChart.tsx @@ -0,0 +1,95 @@ +import { Bank } from '@blockworks-foundation/mango-v4' +import { useQuery } from '@tanstack/react-query' +import { makeApiRequest } from 'apis/birdeye/helpers' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import { useMemo, useState } from 'react' +import { DAILY_SECONDS } from 'utils/constants' +import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart' +import { countLeadingZeros, formatCurrencyValue } from 'utils/numbers' +import { BirdeyePriceResponse } from 'types' +dayjs.extend(relativeTime) + +interface BirdeyeResponse { + data: { items: BirdeyePriceResponse[] } + success: boolean +} + +const fetchBirdeyePrices = async ( + daysToShow: string, + mint: string, +): Promise => { + const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H' + const queryEnd = Math.floor(Date.now() / 1000) + const queryStart = queryEnd - parseInt(daysToShow) * DAILY_SECONDS + const query = `defi/history_price?address=${mint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}` + const response: BirdeyeResponse = await makeApiRequest(query) + + if (response.success && response?.data?.items) { + return response.data.items + } + return [] +} + +const PriceChart = ({ bank }: { bank: Bank }) => { + const [daysToShow, setDaysToShow] = useState('1') + + const { data: birdeyePrices, isLoading: loadingBirdeyePrices } = useQuery( + ['birdeye-token-prices', daysToShow, bank.mint], + () => fetchBirdeyePrices(daysToShow, bank.mint.toString()), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 10, + retry: 3, + enabled: !!bank, + refetchOnWindowFocus: false, + }, + ) + + const chartData = useMemo(() => { + if (!birdeyePrices || !birdeyePrices.length) return [] + return birdeyePrices.map((item) => { + const decimals = countLeadingZeros(item.value) + 3 + const floatPrice = parseFloat(item.value.toString()) + const roundedPrice = +floatPrice.toFixed(decimals) + return { + unixTime: item.unixTime * 1000, + value: roundedPrice, + } + }) + }, [birdeyePrices]) + + return ( + <> +
+ + x < 0.00001 ? x.toExponential() : formatCurrencyValue(x) + } + title="" + xKey="unixTime" + yKey="value" + yDecimals={countLeadingZeros(bank.uiPrice) + 3} + domain={['dataMin', 'dataMax']} + /> +
+ + ) +} + +export default PriceChart diff --git a/components/token/TokenPage.tsx b/components/token/TokenPage.tsx index 7eb121c6..579ca56a 100644 --- a/components/token/TokenPage.tsx +++ b/components/token/TokenPage.tsx @@ -1,28 +1,29 @@ -import Change from '@components/shared/Change' import DailyRange from '@components/shared/DailyRange' import { useTranslation } from 'next-i18next' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' -import FlipNumbers from 'react-flip-numbers' -import { formatCurrencyValue } from 'utils/numbers' import Link from 'next/link' import SheenLoader from '@components/shared/SheenLoader' import useMangoGroup from 'hooks/useMangoGroup' import useJupiterMints from 'hooks/useJupiterMints' -import useLocalStorageState from 'hooks/useLocalStorageState' -import { ANIMATION_SETTINGS_KEY } from 'utils/constants' -import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import ActionPanel from './ActionPanel' import ChartTabs from './ChartTabs' -import CoingeckoStats from './CoingeckoStats' import { useQuery } from '@tanstack/react-query' -import FormatNumericValue from '@components/shared/FormatNumericValue' import TopTokenAccounts from './TopTokenAccounts' import TokenParams from './TokenParams' import { formatTokenSymbol } from 'utils/tokens' import TokenLogo from '@components/shared/TokenLogo' -import { ArrowLeftIcon } from '@heroicons/react/20/solid' +import { + ArrowLeftIcon, + ArrowTopRightOnSquareIcon, + ArrowTrendingUpIcon, + ArrowsRightLeftIcon, +} from '@heroicons/react/20/solid' import RateCurveChart from './RateCurveChart' +import PriceChart from './PriceChart' +import Button from '@components/shared/Button' +import mangoStore from '@store/mangoStore' +import { fetchCMSTokenPage } from 'utils/contentful' const DEFAULT_COINGECKO_VALUES = { ath: 0, @@ -65,15 +66,15 @@ const fetchTokenInfo = async (tokenId: string | undefined) => { const TokenPage = () => { const { t } = useTranslation(['common', 'token']) + // const [cmsTokenData, setCmsTokenData] = useState( + // undefined, + // ) const [loading, setLoading] = useState(true) const router = useRouter() const { token } = router.query const { group } = useMangoGroup() const { mangoTokens } = useJupiterMints() - const [animationSettings] = useLocalStorageState( - ANIMATION_SETTINGS_KEY, - INITIAL_ANIMATION_SETTINGS, - ) + const spotMarkets = mangoStore((s) => s.serumMarkets) const bankName = useMemo(() => { if (!token) return @@ -84,6 +85,16 @@ const TokenPage = () => { : token.toString() }, [token]) + // useEffect(() => { + // if (bankName) { + // const fetchCmsData = async () => { + // const tokenData = await fetchCMSTokenPage(bankName) + // setCmsTokenData(tokenData) + // } + // fetchCmsData() + // } + // }, [bankName]) + const bank = useMemo(() => { if (group && bankName) { const bank = group.banksMapByName.get(bankName) @@ -102,23 +113,64 @@ const TokenPage = () => { } }, [bank, mangoTokens]) - const { data: coingeckoTokenInfo, isLoading: loadingCoingeckoInfo } = - useQuery( - ['coingecko-token-info', coingeckoId], - () => fetchTokenInfo(coingeckoId), - { - cacheTime: 1000 * 60 * 15, - staleTime: 1000 * 60 * 5, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!coingeckoId, - }, - ) + const { data: coingeckoTokenInfo } = useQuery( + ['coingecko-token-info', coingeckoId], + () => fetchTokenInfo(coingeckoId), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 5, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!coingeckoId, + }, + ) - const { high_24h, low_24h, price_change_percentage_24h } = - coingeckoTokenInfo?.market_data - ? coingeckoTokenInfo.market_data - : DEFAULT_COINGECKO_VALUES + const { data: cmsTokenData } = useQuery( + ['cms-token-data', bankName], + () => fetchCMSTokenPage(bankName), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 5, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!bankName, + }, + ) + + const { high_24h, low_24h } = coingeckoTokenInfo?.market_data + ? coingeckoTokenInfo.market_data + : DEFAULT_COINGECKO_VALUES + + const formatCoingeckoName = (name: string) => { + if (name === 'Wrapped Solana') return 'Solana' + if (name.includes('Wormhole')) return name.replace('Wormhole', 'Portal') + return name + } + + const handleTrade = () => { + const markets = spotMarkets.filter( + (m) => m.baseTokenIndex === bank?.tokenIndex, + ) + if (markets) { + if (markets.length === 1) { + router.push(`/trade?name=${markets[0].name}`) + } + if (markets.length > 1) { + const market = markets.find((mkt) => !mkt.reduceOnly) + if (market) { + router.push(`/trade?name=${market.name}`) + } + } + } + } + + const handleSwap = () => { + if (bank?.name === 'USDC') { + router.push(`/swap?in=USDC&out=SOL`) + } else { + router.push(`/swap?in=USDC&out=${bank?.name}`) + } + } return ( <> @@ -139,55 +191,69 @@ const TokenPage = () => {
{bank && bankName ? ( <> -
-
-
- - {coingeckoTokenInfo ? ( -

- {coingeckoTokenInfo.name} -

- ) : ( -

{bank.name}

- )} +
+
+
+
-
-
- {animationSettings['number-scroll'] ? ( - +
+
+ {coingeckoTokenInfo?.name ? ( +

+ {formatCoingeckoName(coingeckoTokenInfo.name)} +

) : ( - +

{bank.name}

)} + {cmsTokenData?.length ? ( + + What is {bank?.name}? + + + ) : null}
- {coingeckoTokenInfo?.market_data ? ( -
- -
+ {high_24h.usd && low_24h.usd ? ( + ) : null}
- {high_24h.usd && low_24h.usd ? ( - - ) : null}
- +
+ + +
- -
- +
+
+ +
+
+ +
- - {coingeckoTokenInfo?.market_data ? ( + {/* {coingeckoTokenInfo?.market_data ? ( { 🦎

No CoinGecko data...

- )} + )} */} + {/*
+

{bank?.name} on Mango

+
*/} + +
+ +
+ ) : loading ? ( diff --git a/hooks/useAccountInterest.ts b/hooks/useAccountInterest.ts new file mode 100644 index 00000000..f6d65719 --- /dev/null +++ b/hooks/useAccountInterest.ts @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query' +import { MANGO_DATA_API_URL } from 'utils/constants' +import useMangoAccount from './useMangoAccount' +import { TotalInterestDataItem } from 'types' + +const fetchInterestData = async (mangoAccountPk: string) => { + try { + const response = await fetch( + `${MANGO_DATA_API_URL}/stats/interest-account-total?mango-account=${mangoAccountPk}`, + ) + const parsedResponse: Omit[] | null = + await response.json() + if (parsedResponse) { + const entries: [string, Omit][] = + Object.entries(parsedResponse).sort((a, b) => b[0].localeCompare(a[0])) + + const stats: TotalInterestDataItem[] = entries + .map(([key, value]) => { + return { ...value, symbol: key } + }) + .filter((x) => x) + return stats + } else return [] + } catch (e) { + console.log('Failed to fetch account funding', e) + return [] + } +} + +export default function useAccountInterest() { + const { mangoAccountAddress } = useMangoAccount() + const { data, isInitialLoading } = useQuery( + ['account-interest-data', mangoAccountAddress], + () => fetchInterestData(mangoAccountAddress), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + }, + ) + return { data, isInitialLoading } +} diff --git a/package.json b/package.json index 9b9ab8fe..37da8309 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "big.js": "6.2.1", "bignumber.js": "9.1.2", "clsx": "1.2.1", + "contentful": "10.6.21", "csv-stringify": "6.3.2", "d3-interpolate": "3.0.1", "date-fns": "2.29.3", diff --git a/public/locales/en/token.json b/public/locales/en/token.json index 7294706e..141ca67d 100644 --- a/public/locales/en/token.json +++ b/public/locales/en/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "These fees accrue in every native token listed on Mango. The values in this chart are derived from the current market value.", "top-borrowers": "Top {{symbol}} Borrowers", "top-depositors": "Top {{symbol}} Depositors", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "Total Supply", "total-value": "Total Value", "volume": "24h Volume" diff --git a/public/locales/es/token.json b/public/locales/es/token.json index 7294706e..141ca67d 100644 --- a/public/locales/es/token.json +++ b/public/locales/es/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "These fees accrue in every native token listed on Mango. The values in this chart are derived from the current market value.", "top-borrowers": "Top {{symbol}} Borrowers", "top-depositors": "Top {{symbol}} Depositors", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "Total Supply", "total-value": "Total Value", "volume": "24h Volume" diff --git a/public/locales/pt/token.json b/public/locales/pt/token.json index bb8b7584..f535f2d4 100644 --- a/public/locales/pt/token.json +++ b/public/locales/pt/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "Estas taxas são acumuladas em todos os tokens nativos listados no Mango. Os valores neste gráfico são derivados do valor de mercado atual.", "top-borrowers": "Principais {{symbol}} Devedores", "top-depositors": "Principais {{symbol}} Depositantes", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "Fornecimento Total", "total-value": "Valor Total", "volume": "Volume de 24h" diff --git a/public/locales/ru/token.json b/public/locales/ru/token.json index 7294706e..141ca67d 100644 --- a/public/locales/ru/token.json +++ b/public/locales/ru/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "These fees accrue in every native token listed on Mango. The values in this chart are derived from the current market value.", "top-borrowers": "Top {{symbol}} Borrowers", "top-depositors": "Top {{symbol}} Depositors", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "Total Supply", "total-value": "Total Value", "volume": "24h Volume" diff --git a/public/locales/zh/token.json b/public/locales/zh/token.json index 835f6b7e..45a32462 100644 --- a/public/locales/zh/token.json +++ b/public/locales/zh/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "这些费用在 Mango 上买卖的所有币种中都会累积。此图表中的值来自当前市场价值。", "top-borrowers": "顶级{{symbol}}借贷者", "top-depositors": "顶级{{symbol}}存款者", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "总供应量", "total-value": "全市值", "volume": "24小时交易量" diff --git a/public/locales/zh_tw/token.json b/public/locales/zh_tw/token.json index 66e29a59..6f15fe63 100644 --- a/public/locales/zh_tw/token.json +++ b/public/locales/zh_tw/token.json @@ -49,6 +49,8 @@ "tooltip-token-fees-collected": "這些費用在 Mango 上買賣的所有幣種中都會累積。此圖表中的值來自當前市場價值。", "top-borrowers": "頂級{{symbol}}借貸者", "top-depositors": "頂級{{symbol}}存款者", + "total-borrows": "Total Borrows", + "total-deposits": "Total Deposits", "total-supply": "總供應量", "total-value": "全市值", "volume": "24小時交易量" diff --git a/utils/contentful.ts b/utils/contentful.ts new file mode 100644 index 00000000..08ab6e95 --- /dev/null +++ b/utils/contentful.ts @@ -0,0 +1,117 @@ +import { + createClient, + Entry, + EntryFieldTypes, + EntrySkeletonType, +} from 'contentful' +import { Document as RichTextDocument } from '@contentful/rich-text-types' + +interface TypeTokenFields { + tokenName: EntryFieldTypes.Symbol + slug: EntryFieldTypes.Symbol + seoTitle: EntryFieldTypes.Symbol + seoDescription: EntryFieldTypes.Text + description?: EntryFieldTypes.RichText + tags: EntryFieldTypes.Array< + EntryFieldTypes.Symbol< + | 'AI' + | 'Bridged (Portal)' + | 'DeFi' + | 'DePIN' + | 'Derivatives' + | 'Domains' + | 'Exchange' + | 'Gaming' + | 'Governance' + | 'Infrastructure' + | 'Layer 1' + | 'Liquid Staking' + | 'Meme' + | 'Payments' + | 'Social' + | 'Stablecoin' + > + > + websiteUrl?: EntryFieldTypes.Symbol + twitterUrl?: EntryFieldTypes.Symbol + whitepaper?: EntryFieldTypes.Symbol + mint: EntryFieldTypes.Symbol + coingeckoId: EntryFieldTypes.Symbol + symbol: EntryFieldTypes.Symbol + spotSymbol: EntryFieldTypes.Symbol + perpSymbol?: EntryFieldTypes.Symbol + ethMint?: EntryFieldTypes.Symbol + erc20TokenDecimals?: EntryFieldTypes.Integer +} + +type TypeTokenSkeleton = EntrySkeletonType +type TokenPageEntry = Entry + +export interface TokenPage { + tokenName: string + symbol: string + slug: string + description: RichTextDocument | undefined + tags: string[] + websiteUrl?: string + twitterUrl?: string + mint: string + ethMint: string | undefined + coingeckoId: string + seoTitle: string + seoDescription: string + perpSymbol: string | undefined + spotSymbol: string + lastModified: string + erc20TokenDecimals: number | undefined +} + +function parseContentfulTokenPage( + tokenPageEntry?: TokenPageEntry, +): TokenPage | null { + if (!tokenPageEntry) { + return null + } + + return { + tokenName: tokenPageEntry.fields.tokenName, + symbol: tokenPageEntry.fields.symbol, + slug: tokenPageEntry.fields.slug, + description: tokenPageEntry.fields.description || undefined, + tags: tokenPageEntry.fields.tags || [], + websiteUrl: tokenPageEntry.fields.websiteUrl || undefined, + twitterUrl: tokenPageEntry.fields.twitterUrl || undefined, + mint: tokenPageEntry.fields.mint, + ethMint: tokenPageEntry.fields.ethMint || undefined, + coingeckoId: tokenPageEntry.fields.coingeckoId, + seoTitle: tokenPageEntry.fields.seoTitle, + seoDescription: tokenPageEntry.fields.seoDescription, + perpSymbol: tokenPageEntry.fields.perpSymbol || undefined, + spotSymbol: tokenPageEntry.fields.spotSymbol, + lastModified: tokenPageEntry.sys.updatedAt, + erc20TokenDecimals: tokenPageEntry.fields.erc20TokenDecimals || undefined, + } +} + +export async function fetchCMSTokenPage( + symbol: string | undefined, +): Promise { + const client = createClient({ + space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID!, + accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN!, + }) + + const tokenPagesResult = await client.getEntries({ + content_type: 'token', + 'fields.symbol[in]': symbol, + include: 2, + order: ['fields.tokenName'], + }) + + const parsedTokenPages = tokenPagesResult.items.map( + (tokenPageEntry) => + parseContentfulTokenPage(tokenPageEntry as TokenPageEntry) as TokenPage, + ) + + return parsedTokenPages +} diff --git a/yarn.lock b/yarn.lock index d62ff163..aaf3f6e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -408,6 +408,11 @@ near-api-js "^0.44.2" near-seed-phrase "^0.2.0" +"@contentful/rich-text-types@^16.0.2": + version "16.3.4" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.3.4.tgz#c5fc9c834dde03d4c4ee189900d304ce1888a74b" + integrity sha512-PyVSrQa5j1hO4grgA0Ivo/taiOvW0uFN79JB5JkTG8U7DnWGI7Ap2As6zN6/E6YvDqb7w2cYRMSGSQ3qfxu8HQ== + "@coral-xyz/anchor@0.28.1-beta.2", "@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.27.0", "@coral-xyz/anchor@^0.28.0", "@coral-xyz/anchor@^0.28.1-beta.2", "@coral-xyz/anchor@~0.27.0": version "0.27.0" resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c" @@ -2572,7 +2577,7 @@ dependencies: "@solana/wallet-adapter-base" "^0.9.23" -"@solana/wallet-adapter-solflare@0.6.27": +"@solana/wallet-adapter-solflare@0.6.27", "@solana/wallet-adapter-solflare@^0.6.28": version "0.6.27" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-solflare/-/wallet-adapter-solflare-0.6.27.tgz#49ba2dfecca4bee048e65d302216d1b732d7e39e" integrity sha512-MBBx9B1pI8ChCT70sgxrmeib1S7G9tRQzfMHqJPdGQ2jGtukY0Puzma2OBsIAsH5Aw9rUUUFZUK+8pzaE+mgAg== @@ -2583,7 +2588,7 @@ "@solflare-wallet/sdk" "^1.3.0" "@wallet-standard/wallet" "^1.0.1" -"@solana/wallet-adapter-solflare@0.6.28", "@solana/wallet-adapter-solflare@^0.6.28": +"@solana/wallet-adapter-solflare@0.6.28": version "0.6.28" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-solflare/-/wallet-adapter-solflare-0.6.28.tgz#3de42a43220cca361050ebd1755078012a5b0fe2" integrity sha512-iiUQtuXp8p4OdruDawsm1dRRnzUCcsu+lKo8OezESskHtbmZw2Ifej0P99AbJbBAcBw7q4GPI6987Vh05Si5rw== @@ -4608,6 +4613,15 @@ axios@^1.1.3, axios@^1.4.0, axios@^1.6.2: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.0: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -5420,6 +5434,36 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +contentful-resolve-response@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.8.1.tgz#b44ff13e12fab7deb00ef6216d8a7171bdda0395" + integrity sha512-VXGK2c8dBIGcRCknqudKmkDr2PzsUYfjLN6hhx71T09UzoXOdA/c0kfDhsf/BBCBWPWcLaUgaJEFU0lCo45TSg== + dependencies: + fast-copy "^2.1.7" + +contentful-sdk-core@^8.1.0: + version "8.1.2" + resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-8.1.2.tgz#a27ea57cfd631b4c6d58e5ca04fcde6d231e2c1b" + integrity sha512-XZvX2JMJF4YiICXLrHFv59KBHaQJ6ElqAP8gSNgnCu4x+pPG7Y1bC2JMNOiyAgJuGQGVUOcNZ5PmK+tsNEayYw== + dependencies: + fast-copy "^2.1.7" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + p-throttle "^4.1.1" + qs "^6.11.2" + +contentful@10.6.21: + version "10.6.21" + resolved "https://registry.yarnpkg.com/contentful/-/contentful-10.6.21.tgz#7e2a8cae91f5f06297df27053538e937490035a7" + integrity sha512-ez3zNJ1A2dJTuoNxSkFhwjkhrQ/jYYTvc8jFeFIMwZuYMHjYwL+mFo1372pSASeJ6QAIf2srKOmukzCGiHtcSg== + dependencies: + "@contentful/rich-text-types" "^16.0.2" + axios "^1.6.0" + contentful-resolve-response "^1.8.1" + contentful-sdk-core "^8.1.0" + json-stringify-safe "^5.0.1" + type-fest "^4.0.0" + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -6775,6 +6819,11 @@ eyes@^0.1.8: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== +fast-copy@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-2.1.7.tgz#affc9475cb4b555fb488572b2a44231d0c9fa39e" + integrity sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA== + fast-copy@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" @@ -6917,6 +6966,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.7, fol resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -8696,6 +8750,16 @@ lodash.isequal@4.5.0, lodash.isequal@^4.0.0, lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -9886,6 +9950,11 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/p-throttle/-/p-throttle-4.1.1.tgz#80b1fbd358af40a8bfa1667f9dc8b72b714ad692" + integrity sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -10297,6 +10366,13 @@ qrcode@1.4.4: pngjs "^3.3.0" yargs "^13.2.4" +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -12255,6 +12331,11 @@ type-fest@^0.7.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== +type-fest@^4.0.0: + version "4.10.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.10.2.tgz#3abdb144d93c5750432aac0d73d3e85fcab45738" + integrity sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw== + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60"