From 98dc764c92b6e056089a20128031137d412907b7 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 28 Mar 2025 01:07:49 +0800 Subject: [PATCH 1/3] refactor: refactor useQuery to avoid dup calculations --- .env.local.example | 5 +- app/api/block/route.ts | 44 ++- app/api/positions/historical/route.ts | 15 - package.json | 3 +- src/components/providers/ClientProviders.tsx | 34 +- src/hooks/useUserPositions.ts | 392 +++++++++++-------- src/hooks/useUserPositionsSummaryData.ts | 286 ++++++++------ src/utils/interest.ts | 2 - src/utils/positions.ts | 3 + yarn.lock | 42 +- 10 files changed, 509 insertions(+), 317 deletions(-) diff --git a/.env.local.example b/.env.local.example index c288e3af..15b545c8 100644 --- a/.env.local.example +++ b/.env.local.example @@ -2,4 +2,7 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_ID= NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="GET_ID_FROM_WALLET_CONNET" # See https://cloud.walletconnect.com ENVIRONMENT=localhost -NEXT_PUBLIC_ALCHEMY_API_KEY= \ No newline at end of file +NEXT_PUBLIC_ALCHEMY_API_KEY= + +# used for querying block with given timestamp +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/app/api/block/route.ts b/app/api/block/route.ts index d76e3491..35d4868c 100644 --- a/app/api/block/route.ts +++ b/app/api/block/route.ts @@ -4,6 +4,27 @@ import { SmartBlockFinder } from '@/utils/blockFinder'; import { SupportedNetworks } from '@/utils/networks'; import { mainnetClient, baseClient } from '@/utils/rpc'; +const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; + +async function getBlockFromEtherscan(timestamp: number, chainId: number): Promise { + try { + const response = await fetch( + `https://api.etherscan.io/v2/api?chainid=${chainId}&module=block&action=getblocknobytime×tamp=${timestamp}&closest=before&apikey=${ETHERSCAN_API_KEY}` + ); + + const data = await response.json(); + + if (data.status === '1' && data.message === 'OK') { + return parseInt(data.result); + } + + return null; + } catch (error) { + console.error('Etherscan API error:', error); + return null; + } +} + export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; @@ -18,16 +39,33 @@ export async function GET(request: NextRequest) { } const numericChainId = parseInt(chainId); + const numericTimestamp = parseInt(timestamp); + + // Fallback to SmartBlockFinder const client = numericChainId === SupportedNetworks.Mainnet ? mainnetClient : baseClient; + + // Try Etherscan API first + const etherscanBlock = await getBlockFromEtherscan(numericTimestamp, numericChainId); + if (etherscanBlock !== null) { + // For Etherscan results, we need to fetch the block to get its timestamp + const block = await client.getBlock({ blockNumber: BigInt(etherscanBlock) }); + + return NextResponse.json({ + blockNumber: Number(block.number), + timestamp: Number(block.timestamp), + }); + } else { + console.log('etherscanBlock is null', timestamp, chainId); + } + + if (!client) { return NextResponse.json({ error: 'Unsupported chain ID' }, { status: 400 }); } const finder = new SmartBlockFinder(client as any as PublicClient, numericChainId); - - console.log('GET functino trying to find nearest block', timestamp); - const block = await finder.findNearestBlock(parseInt(timestamp)); + const block = await finder.findNearestBlock(numericTimestamp); return NextResponse.json({ blockNumber: Number(block.number), diff --git a/app/api/positions/historical/route.ts b/app/api/positions/historical/route.ts index d0a607d7..c897ce73 100644 --- a/app/api/positions/historical/route.ts +++ b/app/api/positions/historical/route.ts @@ -143,13 +143,6 @@ export async function GET(request: NextRequest) { const userAddress = searchParams.get('userAddress'); const chainId = parseInt(searchParams.get('chainId') ?? '1'); - // console.log(`Historical position request:`, { - // blockNumber, - // marketId, - // userAddress, - // chainId, - // }); - if (!marketId || !userAddress || (!blockNumber && blockNumber !== 0)) { console.error('Missing required parameters:', { blockNumber: !!blockNumber, @@ -162,14 +155,6 @@ export async function GET(request: NextRequest) { // Get position data at the specified blockNumber const position = await getPositionAtBlock(marketId, userAddress, blockNumber, chainId); - // console.log(`Successfully retrieved historical position data:`, { - // blockNumber, - // marketId, - // userAddress, - // chainId, - // position, - // }); - return NextResponse.json({ position, }); diff --git a/package.json b/package.json index 466c1a00..c6ed05bc 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "@radix-ui/react-navigation-menu": "^1.1.4", "@rainbow-me/rainbowkit": "2", "@react-spring/web": "^9.7.3", - "@tanstack/react-query": "^5.20.1", + "@tanstack/react-query": "^5.69.0", + "@tanstack/react-query-devtools": "^5.69.0", "@types/react-table": "^7.7.20", "@uniswap/permit2-sdk": "^1.2.1", "abitype": "^0.10.3", diff --git a/src/components/providers/ClientProviders.tsx b/src/components/providers/ClientProviders.tsx index de5389a6..b4026c4a 100644 --- a/src/components/providers/ClientProviders.tsx +++ b/src/components/providers/ClientProviders.tsx @@ -1,26 +1,42 @@ 'use client'; import { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { MarketsProvider } from '@/contexts/MarketsContext'; import { OnboardingProvider } from 'app/positions/components/onboarding/OnboardingContext'; import { ConnectRedirectProvider } from './ConnectRedirectProvider'; import { ThemeProviders } from './ThemeProvider'; import { TokenProvider } from './TokenProvider'; +// Create a client with default configuration +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30000, // Default stale time of 30 seconds + retry: 2, + refetchOnWindowFocus: false, + }, + }, +}); + type ClientProvidersProps = { children: ReactNode; }; export function ClientProviders({ children }: ClientProvidersProps) { return ( - - - - - {children} - - - - + + + + + + {children} + + + + + + ); } diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 1d201e2c..bce3df53 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -1,182 +1,268 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { useState, useEffect, useCallback } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Address } from 'viem'; import { userPositionsQuery } from '@/graphql/queries'; import { SupportedNetworks } from '@/utils/networks'; -import { fetchPositionSnapshot } from '@/utils/positions'; -import { MarketPosition } from '@/utils/types'; +import { fetchPositionSnapshot, type PositionSnapshot } from '@/utils/positions'; +import { MarketPosition, Market } from '@/utils/types'; import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { useUserMarketsCache } from '../hooks/useUserMarketsCache'; import { useMarkets } from './useMarkets'; +import { useCallback } from 'react'; -const useUserPositions = (user: string | undefined, showEmpty = false) => { - const [loading, setLoading] = useState(true); - const [isRefetching, setIsRefetching] = useState(false); - const [data, setData] = useState([]); - const [positionsError, setPositionsError] = useState(null); +interface UserPositionsResponse { + marketPositions: MarketPosition[]; + usedMarkets: Array<{ + marketUniqueKey: string; + chainId: number; + }>; +} - const { markets } = useMarkets(); +interface MarketToFetch { + marketKey: string; + chainId: number; + market: Market; + existingState: PositionSnapshot | null; +} + +type EnhancedMarketPosition = { + state: PositionSnapshot; + market: Market & { warningsWithDetail: ReturnType }; +}; + +type SnapshotResult = { + market: Market; + state: PositionSnapshot | null; +} | null; + +type ValidMarketPosition = MarketPosition & { + market: Market & { + uniqueKey: string; + morphoBlue: { chain: { id: number } }; + }; +}; + +// Query keys for caching +export const positionKeys = { + all: ['positions'] as const, + user: (address: string) => [...positionKeys.all, address] as const, + snapshot: (marketKey: string, userAddress: string, chainId: number) => + [...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const, + enhanced: (user: string | undefined, data: UserPositionsResponse | undefined) => + ['enhanced-positions', user, data] as const +}; +const fetchUserPositions = async ( + user: string, + markets: Market[], + getUserMarkets: () => Array<{ marketUniqueKey: string; chainId: number }>, +): Promise => { + console.log('🔄 Fetching user positions for:', user); + + const [responseMainnet, responseBase] = await Promise.all([ + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Mainnet, + }, + }), + }), + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Base, + }, + }), + }), + ]); + + const [result1, result2] = await Promise.all([ + responseMainnet.json(), + responseBase.json(), + ]); + + console.log('📊 Received positions data from both networks'); + + const usedMarkets = getUserMarkets(); + const marketPositions: MarketPosition[] = []; + + // Collect positions + for (const result of [result1, result2]) { + if (result.data?.userByAddress) { + marketPositions.push( + ...(result.data.userByAddress.marketPositions as MarketPosition[]), + ); + } + } + + return { marketPositions, usedMarkets }; +}; + +const useUserPositions = (user: string | undefined, showEmpty = false) => { + const queryClient = useQueryClient(); + const { markets } = useMarkets(); const { getUserMarkets, batchAddUserMarkets } = useUserMarketsCache(); - const fetchData = useCallback( - async (isRefetch = false, onSuccess?: () => void) => { - if (!user) { - console.error('Missing user address'); - setLoading(false); - setIsRefetching(false); - return; - } + // Main query for user positions + const { + data: positionsData, + isLoading: isLoadingPositions, + isRefetching: isRefetchingPositions, + error: positionsError, + refetch: refetchPositions, + } = useQuery({ + queryKey: positionKeys.user(user ?? ''), + queryFn: async () => { + if (!user) throw new Error('Missing user address'); + return fetchUserPositions(user, markets, getUserMarkets); + }, + enabled: !!user, + staleTime: 30000, // Consider data fresh for 30 seconds + gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes + }); - try { - if (isRefetch) { - setIsRefetching(true); - } else { - setLoading(true); - } - - setPositionsError(null); - - // Fetch position data from both networks - const [responseMainnet, responseBase] = await Promise.all([ - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Mainnet, - }, - }), - }), - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Base, - }, - }), - }), - ]); - - const result1 = await responseMainnet.json(); - const result2 = await responseBase.json(); - - const unknownUsedMarkets = getUserMarkets(); - - const marketPositions: MarketPosition[] = []; - - // Collect positions - for (const result of [result1, result2]) { - if (result.data?.userByAddress) { - marketPositions.push( - ...(result.data.userByAddress.marketPositions as MarketPosition[]), - ); - } - } - - for (const market of unknownUsedMarkets) { - // check if they're already in the marketPositions array - if ( - marketPositions.find( - (position) => - position.market.uniqueKey.toLowerCase() === market.marketUniqueKey.toLowerCase() && - position.market.morphoBlue.chain.id === market.chainId, - ) - ) { - continue; - } + // Query for position snapshots + const { + data: enhancedPositions, + isLoading: isLoadingEnhanced, + isRefetching: isRefetchingEnhanced, + } = useQuery({ + queryKey: positionKeys.enhanced(user, positionsData), + queryFn: async () => { + if (!positionsData || !user) return []; + + console.log('🔄 Fetching position snapshots'); + + const { marketPositions, usedMarkets } = positionsData; - // skip markets we can't find - const marketWithDetails = markets.find((m) => m.uniqueKey === market.marketUniqueKey); - if (!marketWithDetails) { - continue; - } + // We need to fetch snapshots for ALL markets - both from API and used ones + const knownMarkets = marketPositions + .filter((position): position is ValidMarketPosition => + position.market !== undefined && + position.market.uniqueKey !== undefined && + position.market.morphoBlue?.chain?.id !== undefined + ) + .map((position): MarketToFetch => ({ + marketKey: position.market.uniqueKey, + chainId: position.market.morphoBlue.chain.id, + market: position.market, + existingState: position.state + })); - const currentSnapshot = await fetchPositionSnapshot( - market.marketUniqueKey, - user as Address, - market.chainId, - 0, + const marketsToRescan = usedMarkets + .filter(market => { + return !marketPositions.find( + position => + position.market?.uniqueKey?.toLowerCase() === market.marketUniqueKey.toLowerCase() && + position.market?.morphoBlue?.chain?.id === market.chainId ); - - if (currentSnapshot) { - marketPositions.push({ - market: marketWithDetails, - state: currentSnapshot, - }); + }) + .map(market => { + const marketWithDetails = markets.find((m) => + m.uniqueKey === market.marketUniqueKey && + m.morphoBlue?.chain?.id === market.chainId + ); + if (!marketWithDetails || !marketWithDetails.uniqueKey || !marketWithDetails.morphoBlue?.chain?.id) { + return null; } - } - - const enhancedPositions = await Promise.all( - marketPositions - .filter( - (position: MarketPosition) => - showEmpty || position.state.supplyShares.toString() !== '0', - ) - .map(async (position: MarketPosition) => { - // fetch real market position to be accurate - const currentSnapshot = await fetchPositionSnapshot( - position.market.uniqueKey, - user as Address, - position.market.morphoBlue.chain.id, - 0, - ); - - const accuratePositionState = currentSnapshot ? currentSnapshot : position.state; - - // Process positions and calculate earnings - return { - state: accuratePositionState, - market: { - ...position.market, - warningsWithDetail: getMarketWarningsWithDetail(position.market), - }, - }; - }), - ); - - setData(enhancedPositions); - - batchAddUserMarkets( - marketPositions.map((position) => ({ - marketUniqueKey: position.market.uniqueKey, - chainId: position.market.morphoBlue.chain.id, - })), - ); - - onSuccess?.(); - } catch (err) { - console.error('Error fetching positions:', err); - setPositionsError(err); - } finally { - setLoading(false); - setIsRefetching(false); + return { + marketKey: market.marketUniqueKey, + chainId: market.chainId, + market: marketWithDetails, + existingState: null + } as MarketToFetch; + }) + .filter((item): item is MarketToFetch => item !== null); + + const allMarketsToFetch: MarketToFetch[] = [...knownMarkets, ...marketsToRescan]; + + console.log(`🔄 Fetching snapshots for ${allMarketsToFetch.length} markets`); + + // Fetch snapshots in parallel using React Query's built-in caching + const snapshots = await Promise.all( + allMarketsToFetch.map(async ({ marketKey, chainId, market, existingState }): Promise => { + const snapshot = await queryClient.fetchQuery({ + queryKey: positionKeys.snapshot(marketKey, user, chainId), + queryFn: () => fetchPositionSnapshot(marketKey, user as Address, chainId, 0), + staleTime: 30000, + gcTime: 5 * 60 * 1000, + }); + + if (!snapshot && !existingState) return null; + + return { + market, + state: snapshot ?? existingState, + }; + }) + ); + + console.log('📊 Received position snapshots'); + + // Filter out null results and process positions + const validPositions = snapshots + .filter((item): item is NonNullable & { state: NonNullable } => + item !== null && item.state !== null + ) + .filter(position => showEmpty || position.state.supplyShares.toString() !== '0') + .map(position => ({ + state: position.state, + market: { + ...position.market, + warningsWithDetail: getMarketWarningsWithDetail(position.market), + }, + })); + + // Update market cache with all valid positions + const marketsToCache = validPositions + .filter(position => + position.market.uniqueKey && + position.market.morphoBlue?.chain?.id + ) + .map(position => ({ + marketUniqueKey: position.market.uniqueKey, + chainId: position.market.morphoBlue.chain.id, + })); + + if (marketsToCache.length > 0) { + batchAddUserMarkets(marketsToCache); } + + return validPositions; }, - [user, showEmpty, markets, batchAddUserMarkets, getUserMarkets], - ); + enabled: !!positionsData && !!user, + }); + + const refetch = useCallback(async (onSuccess?: () => void) => { + try { + await refetchPositions(); + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error('Error refetching positions:', error); + } + }, [refetchPositions]); - useEffect(() => { - void fetchData(); - }, [fetchData]); + // Consider refetching true if either query is refetching + const isRefetching = isRefetchingPositions || isRefetchingEnhanced; return { - data, - loading, + data: enhancedPositions ?? [], + loading: isLoadingPositions, isRefetching, positionsError, - refetch: (onSuccess?: () => void) => void fetchData(true, onSuccess), + refetch, }; }; diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index 87078b47..fc0cf1f5 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -1,3 +1,4 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { Address } from 'viem'; import { SupportedNetworks } from '@/utils/networks'; @@ -20,148 +21,189 @@ type ChainBlockNumbers = { [K in SupportedNetworks]: BlockNumbers; }; +// Query keys for block numbers and earnings +export const blockKeys = { + all: ['blocks'] as const, + chain: (chainId: number) => [...blockKeys.all, chainId] as const, +}; + +export const earningsKeys = { + all: ['earnings'] as const, + user: (address: string) => [...earningsKeys.all, address] as const, + position: (address: string, marketKey: string) => + [...earningsKeys.user(address), marketKey] as const, +}; + +const fetchBlockNumbers = async () => { + console.log('🔄 [BLOCK NUMBERS] Initial fetch started'); + + const now = Date.now() / 1000; + const DAY = 86400; + const timestamps = { + day: now - DAY, + week: now - 7 * DAY, + month: now - 30 * DAY, + }; + + const newBlockNums = {} as ChainBlockNumbers; + + // Get block numbers for each network and timestamp + await Promise.all( + Object.values(SupportedNetworks) + .filter((chainId): chainId is SupportedNetworks => typeof chainId === 'number') + .map(async (chainId) => { + const [day, week, month] = await Promise.all([ + estimatedBlockNumber(chainId, timestamps.day), + estimatedBlockNumber(chainId, timestamps.week), + estimatedBlockNumber(chainId, timestamps.month), + ]); + + if (day && week && month) { + newBlockNums[chainId] = { + day: day.blockNumber, + week: week.blockNumber, + month: month.blockNumber, + }; + } + }), + ); + + console.log('📊 [BLOCK NUMBERS] Fetch complete'); + return newBlockNums; +}; + const useUserPositionsSummaryData = (user: string | undefined) => { + const queryClient = useQueryClient(); + const [hasInitialData, setHasInitialData] = useState(false); + const { + data: positions, loading: positionsLoading, isRefetching, - data: positions, positionsError, - refetch, + refetch: refetchPositions, } = useUserPositions(user, true); const { fetchTransactions } = useUserTransactions(); - const [positionsWithEarnings, setPositionsWithEarnings] = useState( - [], - ); - const [blockNums, setBlockNums] = useState(); - const [isLoadingBlockNums, setIsLoadingBlockNums] = useState(false); - const [isLoadingEarnings, setIsLoadingEarnings] = useState(false); - const [error, setError] = useState(null); + // Query for block numbers - this runs once and is cached + const { + data: blockNums, + isLoading: isLoadingBlockNums, + } = useQuery({ + queryKey: blockKeys.all, + queryFn: fetchBlockNumbers, + staleTime: 5 * 60 * 1000, // Consider block numbers fresh for 5 minutes + gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes + }); + + // Query for earnings calculations with progressive updates + const { + data: positionsWithEarnings, + isLoading: isLoadingEarningsQuery, + isFetching: isFetchingEarnings, + error, + } = useQuery({ + queryKey: ['positions-earnings', user, positions, blockNums], + queryFn: async () => { + if (!positions || !user || !blockNums) { + console.log('⚠️ [EARNINGS] Missing required data, returning empty earnings'); + return [] as MarketPositionWithEarnings[]; + } - // Loading state for positions that doesn't include earnings calculation - const isPositionsLoading = positionsLoading; + console.log('🔄 [EARNINGS] Starting calculation for', positions.length, 'positions'); + + // Start with empty earnings + const initialPositions = initializePositionsWithEmptyEarnings(positions); + + // Calculate earnings for each position + const positionPromises = positions.map(async (position) => { + const positionKey = earningsKeys.position(user, position.market.uniqueKey); + + console.log('📈 [EARNINGS] Calculating for market:', position.market.uniqueKey); + + const history = await fetchTransactions({ + userAddress: [user], + marketUniqueKeys: [position.market.uniqueKey], + }); + + const chainId = position.market.morphoBlue.chain.id as SupportedNetworks; + const blockNumbers = blockNums[chainId]; + + const earned = await calculateEarnings( + position, + history.items, + user as Address, + chainId, + blockNumbers, + ); - // Loading state that combines all loading states (used for earnings) - const isEarningsLoading = isLoadingBlockNums || isLoadingEarnings; + console.log('✅ [EARNINGS] Completed for market:', position.market.uniqueKey); - useEffect(() => { - const fetchBlockNums = async () => { - try { - setIsLoadingBlockNums(true); - setError(null); - - const now = Date.now() / 1000; - const DAY = 86400; - const timestamps = { - day: now - DAY, - week: now - 7 * DAY, - month: now - 30 * DAY, + return { + ...position, + earned, }; - - const newBlockNums = {} as ChainBlockNumbers; - - // Get block numbers for each network and timestamp - await Promise.all( - Object.values(SupportedNetworks) - .filter((chainId): chainId is SupportedNetworks => typeof chainId === 'number') - .map(async (chainId) => { - const [day, week, month] = await Promise.all([ - estimatedBlockNumber(chainId, timestamps.day), - estimatedBlockNumber(chainId, timestamps.week), - estimatedBlockNumber(chainId, timestamps.month), - ]); - - if (day && week && month) { - newBlockNums[chainId] = { - day: day.blockNumber, - week: week.blockNumber, - month: month.blockNumber, - }; - } - }), - ); - - setBlockNums(newBlockNums); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to fetch block numbers')); - } finally { - setIsLoadingBlockNums(false); + }); + + // Wait for all earnings calculations to complete + const positionsWithCalculatedEarnings = await Promise.all(positionPromises); + + console.log('📊 [EARNINGS] All earnings calculations complete'); + return positionsWithCalculatedEarnings; + }, + placeholderData: (prev) => { + // If we have positions but no earnings data yet, initialize with empty earnings + if (positions?.length) { + console.log('📋 [EARNINGS] Using placeholder data with empty earnings'); + return initializePositionsWithEmptyEarnings(positions); } - }; - - void fetchBlockNums(); - }, []); - - // Create positions with empty earnings as soon as positions are loaded + // If we have previous data, keep it during transitions + if (prev) { + console.log('📋 [EARNINGS] Keeping previous earnings data during transition'); + return prev; + } + return [] as MarketPositionWithEarnings[]; + }, + enabled: !!positions && !!user && !!blockNums, + gcTime: 5 * 60 * 1000, + staleTime: 30000, + }); + + // Update hasInitialData when we first get positions with earnings useEffect(() => { - if (positions && positions.length > 0) { - // Initialize positions with empty earnings data to display immediately - setPositionsWithEarnings(initializePositionsWithEmptyEarnings(positions)); + if (positionsWithEarnings && positionsWithEarnings.length > 0 && !hasInitialData) { + setHasInitialData(true); } - }, [positions]); + }, [positionsWithEarnings, hasInitialData]); - // Calculate real earnings in the background - useEffect(() => { - const updatePositionsWithEarnings = async () => { - try { - if (!positions || !user || !blockNums) return; - - setIsLoadingEarnings(true); - setError(null); - - // Process positions one by one to update earnings progressively - // Potential issue: too slow, parallel processing might be better - for (const position of positions) { - const history = await fetchTransactions({ - userAddress: [user], - marketUniqueKeys: [position.market.uniqueKey], - }); - - const chainId = position.market.morphoBlue.chain.id as SupportedNetworks; - const blockNumbers = blockNums[chainId]; - - const earned = await calculateEarnings( - position, - history.items, - user as Address, - chainId, - blockNumbers, - ); - - // Update this single position with earnings - setPositionsWithEarnings((prev) => { - const updatedPositions = [...prev]; - const positionIndex = updatedPositions.findIndex( - (p) => - p.market.uniqueKey === position.market.uniqueKey && - p.market.morphoBlue.chain.id === position.market.morphoBlue.chain.id, - ); - - if (positionIndex !== -1) { - updatedPositions[positionIndex] = { - ...updatedPositions[positionIndex], - earned, - }; - } - - return updatedPositions; - }); - } - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to calculate earnings')); - } finally { - setIsLoadingEarnings(false); + const refetch = async (onSuccess?: () => void) => { + try { + await refetchPositions(); + if (onSuccess) { + onSuccess(); } - }; - - void updatePositionsWithEarnings(); - }, [positions, user, blockNums, fetchTransactions]); + } catch (error) { + console.error('Error refetching positions:', error); + } + }; + // Consider loading if either: + // 1. We haven't received initial data yet + // 2. Positions are still loading initially + // 3. We have positions but no earnings data yet + const isPositionsLoading = !hasInitialData || positionsLoading || (!!positions?.length && !positionsWithEarnings?.length); + + // Consider earnings loading if: + // 1. Block numbers are loading + // 2. Initial earnings query is loading + // 3. Earnings are being fetched/calculated (even if we have placeholder data) + const isEarningsLoading = isLoadingBlockNums || isLoadingEarningsQuery || isFetchingEarnings; + return { - positions: positionsWithEarnings, - isPositionsLoading, // For initial load of positions only - isEarningsLoading, // For earnings calculation + positions: positionsWithEarnings ?? [], + isPositionsLoading, + isEarningsLoading, isRefetching, error: error ?? positionsError, refetch, diff --git a/src/utils/interest.ts b/src/utils/interest.ts index 439b3c90..c721cdf4 100644 --- a/src/utils/interest.ts +++ b/src/utils/interest.ts @@ -23,8 +23,6 @@ export function calculateEarningsFromSnapshot( .filter((tx) => Number(tx.timestamp) > start && Number(tx.timestamp) < end) .sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)); - console.log('txsWithinPeriod', txsWithinPeriod.length); - const depositsAfter = txsWithinPeriod .filter((tx) => tx.type === UserTxTypes.MarketSupply) .reduce((sum, tx) => sum + BigInt(tx.data?.assets || '0'), 0n); diff --git a/src/utils/positions.ts b/src/utils/positions.ts index 9cc53165..71eb28a0 100644 --- a/src/utils/positions.ts +++ b/src/utils/positions.ts @@ -45,6 +45,9 @@ export async function fetchPositionSnapshot( blockNumber: number, ): Promise { try { + + console.log('fetchPositionSnapshot called', marketId, userAddress, chainId, blockNumber); + // Fetch the position at the specified block number const positionResponse = await fetch( `/api/positions/historical?` + diff --git a/yarn.lock b/yarn.lock index a4502061..f68090e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7575,21 +7575,40 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.51.17": - version: 5.51.17 - resolution: "@tanstack/query-core@npm:5.51.17" - checksum: 10c0/e5df9399be4085c48c3440f00ee59f43186ecfd4bb19e4c59dfe57176a9e62657182305fad69e1da1cb1915fd23337c58b5febb41d59f4bb4498f950df81a318 +"@tanstack/query-core@npm:5.69.0": + version: 5.69.0 + resolution: "@tanstack/query-core@npm:5.69.0" + checksum: 10c0/8ad87046d1a8c18773de10e74d7fef2093cdbe9734e76460945048c00a219ae3d2421eab2b4d54340f3f491813f99ceef7d2849827d95490c8b67de1b87934ae languageName: node linkType: hard -"@tanstack/react-query@npm:^5.20.1": - version: 5.51.18 - resolution: "@tanstack/react-query@npm:5.51.18" +"@tanstack/query-devtools@npm:5.67.2": + version: 5.67.2 + resolution: "@tanstack/query-devtools@npm:5.67.2" + checksum: 10c0/85172bb7cbca204e62507ffb72add4ff8f204b034ea0c6bb82cc44a00756ab196bd8a688b5e857911274aa5e7c467cdda6a393ebce6ba36ce8d471d5b52060fb + languageName: node + linkType: hard + +"@tanstack/react-query-devtools@npm:^5.69.0": + version: 5.69.0 + resolution: "@tanstack/react-query-devtools@npm:5.69.0" dependencies: - "@tanstack/query-core": "npm:5.51.17" + "@tanstack/query-devtools": "npm:5.67.2" peerDependencies: - react: ^18.0.0 - checksum: 10c0/bbf29af19650626c5bee9c6ec2fd158dadf38ef36ebd75b846d2b41869574f0a6c02a98628d91e2d051e88a2a804bf3cb55864050f3dea0ce165a9538b7d6404 + "@tanstack/react-query": ^5.69.0 + react: ^18 || ^19 + checksum: 10c0/e74937f68f205227d3a1bc616daeb4908de6c09426851b64c96b48a2f7468abbfe8a9509eac5c808836f63b0ae5faaba4229e7d1fed5e00cf7dc913da05f705f + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.69.0": + version: 5.69.0 + resolution: "@tanstack/react-query@npm:5.69.0" + dependencies: + "@tanstack/query-core": "npm:5.69.0" + peerDependencies: + react: ^18 || ^19 + checksum: 10c0/3481218d0dd1623af8148037011415ca6f57f2f918e859f6856b7d6cec191ca79029f6fd9e5b1139b1ec252198bda645ce8c1b44ac8dd28adf3c5f978a4126ec languageName: node linkType: hard @@ -15228,7 +15247,8 @@ __metadata: "@radix-ui/react-navigation-menu": "npm:^1.1.4" "@rainbow-me/rainbowkit": "npm:2" "@react-spring/web": "npm:^9.7.3" - "@tanstack/react-query": "npm:^5.20.1" + "@tanstack/react-query": "npm:^5.69.0" + "@tanstack/react-query-devtools": "npm:^5.69.0" "@testing-library/jest-dom": "npm:^6.1.5" "@testing-library/react": "npm:^14.1.2" "@testing-library/user-event": "npm:^14.5.2" From db029b24eae43e2c8a627fc48389730ae236ebde Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 28 Mar 2025 01:13:00 +0800 Subject: [PATCH 2/3] chore: minor --- src/hooks/useUserPositions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index bce3df53..c3127d69 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -56,7 +56,6 @@ export const positionKeys = { const fetchUserPositions = async ( user: string, - markets: Market[], getUserMarkets: () => Array<{ marketUniqueKey: string; chainId: number }>, ): Promise => { console.log('🔄 Fetching user positions for:', user); @@ -124,7 +123,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { queryKey: positionKeys.user(user ?? ''), queryFn: async () => { if (!user) throw new Error('Missing user address'); - return fetchUserPositions(user, markets, getUserMarkets); + return fetchUserPositions(user, getUserMarkets); }, enabled: !!user, staleTime: 30000, // Consider data fresh for 30 seconds @@ -169,7 +168,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { }) .map(market => { const marketWithDetails = markets.find((m) => - m.uniqueKey === market.marketUniqueKey && + m.uniqueKey.toLowerCase() === market.marketUniqueKey.toLowerCase() && m.morphoBlue?.chain?.id === market.chainId ); if (!marketWithDetails || !marketWithDetails.uniqueKey || !marketWithDetails.morphoBlue?.chain?.id) { From b695dd67cf8430194afb33f465f8014879e26b39 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 28 Mar 2025 01:23:14 +0800 Subject: [PATCH 3/3] chore: lint --- .eslintrc.js | 2 +- app/api/block/route.ts | 12 +- app/positions/components/PositionsContent.tsx | 6 +- src/hooks/useUserPositions.ts | 161 +++++++++--------- src/hooks/useUserPositionsSummaryData.ts | 32 ++-- src/utils/positions.ts | 1 - 6 files changed, 103 insertions(+), 111 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ab3e12ca..919ac22c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -121,8 +121,8 @@ module.exports = { // APIs '@typescript-eslint/prefer-includes': 'error', '@typescript-eslint/prefer-nullish-coalescing': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/prefer-string-starts-ends-with': 'error', + '@typescript-eslint/prefer-optional-chain': 'warn', // Hard to migrate // Errors for all try/catch blocks and any types from third-parties diff --git a/app/api/block/route.ts b/app/api/block/route.ts index 35d4868c..a938b5d5 100644 --- a/app/api/block/route.ts +++ b/app/api/block/route.ts @@ -9,15 +9,15 @@ const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; async function getBlockFromEtherscan(timestamp: number, chainId: number): Promise { try { const response = await fetch( - `https://api.etherscan.io/v2/api?chainid=${chainId}&module=block&action=getblocknobytime×tamp=${timestamp}&closest=before&apikey=${ETHERSCAN_API_KEY}` + `https://api.etherscan.io/v2/api?chainid=${chainId}&module=block&action=getblocknobytime×tamp=${timestamp}&closest=before&apikey=${ETHERSCAN_API_KEY}`, ); - const data = await response.json(); - + const data = (await response.json()) as { status: string; message: string; result: string }; + if (data.status === '1' && data.message === 'OK') { return parseInt(data.result); } - + return null; } catch (error) { console.error('Etherscan API error:', error); @@ -44,13 +44,12 @@ export async function GET(request: NextRequest) { // Fallback to SmartBlockFinder const client = numericChainId === SupportedNetworks.Mainnet ? mainnetClient : baseClient; - // Try Etherscan API first const etherscanBlock = await getBlockFromEtherscan(numericTimestamp, numericChainId); if (etherscanBlock !== null) { // For Etherscan results, we need to fetch the block to get its timestamp const block = await client.getBlock({ blockNumber: BigInt(etherscanBlock) }); - + return NextResponse.json({ blockNumber: Number(block.number), timestamp: Number(block.timestamp), @@ -59,7 +58,6 @@ export async function GET(request: NextRequest) { console.log('etherscanBlock is null', timestamp, chainId); } - if (!client) { return NextResponse.json({ error: 'Unsupported chain ID' }, { status: 400 }); } diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index f9236c46..4ad9c3de 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -60,7 +60,7 @@ export default function Positions() { }); const handleRefetch = () => { - refetch(() => toast.info('Data refreshed', { icon: 🚀 })); + void refetch(() => toast.info('Data refreshed', { icon: 🚀 })); }; return ( @@ -112,7 +112,7 @@ export default function Positions() { setShowWithdrawModal(false); setSelectedPosition(null); }} - refetch={refetch} + refetch={() => void refetch()} /> )} @@ -171,7 +171,7 @@ export default function Positions() { setShowWithdrawModal={setShowWithdrawModal} setShowSupplyModal={setShowSupplyModal} setSelectedPosition={setSelectedPosition} - refetch={refetch} + refetch={() => void refetch()} isRefetching={isRefetching} isLoadingEarnings={isEarningsLoading} rebalancerInfo={rebalancerInfo} diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index c3127d69..dd7b4953 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { useCallback } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Address } from 'viem'; import { userPositionsQuery } from '@/graphql/queries'; @@ -10,22 +11,21 @@ import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { useUserMarketsCache } from '../hooks/useUserMarketsCache'; import { useMarkets } from './useMarkets'; -import { useCallback } from 'react'; -interface UserPositionsResponse { +type UserPositionsResponse = { marketPositions: MarketPosition[]; - usedMarkets: Array<{ + usedMarkets: { marketUniqueKey: string; chainId: number; - }>; -} + }[]; +}; -interface MarketToFetch { +type MarketToFetch = { marketKey: string; chainId: number; market: Market; existingState: PositionSnapshot | null; -} +}; type EnhancedMarketPosition = { state: PositionSnapshot; @@ -48,18 +48,18 @@ type ValidMarketPosition = MarketPosition & { export const positionKeys = { all: ['positions'] as const, user: (address: string) => [...positionKeys.all, address] as const, - snapshot: (marketKey: string, userAddress: string, chainId: number) => + snapshot: (marketKey: string, userAddress: string, chainId: number) => [...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const, - enhanced: (user: string | undefined, data: UserPositionsResponse | undefined) => - ['enhanced-positions', user, data] as const + enhanced: (user: string | undefined, data: UserPositionsResponse | undefined) => + ['enhanced-positions', user, data] as const, }; const fetchUserPositions = async ( user: string, - getUserMarkets: () => Array<{ marketUniqueKey: string; chainId: number }>, + getUserMarkets: () => { marketUniqueKey: string; chainId: number }[], ): Promise => { console.log('🔄 Fetching user positions for:', user); - + const [responseMainnet, responseBase] = await Promise.all([ fetch(URLS.MORPHO_BLUE_API, { method: 'POST', @@ -85,10 +85,7 @@ const fetchUserPositions = async ( }), ]); - const [result1, result2] = await Promise.all([ - responseMainnet.json(), - responseBase.json(), - ]); + const [result1, result2] = await Promise.all([responseMainnet.json(), responseBase.json()]); console.log('📊 Received positions data from both networks'); @@ -97,10 +94,8 @@ const fetchUserPositions = async ( // Collect positions for (const result of [result1, result2]) { - if (result.data?.userByAddress) { - marketPositions.push( - ...(result.data.userByAddress.marketPositions as MarketPosition[]), - ); + if (result.data?.userByAddress?.marketPositions) { + marketPositions.push(...(result.data.userByAddress.marketPositions as MarketPosition[])); } } @@ -131,54 +126,59 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { }); // Query for position snapshots - const { - data: enhancedPositions, - isLoading: isLoadingEnhanced, - isRefetching: isRefetchingEnhanced, - } = useQuery({ + const { data: enhancedPositions, isRefetching: isRefetchingEnhanced } = useQuery< + EnhancedMarketPosition[] + >({ queryKey: positionKeys.enhanced(user, positionsData), queryFn: async () => { if (!positionsData || !user) return []; - + console.log('🔄 Fetching position snapshots'); - + const { marketPositions, usedMarkets } = positionsData; // We need to fetch snapshots for ALL markets - both from API and used ones const knownMarkets = marketPositions - .filter((position): position is ValidMarketPosition => - position.market !== undefined && - position.market.uniqueKey !== undefined && - position.market.morphoBlue?.chain?.id !== undefined + .filter( + (position): position is ValidMarketPosition => + position.market?.uniqueKey !== undefined && + position.market?.morphoBlue?.chain?.id !== undefined, ) - .map((position): MarketToFetch => ({ - marketKey: position.market.uniqueKey, - chainId: position.market.morphoBlue.chain.id, - market: position.market, - existingState: position.state - })); + .map( + (position): MarketToFetch => ({ + marketKey: position.market.uniqueKey, + chainId: position.market.morphoBlue.chain.id, + market: position.market, + existingState: position.state, + }), + ); const marketsToRescan = usedMarkets - .filter(market => { + .filter((market) => { return !marketPositions.find( - position => + (position) => position.market?.uniqueKey?.toLowerCase() === market.marketUniqueKey.toLowerCase() && - position.market?.morphoBlue?.chain?.id === market.chainId + position.market?.morphoBlue?.chain?.id === market.chainId, ); }) - .map(market => { - const marketWithDetails = markets.find((m) => - m.uniqueKey.toLowerCase() === market.marketUniqueKey.toLowerCase() && - m.morphoBlue?.chain?.id === market.chainId + .map((market) => { + const marketWithDetails = markets.find( + (m) => + m.uniqueKey?.toLowerCase() === market.marketUniqueKey.toLowerCase() && + m.morphoBlue?.chain?.id === market.chainId, ); - if (!marketWithDetails || !marketWithDetails.uniqueKey || !marketWithDetails.morphoBlue?.chain?.id) { + if ( + !marketWithDetails || + !marketWithDetails.uniqueKey || + !marketWithDetails.morphoBlue?.chain?.id + ) { return null; } return { marketKey: market.marketUniqueKey, chainId: market.chainId, market: marketWithDetails, - existingState: null + existingState: null, } as MarketToFetch; }) .filter((item): item is MarketToFetch => item !== null); @@ -189,32 +189,35 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { // Fetch snapshots in parallel using React Query's built-in caching const snapshots = await Promise.all( - allMarketsToFetch.map(async ({ marketKey, chainId, market, existingState }): Promise => { - const snapshot = await queryClient.fetchQuery({ - queryKey: positionKeys.snapshot(marketKey, user, chainId), - queryFn: () => fetchPositionSnapshot(marketKey, user as Address, chainId, 0), - staleTime: 30000, - gcTime: 5 * 60 * 1000, - }); - - if (!snapshot && !existingState) return null; - - return { - market, - state: snapshot ?? existingState, - }; - }) + allMarketsToFetch.map( + async ({ marketKey, chainId, market, existingState }): Promise => { + const snapshot = await queryClient.fetchQuery({ + queryKey: positionKeys.snapshot(marketKey, user, chainId), + queryFn: async () => fetchPositionSnapshot(marketKey, user as Address, chainId, 0), + staleTime: 30000, + gcTime: 5 * 60 * 1000, + }); + + if (!snapshot && !existingState) return null; + + return { + market, + state: snapshot ?? existingState, + }; + }, + ), ); console.log('📊 Received position snapshots'); // Filter out null results and process positions const validPositions = snapshots - .filter((item): item is NonNullable & { state: NonNullable } => - item !== null && item.state !== null + .filter( + (item): item is NonNullable & { state: NonNullable } => + item !== null && item.state !== null, ) - .filter(position => showEmpty || position.state.supplyShares.toString() !== '0') - .map(position => ({ + .filter((position) => showEmpty || position.state.supplyShares.toString() !== '0') + .map((position) => ({ state: position.state, market: { ...position.market, @@ -224,11 +227,8 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { // Update market cache with all valid positions const marketsToCache = validPositions - .filter(position => - position.market.uniqueKey && - position.market.morphoBlue?.chain?.id - ) - .map(position => ({ + .filter((position) => position.market?.uniqueKey && position.market?.morphoBlue?.chain?.id) + .map((position) => ({ marketUniqueKey: position.market.uniqueKey, chainId: position.market.morphoBlue.chain.id, })); @@ -242,16 +242,19 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { enabled: !!positionsData && !!user, }); - const refetch = useCallback(async (onSuccess?: () => void) => { - try { - await refetchPositions(); - if (onSuccess) { - onSuccess(); + const refetch = useCallback( + async (onSuccess?: () => void) => { + try { + await refetchPositions(); + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error('Error refetching positions:', error); } - } catch (error) { - console.error('Error refetching positions:', error); - } - }, [refetchPositions]); + }, + [refetchPositions], + ); // Consider refetching true if either query is refetching const isRefetching = isRefetchingPositions || isRefetchingEnhanced; diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index fc0cf1f5..65ddbf58 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -1,5 +1,5 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Address } from 'viem'; import { SupportedNetworks } from '@/utils/networks'; import { @@ -30,13 +30,13 @@ export const blockKeys = { export const earningsKeys = { all: ['earnings'] as const, user: (address: string) => [...earningsKeys.all, address] as const, - position: (address: string, marketKey: string) => + position: (address: string, marketKey: string) => [...earningsKeys.user(address), marketKey] as const, }; const fetchBlockNumbers = async () => { console.log('🔄 [BLOCK NUMBERS] Initial fetch started'); - + const now = Date.now() / 1000; const DAY = 86400; const timestamps = { @@ -73,9 +73,8 @@ const fetchBlockNumbers = async () => { }; const useUserPositionsSummaryData = (user: string | undefined) => { - const queryClient = useQueryClient(); const [hasInitialData, setHasInitialData] = useState(false); - + const { data: positions, loading: positionsLoading, @@ -87,10 +86,7 @@ const useUserPositionsSummaryData = (user: string | undefined) => { const { fetchTransactions } = useUserTransactions(); // Query for block numbers - this runs once and is cached - const { - data: blockNums, - isLoading: isLoadingBlockNums, - } = useQuery({ + const { data: blockNums, isLoading: isLoadingBlockNums } = useQuery({ queryKey: blockKeys.all, queryFn: fetchBlockNumbers, staleTime: 5 * 60 * 1000, // Consider block numbers fresh for 5 minutes @@ -113,15 +109,10 @@ const useUserPositionsSummaryData = (user: string | undefined) => { console.log('🔄 [EARNINGS] Starting calculation for', positions.length, 'positions'); - // Start with empty earnings - const initialPositions = initializePositionsWithEmptyEarnings(positions); - // Calculate earnings for each position const positionPromises = positions.map(async (position) => { - const positionKey = earningsKeys.position(user, position.market.uniqueKey); - console.log('📈 [EARNINGS] Calculating for market:', position.market.uniqueKey); - + const history = await fetchTransactions({ userAddress: [user], marketUniqueKeys: [position.market.uniqueKey], @@ -148,7 +139,7 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // Wait for all earnings calculations to complete const positionsWithCalculatedEarnings = await Promise.all(positionPromises); - + console.log('📊 [EARNINGS] All earnings calculations complete'); return positionsWithCalculatedEarnings; }, @@ -183,8 +174,8 @@ const useUserPositionsSummaryData = (user: string | undefined) => { if (onSuccess) { onSuccess(); } - } catch (error) { - console.error('Error refetching positions:', error); + } catch (refetchError) { + console.error('Error refetching positions:', refetchError); } }; @@ -192,14 +183,15 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // 1. We haven't received initial data yet // 2. Positions are still loading initially // 3. We have positions but no earnings data yet - const isPositionsLoading = !hasInitialData || positionsLoading || (!!positions?.length && !positionsWithEarnings?.length); + const isPositionsLoading = + !hasInitialData || positionsLoading || (!!positions?.length && !positionsWithEarnings?.length); // Consider earnings loading if: // 1. Block numbers are loading // 2. Initial earnings query is loading // 3. Earnings are being fetched/calculated (even if we have placeholder data) const isEarningsLoading = isLoadingBlockNums || isLoadingEarningsQuery || isFetchingEarnings; - + return { positions: positionsWithEarnings ?? [], isPositionsLoading, diff --git a/src/utils/positions.ts b/src/utils/positions.ts index 71eb28a0..b8725f78 100644 --- a/src/utils/positions.ts +++ b/src/utils/positions.ts @@ -45,7 +45,6 @@ export async function fetchPositionSnapshot( blockNumber: number, ): Promise { try { - console.log('fetchPositionSnapshot called', marketId, userAddress, chainId, blockNumber); // Fetch the position at the specified block number