From 2a22bf5d01637bd39191e6a9df5ad5c041a9a15e Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 30 Dec 2025 09:01:21 +0800 Subject: [PATCH 1/4] fix: history length --- src/hooks/queries/fetchUserTransactions.ts | 27 ++++++++++++++----- src/hooks/queries/useUserTransactionsQuery.ts | 3 ++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/hooks/queries/fetchUserTransactions.ts b/src/hooks/queries/fetchUserTransactions.ts index c3a9468b..96aa5fbd 100644 --- a/src/hooks/queries/fetchUserTransactions.ts +++ b/src/hooks/queries/fetchUserTransactions.ts @@ -14,6 +14,12 @@ export type TransactionFilters = { first?: number; hash?: string; assetIds?: string[]; + /** + * When true, pass skip directly to API and skip client-side pagination. + * Use for paginated loops that fetch all data across multiple requests. + * When false (default), use client-side pagination for correct multi-network sorting. + */ + useServerSidePagination?: boolean; }; export type TransactionResponse = { @@ -76,11 +82,17 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis // Try Morpho API first if supported if (supportsMorphoApi(network)) { try { + // For server-side pagination: pass skip/first directly to API + // For client-side pagination (default): fetch enough items for skip+first, apply slice later + const useServerPagination = filters.useServerSidePagination ?? false; + const itemsNeeded = useServerPagination + ? (filters.first ?? MAX_ITEMS_PER_SOURCE) + : (filters.skip ?? 0) + (filters.first ?? MAX_ITEMS_PER_SOURCE); const morphoFilters = { ...filters, chainIds: [network], - first: MAX_ITEMS_PER_SOURCE, - skip: 0, + first: Math.min(itemsNeeded, MAX_ITEMS_PER_SOURCE), + skip: useServerPagination ? (filters.skip ?? 0) : 0, }; const morphoResponse = await fetchMorphoTransactions(morphoFilters); if (!morphoResponse.error) { @@ -89,7 +101,7 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis items: networkItems, pageInfo: { count: networkItems.length, - countTotal: networkItems.length, + countTotal: morphoResponse.pageInfo.countTotal, }, error: null, }; @@ -161,10 +173,11 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis // 4. Sort combined results by timestamp combinedItems.sort((a, b) => b.timestamp - a.timestamp); - // 5. Apply client-side pagination - const skip = filters.skip ?? 0; - const first = filters.first ?? combinedItems.length; - const paginatedItems = combinedItems.slice(skip, skip + first); + // 5. Apply client-side pagination (skip when using server-side pagination) + const useServerPagination = filters.useServerSidePagination ?? false; + const paginatedItems = useServerPagination + ? combinedItems + : combinedItems.slice(filters.skip ?? 0, (filters.skip ?? 0) + (filters.first ?? combinedItems.length)); const finalError = errors.length > 0 ? errors.join('; ') : null; diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts index d48f715b..54edfdac 100644 --- a/src/hooks/queries/useUserTransactionsQuery.ts +++ b/src/hooks/queries/useUserTransactionsQuery.ts @@ -47,7 +47,7 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption // Simple case: fetch once with limit return await fetchUserTransactions({ ...filters, - first: pageSize, + first: filters.first ?? pageSize, }); } @@ -61,6 +61,7 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption ...filters, first: pageSize, skip, + useServerSidePagination: true, }); allItems = [...allItems, ...response.items]; From 0bf2188256416868f9c035382e2780a8e7e1868c Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 30 Dec 2025 09:42:27 +0800 Subject: [PATCH 2/4] refactor: remove multiple chainIds --- .../history/components/history-table.tsx | 106 ++++++++---------- .../transaction-history-preview.tsx | 8 +- src/features/history/history-view.tsx | 6 +- src/hooks/queries/fetchUserTransactions.ts | 27 ++--- src/hooks/queries/useUserTransactionsQuery.ts | 1 - 5 files changed, 61 insertions(+), 87 deletions(-) diff --git a/src/features/history/components/history-table.tsx b/src/features/history/components/history-table.tsx index bc07c63b..5274855f 100644 --- a/src/features/history/components/history-table.tsx +++ b/src/features/history/components/history-table.tsx @@ -5,8 +5,9 @@ import { useMemo, useState, useRef, useEffect } from 'react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { Table, TableHeader, TableBody, TableRow, TableCell, TableHead } from '@/components/ui/table'; -import { ChevronDownIcon, TrashIcon, ReloadIcon, GearIcon } from '@radix-ui/react-icons'; +import { ChevronDownIcon, ReloadIcon, GearIcon } from '@radix-ui/react-icons'; import { IoIosArrowRoundForward } from 'react-icons/io'; +import useUserPositions from '@/hooks/useUserPositions'; import Image from 'next/image'; import { formatUnits } from 'viem'; import { motion, AnimatePresence } from 'framer-motion'; @@ -32,11 +33,10 @@ import { useStyledToast } from '@/hooks/useStyledToast'; import { formatReadable } from '@/utils/balance'; import { getNetworkImg, getNetworkName } from '@/utils/networks'; import { groupTransactionsByHash, getWithdrawals, getSupplies, type GroupedTransaction } from '@/utils/transactionGrouping'; -import { UserTxTypes, type Market, type MarketPosition, type UserTransaction } from '@/utils/types'; +import { UserTxTypes, type Market, type UserTransaction } from '@/utils/types'; type HistoryTableProps = { account: string | undefined; - positions: MarketPosition[]; isVaultAdapter?: boolean; }; @@ -70,16 +70,22 @@ const formatTimeAgo = (timestamp: number): string => { return `${diffInYears}y ago`; }; -export function HistoryTable({ account, positions, isVaultAdapter = false }: HistoryTableProps) { +export function HistoryTable({ account, isVaultAdapter = false }: HistoryTableProps) { + + const { data: positions, loading: loadingPosition } = useUserPositions(account, true); + const { allMarkets, loading: loadingMarkets } = useProcessedMarkets(); + const searchParams = useSearchParams(); const [selectedAsset, setSelectedAsset] = useState(null); const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const [hasInitializedFromUrl, setHasInitializedFromUrl] = useState(false); const dropdownRef = useRef(null); - const { allMarkets } = useProcessedMarkets(); const toast = useStyledToast(); + // For vault adapter, get chainId from URL params + const vaultAdapterChainId = isVaultAdapter ? Number(searchParams.get('chainId')) : null; + const [currentPage, setCurrentPage] = useState(1); // Settings state from Zustand store @@ -99,21 +105,28 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His .map((m) => m.uniqueKey); }, [selectedAsset, allMarkets]); + // Determine chainId: from vault adapter URL param or selected asset + const activeChainId = isVaultAdapter ? vaultAdapterChainId : selectedAsset?.chainId; + // Fetch transactions using React Query + // Single-chain queries for correct pagination const { data, - isLoading: loading, + isLoading: loadingHistory, refetch, } = useUserTransactionsQuery({ filters: { userAddress: account ? [account] : [], first: pageSize, skip: (currentPage - 1) * pageSize, - marketUniqueKeys: marketIdFilter, + marketUniqueKeys: isVaultAdapter ? [] : marketIdFilter, + chainIds: activeChainId ? [activeChainId] : undefined, }, - enabled: Boolean(account) && allMarkets.length > 0, + enabled: Boolean(account) && allMarkets.length > 0 && Boolean(activeChainId), }); + const loading = loadingHistory || loadingMarkets || loadingPosition; + const history = data?.items ?? []; const totalPages = data ? Math.ceil(data.pageInfo.countTotal / pageSize) : 0; @@ -134,11 +147,6 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His [history], ); - // Determine MarketIdentity mode based on context - // Use Badge mode when showing single loan asset context (vault adapter or filtered by asset) - // Use Normal mode with Collateral focus when showing all history - const shouldUseBadgeMode = isVaultAdapter || selectedAsset !== null; - // Helper functions const toggleRow = (rowKey: string) => { setExpandedRows((prev) => { @@ -192,34 +200,32 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His return Array.from(assetMap.values()); }, [positions, allMarkets]); - // Handle initial URL parameters for pre-filtering (only once) + // Handle initial position selection (from URL params or default to first position) useEffect(() => { - // Only initialize once and only if we have URL params + // Only initialize once if (hasInitializedFromUrl) return; + // Wait for positions/markets to load + if (allMarkets.length === 0 || uniqueAssets.length === 0) return; const chainIdParam = searchParams.get('chainId'); const tokenAddressParam = searchParams.get('tokenAddress'); - // If no URL params, we're done initializing - if (!chainIdParam || !tokenAddressParam) { - setHasInitializedFromUrl(true); - return; + // Try to match URL params first + if (chainIdParam && tokenAddressParam) { + const chainId = Number.parseInt(chainIdParam, 10); + const matchingAsset = uniqueAssets.find( + (asset) => asset.chainId === chainId && asset.address.toLowerCase() === tokenAddressParam.toLowerCase(), + ); + if (matchingAsset) { + setSelectedAsset(matchingAsset); + setHasInitializedFromUrl(true); + return; + } } - // Wait for markets to load before initializing - if (allMarkets.length === 0) return; - - const chainId = Number.parseInt(chainIdParam, 10); - - // Try to find in uniqueAssets first (from user positions) - const matchingAsset = uniqueAssets.find( - (asset) => asset.chainId === chainId && asset.address.toLowerCase() === tokenAddressParam.toLowerCase(), - ); - - if (matchingAsset) { - setSelectedAsset(matchingAsset); - setHasInitializedFromUrl(true); - } + // Default to first position if no URL params or no match + setSelectedAsset(uniqueAssets[0]); + setHasInitializedFromUrl(true); }, [searchParams, uniqueAssets, allMarkets, hasInitializedFromUrl]); // Reset page when filter changes @@ -343,10 +349,9 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His @@ -490,7 +495,7 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His ) : ( - All positions + Select a position )} @@ -508,7 +513,7 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His />
    {filteredAssets.map((asset, idx) => ( @@ -561,21 +566,6 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His ))}
-
- -
)} @@ -684,10 +674,9 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His {hasMoreWithdrawals && +{withdrawals.length - 1}} @@ -700,10 +689,9 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His {hasMoreSupplies && +{supplies.length - 1}} @@ -819,10 +807,9 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His {hasMoreMarkets && +{marketCount - 1} more} @@ -913,10 +900,9 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His {hasMoreMarkets && +{marketCount - 1} more} diff --git a/src/features/history/components/transaction-history-preview.tsx b/src/features/history/components/transaction-history-preview.tsx index 18663bc0..a675ffb0 100644 --- a/src/features/history/components/transaction-history-preview.tsx +++ b/src/features/history/components/transaction-history-preview.tsx @@ -18,7 +18,7 @@ import type { Market } from '@/utils/types'; type TransactionHistoryPreviewProps = { account: string; - chainId?: number; + chainId: number; isVaultAdapter?: boolean; limit?: number; emptyMessage?: string; @@ -62,7 +62,7 @@ export function TransactionHistoryPreview({ userAddress: account ? [account] : [], first: limit, skip: 0, - chainIds: chainId ? [chainId] : undefined, + chainIds: [chainId], }, enabled: Boolean(account) && allMarkets.length > 0, }); @@ -76,9 +76,9 @@ export function TransactionHistoryPreview({ const historyLink = useMemo(() => { const params = new URLSearchParams(); - if (chainId) params.set('chainId', chainId.toString()); + params.set('chainId', chainId.toString()); if (isVaultAdapter) params.set('isVaultAdapter', 'true'); - return `/history/${account}${params.toString() ? `?${params.toString()}` : ''}`; + return `/history/${account}?${params.toString()}`; }, [account, chainId, isVaultAdapter]); const actions = ( diff --git a/src/features/history/history-view.tsx b/src/features/history/history-view.tsx index 9bb5c4b1..798805b2 100644 --- a/src/features/history/history-view.tsx +++ b/src/features/history/history-view.tsx @@ -2,11 +2,12 @@ import { useSearchParams } from 'next/navigation'; import Header from '@/components/layout/header/Header'; -import useUserPositions from '@/hooks/useUserPositions'; + import { HistoryTable } from './components/history-table'; export default function HistoryContent({ account }: { account: string }) { - const { data: positions } = useUserPositions(account, true); + + const searchParams = useSearchParams(); const isVaultAdapter = searchParams.get('isVaultAdapter') === 'true'; @@ -19,7 +20,6 @@ export default function HistoryContent({ account }: { account: string }) {
diff --git a/src/hooks/queries/fetchUserTransactions.ts b/src/hooks/queries/fetchUserTransactions.ts index 96aa5fbd..9abb21a5 100644 --- a/src/hooks/queries/fetchUserTransactions.ts +++ b/src/hooks/queries/fetchUserTransactions.ts @@ -14,12 +14,6 @@ export type TransactionFilters = { first?: number; hash?: string; assetIds?: string[]; - /** - * When true, pass skip directly to API and skip client-side pagination. - * Use for paginated loops that fetch all data across multiple requests. - * When false (default), use client-side pagination for correct multi-network sorting. - */ - useServerSidePagination?: boolean; }; export type TransactionResponse = { @@ -82,17 +76,14 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis // Try Morpho API first if supported if (supportsMorphoApi(network)) { try { - // For server-side pagination: pass skip/first directly to API - // For client-side pagination (default): fetch enough items for skip+first, apply slice later - const useServerPagination = filters.useServerSidePagination ?? false; - const itemsNeeded = useServerPagination - ? (filters.first ?? MAX_ITEMS_PER_SOURCE) - : (filters.skip ?? 0) + (filters.first ?? MAX_ITEMS_PER_SOURCE); + // Single-chain: pass skip/first directly to API for proper pagination + // Multi-chain: fetch MAX_ITEMS_PER_SOURCE to combine and sort across chains + const isSingleChain = targetNetworks.length === 1; const morphoFilters = { ...filters, chainIds: [network], - first: Math.min(itemsNeeded, MAX_ITEMS_PER_SOURCE), - skip: useServerPagination ? (filters.skip ?? 0) : 0, + first: isSingleChain ? (filters.first ?? MAX_ITEMS_PER_SOURCE) : MAX_ITEMS_PER_SOURCE, + skip: isSingleChain ? (filters.skip ?? 0) : 0, }; const morphoResponse = await fetchMorphoTransactions(morphoFilters); if (!morphoResponse.error) { @@ -173,11 +164,9 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis // 4. Sort combined results by timestamp combinedItems.sort((a, b) => b.timestamp - a.timestamp); - // 5. Apply client-side pagination (skip when using server-side pagination) - const useServerPagination = filters.useServerSidePagination ?? false; - const paginatedItems = useServerPagination - ? combinedItems - : combinedItems.slice(filters.skip ?? 0, (filters.skip ?? 0) + (filters.first ?? combinedItems.length)); + // 5. For single-chain queries, API handles pagination; for multi-chain, no client-side slice needed + // (multi-chain is only used for fetching all data, not paginated display) + const paginatedItems = combinedItems; const finalError = errors.length > 0 ? errors.join('; ') : null; diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts index 54edfdac..c9e0578a 100644 --- a/src/hooks/queries/useUserTransactionsQuery.ts +++ b/src/hooks/queries/useUserTransactionsQuery.ts @@ -61,7 +61,6 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption ...filters, first: pageSize, skip, - useServerSidePagination: true, }); allItems = [...allItems, ...response.items]; From 920bf69a1e652f72323eb83a6ff5ce7f8d0fe479 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 30 Dec 2025 09:53:26 +0800 Subject: [PATCH 3/4] refactor: remove history complexity --- src/data-sources/morpho-api/transactions.ts | 6 +- .../history/components/history-table.tsx | 3 +- .../transaction-history-preview.tsx | 2 +- src/hooks/queries/fetchUserTransactions.ts | 170 ++++-------------- src/hooks/queries/useUserTransactionsQuery.ts | 113 +++++++----- src/hooks/usePositionReport.ts | 2 +- 6 files changed, 117 insertions(+), 179 deletions(-) diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts index 8c8c3fc0..a2dbe516 100644 --- a/src/data-sources/morpho-api/transactions.ts +++ b/src/data-sources/morpho-api/transactions.ts @@ -1,6 +1,5 @@ import { userTransactionsQuery } from '@/graphql/morpho-api-queries'; import type { TransactionFilters, TransactionResponse } from '@/hooks/queries/fetchUserTransactions'; -import { SupportedNetworks } from '@/utils/networks'; import { morphoGraphqlFetcher } from './fetchers'; // Define the expected shape of the GraphQL response for transactions @@ -13,10 +12,9 @@ type MorphoTransactionsApiResponse = { export const fetchMorphoTransactions = async (filters: TransactionFilters): Promise => { // Conditionally construct the 'where' object - const whereClause: Record = { + const whereClause: Record = { userAddress_in: filters.userAddress, // Assuming this is always required - // Default chainIds if none are provided in filters for Morpho API call context - chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet], + chainId_in: [filters.chainId], }; if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) { diff --git a/src/features/history/components/history-table.tsx b/src/features/history/components/history-table.tsx index 5274855f..14e8a614 100644 --- a/src/features/history/components/history-table.tsx +++ b/src/features/history/components/history-table.tsx @@ -71,7 +71,6 @@ const formatTimeAgo = (timestamp: number): string => { }; export function HistoryTable({ account, isVaultAdapter = false }: HistoryTableProps) { - const { data: positions, loading: loadingPosition } = useUserPositions(account, true); const { allMarkets, loading: loadingMarkets } = useProcessedMarkets(); @@ -120,7 +119,7 @@ export function HistoryTable({ account, isVaultAdapter = false }: HistoryTablePr first: pageSize, skip: (currentPage - 1) * pageSize, marketUniqueKeys: isVaultAdapter ? [] : marketIdFilter, - chainIds: activeChainId ? [activeChainId] : undefined, + chainId: activeChainId ?? undefined, }, enabled: Boolean(account) && allMarkets.length > 0 && Boolean(activeChainId), }); diff --git a/src/features/history/components/transaction-history-preview.tsx b/src/features/history/components/transaction-history-preview.tsx index a675ffb0..ecbe6cb9 100644 --- a/src/features/history/components/transaction-history-preview.tsx +++ b/src/features/history/components/transaction-history-preview.tsx @@ -62,7 +62,7 @@ export function TransactionHistoryPreview({ userAddress: account ? [account] : [], first: limit, skip: 0, - chainIds: [chainId], + chainId, }, enabled: Boolean(account) && allMarkets.length > 0, }); diff --git a/src/hooks/queries/fetchUserTransactions.ts b/src/hooks/queries/fetchUserTransactions.ts index 9abb21a5..d506a9d5 100644 --- a/src/hooks/queries/fetchUserTransactions.ts +++ b/src/hooks/queries/fetchUserTransactions.ts @@ -1,13 +1,17 @@ import { supportsMorphoApi } from '@/config/dataSources'; import { fetchMorphoTransactions } from '@/data-sources/morpho-api/transactions'; import { fetchSubgraphTransactions } from '@/data-sources/subgraph/transactions'; -import { SupportedNetworks, isSupportedChain } from '@/utils/networks'; +import { isSupportedChain } from '@/utils/networks'; import type { UserTransaction } from '@/utils/types'; +/** + * Filters for fetching user transactions. + * Requires a single chainId - for multi-chain queries, use useUserTransactionsQuery with paginate: true. + */ export type TransactionFilters = { userAddress: string[]; + chainId: number; marketUniqueKeys?: string[]; - chainIds?: number[]; timestampGte?: number; timestampLte?: number; skip?: number; @@ -25,39 +29,28 @@ export type TransactionResponse = { error: string | null; }; -const MAX_ITEMS_PER_SOURCE = 1000; - /** - * Standalone function to fetch user transactions from Morpho API or Subgraph. - * Can be used imperatively in hooks that need to fetch transactions as part of a larger operation. + * Fetches user transactions for a SINGLE chain from Morpho API or Subgraph. + * For multi-chain queries, use useUserTransactionsQuery with paginate: true. * - * @param filters - Transaction filters + * @param filters - Transaction filters (chainId is required) * @returns Promise resolving to transaction response */ export async function fetchUserTransactions(filters: TransactionFilters): Promise { - // 1. Determine target networks (numeric enum values) - let targetNetworks: SupportedNetworks[]; + const { chainId } = filters; - if (filters.chainIds && filters.chainIds.length > 0) { - // Filter provided chainIds to only include valid, supported numeric values - targetNetworks = filters.chainIds.filter(isSupportedChain) as SupportedNetworks[]; - } else { - // Default to all supported networks (get only numeric values from enum) - targetNetworks = Object.values(SupportedNetworks).filter((value) => typeof value === 'number') as SupportedNetworks[]; - } - - if (targetNetworks.length === 0) { - console.warn('No valid target networks determined.'); + // Validate chainId + if (!isSupportedChain(chainId)) { + console.warn(`Unsupported chain: ${chainId}`); return { items: [], pageInfo: { count: 0, countTotal: 0 }, - error: null, + error: `Unsupported chain: ${chainId}`, }; } - // Check for subgraph user address limitation - const usesSubgraph = targetNetworks.some((network) => !supportsMorphoApi(network)); - if (usesSubgraph && filters.userAddress.length !== 1) { + // Check subgraph user address limitation + if (!supportsMorphoApi(chainId) && filters.userAddress.length !== 1) { const errorMsg = 'Subgraph data source requires exactly one user address.'; console.error(errorMsg); return { @@ -67,115 +60,30 @@ export async function fetchUserTransactions(filters: TransactionFilters): Promis }; } - // 2. Create fetch promises for each network - const results = await Promise.allSettled( - targetNetworks.map(async (network) => { - let networkItems: UserTransaction[] = []; - let networkError: string | null = null; - - // Try Morpho API first if supported - if (supportsMorphoApi(network)) { - try { - // Single-chain: pass skip/first directly to API for proper pagination - // Multi-chain: fetch MAX_ITEMS_PER_SOURCE to combine and sort across chains - const isSingleChain = targetNetworks.length === 1; - const morphoFilters = { - ...filters, - chainIds: [network], - first: isSingleChain ? (filters.first ?? MAX_ITEMS_PER_SOURCE) : MAX_ITEMS_PER_SOURCE, - skip: isSingleChain ? (filters.skip ?? 0) : 0, - }; - const morphoResponse = await fetchMorphoTransactions(morphoFilters); - if (!morphoResponse.error) { - networkItems = morphoResponse.items; - return { - items: networkItems, - pageInfo: { - count: networkItems.length, - countTotal: morphoResponse.pageInfo.countTotal, - }, - error: null, - }; - } - networkError = morphoResponse.error; - } catch (morphoError) { - networkError = `Failed to fetch from Morpho API: ${(morphoError as Error)?.message || 'Unknown error'}`; - } - } - - // Only try Subgraph if Morpho API failed or is not supported - if (!supportsMorphoApi(network) || networkError) { - try { - const subgraphFilters = { - ...filters, - chainIds: [network], - first: MAX_ITEMS_PER_SOURCE, - skip: 0, - }; - const subgraphResponse = await fetchSubgraphTransactions(subgraphFilters, network); - if (!subgraphResponse.error) { - networkItems = subgraphResponse.items; - return { - items: networkItems, - pageInfo: { - count: networkItems.length, - countTotal: networkItems.length, - }, - error: null, - }; - } - networkError = subgraphResponse.error; - } catch (subgraphError) { - networkError = `Failed to fetch from Subgraph: ${(subgraphError as Error)?.message || 'Unknown error'}`; - } - } - - // Only reach here if both Morpho API and Subgraph failed - return { - items: networkItems, - pageInfo: { - count: networkItems.length, - countTotal: networkItems.length, - }, - error: networkError, - }; - }), - ); - - // 3. Combine results - let combinedItems: UserTransaction[] = []; - let combinedTotalCount = 0; - const errors: string[] = []; - - results.forEach((result) => { - if (result.status === 'fulfilled') { - const response = result.value; - if (response.error) { - errors.push(response.error); - } else { - combinedItems = combinedItems.concat(response.items); - combinedTotalCount += response.pageInfo.countTotal; + // Try Morpho API first if supported + if (supportsMorphoApi(chainId)) { + try { + const response = await fetchMorphoTransactions(filters); + if (!response.error) { + return response; } - } else { - errors.push(`Failed to fetch transactions: ${result.reason?.message || 'Unknown error'}`); + // Morpho API returned an error, fall through to Subgraph + } catch (morphoError) { + console.warn(`Morpho API failed for chain ${chainId}, falling back to Subgraph:`, morphoError); + // Fall through to Subgraph } - }); - - // 4. Sort combined results by timestamp - combinedItems.sort((a, b) => b.timestamp - a.timestamp); - - // 5. For single-chain queries, API handles pagination; for multi-chain, no client-side slice needed - // (multi-chain is only used for fetching all data, not paginated display) - const paginatedItems = combinedItems; - - const finalError = errors.length > 0 ? errors.join('; ') : null; + } - return { - items: paginatedItems, - pageInfo: { - count: paginatedItems.length, - countTotal: combinedTotalCount, - }, - error: finalError, - }; + // Fallback to Subgraph + try { + return await fetchSubgraphTransactions(filters, chainId); + } catch (subgraphError) { + const errorMsg = `Failed to fetch transactions: ${(subgraphError as Error)?.message ?? 'Unknown error'}`; + console.error(errorMsg); + return { + items: [], + pageInfo: { count: 0, countTotal: 0 }, + error: errorMsg, + }; + } } diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts index c9e0578a..19e3bbdc 100644 --- a/src/hooks/queries/useUserTransactionsQuery.ts +++ b/src/hooks/queries/useUserTransactionsQuery.ts @@ -1,12 +1,25 @@ import { useQuery } from '@tanstack/react-query'; import { fetchUserTransactions, type TransactionFilters, type TransactionResponse } from './fetchUserTransactions'; +import { ALL_SUPPORTED_NETWORKS } from '@/utils/networks'; +import type { UserTransaction } from '@/utils/types'; + +/** + * Filter options for the hook. + * - For non-paginated queries: `chainId` is required (single chain) + * - For paginated queries: `chainIds` can be used for multi-chain, or `chainId` for single chain + */ +type HookTransactionFilters = Omit & { + chainId?: number; + chainIds?: number[]; +}; type UseUserTransactionsQueryOptions = { - filters: TransactionFilters; + filters: HookTransactionFilters; enabled?: boolean; /** * When true, automatically paginates to fetch ALL transactions. - * Use for report generation when complete accuracy is needed. + * Required when using chainIds with multiple values. + * For multi-chain queries, fetches all chains in parallel. */ paginate?: boolean; /** Page size for pagination (default 1000) */ @@ -17,12 +30,10 @@ type UseUserTransactionsQueryOptions = { * Fetches user transactions from Morpho API or Subgraph using React Query. * * Data fetching strategy: + * - For non-paginated queries: requires single chainId, fetches with skip/first + * - For paginated queries: can use multiple chainIds, fetches ALL data in parallel * - Tries Morpho API first (if supported for the network) * - Falls back to Subgraph if API fails or not supported - * - Combines transactions from all target networks - * - Sorts by timestamp (descending) - * - Supports auto-pagination when paginate=true - * ``` */ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOptions) => { const { filters, enabled = true, paginate = false, pageSize = 1000 } = options; @@ -32,6 +43,7 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption 'user-transactions', filters.userAddress, filters.marketUniqueKeys, + filters.chainId, filters.chainIds, filters.timestampGte, filters.timestampLte, @@ -43,47 +55,68 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption pageSize, ], queryFn: async () => { - if (!paginate) { - // Simple case: fetch once with limit - return await fetchUserTransactions({ - ...filters, - first: filters.first ?? pageSize, - }); - } + if (paginate) { + // Paginate mode: fetch ALL transactions, supports multi-chain + const chainIds = filters.chainIds ?? (filters.chainId ? [filters.chainId] : ALL_SUPPORTED_NETWORKS); + + // Helper to fetch all pages for one chain + const fetchAllForChain = async (chainId: number): Promise => { + const items: UserTransaction[] = []; + let skip = 0; + let hasMore = true; - // Pagination mode: fetch all data across multiple requests - let allItems: typeof filters extends TransactionFilters ? TransactionResponse['items'] : never = []; - let skip = 0; - let hasMore = true; + while (hasMore) { + const response = await fetchUserTransactions({ + ...filters, + chainId, + first: pageSize, + skip, + }); - while (hasMore) { - const response = await fetchUserTransactions({ - ...filters, - first: pageSize, - skip, - }); + items.push(...response.items); + skip += response.items.length; - allItems = [...allItems, ...response.items]; - skip += response.items.length; + // Stop if we got fewer items than requested (last page) + hasMore = response.items.length >= pageSize; - // Stop if we got fewer items than requested (last page) - hasMore = response.items.length >= pageSize; + // Safety: max 50 pages per chain to prevent infinite loops + if (skip >= 50 * pageSize) { + console.warn(`Transaction pagination limit reached for chain ${chainId} (50 pages)`); + break; + } + } + + return items; + }; + + // Fetch ALL chains IN PARALLEL + const results = await Promise.all(chainIds.map(fetchAllForChain)); + const allItems = results.flat(); + + // Sort combined results by timestamp (descending) + allItems.sort((a, b) => b.timestamp - a.timestamp); + + return { + items: allItems, + pageInfo: { + count: allItems.length, + countTotal: allItems.length, + }, + error: null, + }; + } - // Safety: max 50 pages to prevent infinite loops - if (skip >= 50 * pageSize) { - console.warn('Transaction pagination limit reached (50 pages)'); - break; - } + // Non-paginate mode: requires single chainId + if (!filters.chainId) { + throw new Error('chainId is required for non-paginated queries. Use paginate: true for multi-chain queries.'); } - return { - items: allItems, - pageInfo: { - count: allItems.length, - countTotal: allItems.length, - }, - error: null, - }; + // Simple case: fetch once with limit + return await fetchUserTransactions({ + ...filters, + chainId: filters.chainId, + first: filters.first ?? pageSize, + }); }, enabled: enabled && filters.userAddress.length > 0, staleTime: 60 * 1000, diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index c84d6528..46dddbc0 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -84,7 +84,7 @@ export const usePositionReport = ( while (hasMore) { const transactionResult = await fetchUserTransactions({ userAddress: [account], - chainIds: [selectedAsset.chainId], + chainId: selectedAsset.chainId, timestampGte: actualStartTimestamp, timestampLte: actualEndTimestamp, assetIds: [selectedAsset.address], From 59b7b388fd21e39997ef4f930f2fad57f29ac871 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 30 Dec 2025 09:53:40 +0800 Subject: [PATCH 4/4] chore: lint --- src/features/history/history-view.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/history/history-view.tsx b/src/features/history/history-view.tsx index 798805b2..1bbe5868 100644 --- a/src/features/history/history-view.tsx +++ b/src/features/history/history-view.tsx @@ -6,8 +6,6 @@ import Header from '@/components/layout/header/Header'; import { HistoryTable } from './components/history-table'; export default function HistoryContent({ account }: { account: string }) { - - const searchParams = useSearchParams(); const isVaultAdapter = searchParams.get('isVaultAdapter') === 'true';