diff --git a/src/components/DataPrefetcher.tsx b/src/components/DataPrefetcher.tsx index 8be4a2ec..ae47bdd0 100644 --- a/src/components/DataPrefetcher.tsx +++ b/src/components/DataPrefetcher.tsx @@ -2,10 +2,12 @@ import { usePathname } from 'next/navigation'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; +import { useMorphoWhitelistStatusQuery } from '@/hooks/queries/useMorphoWhitelistStatusQuery'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; import { useMerklCampaignsQuery } from '@/hooks/queries/useMerklCampaignsQuery'; function DataPrefetcherContent() { + useMorphoWhitelistStatusQuery(); useMarketsQuery(); useTokensQuery(); useMerklCampaignsQuery(); diff --git a/src/data-sources/morpho-api/market-whitelist-status.ts b/src/data-sources/morpho-api/market-whitelist-status.ts new file mode 100644 index 00000000..8ffe7db7 --- /dev/null +++ b/src/data-sources/morpho-api/market-whitelist-status.ts @@ -0,0 +1,158 @@ +import { supportsMorphoApi } from '@/config/dataSources'; +import { marketsWhitelistStatusQuery } from '@/graphql/morpho-api-queries'; +import { getMarketIdentityKey } from '@/utils/market-identity'; +import { ALL_SUPPORTED_NETWORKS, type SupportedNetworks } from '@/utils/networks'; +import { morphoGraphqlFetcher } from './fetchers'; + +type MorphoWhitelistMarket = { + uniqueKey: string; + listed: boolean; + morphoBlue: { + chain: { + id: number; + }; + }; +}; + +type MorphoWhitelistStatusResponse = { + data?: { + markets?: { + items?: MorphoWhitelistMarket[]; + pageInfo?: { + countTotal: number; + }; + }; + }; + errors?: { message: string }[]; +}; + +type MorphoWhitelistStatusPage = { + items: MorphoWhitelistStatus[]; + totalCount: number; +}; + +export type MorphoWhitelistStatus = { + chainId: number; + uniqueKey: string; + listed: boolean; +}; + +export type MorphoWhitelistStatusRefresh = { + network: SupportedNetworks; + statuses: MorphoWhitelistStatus[]; +}; + +const MORPHO_WHITELIST_PAGE_SIZE = 1_000; +const MORPHO_WHITELIST_TIMEOUT_MS = 15_000; +const MORPHO_WHITELIST_PAGE_BATCH_SIZE = 4; + +const MORPHO_SUPPORTED_NETWORKS = ALL_SUPPORTED_NETWORKS.filter((network) => supportsMorphoApi(network)); + +const fetchMorphoWhitelistStatusPage = async ( + network: SupportedNetworks, + skip: number, + pageSize: number, +): Promise => { + const response = await morphoGraphqlFetcher( + marketsWhitelistStatusQuery, + { + first: pageSize, + skip, + where: { + chainId_in: [network], + }, + }, + { + timeoutMs: MORPHO_WHITELIST_TIMEOUT_MS, + }, + ); + + if (!response?.data?.markets?.items || !response.data.markets.pageInfo) { + console.warn(`[WhitelistStatus] Skipping failed page at skip=${skip} for network ${network}`); + return null; + } + + return { + items: response.data.markets.items.map((market) => ({ + chainId: market.morphoBlue.chain.id, + uniqueKey: market.uniqueKey, + listed: market.listed, + })), + totalCount: response.data.markets.pageInfo.countTotal, + }; +}; + +const fetchMorphoWhitelistStatusesForNetwork = async (network: SupportedNetworks): Promise => { + const firstPage = await fetchMorphoWhitelistStatusPage(network, 0, MORPHO_WHITELIST_PAGE_SIZE); + if (!firstPage) { + throw new Error(`[WhitelistStatus] Failed to fetch first page for network ${network}.`); + } + + const allStatuses = [...firstPage.items]; + const firstPageCount = firstPage.items.length; + const totalCount = firstPage.totalCount; + + if (firstPageCount === 0 && totalCount > 0) { + throw new Error(`[WhitelistStatus] Received empty first page for network ${network} while totalCount=${totalCount}.`); + } + + const remainingOffsets: number[] = []; + for (let nextSkip = firstPageCount; nextSkip < totalCount; nextSkip += MORPHO_WHITELIST_PAGE_SIZE) { + remainingOffsets.push(nextSkip); + } + + for (let index = 0; index < remainingOffsets.length; index += MORPHO_WHITELIST_PAGE_BATCH_SIZE) { + const offsetBatch = remainingOffsets.slice(index, index + MORPHO_WHITELIST_PAGE_BATCH_SIZE); + const settledPages = await Promise.allSettled( + offsetBatch.map((skip) => fetchMorphoWhitelistStatusPage(network, skip, MORPHO_WHITELIST_PAGE_SIZE)), + ); + + for (const settledPage of settledPages) { + if (settledPage.status === 'rejected') { + throw settledPage.reason; + } + if (!settledPage.value) { + throw new Error(`[WhitelistStatus] Failed to fetch one of the paginated whitelist pages for network ${network}.`); + } + + allStatuses.push(...settledPage.value.items); + } + } + + if (allStatuses.length < totalCount) { + throw new Error( + `[WhitelistStatus] Incomplete whitelist dataset for network ${network}: fetched ${allStatuses.length} of ${totalCount}.`, + ); + } + + return allStatuses; +}; + +export const fetchAllMorphoWhitelistStatuses = async (): Promise => { + const settledResults = await Promise.allSettled( + MORPHO_SUPPORTED_NETWORKS.map(async (network) => ({ + network, + statuses: await fetchMorphoWhitelistStatusesForNetwork(network), + })), + ); + const successfulRefreshes: MorphoWhitelistStatusRefresh[] = []; + + for (const settledResult of settledResults) { + if (settledResult.status === 'rejected') { + console.warn('[WhitelistStatus] Failed to fetch one network; continuing with cached/partial data.', settledResult.reason); + continue; + } + + const statusByKey = new Map(); + for (const status of settledResult.value.statuses) { + statusByKey.set(getMarketIdentityKey(status.chainId, status.uniqueKey), status); + } + + successfulRefreshes.push({ + network: settledResult.value.network, + statuses: Array.from(statusByKey.values()), + }); + } + + return successfulRefreshes; +}; diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts index 4b4985fb..5dee8f0f 100644 --- a/src/data-sources/morpho-api/market.ts +++ b/src/data-sources/morpho-api/market.ts @@ -19,7 +19,6 @@ type MorphoApiMarketState = Omit< type MorphoApiMarket = Omit & { oracle: { address: string } | null; - listed: boolean; state: MorphoApiMarketState; supplyingVaults?: { address: string }[]; }; @@ -55,11 +54,12 @@ const MORPHO_MARKETS_PAGE_BATCH_SIZE = 4; // Transform API response to internal Market type const processMarketData = (market: MorphoApiMarket): Market => { - const { oracle, listed, state, supplyingVaults, ...rest } = market; + const { oracle, state, supplyingVaults, ...rest } = market; return { ...rest, oracleAddress: (oracle?.address ?? zeroAddress) as Address, - whitelisted: listed, + // Whitelist status is now overlaid by the dedicated whitelist-status hook. + whitelisted: false, hasUSDPrice: true, supplyingVaults: supplyingVaults ?? [], state: { diff --git a/src/features/market-detail/components/pro-activities-table.tsx b/src/features/market-detail/components/pro-activities-table.tsx index 5b578741..156c3c92 100644 --- a/src/features/market-detail/components/pro-activities-table.tsx +++ b/src/features/market-detail/components/pro-activities-table.tsx @@ -774,7 +774,9 @@ export function ProActivitiesTable({ chainId, market, onSwitchToBasic }: ProActi {renderRowFlow(activity)} - {formatActivityTime(activity.timestamp)} + + {formatActivityTime(activity.timestamp)} + (null); const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' }); @@ -83,11 +83,12 @@ function MarketsTable({ currentPage, setCurrentPage, className, tableClassName, const currentEntries = markets.slice(indexOfFirstEntry, indexOfLastEntry); const totalPages = Math.ceil(markets.length / entriesPerPage); + const shouldUseFullWidthState = loading || filteredMarketsLoading || isEmpty || markets.length === 0; const containerClassName = [ 'flex flex-col gap-2 pb-4', - loading ? 'w-full' : (className ?? 'w-full'), - loading || isEmpty || markets.length === 0 ? 'items-center' : '', + shouldUseFullWidthState ? 'w-full' : (className ?? 'w-full'), + shouldUseFullWidthState ? 'items-center' : '', ] .filter((value): value is string => Boolean(value)) .join(' '); @@ -130,7 +131,7 @@ function MarketsTable({ currentPage, setCurrentPage, className, tableClassName, } className="w-full" > - {loading ? ( + {loading || filteredMarketsLoading ? ( ) : markets.length === 0 ? ( ) : ( diff --git a/src/features/markets/markets-view.tsx b/src/features/markets/markets-view.tsx index 7ebae913..0eb7d589 100644 --- a/src/features/markets/markets-view.tsx +++ b/src/features/markets/markets-view.tsx @@ -4,6 +4,7 @@ import type { Chain } from 'viem'; import Header from '@/components/layout/header/Header'; import { Breadcrumbs } from '@/components/shared/breadcrumbs'; +import { useFilteredMarkets } from '@/hooks/useFilteredMarkets'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; import { useMarketsFilters } from '@/stores/useMarketsFilters'; @@ -20,6 +21,7 @@ export default function Markets() { // Data fetching with React Query const { data: rawMarkets, isLoading: loading, refetch } = useMarketsQuery(); + const { markets, isLoading: filteredMarketsLoading, isWhitelistUnavailable } = useFilteredMarkets(); const filters = useMarketsFilters(); @@ -46,8 +48,10 @@ export default function Markets() { // Effective table view mode - always compact on mobile const effectiveTableViewMode = isMobile ? 'compact' : tableViewMode; - const isLoadingTableState = loading; - const shouldUseFullWidthTableLayout = isLoadingTableState || effectiveTableViewMode === 'compact'; + const isLoadingTableState = loading || filteredMarketsLoading; + const isTableFallbackState = !rawMarkets || markets.length === 0 || isWhitelistUnavailable; + const shouldUseFullWidthTableLayout = + isLoadingTableState || isTableFallbackState || effectiveTableViewMode === 'compact'; // Compute unique collaterals and loan assets for filter dropdowns useEffect(() => { diff --git a/src/graphql/morpho-api-queries.ts b/src/graphql/morpho-api-queries.ts index 31f68021..40f9c7a2 100644 --- a/src/graphql/morpho-api-queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -5,7 +5,6 @@ irmAddress oracle { address } -listed morphoBlue { id address @@ -103,7 +102,6 @@ export const marketsQuery = ` oracle { address } - listed morphoBlue { address chain { @@ -155,6 +153,25 @@ export const marketsQuery = ` } `; +export const marketsWhitelistStatusQuery = ` + query getMarketsWhitelistStatus($first: Int, $skip: Int, $where: MarketFilters) { + markets(first: $first, skip: $skip, where: $where) { + items { + uniqueKey + listed + morphoBlue { + chain { + id + } + } + } + pageInfo { + countTotal + } + } + } +`; + export const userPositionsQuery = ` query getUserMarketPositions($address: String!, $chainId: Int) { userByAddress(address: $address, chainId: $chainId) { diff --git a/src/hooks/queries/useMarketsQuery.ts b/src/hooks/queries/useMarketsQuery.ts index 076ebc20..0a51dbb4 100644 --- a/src/hooks/queries/useMarketsQuery.ts +++ b/src/hooks/queries/useMarketsQuery.ts @@ -3,6 +3,7 @@ import { supportsMorphoApi } from '@/config/dataSources'; import { fetchMonarchMarkets } from '@/data-sources/monarch-api'; import { fetchMorphoMarkets } from '@/data-sources/morpho-api/market'; import { fetchSubgraphMarkets } from '@/data-sources/subgraph/market'; +import { getMarketIdentityKey } from '@/utils/market-identity'; import { ALL_SUPPORTED_NETWORKS, isSupportedChain, type SupportedNetworks } from '@/utils/networks'; import type { Market } from '@/utils/types'; @@ -11,30 +12,6 @@ const toError = (error: unknown): Error => { return new Error(String(error)); }; -const getMarketIdentityKey = (market: Pick): string => - `${market.morphoBlue.chain.id}-${market.uniqueKey.toLowerCase()}`; - -const buildWhitelistLookup = (markets: Market[]): Map => { - return markets.reduce((lookup, market) => { - lookup.set(getMarketIdentityKey(market), market.whitelisted); - return lookup; - }, new Map()); -}; - -const mergeWhitelistFlags = (baseMarkets: Market[], whitelistLookup: Map): Market[] => { - return baseMarkets.map((market) => { - const whitelisted = whitelistLookup.get(getMarketIdentityKey(market)); - if (whitelisted === undefined || whitelisted === market.whitelisted) { - return market; - } - - return { - ...market, - whitelisted, - }; - }); -}; - /** * Fetches markets from all supported networks using React Query. * @@ -96,29 +73,6 @@ export const useMarketsQuery = () => { for (const [network, markets] of monarchMarketsByChain.entries()) { setMarketsForChain(network, markets); } - - // Restore Morpho `listed` as the whitelist source for chains where Morpho API is supported. - const monarchNetworksWithMorphoSupport = Array.from(monarchMarketsByChain.keys()).filter((network) => supportsMorphoApi(network)); - const whitelistSettledResults = await Promise.allSettled( - monarchNetworksWithMorphoSupport.map((network) => fetchMorphoMarkets(network).then((markets) => ({ network, markets }))), - ); - - whitelistSettledResults.forEach((result, index) => { - const network = monarchNetworksWithMorphoSupport[index]; - - if (result.status === 'rejected') { - console.warn(`Morpho whitelist refresh failed for network ${network}; keeping existing whitelist flags.`, result.reason); - return; - } - - const currentMarkets = marketsByChain.get(network); - if (!currentMarkets || currentMarkets.length === 0) { - return; - } - - const whitelistLookup = buildWhitelistLookup(result.value.markets); - marketsByChain.set(network, mergeWhitelistFlags(currentMarkets, whitelistLookup)); - }); } catch (error) { const monarchError = toError(error); console.warn('Monarch multi-chain markets fetch failed. Falling back per chain to Morpho/Subgraph.', monarchError); @@ -168,7 +122,7 @@ export const useMarketsQuery = () => { const dedupedMarkets = Array.from( combinedMarkets .reduce((acc, market) => { - acc.set(`${market.morphoBlue.chain.id}-${market.uniqueKey.toLowerCase()}`, market); + acc.set(getMarketIdentityKey(market.morphoBlue.chain.id, market.uniqueKey), market); return acc; }, new Map()) .values(), diff --git a/src/hooks/queries/useMorphoWhitelistStatusQuery.ts b/src/hooks/queries/useMorphoWhitelistStatusQuery.ts new file mode 100644 index 00000000..5c3cf6c6 --- /dev/null +++ b/src/hooks/queries/useMorphoWhitelistStatusQuery.ts @@ -0,0 +1,62 @@ +import { useEffect, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { fetchAllMorphoWhitelistStatuses } from '@/data-sources/morpho-api/market-whitelist-status'; +import { useMarketWhitelistFlags } from '@/stores/useMarketWhitelistFlags'; + +const EMPTY_LOOKUP = new Map(); + +export const useMorphoWhitelistStatusQuery = () => { + const flagsByNetwork = useMarketWhitelistFlags((state) => state.flagsByNetwork); + const replaceNetworks = useMarketWhitelistFlags((state) => state.replaceNetworks); + + const query = useQuery({ + queryKey: ['morpho-whitelist-status'], + queryFn: async () => { + try { + return await fetchAllMorphoWhitelistStatuses(); + } catch (error) { + console.warn('Morpho whitelist-status refresh failed; continuing with cached whitelist flags.', error); + return []; + } + }, + staleTime: 5 * 60 * 1000, + refetchInterval: 5 * 60 * 1000, + refetchOnWindowFocus: true, + }); + + useEffect(() => { + if (!query.data || query.data.length === 0) { + return; + } + + replaceNetworks(query.data); + }, [query.data, replaceNetworks]); + + const whitelistLookup = useMemo(() => { + const networks = Object.values(flagsByNetwork); + if (networks.length === 0) { + return EMPTY_LOOKUP; + } + + const lookup = new Map(); + + networks.forEach((flags) => { + Object.entries(flags).forEach(([marketKey, listed]) => { + lookup.set(marketKey, listed); + }); + }); + + if (lookup.size === 0) { + return EMPTY_LOOKUP; + } + + return lookup; + }, [flagsByNetwork]); + + return { + whitelistLookup, + isLoading: query.isLoading && whitelistLookup.size === 0, + isFetching: query.isFetching, + refetch: query.refetch, + }; +}; diff --git a/src/hooks/useFilteredMarkets.ts b/src/hooks/useFilteredMarkets.ts index 391c30d1..6b766bb2 100644 --- a/src/hooks/useFilteredMarkets.ts +++ b/src/hooks/useFilteredMarkets.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; +import { useMorphoWhitelistStatusQuery } from '@/hooks/queries/useMorphoWhitelistStatusQuery'; import { useAllOracleMetadata } from '@/hooks/useOracleMetadata'; import { useMarketsFilters } from '@/stores/useMarketsFilters'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; @@ -12,8 +13,15 @@ import { SortColumn } from '@/features/markets/components/constants'; import { getVaultKey } from '@/constants/vaults/known_vaults'; import type { Market } from '@/utils/types'; -export const useFilteredMarkets = (): Market[] => { +type UseFilteredMarketsResult = { + markets: Market[]; + isLoading: boolean; + isWhitelistUnavailable: boolean; +}; + +export const useFilteredMarkets = (): UseFilteredMarketsResult => { const { allMarkets, whitelistedMarkets } = useProcessedMarkets(); + const { whitelistLookup, isLoading: whitelistLoading, isFetching: whitelistFetching } = useMorphoWhitelistStatusQuery(); const { data: oracleMetadataMap } = useAllOracleMetadata(); const filters = useMarketsFilters(); const preferences = useMarketPreferences(); @@ -22,8 +30,13 @@ export const useFilteredMarkets = (): Market[] => { const { findToken } = useTokensQuery(); const officialTrendingKeys = useOfficialTrendingMarketKeys(); const customTagKeys = useCustomTagMarketKeys(); + const shouldBlockWhitelistedFiltering = !showUnwhitelistedMarkets && whitelistLookup.size === 0; + + const markets = useMemo(() => { + if (shouldBlockWhitelistedFiltering) { + return []; + } - return useMemo(() => { let markets = showUnwhitelistedMarkets ? allMarkets : whitelistedMarkets; if (markets.length === 0) return []; @@ -138,6 +151,7 @@ export const useFilteredMarkets = (): Market[] => { }, [ allMarkets, whitelistedMarkets, + shouldBlockWhitelistedFiltering, showUnwhitelistedMarkets, filters, preferences, @@ -147,4 +161,10 @@ export const useFilteredMarkets = (): Market[] => { customTagKeys, oracleMetadataMap, ]); + + return { + markets, + isLoading: shouldBlockWhitelistedFiltering && (whitelistLoading || whitelistFetching), + isWhitelistUnavailable: shouldBlockWhitelistedFiltering && !whitelistLoading && !whitelistFetching, + }; }; diff --git a/src/hooks/useMarketTxContexts.ts b/src/hooks/useMarketTxContexts.ts index e4a7e06a..a98ffaf2 100644 --- a/src/hooks/useMarketTxContexts.ts +++ b/src/hooks/useMarketTxContexts.ts @@ -74,7 +74,7 @@ export const useMarketTxContexts = (marketId: string | undefined, network: Suppo const data = page === 1 ? snapshotQuery.data : pageQuery.data; const isLoading = page === 1 ? snapshotQuery.isLoading : snapshotQuery.isLoading || pageQuery.isLoading; const isFetching = page === 1 ? snapshotQuery.isFetching : snapshotQuery.isFetching || pageQuery.isFetching; - const error = page === 1 ? snapshotQuery.error : snapshotQuery.error ?? pageQuery.error; + const error = page === 1 ? snapshotQuery.error : (snapshotQuery.error ?? pageQuery.error); const refetch = page === 1 ? snapshotQuery.refetch : pageQuery.refetch; useEffect(() => { diff --git a/src/hooks/useProcessedMarkets.ts b/src/hooks/useProcessedMarkets.ts index 52df6052..cf900e7a 100644 --- a/src/hooks/useProcessedMarkets.ts +++ b/src/hooks/useProcessedMarkets.ts @@ -1,12 +1,14 @@ import { useMemo } from 'react'; +import { getTokenPriceKey } from '@/data-sources/morpho-api/prices'; import { useMarketRateEnrichmentQuery } from '@/hooks/queries/useMarketRateEnrichmentQuery'; +import { useMorphoWhitelistStatusQuery } from '@/hooks/queries/useMorphoWhitelistStatusQuery'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; import { useTokenPrices } from '@/hooks/useTokenPrices'; import { useBlacklistedMarkets } from '@/stores/useBlacklistedMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; +import { getMarketIdentityKey } from '@/utils/market-identity'; import { getMarketRateEnrichmentKey, type MarketRateEnrichmentMap } from '@/utils/market-rate-enrichment'; import { isForceUnwhitelisted } from '@/utils/markets'; -import { getTokenPriceKey } from '@/data-sources/morpho-api/prices'; import { formatBalance } from '@/utils/balance'; import type { TokenPriceInput } from '@/data-sources/morpho-api/prices'; import type { Market } from '@/utils/types'; @@ -63,6 +65,7 @@ const computeUsdValue = (assets: string, decimals: number, price: number): numbe */ export const useProcessedMarkets = () => { const { data: rawMarketsFromQuery, isLoading, isRefetching, error, refetch } = useMarketsQuery(); + const { whitelistLookup } = useMorphoWhitelistStatusQuery(); const { getAllBlacklistedKeys, customBlacklistedMarkets } = useBlacklistedMarkets(); const { showUnwhitelistedMarkets } = useAppSettings(); @@ -79,8 +82,21 @@ export const useProcessedMarkets = () => { }; } + const withMergedWhitelistFlags = rawMarketsFromQuery.map((market) => { + const cachedWhitelisted = whitelistLookup.get(getMarketIdentityKey(market.morphoBlue.chain.id, market.uniqueKey)); + + if (cachedWhitelisted === undefined || cachedWhitelisted === market.whitelisted) { + return market; + } + + return { + ...market, + whitelisted: cachedWhitelisted, + }; + }); + // rawMarketsUnfiltered: before blacklist (for blacklist management modal) - const rawMarketsUnfiltered = rawMarketsFromQuery; + const rawMarketsUnfiltered = withMergedWhitelistFlags; // Apply blacklist filter const blacklistFiltered = rawMarketsUnfiltered.filter((market) => !allBlacklistedMarketKeys.has(market.uniqueKey)); @@ -106,7 +122,7 @@ export const useProcessedMarkets = () => { allMarkets, whitelistedMarkets, }; - }, [rawMarketsFromQuery, allBlacklistedMarketKeys]); + }, [rawMarketsFromQuery, allBlacklistedMarketKeys, whitelistLookup]); const { data: marketRateEnrichments = EMPTY_RATE_ENRICHMENTS, diff --git a/src/stores/useMarketWhitelistFlags.ts b/src/stores/useMarketWhitelistFlags.ts new file mode 100644 index 00000000..c0ec3f2e --- /dev/null +++ b/src/stores/useMarketWhitelistFlags.ts @@ -0,0 +1,49 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { getMarketIdentityKey } from '@/utils/market-identity'; +import type { MorphoWhitelistStatusRefresh } from '@/data-sources/morpho-api/market-whitelist-status'; + +type MarketWhitelistFlagsState = { + flagsByNetwork: Record>; +}; + +type MarketWhitelistFlagsActions = { + replaceNetworks: (refreshes: MorphoWhitelistStatusRefresh[]) => void; +}; + +type MarketWhitelistFlagsStore = MarketWhitelistFlagsState & MarketWhitelistFlagsActions; + +export const useMarketWhitelistFlags = create()( + persist( + (set) => ({ + flagsByNetwork: {}, + + replaceNetworks: (refreshes) => { + if (refreshes.length === 0) { + return; + } + + set((state) => { + const nextFlagsByNetwork = { ...state.flagsByNetwork }; + + refreshes.forEach(({ network, statuses }) => { + nextFlagsByNetwork[String(network)] = statuses.reduce>((acc, status) => { + acc[getMarketIdentityKey(status.chainId, status.uniqueKey)] = status.listed; + return acc; + }, {}); + }); + + return { + flagsByNetwork: nextFlagsByNetwork, + }; + }); + }, + }), + { + name: 'monarch_store_marketWhitelistFlags', + partialize: (state) => ({ + flagsByNetwork: state.flagsByNetwork, + }), + }, + ), +); diff --git a/src/utils/market-identity.ts b/src/utils/market-identity.ts new file mode 100644 index 00000000..994606d8 --- /dev/null +++ b/src/utils/market-identity.ts @@ -0,0 +1,3 @@ +export const getMarketIdentityKey = (chainId: number, uniqueKey: string): string => { + return `${chainId}-${uniqueKey.toLowerCase()}`; +}; diff --git a/src/utils/markets.ts b/src/utils/markets.ts index 705e0671..3ac1c050 100644 --- a/src/utils/markets.ts +++ b/src/utils/markets.ts @@ -69,7 +69,13 @@ export const blacklistedMarkets = [ '0x1dca6989b0d2b0a546530b3a739e91402eee2e1536a2d3ded4f5ce589a9cd1c2', // '0xfdb8221edcae73f73485d55c30e706906114bc2ff4634870c5c57e8fb83eae6a', // USDC / K on arbitrum '0x0f9563442d64ab3bd3bcb27058db0b0d4046a4c46f0acd811dacae9551d2b129', // sdeUSD / USDC market from Elixir affected by incident + + // wUSDL markets, decimal changed '0xfd3e5c20340aeba93f78f7dc4657dc1e11b553c68c545acc836321a14b47e457', // wUSDL/wstETH, decimal changed. + '0x8d18658cd2688b702222c11467133c1c2237bd058ba2467e47bc360067ebe038', + '0xa9d6a0caea685bb0099ba2d52a58fb7ed33a6447616c25df93bdb8330337a9c3', + '0x394494c539655eb489089b33eba5119ff7e322646cac3fa3e817814164bab094', + '0x50e26162f35945381884ea34bf5c1d5d9f15c9305febbc1f890c916963ba0f2b' ]; // Market specially whitelisted by Monarch, lowercase