From 77a7ba7e1cac5292a0c4686dfd0ab8ef1b880991 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 28 Dec 2025 00:53:15 +0800 Subject: [PATCH 1/3] refactor: liquidation context --- src/components/providers/ClientProviders.tsx | 9 +- src/contexts/LiquidationsContext.tsx | 41 --------- .../markets/components/market-indicators.tsx | 8 +- .../useLiquidationsQuery.ts} | 87 ++++++++----------- 4 files changed, 42 insertions(+), 103 deletions(-) delete mode 100644 src/contexts/LiquidationsContext.tsx rename src/hooks/{useLiquidations.ts => queries/useLiquidationsQuery.ts} (56%) diff --git a/src/components/providers/ClientProviders.tsx b/src/components/providers/ClientProviders.tsx index ee06c85f..bd553d6a 100644 --- a/src/components/providers/ClientProviders.tsx +++ b/src/components/providers/ClientProviders.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react'; import { GlobalModalProvider } from '@/contexts/GlobalModalContext'; -import { LiquidationsProvider } from '@/contexts/LiquidationsContext'; import { MerklCampaignsProvider } from '@/contexts/MerklCampaignsContext'; import { OracleDataProvider } from '@/contexts/OracleDataContext'; import { OnboardingProvider } from '@/features/positions/components/onboarding/onboarding-context'; @@ -17,11 +16,9 @@ export function ClientProviders({ children }: ClientProvidersProps) { - - - {children} - - + + {children} + diff --git a/src/contexts/LiquidationsContext.tsx b/src/contexts/LiquidationsContext.tsx deleted file mode 100644 index 66680819..00000000 --- a/src/contexts/LiquidationsContext.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { createContext, useContext, type ReactNode, useMemo } from 'react'; -import useLiquidations from '@/hooks/useLiquidations'; - -type LiquidationsContextType = { - isProtectedByLiquidationBots: (marketId: string) => boolean; - loading: boolean; - error: unknown | null; - refetch: () => Promise; -}; - -const LiquidationsContext = createContext(undefined); - -type LiquidationsProviderProps = { - children: ReactNode; -}; - -export function LiquidationsProvider({ children }: LiquidationsProviderProps) { - const { liquidatedMarketKeys, loading, error, refetch } = useLiquidations(); - - const value = useMemo( - () => ({ - isProtectedByLiquidationBots: (marketId: string) => liquidatedMarketKeys.has(marketId), - loading, - error, - refetch, - }), - [liquidatedMarketKeys, loading, error, refetch], - ); - - return {children}; -} - -export function useLiquidationsContext() { - const context = useContext(LiquidationsContext); - if (context === undefined) { - throw new Error('useLiquidationsContext must be used within a LiquidationsProvider'); - } - return context; -} diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 219858de..013fbb7d 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -2,7 +2,7 @@ import { Tooltip } from '@/components/ui/tooltip'; import { FaShieldAlt, FaStar, FaUser } from 'react-icons/fa'; import { FiAlertCircle } from 'react-icons/fi'; import { TooltipContent } from '@/components/shared/tooltip-content'; -import { useLiquidationsContext } from '@/contexts/LiquidationsContext'; +import { useLiquidationsQuery } from '@/hooks/queries/useLiquidationsQuery'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import type { Market } from '@/utils/types'; import { RewardsIndicator } from '@/features/markets/components/rewards-indicator'; @@ -17,9 +17,9 @@ type MarketIndicatorsProps = { }; export function MarketIndicators({ market, showRisk = false, isStared = false, hasUserPosition = false }: MarketIndicatorsProps) { - // Check liquidation protection status on-demand (like Merkl rewards pattern) - const { isProtectedByLiquidationBots } = useLiquidationsContext(); - const hasLiquidationProtection = isProtectedByLiquidationBots(market.uniqueKey); + // Check liquidation protection status using React Query + const { data: liquidatedMarkets } = useLiquidationsQuery(); + const hasLiquidationProtection = liquidatedMarkets?.has(market.uniqueKey) ?? false; // Compute risk warnings if needed const warnings = showRisk ? computeMarketWarnings(market, true) : []; diff --git a/src/hooks/useLiquidations.ts b/src/hooks/queries/useLiquidationsQuery.ts similarity index 56% rename from src/hooks/useLiquidations.ts rename to src/hooks/queries/useLiquidationsQuery.ts index 5c436200..747c6f07 100644 --- a/src/hooks/useLiquidations.ts +++ b/src/hooks/queries/useLiquidationsQuery.ts @@ -1,29 +1,35 @@ -import { useState, useEffect, useCallback } from 'react'; +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'; -const useLiquidations = () => { - const [loading, setLoading] = useState(true); - const [isRefetching, setIsRefetching] = useState(false); - const [liquidatedMarketKeys, setLiquidatedMarketKeys] = useState>(new Set()); - const [error, setError] = useState(null); +/** + * Fetches liquidated market IDs from all supported networks using React Query. + * + * Data fetching strategy: + * - Tries Morpho API first (if supported) + * - Falls back to Subgraph if API fails + * - Combines liquidated market keys from all networks + * + * Cache behavior: + * - staleTime: 10 minutes (data considered fresh) + * - Auto-refetch: Every 10 minutes in background + * - Refetch on window focus: enabled + * + * @example + * ```tsx + * const { data, isLoading, refetch } = useLiquidationsQuery(); + * const isProtected = data?.has(marketId) ?? false; + * ``` + */ +export const useLiquidationsQuery = () => { + return useQuery({ + queryKey: ['liquidations'], + queryFn: async () => { + const combinedLiquidatedKeys = new Set(); + const fetchErrors: unknown[] = []; - const fetchLiquidations = useCallback(async (isRefetch = false) => { - if (isRefetch) { - setIsRefetching(true); - } else { - setLoading(true); - } - setError(null); // Reset error - - // Define the networks to check for liquidations - - const combinedLiquidatedKeys = new Set(); - const fetchErrors: unknown[] = []; - - try { await Promise.all( ALL_SUPPORTED_NETWORKS.map(async (network) => { try { @@ -37,7 +43,6 @@ const useLiquidations = () => { networkLiquidatedKeys = await fetchMorphoApiLiquidatedMarketKeys(network); } catch (morphoError) { console.error('Failed to fetch liquidated markets via Morpho API:', morphoError); - // Continue to Subgraph fallback networkLiquidatedKeys = new Set(); trySubgraph = true; } @@ -53,7 +58,7 @@ const useLiquidations = () => { networkLiquidatedKeys = await fetchSubgraphLiquidatedMarketKeys(network); } catch (subgraphError) { console.error('Failed to fetch liquidated markets via Subgraph:', subgraphError); - throw subgraphError; // Throw to be caught by outer catch + throw subgraphError; } } @@ -66,37 +71,15 @@ const useLiquidations = () => { }), ); - setLiquidatedMarketKeys(combinedLiquidatedKeys); - + // If any network fetch failed, log but still return what we got if (fetchErrors.length > 0) { - setError(fetchErrors[0]); + console.warn(`Failed to fetch liquidations from ${fetchErrors.length} network(s)`, fetchErrors[0]); } - } catch (err) { - console.error('Error fetching liquidated markets:', err); - setError(err); - } finally { - if (isRefetch) { - setIsRefetching(false); - } else { - setLoading(false); - } - } - }, []); - - useEffect(() => { - fetchLiquidations().catch((err) => { - console.error('Error in fetchLiquidations effect:', err); - // Explicitly catch and handle - prevents React error boundary from triggering - }); - }, [fetchLiquidations]); - return { - loading, - isRefetching, - liquidatedMarketKeys, - error, - refetch: async () => fetchLiquidations(true), - }; + return combinedLiquidatedKeys; + }, + staleTime: 10 * 60 * 1000, // Data is fresh for 10 minutes + refetchInterval: 10 * 60 * 1000, // Auto-refetch every 10 minutes + refetchOnWindowFocus: true, // Refetch when user returns to tab + }); }; - -export default useLiquidations; From e44ff841775878d3a476a393e7c010e73a239045 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 28 Dec 2025 00:55:28 +0800 Subject: [PATCH 2/3] chore: copy --- src/modals/wrap-process-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/wrap-process-modal.tsx b/src/modals/wrap-process-modal.tsx index e949850a..059b3b74 100644 --- a/src/modals/wrap-process-modal.tsx +++ b/src/modals/wrap-process-modal.tsx @@ -38,7 +38,7 @@ export function WrapProcessModal({ amount, currentStep, onOpenChange }: WrapProc backdrop="blur" > } /> From 8f6f7fd1d81d077a0c658fb52f53f9d2958640c1 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 28 Dec 2025 01:01:51 +0800 Subject: [PATCH 3/3] misc: fixes --- src/hooks/useTransactionWithToast.tsx | 17 +++++++++++++---- src/modals/wrap-process-modal.tsx | 1 - 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index b49f48d9..6c71b56e 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; @@ -36,6 +36,15 @@ export function useTransactionWithToast({ hash, }); + // Use a ref to store the latest onSuccess callback without it being in the dependency array + // This prevents infinite loops when the callback is recreated on every render + const onSuccessRef = useRef(onSuccess); + + // Update the ref whenever onSuccess changes + useEffect(() => { + onSuccessRef.current = onSuccess; + }, [onSuccess]); + const onClick = useCallback(() => { if (hash) { // if chainId is not supported, use 1 @@ -77,8 +86,8 @@ export function useTransactionWithToast({ onClick, closeButton: true, }); - if (onSuccess) { - onSuccess(); + if (onSuccessRef.current) { + onSuccessRef.current(); } } if (isError || txError) { @@ -96,7 +105,7 @@ export function useTransactionWithToast({ closeButton: true, }); } - }, [hash, isConfirmed, isError, txError, successText, successDescription, errorText, toastId, onClick, onSuccess]); + }, [hash, isConfirmed, isError, txError, successText, successDescription, errorText, toastId, onClick]); return { sendTransactionAsync, sendTransaction, isConfirming, isConfirmed }; } diff --git a/src/modals/wrap-process-modal.tsx b/src/modals/wrap-process-modal.tsx index 059b3b74..7bbaad47 100644 --- a/src/modals/wrap-process-modal.tsx +++ b/src/modals/wrap-process-modal.tsx @@ -4,7 +4,6 @@ import { FaCheckCircle, FaCircle } from 'react-icons/fa'; import { LuArrowRightLeft } from 'react-icons/lu'; import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import type { WrapStep } from '@/hooks/useWrapLegacyMorpho'; -import { formatBalance } from '@/utils/balance'; type WrapProcessModalProps = { amount: bigint;