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;
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 e949850a..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;
@@ -38,7 +37,7 @@ export function WrapProcessModal({ amount, currentStep, onOpenChange }: WrapProc
backdrop="blur"
>
}
/>