From 4f003a61fbbf7dd75666b2a26ed19fae5fc6981e Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 15:18:20 +0800 Subject: [PATCH 1/2] chore: remove unused liquidations code and storage migration Removed: - useMonarchLiquidationsQuery (unused hook) - useLiquidationsQuery (deprecated fallback) - morpho-api/liquidations.ts (unused data source) - subgraph/liquidations.ts (unused data source) - app/api/monarch/liquidations route (unused API proxy) - StorageMigrator component (migration complete) - storage-migration.ts utility Updated: - app/layout.tsx: removed StorageMigrator - useMarketMetricsQuery: cleaned up comments --- app/api/monarch/liquidations/route.ts | 32 -- app/layout.tsx | 2 - src/components/StorageMigrator.tsx | 438 ------------------ src/data-sources/morpho-api/liquidations.ts | 116 ----- src/data-sources/subgraph/liquidations.ts | 82 ---- src/hooks/queries/useLiquidationsQuery.ts | 79 ---- src/hooks/queries/useMarketMetricsQuery.ts | 9 +- .../queries/useMonarchLiquidationsQuery.ts | 29 -- src/utils/storage-migration.ts | 264 ----------- 9 files changed, 2 insertions(+), 1049 deletions(-) delete mode 100644 app/api/monarch/liquidations/route.ts delete mode 100644 src/components/StorageMigrator.tsx delete mode 100644 src/data-sources/morpho-api/liquidations.ts delete mode 100644 src/data-sources/subgraph/liquidations.ts delete mode 100644 src/hooks/queries/useLiquidationsQuery.ts delete mode 100644 src/hooks/queries/useMonarchLiquidationsQuery.ts delete mode 100644 src/utils/storage-migration.ts diff --git a/app/api/monarch/liquidations/route.ts b/app/api/monarch/liquidations/route.ts deleted file mode 100644 index 66f039b1..00000000 --- a/app/api/monarch/liquidations/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { type NextRequest, NextResponse } from 'next/server'; -import { MONARCH_API_KEY, getMonarchUrl } from '../utils'; - -export async function GET(req: NextRequest) { - if (!MONARCH_API_KEY) { - console.error('[Monarch Liquidations API] Missing MONARCH_API_KEY'); - return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); - } - - const chainId = req.nextUrl.searchParams.get('chain_id'); - - try { - const url = getMonarchUrl('/v1/liquidations'); - if (chainId) url.searchParams.set('chain_id', chainId); - - const response = await fetch(url, { - headers: { 'X-API-Key': MONARCH_API_KEY }, - next: { revalidate: 300 }, - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[Monarch Liquidations API] Error:', response.status, errorText); - return NextResponse.json({ error: 'Failed to fetch liquidations' }, { status: response.status }); - } - - return NextResponse.json(await response.json()); - } catch (error) { - console.error('[Monarch Liquidations API] Failed to fetch:', error); - return NextResponse.json({ error: 'Failed to fetch liquidations' }, { status: 500 }); - } -} diff --git a/app/layout.tsx b/app/layout.tsx index b8802b13..96eaa9de 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,6 @@ import { VaultRegistryProvider } from '@/contexts/VaultRegistryContext'; import OnchainProviders from '@/OnchainProviders'; import { ModalRenderer } from '@/components/modals/ModalRenderer'; import { GlobalTransactionModals } from '@/components/common/GlobalTransactionModals'; -import { StorageMigrator } from '@/components/StorageMigrator'; import { DataPrefetcher } from '@/components/DataPrefetcher'; import { initAnalytics } from '@/utils/analytics'; @@ -40,7 +39,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {children} diff --git a/src/components/StorageMigrator.tsx b/src/components/StorageMigrator.tsx deleted file mode 100644 index 13d07e9f..00000000 --- a/src/components/StorageMigrator.tsx +++ /dev/null @@ -1,438 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import storage from 'local-storage-fallback'; -import type { TrustedVault } from '@/constants/vaults/known_vaults'; -import { useTrustedVaults } from '@/stores/useTrustedVaults'; -import { useMarketPreferences } from '@/stores/useMarketPreferences'; -import { useAppSettings } from '@/stores/useAppSettings'; -import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; -import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; -import { useBlacklistedMarkets, type BlacklistedMarket } from '@/stores/useBlacklistedMarkets'; -import { useCustomRpc, type CustomRpcUrls } from '@/stores/useCustomRpc'; -import { useTransactionFiltersStore } from '@/stores/useTransactionFilters'; -import { useUserMarketsCacheStore } from '@/stores/useUserMarketsCache'; -import { SortColumn } from '@/features/markets/components/constants'; -import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; -import { DEFAULT_COLUMN_VISIBILITY } from '@/features/markets/components/column-visibility'; -import { - type MigrationDefinition, - isMigrationComplete, - isMigrationExpired, - markMigrationComplete, - executeAllMigrations, -} from '@/utils/storage-migration'; - -/** - * One-Time Storage Migrator Component - * - * **Purpose:** Migrate user data from old useLocalStorage format to new Zustand stores - * - * **Timeline:** - * - Created: January 2025 - * - Expires: February 1, 2025 - * - * - * **To delete (after Feb 2025):** - * 1. Remove `` from `app/layout.tsx` - * 2. Delete this file: `src/components/StorageMigrator.tsx` - * 3. Delete `src/utils/storage-migration.ts` - * - * @returns null - This component renders nothing - */ - -// Type definitions for migrations -type SymbolFilters = Record< - string, - { - minSupplyAmount: string; - minBorrowAmount: string; - } ->; - -type MarketIdentifier = { - marketUniqueKey: string; - chainId: number; -}; - -type UserMarketsCache = Record; - -/** - * Helper: Safely parse JSON from localStorage - */ -function _safeParseJSON(value: string | null, defaultValue: T): T { - if (!value) return defaultValue; - try { - return JSON.parse(value) as T; - } catch { - return defaultValue; - } -} - -/** - * Helper: Get localStorage value or default - */ -function getStorageValue(key: string, defaultValue: T): T { - const value = storage.getItem(key); - if (value === null) return defaultValue; - - // If default is a boolean/number, parse accordingly - if (typeof defaultValue === 'boolean') { - return (value === 'true') as T; - } - if (typeof defaultValue === 'number') { - const parsed = Number(value); - return (Number.isNaN(parsed) ? defaultValue : parsed) as T; - } - - // Try JSON parse - try { - return JSON.parse(value) as T; - } catch { - return value as T; - } -} - -export function StorageMigrator() { - useEffect(() => { - // Skip if already completed - if (isMigrationComplete()) { - console.log('โœ… Storage migrations already completed - skipping'); - return; - } - - // Skip if expired (with warning) - if (isMigrationExpired()) { - return; - } - - // Define all migrations - const migrations: MigrationDefinition[] = [ - // ======================================== - // Migration 1: Trusted Vaults - // ======================================== - { - oldKey: 'userTrustedVaults', - newKey: 'monarch_store_trustedVaults', - storeName: 'useTrustedVaults', - description: 'Migrated Trusted Vaults to Zustand store', - validate: (data): data is TrustedVault[] => { - if (!Array.isArray(data)) return false; - return data.every( - (item) => - item && - typeof item === 'object' && - 'address' in item && - 'chainId' in item && - typeof item.address === 'string' && - typeof item.chainId === 'number', - ); - }, - migrate: (oldData: TrustedVault[]) => { - try { - const store = useTrustedVaults.getState(); - store.setVaults(oldData); - return true; - } catch (error) { - console.error('Error migrating trusted vaults:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 2: Market Preferences (MULTI-KEY) - // ======================================== - { - oldKey: 'monarch_marketsSortColumn', // Primary key to check - newKey: 'monarch_store_marketPreferences', - storeName: 'useMarketPreferences', - description: 'Migrated Market Preferences to Zustand store (15 keys)', - // No validate function - we handle all keys ourselves - migrate: () => { - try { - // Read all old market preference keys - const sortColumn = getStorageValue('monarch_marketsSortColumn', SortColumn.Supply); - const sortDirection = getStorageValue('monarch_marketsSortDirection', -1); - const entriesPerPage = getStorageValue('monarch_marketsEntriesPerPage', 8); - const includeUnknownTokens = getStorageValue('includeUnknownTokens', false); - const showUnknownOracle = getStorageValue('showUnknownOracle', false); - const trustedVaultsOnly = getStorageValue('monarch_marketsTrustedVaultsOnly', false); - const columnVisibility = getStorageValue('monarch_marketsColumnVisibility', DEFAULT_COLUMN_VISIBILITY); - const tableViewMode = getStorageValue('monarch_marketsTableViewMode', 'compact'); - const usdMinSupply = getStorageValue('monarch_marketsUsdMinSupply_2', DEFAULT_MIN_SUPPLY_USD.toString()); - const usdMinBorrow = getStorageValue('monarch_marketsUsdMinBorrow', ''); - const usdMinLiquidity = getStorageValue('monarch_marketsUsdMinLiquidity', ''); - const minSupplyEnabled = getStorageValue('monarch_minSupplyEnabled', false); - const minBorrowEnabled = getStorageValue('monarch_minBorrowEnabled', false); - const minLiquidityEnabled = getStorageValue('monarch_minLiquidityEnabled', false); - const starredMarkets = getStorageValue('monarch_marketsFavorites', []); - - // Write to store - const store = useMarketPreferences.getState(); - store.setAll({ - sortColumn, - sortDirection, - entriesPerPage, - includeUnknownTokens, - showUnknownOracle, - trustedVaultsOnly, - columnVisibility, - tableViewMode, - usdMinSupply, - usdMinBorrow, - usdMinLiquidity, - minSupplyEnabled, - minBorrowEnabled, - minLiquidityEnabled, - starredMarkets, - }); - - return true; - } catch (error) { - console.error('Error migrating market preferences:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 3: App Settings (MULTI-KEY) - // ======================================== - { - oldKey: 'usePermit2', // Primary key to check - newKey: 'monarch_store_appSettings', - storeName: 'useAppSettings', - description: 'Migrated App Settings to Zustand store (5 keys)', - // No validate function - we handle all keys ourselves - migrate: () => { - try { - // Read all old app setting keys - const usePermit2 = getStorageValue('usePermit2', true); - const useEth = getStorageValue('useEth', false); - const showUnwhitelistedMarkets = getStorageValue('showUnwhitelistedMarkets', false); - const showFullRewardAPY = getStorageValue('showFullRewardAPY', false); - const isAprDisplay = getStorageValue('settings-apr-display', false); - - // Write to store - const store = useAppSettings.getState(); - store.setAll({ - usePermit2, - useEth, - showUnwhitelistedMarkets, - showFullRewardAPY, - isAprDisplay, - }); - - return true; - } catch (error) { - console.error('Error migrating app settings:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 4: History Preferences (MULTI-KEY) - // ======================================== - { - oldKey: 'monarch_historyEntriesPerPage', // Primary key to check - newKey: 'monarch_store_historyPreferences', - storeName: 'useHistoryPreferences', - description: 'Migrated History Preferences to Zustand store (2 keys)', - // No validate function - we handle all keys ourselves - migrate: () => { - try { - // Read all old history preference keys - const entriesPerPage = getStorageValue('monarch_historyEntriesPerPage', 10); - const isGroupedView = getStorageValue('monarch_historyGroupedView', true); - - // Write to store - const store = useHistoryPreferences.getState(); - store.setAll({ - entriesPerPage, - isGroupedView, - }); - - return true; - } catch (error) { - console.error('Error migrating history preferences:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 5: Positions Preferences - // ======================================== - { - oldKey: 'positions:show-collateral-exposure', - newKey: 'monarch_store_positionsPreferences', - storeName: 'usePositionsPreferences', - description: 'Migrated Positions Preferences to Zustand store', - // No validate function - we handle it ourselves - migrate: () => { - try { - const showCollateralExposure = getStorageValue('positions:show-collateral-exposure', true); - - const store = usePositionsPreferences.getState(); - store.setShowCollateralExposure(showCollateralExposure); - - return true; - } catch (error) { - console.error('Error migrating positions preferences:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 6: Blacklisted Markets - // ======================================== - { - oldKey: 'customBlacklistedMarkets', - newKey: 'monarch_store_blacklistedMarkets', - storeName: 'useBlacklistedMarkets', - description: 'Migrated Blacklisted Markets to Zustand store', - validate: (data): data is BlacklistedMarket[] => { - if (!Array.isArray(data)) return false; - return data.every( - (item) => - item && - typeof item === 'object' && - 'uniqueKey' in item && - 'chainId' in item && - 'addedAt' in item && - typeof item.uniqueKey === 'string' && - typeof item.chainId === 'number' && - typeof item.addedAt === 'number', - ); - }, - migrate: (oldData: BlacklistedMarket[]) => { - try { - const store = useBlacklistedMarkets.getState(); - store.setAll({ customBlacklistedMarkets: oldData }); - return true; - } catch (error) { - console.error('Error migrating blacklisted markets:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 7: Custom RPC URLs - // ======================================== - { - oldKey: 'customRpcUrls', - newKey: 'monarch_store_customRpc', - storeName: 'useCustomRpc', - description: 'Migrated Custom RPC URLs to Zustand store', - validate: (data): data is CustomRpcUrls => { - return typeof data === 'object' && data !== null; - }, - migrate: (oldData: CustomRpcUrls) => { - try { - const store = useCustomRpc.getState(); - store.setAll({ customRpcUrls: oldData }); - return true; - } catch (error) { - console.error('Error migrating custom RPC URLs:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 8: Transaction Filters - // ======================================== - { - oldKey: 'monarch_transaction_filters_v2', - newKey: 'monarch_store_transactionFilters', - storeName: 'useTransactionFilters', - description: 'Migrated Transaction Filters to Zustand store', - validate: (data): data is SymbolFilters => { - return typeof data === 'object' && data !== null; - }, - migrate: (oldData: SymbolFilters) => { - try { - const store = useTransactionFiltersStore.getState(); - store.setAll({ filters: oldData }); - return true; - } catch (error) { - console.error('Error migrating transaction filters:', error); - return false; - } - }, - }, - - // ======================================== - // Migration 9: User Markets Cache - // ======================================== - { - oldKey: 'monarch_cache_market_unique_keys', - newKey: 'monarch_store_userMarketsCache', - storeName: 'useUserMarketsCache', - description: 'Migrated User Markets Cache to Zustand store', - validate: (data): data is UserMarketsCache => { - return typeof data === 'object' && data !== null; - }, - migrate: (oldData: UserMarketsCache) => { - try { - const store = useUserMarketsCacheStore.getState(); - store.setAll({ cache: oldData }); - return true; - } catch (error) { - console.error('Error migrating user markets cache:', error); - return false; - } - }, - }, - ]; - - // Execute all migrations - const results = executeAllMigrations(migrations); - - // Clean up ALL old localStorage keys after migrations - // (executeMigration only deletes the primary oldKey, not related keys) - const anySuccess = results.some((r) => r.status === 'success'); - if (anySuccess || results.every((r) => r.status === 'skipped')) { - console.log('๐Ÿงน Cleaning up old localStorage keys...'); - - // Market Preferences keys - const marketKeys = [ - 'monarch_marketsSortDirection', - 'monarch_marketsEntriesPerPage', - 'includeUnknownTokens', - 'showUnknownOracle', - 'monarch_marketsTrustedVaultsOnly', - 'monarch_marketsColumnVisibility', - 'monarch_marketsTableViewMode', - 'monarch_marketsUsdMinSupply_2', - 'monarch_marketsUsdMinBorrow', - 'monarch_marketsUsdMinLiquidity', - 'monarch_minSupplyEnabled', - 'monarch_minBorrowEnabled', - 'monarch_minLiquidityEnabled', - 'monarch_marketsFavorites', - ]; - - // App Settings keys - const appKeys = ['useEth', 'showUnwhitelistedMarkets', 'showFullRewardAPY', 'settings-apr-display']; - - // History Preferences keys - const historyKeys = ['monarch_historyGroupedView']; - - // Delete all secondary keys (primary keys already deleted by executeMigration) - [...marketKeys, ...appKeys, ...historyKeys].forEach((key) => { - storage.removeItem(key); - }); - - console.log('โœ… Cleanup complete'); - markMigrationComplete(); - } - }, []); // Run once on mount - - // Render nothing - return null; -} diff --git a/src/data-sources/morpho-api/liquidations.ts b/src/data-sources/morpho-api/liquidations.ts deleted file mode 100644 index 1fa0ef33..00000000 --- a/src/data-sources/morpho-api/liquidations.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @deprecated_after_monarch_api_stable - * This fetcher is kept as a fallback while Monarch Metrics API is being validated. - * Used by useLiquidationsQuery.ts which is also deprecated. - * - * Once the Monarch API is confirmed stable, this file can be removed. - * See useLiquidationsQuery.ts for the full list of related files. - */ -import type { SupportedNetworks } from '@/utils/networks'; -import { URLS } from '@/utils/urls'; - -const liquidationsQuery = ` - query getLiquidations($first: Int, $skip: Int, $chainId: Int!) { - transactions( - where: { type_in: [MarketLiquidation], chainId_in: [$chainId] } - first: $first - skip: $skip - ) { - items { - data { - ... on MarketLiquidationTransactionData { - market { - uniqueKey - } - } - } - } - pageInfo { - countTotal - count - limit - skip - } - } - } -`; - -type LiquidationTransactionItem = { - data: { - market?: { - uniqueKey: string; - }; - }; -}; - -type PageInfo = { - countTotal: number; - count: number; - limit: number; - skip: number; -}; - -type QueryResult = { - data: { - transactions: { - items: LiquidationTransactionItem[]; - pageInfo: PageInfo; - }; - }; - errors?: any[]; // Add optional errors field -}; - -export const fetchMorphoApiLiquidatedMarketKeys = async (network: SupportedNetworks): Promise> => { - const liquidatedKeys = new Set(); - let skip = 0; - const pageSize = 1000; - let totalCount = 0; - - try { - do { - const response = await fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: liquidationsQuery, - variables: { first: pageSize, skip, chainId: network }, // Pass chainId - }), - }); - - if (!response.ok) { - throw new Error(`Morpho API request failed with status ${response.status}`); - } - - const result = (await response.json()) as QueryResult; - - if (result.errors) { - console.error('GraphQL errors:', result.errors); - throw new Error(`GraphQL error fetching liquidations for network ${network}`); - } - - if (!result.data?.transactions) { - console.warn(`No transactions data found for network ${network} at skip ${skip}`); - break; - } - - const { items, pageInfo } = result.data.transactions; - - for (const tx of items) { - if (tx.data?.market?.uniqueKey) { - liquidatedKeys.add(tx.data.market.uniqueKey); - } - } - - totalCount = pageInfo.countTotal; - skip += pageInfo.count; - - if (pageInfo.count === 0 && skip < totalCount) break; - } while (skip < totalCount); - } catch (error) { - console.error(`Error fetching liquidations via Morpho API for network ${network}:`, error); - throw error; - } - - console.log(`[Morpho API] Fetched ${liquidatedKeys.size} liquidated market keys for ${network}`); - return liquidatedKeys; -}; diff --git a/src/data-sources/subgraph/liquidations.ts b/src/data-sources/subgraph/liquidations.ts deleted file mode 100644 index 7060d8bd..00000000 --- a/src/data-sources/subgraph/liquidations.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @deprecated_after_monarch_api_stable - * This fetcher is kept as a fallback while Monarch Metrics API is being validated. - * Used by useLiquidationsQuery.ts which is also deprecated. - * - * Once the Monarch API is confirmed stable, this file can be removed. - * See useLiquidationsQuery.ts for the full list of related files. - */ -import { subgraphMarketsWithLiquidationCheckQuery } from '@/graphql/morpho-subgraph-queries'; -import type { SupportedNetworks } from '@/utils/networks'; -import { getSubgraphUrl } from '@/utils/subgraph-urls'; -import { blacklistTokens } from '@/utils/tokens'; -import { subgraphGraphqlFetcher } from './fetchers'; - -// Define the expected structure of the response for the liquidation check query -type SubgraphMarketLiquidationCheck = { - id: string; // Market unique key - liquidates: { id: string }[]; // Array will be non-empty if liquidations exist -}; - -type SubgraphMarketsLiquidationCheckResponse = { - data: { - markets: SubgraphMarketLiquidationCheck[]; - }; - errors?: any[]; -}; - -export const fetchSubgraphLiquidatedMarketKeys = async (network: SupportedNetworks): Promise> => { - const subgraphApiUrl = getSubgraphUrl(network); - if (!subgraphApiUrl) { - console.warn(`Subgraph URL for network ${network} is not defined. Skipping liquidation check.`); - return new Set(); - } - - const liquidatedKeys = new Set(); - const pageSize = 1000; - let skip = 0; - while (true) { - const variables = { - first: pageSize, - skip, - where: { inputToken_not_in: blacklistTokens }, - }; - let markets: SubgraphMarketLiquidationCheck[] | undefined; - try { - const page = await subgraphGraphqlFetcher( - subgraphApiUrl, - subgraphMarketsWithLiquidationCheckQuery, - variables, - ); - - if (page.errors) { - console.error('GraphQL errors:', page.errors); - break; - } - - markets = page.data?.markets; - } catch (error) { - console.error(`Error fetching liquidated market keys from subgraph for network ${network}:`, error); - break; - } - - if (!markets) { - console.warn(`No market data returned for liquidation check on network ${network} at skip ${skip}.`); - break; // Exit loop if no markets are returned - } - - for (const market of markets) { - if (market.liquidates?.length > 0) { - liquidatedKeys.add(market.id); - } - } - - if (markets.length < pageSize) { - break; // Exit loop if the number of returned markets is less than the page size - } - skip += pageSize; - } - - console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Subgraph for ${network}.`); - return liquidatedKeys; -}; diff --git a/src/hooks/queries/useLiquidationsQuery.ts b/src/hooks/queries/useLiquidationsQuery.ts deleted file mode 100644 index 3db8f9bc..00000000 --- a/src/hooks/queries/useLiquidationsQuery.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @deprecated_after_monarch_api_stable - * This query is kept as a fallback while Monarch Metrics API is being validated. - * The primary source is now useEverLiquidated() in useMarketMetricsQuery.ts. - * - * Once the Monarch API is confirmed stable, this file and related data sources can be removed: - * - src/hooks/queries/useLiquidationsQuery.ts (this file) - * - src/data-sources/morpho-api/liquidations.ts - * - src/data-sources/subgraph/liquidations.ts - * - * Note: useMarketLiquidations.ts (detailed transactions) is SEPARATE and should be kept. - */ -import { useQuery } from '@tanstack/react-query'; -import { supportsMorphoApi } from '@/config/dataSources'; -import { fetchMorphoApiLiquidatedMarketKeys } from '@/data-sources/morpho-api/liquidations'; -import { fetchSubgraphLiquidatedMarketKeys } from '@/data-sources/subgraph/liquidations'; -import { ALL_SUPPORTED_NETWORKS } from '@/utils/networks'; - -export const useLiquidationsQuery = (options: { enabled?: boolean } = {}) => { - const { enabled = true } = options; - - return useQuery({ - queryKey: ['liquidations'], - enabled, - queryFn: async () => { - const combinedLiquidatedKeys = new Set(); - const fetchErrors: unknown[] = []; - - await Promise.all( - ALL_SUPPORTED_NETWORKS.map(async (network) => { - try { - let networkLiquidatedKeys: Set; - let trySubgraph = false; - - if (supportsMorphoApi(network)) { - try { - console.log(`Attempting to fetch liquidated markets via Morpho API for ${network}`); - networkLiquidatedKeys = await fetchMorphoApiLiquidatedMarketKeys(network); - } catch (morphoError) { - console.error('Failed to fetch liquidated markets via Morpho API:', morphoError); - networkLiquidatedKeys = new Set(); - trySubgraph = true; - } - } else { - networkLiquidatedKeys = new Set(); - trySubgraph = true; - } - - if (trySubgraph) { - try { - console.log(`Attempting to fetch liquidated markets via Subgraph for ${network}`); - networkLiquidatedKeys = await fetchSubgraphLiquidatedMarketKeys(network); - } catch (subgraphError) { - console.error('Failed to fetch liquidated markets via Subgraph:', subgraphError); - throw subgraphError; - } - } - - for (const key of networkLiquidatedKeys) { - combinedLiquidatedKeys.add(key); - } - } catch (networkError) { - console.error(`Failed to fetch liquidated markets for network ${network}:`, networkError); - fetchErrors.push(networkError); - } - }), - ); - - if (fetchErrors.length > 0) { - console.warn(`Failed to fetch liquidations from ${fetchErrors.length} network(s)`, fetchErrors[0]); - } - - return combinedLiquidatedKeys; - }, - staleTime: 10 * 60 * 1000, - refetchInterval: 10 * 60 * 1000, - refetchOnWindowFocus: true, - }); -}; diff --git a/src/hooks/queries/useMarketMetricsQuery.ts b/src/hooks/queries/useMarketMetricsQuery.ts index 47fd324f..42e8c456 100644 --- a/src/hooks/queries/useMarketMetricsQuery.ts +++ b/src/hooks/queries/useMarketMetricsQuery.ts @@ -268,16 +268,11 @@ export const useTrendingMarketKeys = () => { /** * Returns whether a market has ever been liquidated. - * Primary: Uses Monarch API /v1/liquidations endpoint - * Fallback: Uses old Morpho API/Subgraph if Monarch data is stale (>2 hours) - * - * @deprecated_fallback The fallback to useLiquidationsQuery can be removed - * once Monarch API stability is confirmed. + * Uses everLiquidated field from Monarch API market metrics. */ export const useEverLiquidated = (chainId: number, uniqueKey: string): boolean => { const { metricsMap } = useMarketMetricsMap(); - - + return useMemo(() => { const key = `${chainId}-${uniqueKey.toLowerCase()}`; const metrics = metricsMap.get(key); diff --git a/src/hooks/queries/useMonarchLiquidationsQuery.ts b/src/hooks/queries/useMonarchLiquidationsQuery.ts deleted file mode 100644 index 27091439..00000000 --- a/src/hooks/queries/useMonarchLiquidationsQuery.ts +++ /dev/null @@ -1,29 +0,0 @@ - -import { useQuery } from '@tanstack/react-query'; - -/** - * Response from Monarch API /v1/liquidations endpoint. - */ -export type MonarchLiquidationsResponse = { - count: number; - lastUpdatedAt: number; // Unix timestamp (seconds) - markets: Array<{ marketUniqueKey: string; chainId: number }>; -}; - -/** - * Fetches liquidated market data from Monarch API. - * Returns all markets that have ever had a liquidation event. - */ -export const useMonarchLiquidationsQuery = () => { - return useQuery({ - queryKey: ['monarch-liquidations'], - queryFn: async (): Promise => { - const response = await fetch('/api/monarch/liquidations'); - if (!response.ok) throw new Error('Failed to fetch liquidations from Monarch API'); - return response.json(); - }, - staleTime: 5 * 60 * 1000, // 5 min - refetchInterval: 5 * 60 * 1000, - refetchOnWindowFocus: false, - }); -}; diff --git a/src/utils/storage-migration.ts b/src/utils/storage-migration.ts deleted file mode 100644 index caadda55..00000000 --- a/src/utils/storage-migration.ts +++ /dev/null @@ -1,264 +0,0 @@ -import storage from 'local-storage-fallback'; - -/** - * One-time storage migration system - * - * Purpose: Migrate from useLocalStorage to Zustand stores while preserving user data - * - * Timeline: - * - Expiry: February 1, 2026 - * - Delete after: February 2026 (1 month grace period) - * - * Safety features: - * - Idempotent (safe to run multiple times) - * - Validates data before migrating - * - Logs all actions for debugging - * - Never deletes old data until migration succeeds - * - Auto-disables after expiry date - */ - -// Migration configuration -export const MIGRATION_STATUS_KEY = 'monarch_migration_v1_complete'; -export const MIGRATION_EXPIRY_DATE = '2026-02-01'; -export const MIGRATION_VERSION = 'v1'; - -/** - * Migration definition for a single localStorage key โ†’ Zustand store - */ -export type MigrationDefinition = { - /** Old localStorage key to migrate from */ - oldKey: string; - - /** New Zustand persist key (MUST be different from oldKey!) */ - newKey: string; - - /** Zustand store name (for logging) */ - storeName: string; - - /** - * Migration function that receives old data and store instance - * Returns true if migration succeeded, false if skipped - */ - migrate: (oldData: T) => boolean; - - /** - * Optional validation function - * Returns true if data is valid and should be migrated - */ - validate?: (data: unknown) => data is T; - - /** - * Optional description for logging - */ - description?: string; -}; - -/** - * Result of a single migration attempt - */ -export type MigrationResult = { - oldKey: string; - storeName: string; - status: 'success' | 'skipped' | 'error'; - reason?: string; - error?: unknown; -}; - -/** - * Check if migrations have already been completed - */ -export function isMigrationComplete(): boolean { - try { - const status = storage.getItem(MIGRATION_STATUS_KEY); - return status === 'true'; - } catch (error) { - console.warn('Error checking migration status:', error); - return false; - } -} - -/** - * Mark migrations as complete - */ -export function markMigrationComplete(): void { - try { - storage.setItem(MIGRATION_STATUS_KEY, 'true'); - console.log(`โœ… Migration ${MIGRATION_VERSION} marked as complete`); - } catch (error) { - console.error('Error marking migration complete:', error); - } -} - -/** - * Check if migration component has expired - * If expired, logs warning and suggests removal - */ -export function isMigrationExpired(): boolean { - const now = new Date(); - const expiry = new Date(MIGRATION_EXPIRY_DATE); - - if (now > expiry) { - console.warn( - `โš ๏ธ MIGRATION COMPONENT EXPIRED (${MIGRATION_EXPIRY_DATE})\n` + - ' Please remove StorageMigrator component from codebase:\n' + - ' 1. Remove from app/layout.tsx\n' + - ' 2. Delete src/components/StorageMigrator.tsx\n' + - ' 3. Delete src/utils/storage-migration.ts', - ); - return true; - } - - return false; -} - -/** - * Execute a single migration - */ -export function executeMigration(definition: MigrationDefinition): MigrationResult { - const { oldKey, newKey, storeName, migrate, validate, description } = definition; - - try { - // CRITICAL: Verify old and new keys are different! - if (oldKey === newKey) { - return { - oldKey, - storeName, - status: 'error', - reason: 'CRITICAL: oldKey and newKey must be different to prevent data loss!', - error: new Error('oldKey === newKey'), - }; - } - - // Check if old data exists - const oldDataRaw = storage.getItem(oldKey); - - if (!oldDataRaw) { - return { - oldKey, - storeName, - status: 'skipped', - reason: 'No data found in old localStorage key', - }; - } - - // Check if new key already has data (migration already happened?) - const newDataRaw = storage.getItem(newKey); - if (newDataRaw) { - console.log(`โš ๏ธ New key "${newKey}" already has data - skipping migration`); - // Don't delete old data yet - maybe user manually migrated - return { - oldKey, - storeName, - status: 'skipped', - reason: 'New key already has data (migration already completed?)', - }; - } - - // Parse old data - let oldData: unknown; - try { - oldData = JSON.parse(oldDataRaw); - } catch (parseError) { - return { - oldKey, - storeName, - status: 'error', - reason: 'Failed to parse JSON from old key', - error: parseError, - }; - } - - // Validate if validation function provided - if (validate && !validate(oldData)) { - return { - oldKey, - storeName, - status: 'skipped', - reason: 'Data validation failed', - }; - } - - // Execute migration (writes to new key via Zustand) - const success = migrate(oldData as T); - - if (!success) { - return { - oldKey, - storeName, - status: 'skipped', - reason: 'Migration function returned false', - }; - } - - // Verify new data was written - const verifyNewData = storage.getItem(newKey); - if (!verifyNewData) { - return { - oldKey, - storeName, - status: 'error', - reason: 'Migration function succeeded but new key has no data!', - error: new Error('New key is empty after migration'), - }; - } - - // Migration succeeded AND verified - NOW it's safe to remove old data - storage.removeItem(oldKey); - - return { - oldKey, - storeName, - status: 'success', - reason: description, - }; - } catch (error) { - return { - oldKey, - storeName, - status: 'error', - reason: 'Unexpected error during migration', - error, - }; - } -} - -/** - * Execute all migrations and return results - */ -export function executeAllMigrations(migrations: MigrationDefinition[]): MigrationResult[] { - console.log(`๐Ÿš€ Starting storage migration ${MIGRATION_VERSION}...`); - console.log(`๐Ÿ“ฆ ${migrations.length} migrations to process`); - - const results: MigrationResult[] = []; - - for (const migration of migrations) { - const result = executeMigration(migration); - results.push(result); - - // Log result - const emoji = result.status === 'success' ? 'โœ…' : result.status === 'error' ? 'โŒ' : 'โญ๏ธ'; - const message = `${emoji} ${migration.oldKey} โ†’ ${migration.storeName}`; - - if (result.status === 'success') { - console.log(message, result.reason ? `(${result.reason})` : ''); - } else if (result.status === 'error') { - console.error(message, result.reason, result.error); - } else { - console.log(message, `(${result.reason})`); - } - } - - // Summary - const successful = results.filter((r) => r.status === 'success').length; - const skipped = results.filter((r) => r.status === 'skipped').length; - const errors = results.filter((r) => r.status === 'error').length; - - console.log( - `\n๐Ÿ“Š Migration ${MIGRATION_VERSION} Summary:\n` + - ` โœ… Successful: ${successful}\n` + - ` โญ๏ธ Skipped: ${skipped}\n` + - ` โŒ Errors: ${errors}\n`, - ); - - return results; -} From 01ded6818c4c7eca42163c96311f90a6518af3a9 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 15:32:57 +0800 Subject: [PATCH 2/2] fix: reduce Monarch API call frequency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMarketMetricsQuery: refetchInterval 1min โ†’ 5min (match staleTime) - useMarketData: restore 30s staleTime for accuracy on market views - docs: update TECHNICAL_OVERVIEW with correct hooks and timings - Add useMarketMetricsQuery (5 min stale) - Fix useLiquidationsQuery โ†’ useMarketLiquidations - Correct stale times to match code --- docs/TECHNICAL_OVERVIEW.md | 6 ++++-- src/hooks/queries/useMarketMetricsQuery.ts | 2 +- src/hooks/useMarketData.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/TECHNICAL_OVERVIEW.md b/docs/TECHNICAL_OVERVIEW.md index 112bca11..fd58f567 100644 --- a/docs/TECHNICAL_OVERVIEW.md +++ b/docs/TECHNICAL_OVERVIEW.md @@ -146,6 +146,7 @@ Fallback: Subgraph (The Graph / Goldsky) | Data Type | Source | Refresh | Query Hook | |-----------|--------|---------|------------| | Markets list | Morpho API/Subgraph | 5 min stale | `useMarketsQuery` | +| Market metrics (flows, trending) | Monarch API | 5 min stale | `useMarketMetricsQuery` | | Market state (APY, utilization) | Morpho API | 30s stale | `useMarketData` | | User positions | Morpho API + on-chain | 5 min | `useUserPositions` | | Vaults list | Morpho API | 5 min | `useAllMorphoVaultsQuery` | @@ -153,7 +154,7 @@ Fallback: Subgraph (The Graph / Goldsky) | Token balances | On-chain multicall | 5 min | `useUserBalancesQuery` | | Oracle prices | Morpho API | 5 min | `useOracleDataQuery` | | Merkl rewards | Merkl API | On demand | `useMerklCampaignsQuery` | -| Liquidations | Morpho API | On demand | `useLiquidationsQuery` | +| Market liquidations | Morpho API/Subgraph | 5 min stale | `useMarketLiquidations` | ### Data Flow Patterns @@ -225,11 +226,12 @@ All hooks in `/src/hooks/queries/` follow React Query patterns: | Hook | Key | Stale Time | Refetch | Focus | |------|-----|------------|---------|-------| | `useMarketsQuery` | `['markets']` | 5 min | 5 min | Yes | +| `useMarketMetricsQuery` | `['market-metrics', ...]` | 5 min | 5 min | No | | `useTokensQuery` | `['tokens']` | 5 min | 5 min | Yes | | `useOracleDataQuery` | `['oracle-data']` | 5 min | 5 min | Yes | | `useUserBalancesQuery` | `['user-balances', addr, networks]` | 30s | - | Yes | | `useUserVaultsV2Query` | `['user-vaults-v2', addr]` | 60s | - | Yes | -| `useLiquidationsQuery` | `['liquidations']` | 10 min | 10 min | Yes | +| `useMarketLiquidations` | `['marketLiquidations', id, net]` | 5 min | - | Yes | | `useUserTransactionsQuery` | `['user-transactions', ...]` | 60s | - | No | | `useAllocationsQuery` | `['vault-allocations', ...]` | 30s | - | No | diff --git a/src/hooks/queries/useMarketMetricsQuery.ts b/src/hooks/queries/useMarketMetricsQuery.ts index 42e8c456..b49b9b9f 100644 --- a/src/hooks/queries/useMarketMetricsQuery.ts +++ b/src/hooks/queries/useMarketMetricsQuery.ts @@ -147,7 +147,7 @@ export const useMarketMetricsQuery = (params: MarketMetricsParams = {}) => { queryKey: ['market-metrics', { chainId, sortBy, sortOrder }], queryFn: () => fetchAllMarketMetrics({ chainId, sortBy, sortOrder }), staleTime: 5 * 60 * 1000, // 5 minutes - matches API update frequency - refetchInterval: 1 * 60 * 1000, + refetchInterval: 5 * 60 * 1000, // Match staleTime - no point refetching more often refetchOnWindowFocus: false, // Don't refetch on focus since data is slow-changing enabled, }); diff --git a/src/hooks/useMarketData.ts b/src/hooks/useMarketData.ts index d1f1f21e..4aa1c398 100644 --- a/src/hooks/useMarketData.ts +++ b/src/hooks/useMarketData.ts @@ -92,8 +92,8 @@ export const useMarketData = (uniqueKey: string | undefined, network: SupportedN return finalMarket; }, enabled: !!uniqueKey && !!network, - staleTime: 1000 * 60 * 2, - refetchInterval: 1000 * 60 * 2, // Match staleTime for consistency + staleTime: 30_000, // 30 seconds - individual market view needs accuracy + refetchInterval: 30_000, // Match staleTime for consistency placeholderData: (previousData) => previousData ?? null, retry: 1, });