From 6cf4e143dc47845b61754a8d46d8f7fdb2ecdab2 Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Fri, 27 Mar 2026 14:04:35 +0800 Subject: [PATCH 1/7] refactor: decouple morpho whitelist refresh --- .../morpho-api/market-whitelist-status.ts | 158 ++++++++++++++++++ src/graphql/morpho-api-queries.ts | 19 +++ src/hooks/queries/useMarketsQuery.ts | 50 +----- .../queries/useMorphoWhitelistStatusQuery.ts | 82 +++++++++ src/hooks/useProcessedMarkets.ts | 22 ++- src/stores/useMarketWhitelistFlags.ts | 56 +++++++ src/utils/market-identity.ts | 3 + 7 files changed, 339 insertions(+), 51 deletions(-) create mode 100644 src/data-sources/morpho-api/market-whitelist-status.ts create mode 100644 src/hooks/queries/useMorphoWhitelistStatusQuery.ts create mode 100644 src/stores/useMarketWhitelistFlags.ts create mode 100644 src/utils/market-identity.ts 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..487123cd --- /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 = 500; +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/graphql/morpho-api-queries.ts b/src/graphql/morpho-api-queries.ts index 31f68021..1fc61d76 100644 --- a/src/graphql/morpho-api-queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -155,6 +155,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..073dc988 --- /dev/null +++ b/src/hooks/queries/useMorphoWhitelistStatusQuery.ts @@ -0,0 +1,82 @@ +import { useEffect, useMemo, useState } 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(); +const WHITELIST_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000; +const WHITELIST_CACHE_TICK_MS = 60 * 1000; + +export const useMorphoWhitelistStatusQuery = () => { + const flagsByNetwork = useMarketWhitelistFlags((state) => state.flagsByNetwork); + const lastSyncedAtByNetwork = useMarketWhitelistFlags((state) => state.lastSyncedAtByNetwork); + const replaceNetworks = useMarketWhitelistFlags((state) => state.replaceNetworks); + const [currentTime, setCurrentTime] = useState(() => Date.now()); + + 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, + retry: 1, + }); + + useEffect(() => { + if (!query.data || query.data.length === 0) { + return; + } + + replaceNetworks(query.data); + }, [query.data, replaceNetworks]); + + useEffect(() => { + const intervalId = globalThis.setInterval(() => { + setCurrentTime(Date.now()); + }, WHITELIST_CACHE_TICK_MS); + + return () => { + globalThis.clearInterval(intervalId); + }; + }, []); + + const whitelistLookup = useMemo(() => { + const networks = Object.entries(flagsByNetwork); + if (networks.length === 0) { + return EMPTY_LOOKUP; + } + + const lookup = new Map(); + + networks.forEach(([network, flags]) => { + const lastSyncedAt = lastSyncedAtByNetwork[network]; + if (!lastSyncedAt || currentTime - lastSyncedAt > WHITELIST_CACHE_MAX_AGE_MS) { + return; + } + + Object.entries(flags).forEach(([marketKey, listed]) => { + lookup.set(marketKey, listed); + }); + }); + + if (lookup.size === 0) { + return EMPTY_LOOKUP; + } + + return lookup; + }, [currentTime, flagsByNetwork, lastSyncedAtByNetwork]); + + return { + whitelistLookup, + isLoading: query.isLoading && whitelistLookup.size === 0, + isFetching: query.isFetching, + refetch: query.refetch, + }; +}; 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..90df1553 --- /dev/null +++ b/src/stores/useMarketWhitelistFlags.ts @@ -0,0 +1,56 @@ +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>; + lastSyncedAtByNetwork: Record; +}; + +type MarketWhitelistFlagsActions = { + replaceNetworks: (refreshes: MorphoWhitelistStatusRefresh[]) => void; +}; + +type MarketWhitelistFlagsStore = MarketWhitelistFlagsState & MarketWhitelistFlagsActions; + +export const useMarketWhitelistFlags = create()( + persist( + (set) => ({ + flagsByNetwork: {}, + lastSyncedAtByNetwork: {}, + + replaceNetworks: (refreshes) => { + if (refreshes.length === 0) { + return; + } + + set((state) => { + const nextFlagsByNetwork = { ...state.flagsByNetwork }; + const nextLastSyncedAtByNetwork = { ...state.lastSyncedAtByNetwork }; + const syncTime = Date.now(); + + refreshes.forEach(({ network, statuses }) => { + nextFlagsByNetwork[String(network)] = statuses.reduce>((acc, status) => { + acc[getMarketIdentityKey(status.chainId, status.uniqueKey)] = status.listed; + return acc; + }, {}); + nextLastSyncedAtByNetwork[String(network)] = syncTime; + }); + + return { + flagsByNetwork: nextFlagsByNetwork, + lastSyncedAtByNetwork: nextLastSyncedAtByNetwork, + }; + }); + }, + }), + { + name: 'monarch_store_marketWhitelistFlags', + partialize: (state) => ({ + flagsByNetwork: state.flagsByNetwork, + lastSyncedAtByNetwork: state.lastSyncedAtByNetwork, + }), + }, + ), +); 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()}`; +}; From fa7db8280fd074fffd6b4b8a78693e64e77a0552 Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Fri, 27 Mar 2026 17:14:11 +0800 Subject: [PATCH 2/7] perf: increase morpho whitelist page size --- src/data-sources/morpho-api/market-whitelist-status.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data-sources/morpho-api/market-whitelist-status.ts b/src/data-sources/morpho-api/market-whitelist-status.ts index 487123cd..8ffe7db7 100644 --- a/src/data-sources/morpho-api/market-whitelist-status.ts +++ b/src/data-sources/morpho-api/market-whitelist-status.ts @@ -42,7 +42,7 @@ export type MorphoWhitelistStatusRefresh = { statuses: MorphoWhitelistStatus[]; }; -const MORPHO_WHITELIST_PAGE_SIZE = 500; +const MORPHO_WHITELIST_PAGE_SIZE = 1_000; const MORPHO_WHITELIST_TIMEOUT_MS = 15_000; const MORPHO_WHITELIST_PAGE_BATCH_SIZE = 4; From 56d9e59bb52d61c9ab31d4078795cc420c7c6e93 Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Fri, 27 Mar 2026 18:14:25 +0800 Subject: [PATCH 3/7] chore: remove dead whitelist retry --- src/hooks/queries/useMorphoWhitelistStatusQuery.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/queries/useMorphoWhitelistStatusQuery.ts b/src/hooks/queries/useMorphoWhitelistStatusQuery.ts index 073dc988..dc4b9323 100644 --- a/src/hooks/queries/useMorphoWhitelistStatusQuery.ts +++ b/src/hooks/queries/useMorphoWhitelistStatusQuery.ts @@ -26,7 +26,6 @@ export const useMorphoWhitelistStatusQuery = () => { staleTime: 5 * 60 * 1000, refetchInterval: 5 * 60 * 1000, refetchOnWindowFocus: true, - retry: 1, }); useEffect(() => { From 979ea8d8a91a3353968d8fb56e9b0c8cb2982fb4 Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Fri, 27 Mar 2026 18:30:51 +0800 Subject: [PATCH 4/7] refactor: prefer persisted whitelist cache --- .../components/pro-activities-table.tsx | 4 ++- .../queries/useMorphoWhitelistStatusQuery.ts | 27 +++---------------- src/hooks/useMarketTxContexts.ts | 2 +- src/stores/useMarketWhitelistFlags.ts | 7 ----- 4 files changed, 8 insertions(+), 32 deletions(-) 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)} + (); -const WHITELIST_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000; -const WHITELIST_CACHE_TICK_MS = 60 * 1000; export const useMorphoWhitelistStatusQuery = () => { const flagsByNetwork = useMarketWhitelistFlags((state) => state.flagsByNetwork); - const lastSyncedAtByNetwork = useMarketWhitelistFlags((state) => state.lastSyncedAtByNetwork); const replaceNetworks = useMarketWhitelistFlags((state) => state.replaceNetworks); - const [currentTime, setCurrentTime] = useState(() => Date.now()); const query = useQuery({ queryKey: ['morpho-whitelist-status'], @@ -36,30 +32,15 @@ export const useMorphoWhitelistStatusQuery = () => { replaceNetworks(query.data); }, [query.data, replaceNetworks]); - useEffect(() => { - const intervalId = globalThis.setInterval(() => { - setCurrentTime(Date.now()); - }, WHITELIST_CACHE_TICK_MS); - - return () => { - globalThis.clearInterval(intervalId); - }; - }, []); - const whitelistLookup = useMemo(() => { - const networks = Object.entries(flagsByNetwork); + const networks = Object.values(flagsByNetwork); if (networks.length === 0) { return EMPTY_LOOKUP; } const lookup = new Map(); - networks.forEach(([network, flags]) => { - const lastSyncedAt = lastSyncedAtByNetwork[network]; - if (!lastSyncedAt || currentTime - lastSyncedAt > WHITELIST_CACHE_MAX_AGE_MS) { - return; - } - + networks.forEach((flags) => { Object.entries(flags).forEach(([marketKey, listed]) => { lookup.set(marketKey, listed); }); @@ -70,7 +51,7 @@ export const useMorphoWhitelistStatusQuery = () => { } return lookup; - }, [currentTime, flagsByNetwork, lastSyncedAtByNetwork]); + }, [flagsByNetwork]); return { whitelistLookup, 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/stores/useMarketWhitelistFlags.ts b/src/stores/useMarketWhitelistFlags.ts index 90df1553..c0ec3f2e 100644 --- a/src/stores/useMarketWhitelistFlags.ts +++ b/src/stores/useMarketWhitelistFlags.ts @@ -5,7 +5,6 @@ import type { MorphoWhitelistStatusRefresh } from '@/data-sources/morpho-api/mar type MarketWhitelistFlagsState = { flagsByNetwork: Record>; - lastSyncedAtByNetwork: Record; }; type MarketWhitelistFlagsActions = { @@ -18,7 +17,6 @@ export const useMarketWhitelistFlags = create()( persist( (set) => ({ flagsByNetwork: {}, - lastSyncedAtByNetwork: {}, replaceNetworks: (refreshes) => { if (refreshes.length === 0) { @@ -27,20 +25,16 @@ export const useMarketWhitelistFlags = create()( set((state) => { const nextFlagsByNetwork = { ...state.flagsByNetwork }; - const nextLastSyncedAtByNetwork = { ...state.lastSyncedAtByNetwork }; - const syncTime = Date.now(); refreshes.forEach(({ network, statuses }) => { nextFlagsByNetwork[String(network)] = statuses.reduce>((acc, status) => { acc[getMarketIdentityKey(status.chainId, status.uniqueKey)] = status.listed; return acc; }, {}); - nextLastSyncedAtByNetwork[String(network)] = syncTime; }); return { flagsByNetwork: nextFlagsByNetwork, - lastSyncedAtByNetwork: nextLastSyncedAtByNetwork, }; }); }, @@ -49,7 +43,6 @@ export const useMarketWhitelistFlags = create()( name: 'monarch_store_marketWhitelistFlags', partialize: (state) => ({ flagsByNetwork: state.flagsByNetwork, - lastSyncedAtByNetwork: state.lastSyncedAtByNetwork, }), }, ), From e019848ae18789b81b1fe9c9b113ae3374cdb497 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 27 Mar 2026 18:58:16 +0800 Subject: [PATCH 5/7] blacklist more tokens --- src/utils/markets.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 1a1b27d501d50dec9dad14af551dbc1bc61cbc16 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 27 Mar 2026 19:39:06 +0800 Subject: [PATCH 6/7] chore: fixes --- src/components/DataPrefetcher.tsx | 2 ++ src/data-sources/morpho-api/market.ts | 6 ++--- .../components/table/markets-table.tsx | 19 +++++++++------ src/features/markets/markets-view.tsx | 8 +++++-- src/graphql/morpho-api-queries.ts | 2 -- src/hooks/useFilteredMarkets.ts | 24 +++++++++++++++++-- 6 files changed, 45 insertions(+), 16 deletions(-) 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.ts b/src/data-sources/morpho-api/market.ts index 4b4985fb..dfe1ed24 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: true, hasUSDPrice: true, supplyingVaults: supplyingVaults ?? [], state: { diff --git a/src/features/markets/components/table/markets-table.tsx b/src/features/markets/components/table/markets-table.tsx index 1f1a1a9f..9f627129 100644 --- a/src/features/markets/components/table/markets-table.tsx +++ b/src/features/markets/components/table/markets-table.tsx @@ -33,7 +33,7 @@ function MarketsTable({ currentPage, setCurrentPage, className, tableClassName, // Get trusted vaults directly from store (no prop drilling!) const { vaults: trustedVaults } = useTrustedVaults(); - const markets = useFilteredMarkets(); + const { markets, isLoading: filteredMarketsLoading, isWhitelistUnavailable } = useFilteredMarkets(); const isEmpty = !rawMarkets; const [expandedRowId, setExpandedRowId] = useState(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 1fc61d76..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 { 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, + }; }; From 76f613bec79a21b25f5c308f2abd9c61679b95e0 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 27 Mar 2026 19:48:23 +0800 Subject: [PATCH 7/7] chore: default whitelist to false --- src/data-sources/morpho-api/market.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts index dfe1ed24..5dee8f0f 100644 --- a/src/data-sources/morpho-api/market.ts +++ b/src/data-sources/morpho-api/market.ts @@ -59,7 +59,7 @@ const processMarketData = (market: MorphoApiMarket): Market => { ...rest, oracleAddress: (oracle?.address ?? zeroAddress) as Address, // Whitelist status is now overlaid by the dedicated whitelist-status hook. - whitelisted: true, + whitelisted: false, hasUSDPrice: true, supplyingVaults: supplyingVaults ?? [], state: {