diff --git a/package-lock.json b/package-lock.json index 2156a00ff..298bd9839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@rainbow-me/rainbowkit": "2.2.10", "@reserve-protocol/dtf-rebalance-lib": "^3.2.1", - "@reserve-protocol/react-zapper": "^1.5.9", + "@reserve-protocol/react-zapper": "^1.5.11", "@reserve-protocol/rtokens": "^1.1.23", "@reserve-protocol/trusted-fillers-sdk": "^0.3.0", "@sentry/react": "^9.16.0", @@ -7439,9 +7439,9 @@ } }, "node_modules/@reserve-protocol/react-zapper": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/@reserve-protocol/react-zapper/-/react-zapper-1.5.9.tgz", - "integrity": "sha512-xylZcT2bCpLYle0D1EcCCltLu7YbKXnx77wnSeChzuvobQSq8BEdzXxghpPxYFunNTD6wio/ggGB+TCIOlC6yA==", + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@reserve-protocol/react-zapper/-/react-zapper-1.5.11.tgz", + "integrity": "sha512-k5k9sZAvrZNo36azoL1VpYIbmbh3zjPuhTGhAN75f9HZxJmn3j1V96IA6SXN3Qhk5LjleRJcl++Grx5CeU1oRA==", "license": "MIT", "dependencies": { "@lucide/lab": "^0.1.2", diff --git a/package.json b/package.json index a58e87145..5f7fc2f1e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@rainbow-me/rainbowkit": "2.2.10", "@reserve-protocol/dtf-rebalance-lib": "^3.2.1", - "@reserve-protocol/react-zapper": "^1.5.9", + "@reserve-protocol/react-zapper": "^1.5.11", "@reserve-protocol/rtokens": "^1.1.23", "@reserve-protocol/trusted-fillers-sdk": "^0.3.0", "@sentry/react": "^9.16.0", diff --git a/src/components/layout/navigation/TokenNavigation.tsx b/src/components/layout/navigation/TokenNavigation.tsx index e9410f3c7..3e1abf419 100644 --- a/src/components/layout/navigation/TokenNavigation.tsx +++ b/src/components/layout/navigation/TokenNavigation.tsx @@ -1,3 +1,4 @@ +import { useDTFStatus } from '@/hooks/use-dtf-status' import { t } from '@lingui/macro' import AuctionsIcon from 'components/icons/AuctionsIcon' import GovernanceIcon from 'components/icons/GovernanceIcon' @@ -7,6 +8,7 @@ import StakeIcon from 'components/icons/StakeIcon' import { CurrentRTokenLogo } from 'components/icons/TokenLogo' import { navigationIndexAtom } from 'components/section-navigation/atoms' import useSectionNavigate from '@/components/section-navigation/use-section-navigate' +import useRToken from 'hooks/useRToken' import { useAtomValue } from 'jotai' import React, { useEffect, useMemo, useState } from 'react' import { ChevronDown, ChevronRight } from 'lucide-react' @@ -24,6 +26,7 @@ interface NavigationItem { label: string route: string subnav?: SubNavItem[] + disabled?: boolean } interface NavContentProps extends NavigationItem { @@ -141,16 +144,30 @@ const NavContent = ({ ) } -const NavItem = (props: NavigationItem) => ( - - {({ isActive }) => } - -) +const NavItem = (props: NavigationItem) => { + if (props.disabled) { + return ( +
+ +
+ ) + } + + return ( + + {({ isActive }) => } + + ) +} const TokenNavigation = () => { + const rToken = useRToken() + const status = useDTFStatus(rToken?.address, rToken?.chainId) + const isDeprecated = status === 'deprecated' + const navigation: NavigationItem[] = useMemo( () => [ { @@ -179,11 +196,13 @@ const TokenNavigation = () => { icon: , label: t`Auctions`, route: ROUTES.AUCTIONS, + disabled: isDeprecated, }, { icon: , label: t`Governance`, route: ROUTES.GOVERNANCE, + disabled: isDeprecated, }, { icon: , @@ -202,7 +221,7 @@ const TokenNavigation = () => { ], }, ], - [] + [isDeprecated] ) return ( diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index dd8181ef1..07b8f73b8 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -204,6 +204,7 @@ interface DataTableComponentProps initialSorting?: SortingState loading?: boolean loadingSkeleton?: React.ReactNode + getRowClassName?: (row: Row) => string | undefined } const CustomTableRow = ({ @@ -215,6 +216,7 @@ const CustomTableRow = ({ expandedRows, index, hoverRowComponent, + getRowClassName, }: { row: Row handleRowClick: (row: Row, event: React.MouseEvent) => void @@ -227,6 +229,7 @@ const CustomTableRow = ({ row: Row children: React.ReactNode }) => React.ReactElement + getRowClassName?: (row: Row) => string | undefined }) => { const baseRow = ( {row.getVisibleCells().map((cell) => ( @@ -276,6 +280,7 @@ function DataTable({ initialSorting = [], loading = false, loadingSkeleton, + getRowClassName, }: DataTableComponentProps) { const [sorting, setSorting] = React.useState(initialSorting) const [paginationState, setPaginationState] = React.useState( @@ -375,6 +380,7 @@ function DataTable({ expandedRows={expandedRows} index={index} hoverRowComponent={hoverRowComponent} + getRowClassName={getRowClassName} /> {!!renderSubComponent && row.getIsExpanded() && ( d.address) +) + +type DTFStatusItem = { + address: string + chainId: number + status: 'active' | 'deprecated' +} + +const useDiscoverDTFs = () => { + return useQuery({ + queryKey: ['discover-dtfs-status'], + queryFn: async (): Promise => { + const response = await fetch(`${RESERVE_API}discover/dtfs`) + if (!response.ok) throw new Error('Failed to fetch dtf list') + const data = await response.json() + return data.map((item: any) => ({ + address: item.address?.toLowerCase(), + chainId: item.chainId, + status: item.status ?? 'active', + })) + }, + refetchInterval: REFRESH_INTERVAL, + staleTime: REFRESH_INTERVAL, + }) +} + +const isKnownDeprecated = (address: string, chainId?: number): boolean => { + const lower = address.toLowerCase() + return KNOWN_DEPRECATED.some( + (d) => d.address === lower && d.chainId === chainId + ) +} + +export const useDTFStatus = ( + address?: string, + chainId?: number +): 'active' | 'deprecated' => { + const { data } = useDiscoverDTFs() + + if (!address) return 'active' + + // Fast path: check hardcoded list before API loads + if (!data) { + return isKnownDeprecated(address, chainId) ? 'deprecated' : 'active' + } + + const match = data.find( + (item) => + item.address === address.toLowerCase() && item.chainId === chainId + ) + + return match?.status ?? 'active' +} + +export const useDeprecatedAddresses = (): Set => { + const { data } = useDiscoverDTFs() + + return useMemo(() => { + if (!data) return KNOWN_DEPRECATED_ADDRESSES + + const deprecated = new Set() + for (const item of data) { + if (item.status === 'deprecated') { + deprecated.add(item.address) + } + } + return deprecated + }, [data]) +} diff --git a/src/hooks/useIndexDTFList.ts b/src/hooks/useIndexDTFList.ts index b8a0ae676..95d5a10d6 100644 --- a/src/hooks/useIndexDTFList.ts +++ b/src/hooks/useIndexDTFList.ts @@ -1,5 +1,3 @@ -import { ChainId } from '@/utils/chains' -// import { RESERVE_API } from '@/utils/constants' import { useQuery } from '@tanstack/react-query' import { Address } from 'viem' @@ -18,6 +16,7 @@ export type IndexDTFItem = { performance: Performance[] performancePercent: number chainId: number + status: 'active' | 'deprecated' brand?: { icon?: string cover?: string @@ -25,44 +24,37 @@ export type IndexDTFItem = { } } -const calculatePercentageChange = (performance: Performance[]) => { - if (performance.length === 0) { - return 0 - } - const firstValue = performance[0].value - const lastValue = performance[performance.length - 1].value - return ((lastValue - firstValue) / firstValue) * 100 -} - const REFRESH_INTERVAL = 1000 * 60 * 10 // 10 minutes const useIndexDTFList = () => { return useQuery({ queryKey: ['index-dtf-list'], queryFn: async (): Promise => { - const f = async (chain: number) => { - const response = await fetch( - `${RESERVE_API}discover/dtf?chainId=${chain}&limit=100` - ) - - if (!response.ok) { - throw new Error('Failed to fetch dtf list') - } - - const data = await response.json() + const response = await fetch(`${RESERVE_API}discover/dtfs?performance=true&brand=true`) - return data.map((item: any) => ({ - ...item, - performancePercent: calculatePercentageChange(item.performance), - performance: item.performance, - })) as IndexDTFItem[] + if (!response.ok) { + throw new Error('Failed to fetch dtf list') } - const responses = await Promise.all( - [ChainId.Base, ChainId.Mainnet, ChainId.BSC].map(f) - ) + const data = await response.json() - return responses.flat().sort((x, y) => y.marketCap - x.marketCap) + return data + .filter((item: any) => item.type === 'index') + .map((item: any) => ({ + address: item.address, + symbol: item.symbol, + name: item.name, + price: item.price, + fee: item.fee, + marketCap: item.marketCap, + chainId: item.chainId, + basket: item.basket, + status: item.status ?? 'active', + performance: item.performance ?? [], + performancePercent: 0, + brand: item.brand, + })) + .sort((x: IndexDTFItem, y: IndexDTFItem) => y.marketCap - x.marketCap) }, refetchInterval: REFRESH_INTERVAL, staleTime: REFRESH_INTERVAL, diff --git a/src/state/dtf/atoms.ts b/src/state/dtf/atoms.ts index 4f194c734..27e3a7a51 100644 --- a/src/state/dtf/atoms.ts +++ b/src/state/dtf/atoms.ts @@ -224,6 +224,8 @@ export const hasBridgedAssetsAtom = atom((get) => { ) }) +export const indexDTFStatusAtom = atom<'active' | 'deprecated'>('active') + export const isSingletonRebalanceAtom = atom((get) => { const version = get(indexDTFVersionAtom) diff --git a/src/views/discover/components/index/components/index-dtf-card.tsx b/src/views/discover/components/index/components/index-dtf-card.tsx index 727d362af..e68b629d3 100644 --- a/src/views/discover/components/index/components/index-dtf-card.tsx +++ b/src/views/discover/components/index/components/index-dtf-card.tsx @@ -20,7 +20,10 @@ const IndexDTFCard = ({ dtf }: { dtf: IndexDTFItem }) => { return (
diff --git a/src/views/discover/components/index/components/index-dtf-table.tsx b/src/views/discover/components/index/components/index-dtf-table.tsx index 4643277f9..d746e74ef 100644 --- a/src/views/discover/components/index/components/index-dtf-table.tsx +++ b/src/views/discover/components/index/components/index-dtf-table.tsx @@ -250,6 +250,9 @@ const IndexDTFTable = ({ : undefined } onRowClick={handleRowClick} + getRowClassName={(row) => + row.original.status === 'deprecated' ? 'opacity-30' : undefined + } className={cn( 'hidden lg:block', '[&_table]:bg-card [&_table]:rounded-[20px] [&_table]:text-base', diff --git a/src/views/discover/components/index/hooks/use-filtered-dtf-index.tsx b/src/views/discover/components/index/hooks/use-filtered-dtf-index.tsx index abbf37302..78f56fc8d 100644 --- a/src/views/discover/components/index/hooks/use-filtered-dtf-index.tsx +++ b/src/views/discover/components/index/hooks/use-filtered-dtf-index.tsx @@ -14,6 +14,10 @@ const useFilteredDTFIndex = () => { } const filtered = data.filter((dtf) => { + if (!search && dtf.status === 'deprecated') { + return false + } + if (!chains.length || !chains.includes(dtf.chainId)) { return false } diff --git a/src/views/discover/components/yield/components/RTokenCard.tsx b/src/views/discover/components/yield/components/RTokenCard.tsx index 521b61639..1b341540b 100644 --- a/src/views/discover/components/yield/components/RTokenCard.tsx +++ b/src/views/discover/components/yield/components/RTokenCard.tsx @@ -1,3 +1,4 @@ +import { cn } from '@/lib/utils' import CollateralPieChartWrapper from '@/views/yield-dtf/overview/components/collateral-pie-chart-wrapper' import RTokenAddresses from '@/views/yield-dtf/overview/components/rtoken-addresses' import { trackClick } from '@/hooks/useTrackPage' @@ -20,6 +21,7 @@ import TokenActions from './TokenActions' interface Props { token: ListedToken + deprecated?: boolean } const ChainBadge = ({ chain }: { chain: number }) => ( @@ -47,7 +49,7 @@ const ChainBadge = ({ chain }: { chain: number }) => ( ) // TODO: Component should be splitted -const RTokenCard = ({ token }: Props) => { +const RTokenCard = ({ token, deprecated }: Props) => { const navigate = useNavigate() const { priceETHTerms, supplyETHTerms } = usePriceETH(token) @@ -57,7 +59,10 @@ const RTokenCard = ({ token }: Props) => { return (
{ e.stopPropagation() trackClick( diff --git a/src/views/discover/components/yield/components/yield-dtf-list.tsx b/src/views/discover/components/yield/components/yield-dtf-list.tsx index 4f02f8a98..10e847092 100644 --- a/src/views/discover/components/yield/components/yield-dtf-list.tsx +++ b/src/views/discover/components/yield/components/yield-dtf-list.tsx @@ -1,3 +1,4 @@ +import { useDeprecatedAddresses } from '@/hooks/use-dtf-status' import useTokenList from 'hooks/useTokenList' import { useAtomValue } from 'jotai' import { useMemo } from 'react' @@ -20,9 +21,16 @@ const YieldDTfList = ({ stablecoins = false }: { stablecoins?: boolean }) => { const chains = useAtomValue(chainsFilterAtom) const targets = useAtomValue(targetFilterAtom) const search = useAtomValue(searchFilterAtom) + const deprecatedAddresses = useDeprecatedAddresses() const filteredList = useMemo(() => { return list.filter((token) => { + const isDeprecated = deprecatedAddresses.has(token.id.toLowerCase()) + + if (!search && isDeprecated) { + return false + } + if (!chains.length || !chains.includes(token.chain.toString())) { return false } @@ -59,14 +67,18 @@ const YieldDTfList = ({ stablecoins = false }: { stablecoins?: boolean }) => { return true }) - }, [list, chains, targets, search, stablecoins]) + }, [list, chains, targets, search, stablecoins, deprecatedAddresses]) return (
{isLoading && !filteredList.length && } {filteredList.map((token) => ( - + ))}
) diff --git a/src/views/index-dtf/components/navigation/index.tsx b/src/views/index-dtf/components/navigation/index.tsx index 31372952c..8cf7bea63 100644 --- a/src/views/index-dtf/components/navigation/index.tsx +++ b/src/views/index-dtf/components/navigation/index.tsx @@ -2,7 +2,7 @@ import TokenLogo from '@/components/token-logo' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { cn } from '@/lib/utils' -import { indexDTFAtom, indexDTFBrandAtom } from '@/state/dtf/atoms' +import { indexDTFAtom, indexDTFBrandAtom, indexDTFStatusAtom } from '@/state/dtf/atoms' import { ROUTES } from '@/utils/constants' import { t } from '@lingui/macro' import { useAtomValue } from 'jotai' @@ -105,8 +105,29 @@ const NavigationHeader = () => { ) } +const DisabledNavigationItem = ({ + icon, + label, +}: { + icon: React.ReactNode + label: string +}) => { + return ( +
+
+ {icon} +
+
{label}
+
+ ) +} + +const DISABLED_ROUTES_WHEN_DEPRECATED: string[] = [ROUTES.GOVERNANCE, ROUTES.AUCTIONS] + const NavigationItems = () => { const dtf = useAtomValue(indexDTFAtom) + const status = useAtomValue(indexDTFStatusAtom) + const isDeprecated = status === 'deprecated' const items = useMemo( () => [ @@ -141,9 +162,18 @@ const NavigationItems = () => { return (
- {items.map((item) => ( - - ))} + {items.map((item) => + isDeprecated && + DISABLED_ROUTES_WHEN_DEPRECATED.includes(item.route) ? ( + + ) : ( + + ) + )}
) } diff --git a/src/views/index-dtf/index-dtf-container.tsx b/src/views/index-dtf/index-dtf-container.tsx index 33840e168..88633e5f1 100644 --- a/src/views/index-dtf/index-dtf-container.tsx +++ b/src/views/index-dtf/index-dtf-container.tsx @@ -19,10 +19,12 @@ import { indexDTFExposureDataAtom, indexDTFPerformanceLoadingAtom, indexDTFRebalanceControlAtom, + indexDTFStatusAtom, indexDTFVersionAtom, iTokenAddressAtom, performanceTimeRangeAtom, } from '@/state/dtf/atoms' +import { useDTFStatus } from '@/hooks/use-dtf-status' import { isAddress } from '@/utils' import { AvailableChain, supportedChains } from '@/utils/chains' import { NETWORKS, RESERVE_API, ROUTES } from '@/utils/constants' @@ -207,8 +209,26 @@ const resetStateAtom = atom(null, (_, set) => { set(indexDTFPerformanceLoadingAtom, false) set(indexDTFExposureDataAtom, null) set(performanceTimeRangeAtom, '7d') + set(indexDTFStatusAtom, 'active') }) +const DeprecationStatusUpdater = ({ + tokenAddress, + chainId, +}: { + tokenAddress?: string + chainId: number +}) => { + const status = useDTFStatus(tokenAddress, chainId) + const setStatus = useSetAtom(indexDTFStatusAtom) + + useEffect(() => { + setStatus(status) + }, [status]) + + return null +} + export const indexDTFRefreshFnAtom = atom<(() => void) | null>(null) // TODO: Hook currently re-renders a lot because of a wagmi bug, different component to avoid tree re-renders @@ -280,6 +300,7 @@ const Updater = () => { +
) diff --git a/src/views/index-dtf/issuance/index.tsx b/src/views/index-dtf/issuance/index.tsx index d9968a014..e1d3a50c8 100644 --- a/src/views/index-dtf/issuance/index.tsx +++ b/src/views/index-dtf/issuance/index.tsx @@ -1,6 +1,6 @@ import { devModeAtom } from '@/state/atoms' import { wagmiConfig } from '@/state/chain' -import { indexDTFAtom } from '@/state/dtf/atoms' +import { indexDTFAtom, indexDTFStatusAtom } from '@/state/dtf/atoms' import { RESERVE_API } from '@/utils/constants' import { useZapperModal, ZapperProps } from '@reserve-protocol/react-zapper' import { atom, useAtomValue } from 'jotai' @@ -28,6 +28,7 @@ const IndexDTFIssuance = () => { const indexDTF = useAtomValue(indexDTFAtom) const quoteSource = useAtomValue(indexDTFQuoteSourceAtom) const devMode = useAtomValue(devModeAtom) + const isDeprecated = useAtomValue(indexDTFStatusAtom) === 'deprecated' const { currentTab } = useZapperModal() const { trackClick } = useTrackIndexDTFClick('overview', 'mint') @@ -46,6 +47,7 @@ const IndexDTFIssuance = () => { apiUrl={RESERVE_API} debug={devMode} defaultSource={quoteSource} + sellOnly={isDeprecated} />
diff --git a/src/views/index-dtf/overview/components/index-ctas-overview-mobile.tsx b/src/views/index-dtf/overview/components/index-ctas-overview-mobile.tsx index f774e442c..4d4b17915 100644 --- a/src/views/index-dtf/overview/components/index-ctas-overview-mobile.tsx +++ b/src/views/index-dtf/overview/components/index-ctas-overview-mobile.tsx @@ -1,8 +1,12 @@ import { Button } from '@/components/ui/button' +import { indexDTFStatusAtom } from '@/state/dtf/atoms' import { useZapperModal } from '@reserve-protocol/react-zapper' +import { useAtomValue } from 'jotai' const IndexCTAsOverviewMobile = () => { const { open, setTab } = useZapperModal() + const isDeprecated = useAtomValue(indexDTFStatusAtom) === 'deprecated' + return (
@@ -18,6 +22,7 @@ const IndexCTAsOverviewMobile = () => { )} + {isDeprecated && ( + + )}