From 8b8076810524428430e54efed36d11c5081d5caf Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 15:47:33 +0800
Subject: [PATCH 01/11] refactor: earning calculation
---
app/api/block/route.ts | 77 -----
.../positions-report-view.tsx | 4 +-
.../supplied-morpho-blue-grouped-table.tsx | 3 +-
src/features/positions/positions-view.tsx | 46 ++-
.../queries/usePositionSnapshotsQuery.ts | 60 ++++
src/hooks/queries/useUserTransactionsQuery.ts | 102 +++++-
src/hooks/usePositionReport.ts | 86 ++---
src/hooks/usePositionsWithEarnings.ts | 63 ++++
src/hooks/useUserPositionsSummaryData.ts | 293 +++++++-----------
src/stores/usePositionsFilters.ts | 56 ++++
src/utils/blockEstimation.ts | 35 +++
src/utils/rpc.ts | 40 ---
12 files changed, 508 insertions(+), 357 deletions(-)
delete mode 100644 app/api/block/route.ts
create mode 100644 src/hooks/queries/usePositionSnapshotsQuery.ts
create mode 100644 src/hooks/usePositionsWithEarnings.ts
create mode 100644 src/stores/usePositionsFilters.ts
create mode 100644 src/utils/blockEstimation.ts
diff --git a/app/api/block/route.ts b/app/api/block/route.ts
deleted file mode 100644
index 129468e2..00000000
--- a/app/api/block/route.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { type NextRequest, NextResponse } from 'next/server';
-import type { PublicClient } from 'viem';
-import { SmartBlockFinder } from '@/utils/blockFinder';
-import type { SupportedNetworks } from '@/utils/networks';
-import { getClient } 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()) as {
- status: string;
- message: string;
- result: string;
- };
-
- if (data.status === '1' && data.message === 'OK') {
- return Number.parseInt(data.result, 10);
- }
-
- return null;
- } catch (error) {
- console.error('Etherscan API error:', error);
- return null;
- }
-}
-
-export async function GET(request: NextRequest) {
- try {
- const searchParams = request.nextUrl.searchParams;
- const timestamp = searchParams.get('timestamp');
- const chainId = searchParams.get('chainId');
-
- if (!timestamp || !chainId) {
- return NextResponse.json({ error: 'Missing required parameters: timestamp and chainId' }, { status: 400 });
- }
-
- const numericChainId = Number.parseInt(chainId, 10);
- const numericTimestamp = Number.parseInt(timestamp, 10);
-
- // Fallback to SmartBlockFinder
- const client = getClient(numericChainId as SupportedNetworks);
-
- // 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),
- });
- }
-
- if (!client) {
- return NextResponse.json({ error: 'Unsupported chain ID' }, { status: 400 });
- }
-
- const finder = new SmartBlockFinder(client as any as PublicClient, numericChainId);
- const block = await finder.findNearestBlock(numericTimestamp);
-
- return NextResponse.json({
- blockNumber: Number(block.number),
- timestamp: Number(block.timestamp),
- });
- } catch (error) {
- console.error('Error finding block:', error);
- return NextResponse.json({ error: 'Internal server error', details: (error as Error).message }, { status: 500 });
- }
-}
diff --git a/src/features/positions-report/positions-report-view.tsx b/src/features/positions-report/positions-report-view.tsx
index 8148f1e6..134ea1ff 100644
--- a/src/features/positions-report/positions-report-view.tsx
+++ b/src/features/positions-report/positions-report-view.tsx
@@ -24,7 +24,9 @@ type ReportState = {
};
export default function ReportContent({ account }: { account: Address }) {
- const { loading, data: positions } = useUserPositions(account, true);
+ // Fetch ALL positions including closed ones (onlySupplied: false)
+ // This ensures report includes markets that were active during the selected period
+ const { loading, data: positions } = useUserPositions(account, false);
const [selectedAsset, setSelectedAsset] = useState(null);
// Get today's date and 2 months ago
diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
index 48af610f..9d289fed 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -101,6 +101,7 @@ type SuppliedMorphoBlueGroupedTableProps = {
isLoadingEarnings?: boolean;
earningsPeriod: EarningsPeriod;
setEarningsPeriod: (period: EarningsPeriod) => void;
+ chainBlockData?: Record;
};
export function SuppliedMorphoBlueGroupedTable({
@@ -111,6 +112,7 @@ export function SuppliedMorphoBlueGroupedTable({
account,
earningsPeriod,
setEarningsPeriod,
+ chainBlockData,
}: SuppliedMorphoBlueGroupedTableProps) {
const [expandedRows, setExpandedRows] = useState>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
@@ -130,7 +132,6 @@ export function SuppliedMorphoBlueGroupedTable({
}, [account, address]);
const periodLabels: Record = {
- all: 'All Time',
day: '1D',
week: '7D',
month: '30D',
diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx
index 45bd2c33..a42ed926 100644
--- a/src/features/positions/positions-view.tsx
+++ b/src/features/positions/positions-view.tsx
@@ -34,6 +34,8 @@ export default function Positions() {
positions: marketPositions,
refetch,
loadingStates,
+ isTruncated,
+ actualBlockData,
} = useUserPositionsSummaryData(account, earningsPeriod);
// Fetch user's auto vaults
@@ -52,7 +54,6 @@ export default function Positions() {
const loadingMessage = useMemo(() => {
if (isMarketsLoading) return 'Loading markets...';
if (loadingStates.positions) return 'Loading user positions...';
- if (loadingStates.blocks) return 'Fetching block numbers...';
if (loadingStates.snapshots) return 'Loading historical snapshots...';
if (loadingStates.transactions) return 'Loading transaction history...';
return 'Loading...';
@@ -103,15 +104,40 @@ export default function Positions() {
{/* Morpho Blue Positions Section */}
{!loading && hasSuppliedMarkets && (
- void refetch()}
- isRefetching={isRefetching}
- isLoadingEarnings={isEarningsLoading}
- earningsPeriod={earningsPeriod}
- setEarningsPeriod={setEarningsPeriod}
- />
+ <>
+ {/* Data Truncation Warning */}
+ {isTruncated && (
+
+
+
⚠️
+
+
Transaction history exceeds 1,000 entries
+
+ Earnings calculations may be incomplete.{' '}
+
+ Use Position Report
+ {' '}
+ for complete and accurate data.
+
+
+
+
+ )}
+
+ void refetch()}
+ isRefetching={isRefetching}
+ isLoadingEarnings={isEarningsLoading}
+ earningsPeriod={earningsPeriod}
+ setEarningsPeriod={setEarningsPeriod}
+ chainBlockData={actualBlockData}
+ />
+ >
)}
{/* Auto Vaults Section (progressive loading) */}
diff --git a/src/hooks/queries/usePositionSnapshotsQuery.ts b/src/hooks/queries/usePositionSnapshotsQuery.ts
new file mode 100644
index 00000000..ac0f2d0c
--- /dev/null
+++ b/src/hooks/queries/usePositionSnapshotsQuery.ts
@@ -0,0 +1,60 @@
+import { useQuery } from '@tanstack/react-query';
+import type { Address } from 'viem';
+import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
+import type { SupportedNetworks } from '@/utils/networks';
+import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions';
+import { getClient } from '@/utils/rpc';
+
+type PositionSnapshotsQueryOptions = {
+ /** User address to fetch snapshots for */
+ user: Address;
+ /** Chain ID where positions exist */
+ chainId: SupportedNetworks;
+ /** Market unique keys to fetch snapshots for */
+ marketIds: string[];
+ /** Block number to fetch snapshots at */
+ blockNumber: number;
+ /** Whether to enable the query (default: true) */
+ enabled?: boolean;
+};
+
+/**
+ * Fetches position snapshots at a specific block number using React Query.
+ *
+ * This hook fetches historical balances for user positions at a specific block,
+ * which is essential for calculating earnings over a time period.
+ *
+ * Cache behavior:
+ * - staleTime: 5 minutes (historical snapshots don't change)
+ * - Only runs when marketIds are provided and blockNumber > 0
+ *
+ * @example
+ * ```tsx
+ * const { data: snapshots, isLoading } = usePositionSnapshotsQuery({
+ * user: '0x...' as Address,
+ * chainId: SupportedNetworks.Mainnet,
+ * marketIds: ['market1', 'market2'],
+ * blockNumber: 19000000,
+ * });
+ *
+ * const snapshot = snapshots?.get('market1');
+ * const pastBalance = snapshot ? BigInt(snapshot.supplyAssets) : 0n;
+ * ```
+ */
+export const usePositionSnapshotsQuery = (options: PositionSnapshotsQueryOptions) => {
+ const { user, chainId, marketIds, blockNumber, enabled = true } = options;
+ const { customRpcUrls } = useCustomRpcContext();
+
+ return useQuery
+ {report.startBlock && report.endBlock && (
+
+ Calculated from block {report.startBlock.toLocaleString()} to {report.endBlock.toLocaleString()}
+
+ )}
diff --git a/src/features/positions-report/positions-report-view.tsx b/src/features/positions-report/positions-report-view.tsx
index 134ea1ff..2fde0e9b 100644
--- a/src/features/positions-report/positions-report-view.tsx
+++ b/src/features/positions-report/positions-report-view.tsx
@@ -24,9 +24,9 @@ type ReportState = {
};
export default function ReportContent({ account }: { account: Address }) {
- // Fetch ALL positions including closed ones (onlySupplied: false)
+ // Fetch ALL positions including closed ones (showEmpty: true)
// This ensures report includes markets that were active during the selected period
- const { loading, data: positions } = useUserPositions(account, false);
+ const { loading, data: positions } = useUserPositions(account, true);
const [selectedAsset, setSelectedAsset] = useState
(null);
// Get today's date and 2 months ago
diff --git a/src/features/positions/components/collateral-icons-display.tsx b/src/features/positions/components/collateral-icons-display.tsx
index ba8e40ba..2524988b 100644
--- a/src/features/positions/components/collateral-icons-display.tsx
+++ b/src/features/positions/components/collateral-icons-display.tsx
@@ -83,7 +83,7 @@ type CollateralIconsDisplayProps = {
*/
export function CollateralIconsDisplay({ collaterals, chainId, maxDisplay = 8, iconSize = 20 }: CollateralIconsDisplayProps) {
if (collaterals.length === 0) {
- return No known collaterals;
+ return - ;
}
// Sort by amount descending
diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx
index a42ed926..cf8a79bd 100644
--- a/src/features/positions/positions-view.tsx
+++ b/src/features/positions/positions-view.tsx
@@ -34,7 +34,6 @@ export default function Positions() {
positions: marketPositions,
refetch,
loadingStates,
- isTruncated,
actualBlockData,
} = useUserPositionsSummaryData(account, earningsPeriod);
@@ -104,40 +103,16 @@ export default function Positions() {
{/* Morpho Blue Positions Section */}
{!loading && hasSuppliedMarkets && (
- <>
- {/* Data Truncation Warning */}
- {isTruncated && (
-
-
-
⚠️
-
-
Transaction history exceeds 1,000 entries
-
- Earnings calculations may be incomplete.{' '}
-
- Use Position Report
- {' '}
- for complete and accurate data.
-
-
-
-
- )}
-
- void refetch()}
- isRefetching={isRefetching}
- isLoadingEarnings={isEarningsLoading}
- earningsPeriod={earningsPeriod}
- setEarningsPeriod={setEarningsPeriod}
- chainBlockData={actualBlockData}
- />
- >
+ void refetch()}
+ isRefetching={isRefetching}
+ isLoadingEarnings={isEarningsLoading}
+ earningsPeriod={earningsPeriod}
+ setEarningsPeriod={setEarningsPeriod}
+ chainBlockData={actualBlockData}
+ />
)}
{/* Auto Vaults Section (progressive loading) */}
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 3000fb3e..697f6a62 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -301,6 +301,7 @@ export const subgraphUserTransactionsQuery = `
$skip: Int!
$timestamp_gt: BigInt! # Always filter from timestamp 0
$timestamp_lt: BigInt! # Always filter up to current time
+ $market_in: [Bytes!] # Optional market filter
) {
account(id: $userId) {
deposits(
@@ -311,6 +312,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
+ market_in: $market_in
}
) {
id
@@ -331,6 +333,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
+ market_in: $market_in
}
) {
id
@@ -351,6 +354,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
+ market_in: $market_in
}
) {
id
@@ -370,6 +374,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
+ market_in: $market_in
}
) {
id
@@ -389,6 +394,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
+ market_in: $market_in
}
) {
id
diff --git a/src/hooks/queries/useBlocksAtTimestamp.ts b/src/hooks/queries/useBlocksAtTimestamp.ts
new file mode 100644
index 00000000..aead3c13
--- /dev/null
+++ b/src/hooks/queries/useBlocksAtTimestamp.ts
@@ -0,0 +1,94 @@
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import type { SupportedNetworks } from '@/utils/networks';
+import { getClient } from '@/utils/rpc';
+import { estimateBlockAtTimestamp } from '@/utils/blockEstimation';
+
+/**
+ * Estimates and fetches blocks at a specific timestamp across multiple chains.
+ *
+ * Two-step process:
+ * 1. Client-side estimation using estimateBlockAtTimestamp (instant)
+ * 2. On-chain verification by fetching actual block timestamps
+ *
+ * This ensures accurate time ranges for historical queries while providing
+ * fast initial estimates.
+ *
+ * Cache behavior:
+ * - staleTime: 5 minutes (historical blocks don't change)
+ * - gcTime: 10 minutes
+ * - Only runs when currentBlocks are available
+ *
+ * @param timestamp - Target timestamp (seconds since epoch)
+ * @param chainIds - Array of chain IDs to estimate blocks for
+ * @param currentBlocks - Current block numbers from useCurrentBlocks
+ * @param customRpcUrls - Optional custom RPC URLs by chain ID
+ * @returns React Query result with Record
+ *
+ * @example
+ * ```tsx
+ * const oneDayAgo = Math.floor(Date.now() / 1000) - 86400;
+ * const { data: blocks } = useBlocksAtTimestamp(
+ * oneDayAgo,
+ * [1, 8453],
+ * currentBlocks,
+ * customRpcUrls
+ * );
+ * // blocks = {
+ * // 1: { block: 12340000, timestamp: 1234567890 },
+ * // 8453: { block: 9870000, timestamp: 1234567892 }
+ * // }
+ * ```
+ */
+export const useBlocksAtTimestamp = (
+ timestamp: number,
+ chainIds: SupportedNetworks[],
+ currentBlocks: Record | undefined,
+ customRpcUrls?: Record,
+) => {
+ // Step 1: Client-side estimation (instant, no RPC calls)
+ const estimatedBlocks = useMemo(() => {
+ if (!currentBlocks) return {};
+
+ const blocks: Record = {};
+ chainIds.forEach((chainId) => {
+ const currentBlock = currentBlocks[chainId];
+ if (currentBlock) {
+ blocks[chainId] = estimateBlockAtTimestamp(chainId, timestamp, currentBlock);
+ }
+ });
+
+ return blocks;
+ }, [timestamp, chainIds, currentBlocks]);
+
+ // Step 2: Fetch actual blocks to verify timestamps
+ return useQuery({
+ queryKey: ['blocks-at-timestamp', timestamp, chainIds.sort().join(',')],
+ queryFn: async () => {
+ const blockData: Record = {};
+
+ await Promise.all(
+ Object.entries(estimatedBlocks).map(async ([chainId, blockNum]) => {
+ try {
+ const client = getClient(
+ Number(chainId) as SupportedNetworks,
+ customRpcUrls?.[Number(chainId) as SupportedNetworks],
+ );
+ const block = await client.getBlock({ blockNumber: BigInt(blockNum) });
+ blockData[Number(chainId)] = {
+ block: blockNum,
+ timestamp: Number(block.timestamp),
+ };
+ } catch (error) {
+ console.error(`Failed to get block ${blockNum} on chain ${chainId}:`, error);
+ }
+ }),
+ );
+
+ return blockData;
+ },
+ enabled: !!currentBlocks && Object.keys(estimatedBlocks).length > 0,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 10 * 60 * 1000, // 10 minutes
+ });
+};
diff --git a/src/hooks/queries/useCurrentBlocks.ts b/src/hooks/queries/useCurrentBlocks.ts
new file mode 100644
index 00000000..4bf7fbb5
--- /dev/null
+++ b/src/hooks/queries/useCurrentBlocks.ts
@@ -0,0 +1,55 @@
+import { useQuery } from '@tanstack/react-query';
+import type { SupportedNetworks } from '@/utils/networks';
+import { getClient } from '@/utils/rpc';
+
+/**
+ * Fetches current block numbers for the specified chains.
+ *
+ * This hook provides real-time block numbers that can be used for:
+ * - Block estimation calculations
+ * - Snapshot queries at current state
+ * - Time-to-block conversions
+ *
+ * Cache behavior:
+ * - staleTime: 30 seconds (blocks change frequently)
+ * - gcTime: 60 seconds
+ * - Refetches automatically when chains change
+ *
+ * @param chainIds - Array of chain IDs to fetch blocks for
+ * @param customRpcUrls - Optional custom RPC URLs by chain ID
+ * @returns React Query result with Record
+ *
+ * @example
+ * ```tsx
+ * const { data: currentBlocks } = useCurrentBlocks([1, 8453], customRpcUrls);
+ * // currentBlocks = { 1: 12345678, 8453: 9876543 }
+ * ```
+ */
+export const useCurrentBlocks = (
+ chainIds: SupportedNetworks[],
+ customRpcUrls?: Record,
+) => {
+ return useQuery({
+ queryKey: ['current-blocks', chainIds.sort().join(',')],
+ queryFn: async () => {
+ const blocks: Record = {};
+
+ await Promise.all(
+ chainIds.map(async (chainId) => {
+ try {
+ const client = getClient(chainId, customRpcUrls?.[chainId]);
+ const blockNumber = await client.getBlockNumber();
+ blocks[chainId] = Number(blockNumber);
+ } catch (error) {
+ console.error(`Failed to get current block for chain ${chainId}:`, error);
+ }
+ }),
+ );
+
+ return blocks;
+ },
+ enabled: chainIds.length > 0,
+ staleTime: 30_000, // 30 seconds
+ gcTime: 60_000, // 1 minute
+ });
+};
diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts
index d9b43934..862be16e 100644
--- a/src/hooks/queries/useUserTransactionsQuery.ts
+++ b/src/hooks/queries/useUserTransactionsQuery.ts
@@ -7,7 +7,7 @@ type UseUserTransactionsQueryOptions = {
/**
* When true, automatically paginates to fetch ALL transactions.
* Use for report generation when complete accuracy is needed.
- * When false (default), fetches up to 1000 transactions and returns isTruncated flag.
+ * When false (default), fetches up to 1000 transactions (single page).
* Use for summary pages when speed is prioritized.
*/
paginate?: boolean;
@@ -15,10 +15,7 @@ type UseUserTransactionsQueryOptions = {
pageSize?: number;
};
-type TransactionQueryResult = TransactionResponse & {
- /** Indicates if data was truncated due to pagination limits */
- isTruncated: boolean;
-};
+type TransactionQueryResult = TransactionResponse;
/**
* Fetches user transactions from Morpho API or Subgraph using React Query.
@@ -37,7 +34,7 @@ type TransactionQueryResult = TransactionResponse & {
*
* @example
* ```tsx
- * // Summary page (fast, may be truncated)
+ * // Summary page (fast, single page)
* const { data, isLoading } = useUserTransactionsQuery({
* filters: {
* userAddress: ['0x...'],
@@ -45,11 +42,8 @@ type TransactionQueryResult = TransactionResponse & {
* },
* paginate: false,
* });
- * if (data?.isTruncated) {
- * // Show warning to user
- * }
*
- * // Report page (complete data)
+ * // Report page (complete data with auto-pagination)
* const { data } = useUserTransactionsQuery({
* filters: {
* userAddress: ['0x...'],
@@ -80,18 +74,10 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
queryFn: async () => {
if (!paginate) {
// Simple case: fetch once with limit
- const response = await fetchUserTransactions({
+ return await fetchUserTransactions({
...filters,
first: pageSize,
});
-
- // Check if data was truncated (fetched items equals page size, likely more exist)
- const isTruncated = response.items.length >= pageSize || response.pageInfo.countTotal > response.items.length;
-
- return {
- ...response,
- isTruncated,
- };
}
// Pagination mode: fetch all data across multiple requests
@@ -126,7 +112,6 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
countTotal: allItems.length,
},
error: null,
- isTruncated: false, // We fetched everything
};
},
enabled: enabled && filters.userAddress.length > 0,
diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts
index 63adadf7..6c92129e 100644
--- a/src/hooks/usePositionReport.ts
+++ b/src/hooks/usePositionReport.ts
@@ -28,6 +28,10 @@ export type ReportSummary = {
period: number;
marketReports: PositionReport[];
groupedEarnings: EarningsCalculation;
+ startBlock: number;
+ endBlock: number;
+ startTimestamp: number;
+ endTimestamp: number;
};
export const usePositionReport = (
@@ -182,6 +186,10 @@ export const usePositionReport = (
period,
marketReports,
groupedEarnings,
+ startBlock: startBlockEstimate,
+ endBlock: endBlockEstimate,
+ startTimestamp: actualStartTimestamp,
+ endTimestamp: actualEndTimestamp,
};
};
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index d9d2256e..a61485fe 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -38,7 +38,8 @@ export const positionKeys = {
snapshot: (marketKey: string, userAddress: string, chainId: number) =>
[...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const,
// Key for the final enhanced position data, dependent on initialData result
- enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined) =>
+ // marketsCount triggers re-fetch when markets finish loading
+ enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined, marketsCount: number) =>
[
'enhanced-positions',
user,
@@ -46,6 +47,7 @@ export const positionKeys = {
.map((k) => `${k.marketUniqueKey.toLowerCase()}-${k.chainId}`)
.sort()
.join(','),
+ marketsCount,
] as const,
};
@@ -67,6 +69,8 @@ const fetchSourceMarketKeys = async (user: string, chainIds?: SupportedNetworks[
try {
console.log(`Attempting to fetch positions via Morpho API for network ${network}`);
markets = await fetchMorphoUserPositionMarkets(user, network);
+
+ console.log('Fetched market keys for network', network, markets.length)
} catch (morphoError) {
console.error(`Failed to fetch positions via Morpho API for network ${network}:`, morphoError);
// Continue to Subgraph fallback
@@ -140,7 +144,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
// console.log(`[Positions] Query 1: Final unique keys count: ${finalMarketKeys.length}`);
return { finalMarketKeys };
},
- enabled: !!user && allMarkets.length > 0,
+ enabled: !!user,
staleTime: 0,
});
@@ -150,7 +154,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
isLoading: isLoadingEnhanced,
isRefetching: isRefetchingEnhanced,
} = useQuery({
- queryKey: positionKeys.enhanced(user, initialData),
+ queryKey: positionKeys.enhanced(user, initialData, allMarkets.length),
queryFn: async () => {
if (!initialData || !user) throw new Error('Assertion failed: initialData/user should be defined here.');
@@ -166,6 +170,8 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
marketsByChain.set(marketInfo.chainId, existing);
});
+ console.log('All markets by chain', marketsByChain)
+
// Build market data map from allMarkets context (no need to fetch individually)
const marketDataMap = new Map();
allMarkets.forEach((market) => {
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index 71c29f84..2586073e 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { Address } from 'viem';
-import { estimateBlockAtTimestamp } from '@/utils/blockEstimation';
import type { SupportedNetworks } from '@/utils/networks';
import { getClient } from '@/utils/rpc';
import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions';
@@ -10,6 +9,8 @@ import { useUserTransactionsQuery } from './queries/useUserTransactionsQuery';
import { usePositionsWithEarnings, getPeriodTimestamp } from './usePositionsWithEarnings';
import type { EarningsPeriod } from '@/stores/usePositionsFilters';
import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
+import { useCurrentBlocks } from './queries/useCurrentBlocks';
+import { useBlocksAtTimestamp } from './queries/useBlocksAtTimestamp';
// Re-export EarningsPeriod for backward compatibility
export type { EarningsPeriod } from '@/stores/usePositionsFilters';
@@ -18,76 +19,19 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
const queryClient = useQueryClient();
const { customRpcUrls } = useCustomRpcContext();
- const { data: positions, loading: positionsLoading, isRefetching, positionsError } = useUserPositions(user, true, chainIds);
+ // Only fetch opened positions
+ const { data: positions, loading: positionsLoading, isRefetching, positionsError } = useUserPositions(user, false, chainIds);
+
const uniqueChainIds = useMemo(
() => chainIds ?? [...new Set(positions?.map((p) => p.market.morphoBlue.chain.id as SupportedNetworks) ?? [])],
[chainIds, positions],
);
- const { data: currentBlocks } = useQuery({
- queryKey: ['current-blocks', uniqueChainIds],
- queryFn: async () => {
- const blocks: Record = {};
- await Promise.all(
- uniqueChainIds.map(async (chainId) => {
- try {
- const client = getClient(chainId, customRpcUrls[chainId]);
- const blockNumber = await client.getBlockNumber();
- blocks[chainId] = Number(blockNumber);
- } catch (error) {
- console.error(`Failed to get current block for chain ${chainId}:`, error);
- }
- }),
- );
- return blocks;
- },
- enabled: uniqueChainIds.length > 0,
- staleTime: 30_000,
- gcTime: 60_000,
- });
-
- const snapshotBlocks = useMemo(() => {
- if (!currentBlocks) return {};
-
- const timestamp = getPeriodTimestamp(period);
- const blocks: Record = {};
-
- uniqueChainIds.forEach((chainId) => {
- const currentBlock = currentBlocks[chainId];
- if (currentBlock) {
- blocks[chainId] = estimateBlockAtTimestamp(chainId, timestamp, currentBlock);
- }
- });
-
- return blocks;
- }, [period, uniqueChainIds, currentBlocks]);
+ // Use extracted block hooks for cleaner code
+ const { data: currentBlocks } = useCurrentBlocks(uniqueChainIds, customRpcUrls);
- const { data: actualBlockData } = useQuery({
- queryKey: ['block-timestamps', snapshotBlocks],
- queryFn: async () => {
- const blockData: Record = {};
-
- await Promise.all(
- Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
- try {
- const client = getClient(Number(chainId) as SupportedNetworks, customRpcUrls[Number(chainId) as SupportedNetworks]);
- const block = await client.getBlock({ blockNumber: BigInt(blockNum) });
- blockData[Number(chainId)] = {
- block: blockNum,
- timestamp: Number(block.timestamp),
- };
- } catch (error) {
- console.error(`Failed to get block ${blockNum} on chain ${chainId}:`, error);
- }
- }),
- );
-
- return blockData;
- },
- enabled: Object.keys(snapshotBlocks).length > 0,
- staleTime: 5 * 60 * 1000,
- gcTime: 10 * 60 * 1000,
- });
+ const periodTimestamp = useMemo(() => getPeriodTimestamp(period), [period]);
+ const { data: actualBlockData } = useBlocksAtTimestamp(periodTimestamp, uniqueChainIds, currentBlocks, customRpcUrls);
const endTimestamp = useMemo(() => Math.floor(Date.now() / 1000), []);
@@ -97,19 +41,19 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
marketUniqueKeys: positions?.map((p) => p.market.uniqueKey),
chainIds: uniqueChainIds,
},
- paginate: false,
+ paginate: true, // Always fetch all transactions for accuracy
enabled: !!positions && !!user,
});
const { data: allSnapshots, isLoading: isLoadingSnapshots } = useQuery({
- queryKey: ['all-position-snapshots', snapshotBlocks, user, positions?.map((p) => p.market.uniqueKey)],
+ queryKey: ['all-position-snapshots', actualBlockData, user, positions?.map((p) => p.market.uniqueKey)],
queryFn: async () => {
- if (!positions || !user) return {};
+ if (!positions || !user || !actualBlockData) return {};
const snapshotsByChain: Record> = {};
await Promise.all(
- Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
+ Object.entries(actualBlockData).map(async ([chainId, blockData]) => {
const chainIdNum = Number(chainId);
const chainPositions = positions.filter((p) => p.market.morphoBlue.chain.id === chainIdNum);
@@ -118,7 +62,7 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
const client = getClient(chainIdNum as SupportedNetworks, customRpcUrls[chainIdNum as SupportedNetworks]);
const marketIds = chainPositions.map((p) => p.market.uniqueKey);
- const snapshots = await fetchPositionsSnapshots(marketIds, user as Address, chainIdNum, blockNum, client);
+ const snapshots = await fetchPositionsSnapshots(marketIds, user as Address, chainIdNum, blockData.block, client);
snapshotsByChain[chainIdNum] = snapshots;
}),
@@ -126,7 +70,7 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
return snapshotsByChain;
},
- enabled: !!positions && !!user && Object.keys(snapshotBlocks).length > 0,
+ enabled: !!positions && !!user && !!actualBlockData && Object.keys(actualBlockData).length > 0,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});
@@ -179,7 +123,6 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
isPositionsLoading: positionsLoading,
isEarningsLoading,
isRefetching,
- isTruncated: txData?.isTruncated ?? false,
error: positionsError,
refetch,
loadingStates,
From b2a5a7d170498003c52d5119e2627bc972d8e0ad Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 17:32:23 +0800
Subject: [PATCH 03/11] chore: test
---
src/data-sources/morpho-api/transactions.ts | 8 +-
.../positions-report-view.tsx | 4 +-
.../supplied-morpho-blue-grouped-table.tsx | 13 +++
src/features/positions/positions-view.tsx | 2 +
src/hooks/queries/useBlocksAtTimestamp.ts | 94 -------------------
src/hooks/queries/useCurrentBlocks.ts | 55 -----------
src/hooks/queries/useUserTransactionsQuery.ts | 27 +++++-
src/hooks/useUserPositions.ts | 10 +-
src/hooks/useUserPositionsSummaryData.ts | 85 ++++++++++++++---
9 files changed, 119 insertions(+), 179 deletions(-)
delete mode 100644 src/hooks/queries/useBlocksAtTimestamp.ts
delete mode 100644 src/hooks/queries/useCurrentBlocks.ts
diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts
index 7d91402d..1dc0c6be 100644
--- a/src/data-sources/morpho-api/transactions.ts
+++ b/src/data-sources/morpho-api/transactions.ts
@@ -19,9 +19,9 @@ export const fetchMorphoTransactions = async (filters: TransactionFilters): Prom
chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet],
};
- if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
- whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
- }
+ // if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
+ // whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
+ // }
if (filters.timestampGte !== undefined && filters.timestampGte !== null) {
whereClause.timestamp_gte = filters.timestampGte;
}
@@ -36,6 +36,8 @@ export const fetchMorphoTransactions = async (filters: TransactionFilters): Prom
}
try {
+ console.log('try', whereClause)
+
const result = await morphoGraphqlFetcher(userTransactionsQuery, {
where: whereClause,
first: filters.first ?? 1000,
diff --git a/src/features/positions-report/positions-report-view.tsx b/src/features/positions-report/positions-report-view.tsx
index 2fde0e9b..134ea1ff 100644
--- a/src/features/positions-report/positions-report-view.tsx
+++ b/src/features/positions-report/positions-report-view.tsx
@@ -24,9 +24,9 @@ type ReportState = {
};
export default function ReportContent({ account }: { account: Address }) {
- // Fetch ALL positions including closed ones (showEmpty: true)
+ // Fetch ALL positions including closed ones (onlySupplied: false)
// This ensures report includes markets that were active during the selected period
- const { loading, data: positions } = useUserPositions(account, true);
+ const { loading, data: positions } = useUserPositions(account, false);
const [selectedAsset, setSelectedAsset] = useState(null);
// Get today's date and 2 months ago
diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
index 9d289fed..b617160d 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -102,6 +102,7 @@ type SuppliedMorphoBlueGroupedTableProps = {
earningsPeriod: EarningsPeriod;
setEarningsPeriod: (period: EarningsPeriod) => void;
chainBlockData?: Record;
+ isTruncated?: boolean;
};
export function SuppliedMorphoBlueGroupedTable({
@@ -113,6 +114,7 @@ export function SuppliedMorphoBlueGroupedTable({
earningsPeriod,
setEarningsPeriod,
chainBlockData,
+ isTruncated,
}: SuppliedMorphoBlueGroupedTableProps) {
const [expandedRows, setExpandedRows] = useState>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
@@ -307,6 +309,17 @@ export function SuppliedMorphoBlueGroupedTable({
margin={3}
/>
+ ) : isTruncated ? (
+
+ }
+ >
+ -
+
) : (
{(() => {
diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx
index cf8a79bd..19f2f488 100644
--- a/src/features/positions/positions-view.tsx
+++ b/src/features/positions/positions-view.tsx
@@ -34,6 +34,7 @@ export default function Positions() {
positions: marketPositions,
refetch,
loadingStates,
+ isTruncated,
actualBlockData,
} = useUserPositionsSummaryData(account, earningsPeriod);
@@ -112,6 +113,7 @@ export default function Positions() {
earningsPeriod={earningsPeriod}
setEarningsPeriod={setEarningsPeriod}
chainBlockData={actualBlockData}
+ isTruncated={isTruncated}
/>
)}
diff --git a/src/hooks/queries/useBlocksAtTimestamp.ts b/src/hooks/queries/useBlocksAtTimestamp.ts
deleted file mode 100644
index aead3c13..00000000
--- a/src/hooks/queries/useBlocksAtTimestamp.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { useMemo } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import type { SupportedNetworks } from '@/utils/networks';
-import { getClient } from '@/utils/rpc';
-import { estimateBlockAtTimestamp } from '@/utils/blockEstimation';
-
-/**
- * Estimates and fetches blocks at a specific timestamp across multiple chains.
- *
- * Two-step process:
- * 1. Client-side estimation using estimateBlockAtTimestamp (instant)
- * 2. On-chain verification by fetching actual block timestamps
- *
- * This ensures accurate time ranges for historical queries while providing
- * fast initial estimates.
- *
- * Cache behavior:
- * - staleTime: 5 minutes (historical blocks don't change)
- * - gcTime: 10 minutes
- * - Only runs when currentBlocks are available
- *
- * @param timestamp - Target timestamp (seconds since epoch)
- * @param chainIds - Array of chain IDs to estimate blocks for
- * @param currentBlocks - Current block numbers from useCurrentBlocks
- * @param customRpcUrls - Optional custom RPC URLs by chain ID
- * @returns React Query result with Record
- *
- * @example
- * ```tsx
- * const oneDayAgo = Math.floor(Date.now() / 1000) - 86400;
- * const { data: blocks } = useBlocksAtTimestamp(
- * oneDayAgo,
- * [1, 8453],
- * currentBlocks,
- * customRpcUrls
- * );
- * // blocks = {
- * // 1: { block: 12340000, timestamp: 1234567890 },
- * // 8453: { block: 9870000, timestamp: 1234567892 }
- * // }
- * ```
- */
-export const useBlocksAtTimestamp = (
- timestamp: number,
- chainIds: SupportedNetworks[],
- currentBlocks: Record | undefined,
- customRpcUrls?: Record,
-) => {
- // Step 1: Client-side estimation (instant, no RPC calls)
- const estimatedBlocks = useMemo(() => {
- if (!currentBlocks) return {};
-
- const blocks: Record = {};
- chainIds.forEach((chainId) => {
- const currentBlock = currentBlocks[chainId];
- if (currentBlock) {
- blocks[chainId] = estimateBlockAtTimestamp(chainId, timestamp, currentBlock);
- }
- });
-
- return blocks;
- }, [timestamp, chainIds, currentBlocks]);
-
- // Step 2: Fetch actual blocks to verify timestamps
- return useQuery({
- queryKey: ['blocks-at-timestamp', timestamp, chainIds.sort().join(',')],
- queryFn: async () => {
- const blockData: Record = {};
-
- await Promise.all(
- Object.entries(estimatedBlocks).map(async ([chainId, blockNum]) => {
- try {
- const client = getClient(
- Number(chainId) as SupportedNetworks,
- customRpcUrls?.[Number(chainId) as SupportedNetworks],
- );
- const block = await client.getBlock({ blockNumber: BigInt(blockNum) });
- blockData[Number(chainId)] = {
- block: blockNum,
- timestamp: Number(block.timestamp),
- };
- } catch (error) {
- console.error(`Failed to get block ${blockNum} on chain ${chainId}:`, error);
- }
- }),
- );
-
- return blockData;
- },
- enabled: !!currentBlocks && Object.keys(estimatedBlocks).length > 0,
- staleTime: 5 * 60 * 1000, // 5 minutes
- gcTime: 10 * 60 * 1000, // 10 minutes
- });
-};
diff --git a/src/hooks/queries/useCurrentBlocks.ts b/src/hooks/queries/useCurrentBlocks.ts
deleted file mode 100644
index 4bf7fbb5..00000000
--- a/src/hooks/queries/useCurrentBlocks.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import type { SupportedNetworks } from '@/utils/networks';
-import { getClient } from '@/utils/rpc';
-
-/**
- * Fetches current block numbers for the specified chains.
- *
- * This hook provides real-time block numbers that can be used for:
- * - Block estimation calculations
- * - Snapshot queries at current state
- * - Time-to-block conversions
- *
- * Cache behavior:
- * - staleTime: 30 seconds (blocks change frequently)
- * - gcTime: 60 seconds
- * - Refetches automatically when chains change
- *
- * @param chainIds - Array of chain IDs to fetch blocks for
- * @param customRpcUrls - Optional custom RPC URLs by chain ID
- * @returns React Query result with Record
- *
- * @example
- * ```tsx
- * const { data: currentBlocks } = useCurrentBlocks([1, 8453], customRpcUrls);
- * // currentBlocks = { 1: 12345678, 8453: 9876543 }
- * ```
- */
-export const useCurrentBlocks = (
- chainIds: SupportedNetworks[],
- customRpcUrls?: Record,
-) => {
- return useQuery({
- queryKey: ['current-blocks', chainIds.sort().join(',')],
- queryFn: async () => {
- const blocks: Record = {};
-
- await Promise.all(
- chainIds.map(async (chainId) => {
- try {
- const client = getClient(chainId, customRpcUrls?.[chainId]);
- const blockNumber = await client.getBlockNumber();
- blocks[chainId] = Number(blockNumber);
- } catch (error) {
- console.error(`Failed to get current block for chain ${chainId}:`, error);
- }
- }),
- );
-
- return blocks;
- },
- enabled: chainIds.length > 0,
- staleTime: 30_000, // 30 seconds
- gcTime: 60_000, // 1 minute
- });
-};
diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts
index 862be16e..676f74e1 100644
--- a/src/hooks/queries/useUserTransactionsQuery.ts
+++ b/src/hooks/queries/useUserTransactionsQuery.ts
@@ -7,7 +7,7 @@ type UseUserTransactionsQueryOptions = {
/**
* When true, automatically paginates to fetch ALL transactions.
* Use for report generation when complete accuracy is needed.
- * When false (default), fetches up to 1000 transactions (single page).
+ * When false (default), fetches up to 1000 transactions and returns isTruncated flag.
* Use for summary pages when speed is prioritized.
*/
paginate?: boolean;
@@ -15,7 +15,10 @@ type UseUserTransactionsQueryOptions = {
pageSize?: number;
};
-type TransactionQueryResult = TransactionResponse;
+type TransactionQueryResult = TransactionResponse & {
+ /** Indicates if data was truncated due to pagination limits */
+ isTruncated: boolean;
+};
/**
* Fetches user transactions from Morpho API or Subgraph using React Query.
@@ -34,7 +37,7 @@ type TransactionQueryResult = TransactionResponse;
*
* @example
* ```tsx
- * // Summary page (fast, single page)
+ * // Summary page (fast, may be truncated)
* const { data, isLoading } = useUserTransactionsQuery({
* filters: {
* userAddress: ['0x...'],
@@ -42,8 +45,11 @@ type TransactionQueryResult = TransactionResponse;
* },
* paginate: false,
* });
+ * if (data?.isTruncated) {
+ * // Show warning to user
+ * }
*
- * // Report page (complete data with auto-pagination)
+ * // Report page (complete data)
* const { data } = useUserTransactionsQuery({
* filters: {
* userAddress: ['0x...'],
@@ -74,10 +80,20 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
queryFn: async () => {
if (!paginate) {
// Simple case: fetch once with limit
- return await fetchUserTransactions({
+ const response = await fetchUserTransactions({
...filters,
first: pageSize,
});
+
+ // Check if data was truncated
+ // Since we're now filtering at GraphQL level, if we get exactly pageSize items,
+ // there might be more available
+ const isTruncated = response.items.length >= pageSize;
+
+ return {
+ ...response,
+ isTruncated,
+ };
}
// Pagination mode: fetch all data across multiple requests
@@ -112,6 +128,7 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
countTotal: allItems.length,
},
error: null,
+ isTruncated: false, // We fetched everything
};
},
enabled: enabled && filters.userAddress.length > 0,
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index a61485fe..201d22c3 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -38,8 +38,7 @@ export const positionKeys = {
snapshot: (marketKey: string, userAddress: string, chainId: number) =>
[...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const,
// Key for the final enhanced position data, dependent on initialData result
- // marketsCount triggers re-fetch when markets finish loading
- enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined, marketsCount: number) =>
+ enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined) =>
[
'enhanced-positions',
user,
@@ -47,7 +46,6 @@ export const positionKeys = {
.map((k) => `${k.marketUniqueKey.toLowerCase()}-${k.chainId}`)
.sort()
.join(','),
- marketsCount,
] as const,
};
@@ -69,8 +67,6 @@ const fetchSourceMarketKeys = async (user: string, chainIds?: SupportedNetworks[
try {
console.log(`Attempting to fetch positions via Morpho API for network ${network}`);
markets = await fetchMorphoUserPositionMarkets(user, network);
-
- console.log('Fetched market keys for network', network, markets.length)
} catch (morphoError) {
console.error(`Failed to fetch positions via Morpho API for network ${network}:`, morphoError);
// Continue to Subgraph fallback
@@ -144,7 +140,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
// console.log(`[Positions] Query 1: Final unique keys count: ${finalMarketKeys.length}`);
return { finalMarketKeys };
},
- enabled: !!user,
+ enabled: !!user && allMarkets.length > 0,
staleTime: 0,
});
@@ -154,7 +150,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
isLoading: isLoadingEnhanced,
isRefetching: isRefetchingEnhanced,
} = useQuery({
- queryKey: positionKeys.enhanced(user, initialData, allMarkets.length),
+ queryKey: positionKeys.enhanced(user, initialData),
queryFn: async () => {
if (!initialData || !user) throw new Error('Assertion failed: initialData/user should be defined here.');
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index 2586073e..696edd59 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { Address } from 'viem';
+import { estimateBlockAtTimestamp } from '@/utils/blockEstimation';
import type { SupportedNetworks } from '@/utils/networks';
import { getClient } from '@/utils/rpc';
import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions';
@@ -9,8 +10,6 @@ import { useUserTransactionsQuery } from './queries/useUserTransactionsQuery';
import { usePositionsWithEarnings, getPeriodTimestamp } from './usePositionsWithEarnings';
import type { EarningsPeriod } from '@/stores/usePositionsFilters';
import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
-import { useCurrentBlocks } from './queries/useCurrentBlocks';
-import { useBlocksAtTimestamp } from './queries/useBlocksAtTimestamp';
// Re-export EarningsPeriod for backward compatibility
export type { EarningsPeriod } from '@/stores/usePositionsFilters';
@@ -27,33 +26,92 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
[chainIds, positions],
);
- // Use extracted block hooks for cleaner code
- const { data: currentBlocks } = useCurrentBlocks(uniqueChainIds, customRpcUrls);
+ const { data: currentBlocks } = useQuery({
+ queryKey: ['current-blocks', uniqueChainIds],
+ queryFn: async () => {
+ const blocks: Record = {};
+ await Promise.all(
+ uniqueChainIds.map(async (chainId) => {
+ try {
+ const client = getClient(chainId, customRpcUrls[chainId]);
+ const blockNumber = await client.getBlockNumber();
+ blocks[chainId] = Number(blockNumber);
+ } catch (error) {
+ console.error(`Failed to get current block for chain ${chainId}:`, error);
+ }
+ }),
+ );
+ return blocks;
+ },
+ enabled: uniqueChainIds.length > 0,
+ staleTime: 30_000,
+ gcTime: 60_000,
+ });
+
+ const snapshotBlocks = useMemo(() => {
+ if (!currentBlocks) return {};
+
+ const timestamp = getPeriodTimestamp(period);
+ const blocks: Record = {};
+
+ uniqueChainIds.forEach((chainId) => {
+ const currentBlock = currentBlocks[chainId];
+ if (currentBlock) {
+ blocks[chainId] = estimateBlockAtTimestamp(chainId, timestamp, currentBlock);
+ }
+ });
+
+ return blocks;
+ }, [period, uniqueChainIds, currentBlocks]);
- const periodTimestamp = useMemo(() => getPeriodTimestamp(period), [period]);
- const { data: actualBlockData } = useBlocksAtTimestamp(periodTimestamp, uniqueChainIds, currentBlocks, customRpcUrls);
+ const { data: actualBlockData } = useQuery({
+ queryKey: ['block-timestamps', snapshotBlocks],
+ queryFn: async () => {
+ const blockData: Record = {};
+
+ await Promise.all(
+ Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
+ try {
+ const client = getClient(Number(chainId) as SupportedNetworks, customRpcUrls[Number(chainId) as SupportedNetworks]);
+ const block = await client.getBlock({ blockNumber: BigInt(blockNum) });
+ blockData[Number(chainId)] = {
+ block: blockNum,
+ timestamp: Number(block.timestamp),
+ };
+ } catch (error) {
+ console.error(`Failed to get block ${blockNum} on chain ${chainId}:`, error);
+ }
+ }),
+ );
+
+ return blockData;
+ },
+ enabled: Object.keys(snapshotBlocks).length > 0,
+ staleTime: 5 * 60 * 1000,
+ gcTime: 10 * 60 * 1000,
+ });
const endTimestamp = useMemo(() => Math.floor(Date.now() / 1000), []);
const { data: txData, isLoading: isLoadingTransactions } = useUserTransactionsQuery({
filters: {
userAddress: user ? [user] : [],
- marketUniqueKeys: positions?.map((p) => p.market.uniqueKey),
+ marketUniqueKeys: positions?.map((p) => p.market.uniqueKey) ?? [],
chainIds: uniqueChainIds,
},
- paginate: true, // Always fetch all transactions for accuracy
+ paginate: false,
enabled: !!positions && !!user,
});
const { data: allSnapshots, isLoading: isLoadingSnapshots } = useQuery({
- queryKey: ['all-position-snapshots', actualBlockData, user, positions?.map((p) => p.market.uniqueKey)],
+ queryKey: ['all-position-snapshots', snapshotBlocks, user, positions?.map((p) => p.market.uniqueKey)],
queryFn: async () => {
- if (!positions || !user || !actualBlockData) return {};
+ if (!positions || !user) return {};
const snapshotsByChain: Record> = {};
await Promise.all(
- Object.entries(actualBlockData).map(async ([chainId, blockData]) => {
+ Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
const chainIdNum = Number(chainId);
const chainPositions = positions.filter((p) => p.market.morphoBlue.chain.id === chainIdNum);
@@ -62,7 +120,7 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
const client = getClient(chainIdNum as SupportedNetworks, customRpcUrls[chainIdNum as SupportedNetworks]);
const marketIds = chainPositions.map((p) => p.market.uniqueKey);
- const snapshots = await fetchPositionsSnapshots(marketIds, user as Address, chainIdNum, blockData.block, client);
+ const snapshots = await fetchPositionsSnapshots(marketIds, user as Address, chainIdNum, blockNum, client);
snapshotsByChain[chainIdNum] = snapshots;
}),
@@ -70,7 +128,7 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
return snapshotsByChain;
},
- enabled: !!positions && !!user && !!actualBlockData && Object.keys(actualBlockData).length > 0,
+ enabled: !!positions && !!user && Object.keys(snapshotBlocks).length > 0,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});
@@ -123,6 +181,7 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
isPositionsLoading: positionsLoading,
isEarningsLoading,
isRefetching,
+ isTruncated: txData?.isTruncated ?? false,
error: positionsError,
refetch,
loadingStates,
From 2c4b37ba439f358d1670acfd1954f1a95d182777 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 18:03:30 +0800
Subject: [PATCH 04/11] fix: history query
---
src/data-sources/morpho-api/transactions.ts | 10 ++++----
src/data-sources/subgraph/transactions.ts | 6 +++--
.../components/asset-selector.tsx | 2 --
.../positions-report-view.tsx | 17 +++----------
src/graphql/morpho-subgraph-queries.ts | 24 +++++++++++--------
src/hooks/usePositionReport.ts | 6 ++---
src/hooks/useUserPositions.ts | 2 +-
7 files changed, 30 insertions(+), 37 deletions(-)
diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts
index 1dc0c6be..674a4241 100644
--- a/src/data-sources/morpho-api/transactions.ts
+++ b/src/data-sources/morpho-api/transactions.ts
@@ -19,9 +19,9 @@ export const fetchMorphoTransactions = async (filters: TransactionFilters): Prom
chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet],
};
- // if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
- // whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
- // }
+ if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
+ whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
+ }
if (filters.timestampGte !== undefined && filters.timestampGte !== null) {
whereClause.timestamp_gte = filters.timestampGte;
}
@@ -32,11 +32,11 @@ export const fetchMorphoTransactions = async (filters: TransactionFilters): Prom
whereClause.hash = filters.hash;
}
if (filters.assetIds && filters.assetIds.length > 0) {
- whereClause.assetId_in = filters.assetIds;
+ whereClause.assetAddress_in = filters.assetIds;
}
try {
- console.log('try', whereClause)
+ console.log('try', whereClause);
const result = await morphoGraphqlFetcher(userTransactionsQuery, {
where: whereClause,
diff --git a/src/data-sources/subgraph/transactions.ts b/src/data-sources/subgraph/transactions.ts
index 7ea5f6ab..5091ed43 100644
--- a/src/data-sources/subgraph/transactions.ts
+++ b/src/data-sources/subgraph/transactions.ts
@@ -1,4 +1,4 @@
-import { subgraphUserTransactionsQuery } from '@/graphql/morpho-subgraph-queries';
+import { getSubgraphUserTransactionsQuery } from '@/graphql/morpho-subgraph-queries';
import type { TransactionFilters, TransactionResponse } from '@/hooks/queries/fetchUserTransactions';
import type { SupportedNetworks } from '@/utils/networks';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
@@ -175,8 +175,10 @@ export const fetchSubgraphTransactions = async (filters: TransactionFilters, net
variables.timestamp_lte = filters.timestampLte;
}
+ const useMarketFilter = variables.market_in !== undefined;
+
const requestBody = {
- query: subgraphUserTransactionsQuery,
+ query: getSubgraphUserTransactionsQuery(useMarketFilter),
variables: variables,
};
diff --git a/src/features/positions-report/components/asset-selector.tsx b/src/features/positions-report/components/asset-selector.tsx
index a17b5ae1..9be12004 100644
--- a/src/features/positions-report/components/asset-selector.tsx
+++ b/src/features/positions-report/components/asset-selector.tsx
@@ -23,8 +23,6 @@ export function AssetSelector({ selectedAsset, assets, onSelect }: AssetSelector
const [query, setQuery] = useState('');
const dropdownRef = useRef(null);
- console.log('query', query);
-
const filteredAssets = assets.filter((asset) => asset.symbol.toLowerCase().includes(query.toLowerCase()));
// Close dropdown when clicking outside
diff --git a/src/features/positions-report/positions-report-view.tsx b/src/features/positions-report/positions-report-view.tsx
index 134ea1ff..944c9ca3 100644
--- a/src/features/positions-report/positions-report-view.tsx
+++ b/src/features/positions-report/positions-report-view.tsx
@@ -26,7 +26,7 @@ type ReportState = {
export default function ReportContent({ account }: { account: Address }) {
// Fetch ALL positions including closed ones (onlySupplied: false)
// This ensures report includes markets that were active during the selected period
- const { loading, data: positions } = useUserPositions(account, false);
+ const { loading, data: positions } = useUserPositions(account, true);
const [selectedAsset, setSelectedAsset] = useState(null);
// Get today's date and 2 months ago
@@ -55,17 +55,6 @@ export default function ReportContent({ account }: { account: Address }) {
// Calculate maximum allowed date (today)
const maxDate = useMemo(() => now(getLocalTimeZone()), []);
- // Check if current inputs match the report state
- const isReportCurrent = useMemo(() => {
- if (!reportState || !selectedAsset) return false;
- return (
- reportState.asset.address === selectedAsset.address &&
- reportState.asset.chainId === selectedAsset.chainId &&
- reportState.startDate.compare(startDate) === 0 &&
- reportState.endDate.compare(endDate) === 0
- );
- }, [reportState, selectedAsset, startDate, endDate]);
-
// Reset report when inputs change
useEffect(() => {
if (!reportState || !selectedAsset) return;
@@ -103,7 +92,7 @@ export default function ReportContent({ account }: { account: Address }) {
// Generate report
const handleGenerateReport = async () => {
- if (!selectedAsset || isGenerating || isReportCurrent) return;
+ if (!selectedAsset || isGenerating) return;
setIsGenerating(true);
try {
@@ -235,7 +224,7 @@ export default function ReportContent({ account }: { account: Address }) {
onClick={() => {
void handleGenerateReport();
}}
- disabled={!selectedAsset || isGenerating || isReportCurrent || !!startDateError || !!endDateError}
+ disabled={!selectedAsset || isGenerating || !!startDateError || !!endDateError}
className="inline-flex h-14 min-w-[120px] items-center gap-2"
variant="primary"
>
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 697f6a62..cf753d95 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -293,15 +293,18 @@ export const subgraphUserMarketPositionQuery = `
`;
// --- End Query ---
-// Note: The exact field names might need adjustment based on the specific Subgraph schema.
-export const subgraphUserTransactionsQuery = `
+export const getSubgraphUserTransactionsQuery = (useMarketFilter: boolean) => {
+ // only append this in where if marketIn is defined
+ const additionalQuery = useMarketFilter ? 'market_in: $market_in' : '';
+
+ return `
query GetUserTransactions(
$userId: ID!
$first: Int!
$skip: Int!
- $timestamp_gt: BigInt! # Always filter from timestamp 0
- $timestamp_lt: BigInt! # Always filter up to current time
- $market_in: [Bytes!] # Optional market filter
+ $timestamp_gt: BigInt!
+ $timestamp_lt: BigInt!
+ ${useMarketFilter ? '$market_in: [Bytes!]}' : ''}
) {
account(id: $userId) {
deposits(
@@ -312,7 +315,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
- market_in: $market_in
+ ${additionalQuery}
}
) {
id
@@ -333,7 +336,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
- market_in: $market_in
+ ${additionalQuery}
}
) {
id
@@ -354,7 +357,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
- market_in: $market_in
+ ${additionalQuery}
}
) {
id
@@ -374,7 +377,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
- market_in: $market_in
+ ${additionalQuery}
}
) {
id
@@ -394,7 +397,7 @@ export const subgraphUserTransactionsQuery = `
where: {
timestamp_gt: $timestamp_gt
timestamp_lt: $timestamp_lt
- market_in: $market_in
+ ${additionalQuery}
}
) {
id
@@ -408,6 +411,7 @@ export const subgraphUserTransactionsQuery = `
}
}
`;
+};
export const marketPositionsQuery = `
query getMarketPositions($market: String!, $minShares: BigInt!, $first: Int!, $skip: Int!) {
diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts
index 6c92129e..c84d6528 100644
--- a/src/hooks/usePositionReport.ts
+++ b/src/hooks/usePositionReport.ts
@@ -85,9 +85,9 @@ export const usePositionReport = (
const transactionResult = await fetchUserTransactions({
userAddress: [account],
chainIds: [selectedAsset.chainId],
- timestampGte: actualStartTimestamp, // ✅ Use actual timestamp from block
- timestampLte: actualEndTimestamp, // ✅ Use actual timestamp from block
- assetIds: [selectedAsset.address], // Query by asset to find ALL markets
+ timestampGte: actualStartTimestamp,
+ timestampLte: actualEndTimestamp,
+ assetIds: [selectedAsset.address],
first: PAGE_SIZE,
skip,
});
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index 201d22c3..34196033 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -166,7 +166,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
marketsByChain.set(marketInfo.chainId, existing);
});
- console.log('All markets by chain', marketsByChain)
+ console.log('All markets by chain', marketsByChain);
// Build market data map from allMarkets context (no need to fetch individually)
const marketDataMap = new Map();
From 5ea3fb9ea969320340b68690e1dc1decc32a0074 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 18:06:43 +0800
Subject: [PATCH 05/11] chore: remove isTruncated
---
.../supplied-morpho-blue-grouped-table.tsx | 15 -------------
src/features/positions/positions-view.tsx | 4 ----
src/hooks/queries/useUserTransactionsQuery.ts | 22 +------------------
src/hooks/useUserPositionsSummaryData.ts | 1 -
4 files changed, 1 insertion(+), 41 deletions(-)
diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
index b617160d..47e071d2 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -101,8 +101,6 @@ type SuppliedMorphoBlueGroupedTableProps = {
isLoadingEarnings?: boolean;
earningsPeriod: EarningsPeriod;
setEarningsPeriod: (period: EarningsPeriod) => void;
- chainBlockData?: Record;
- isTruncated?: boolean;
};
export function SuppliedMorphoBlueGroupedTable({
@@ -113,8 +111,6 @@ export function SuppliedMorphoBlueGroupedTable({
account,
earningsPeriod,
setEarningsPeriod,
- chainBlockData,
- isTruncated,
}: SuppliedMorphoBlueGroupedTableProps) {
const [expandedRows, setExpandedRows] = useState>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
@@ -309,17 +305,6 @@ export function SuppliedMorphoBlueGroupedTable({
margin={3}
/>
- ) : isTruncated ? (
-
- }
- >
- -
-
) : (
{(() => {
diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx
index 19f2f488..edcf8b4a 100644
--- a/src/features/positions/positions-view.tsx
+++ b/src/features/positions/positions-view.tsx
@@ -34,8 +34,6 @@ export default function Positions() {
positions: marketPositions,
refetch,
loadingStates,
- isTruncated,
- actualBlockData,
} = useUserPositionsSummaryData(account, earningsPeriod);
// Fetch user's auto vaults
@@ -112,8 +110,6 @@ export default function Positions() {
isLoadingEarnings={isEarningsLoading}
earningsPeriod={earningsPeriod}
setEarningsPeriod={setEarningsPeriod}
- chainBlockData={actualBlockData}
- isTruncated={isTruncated}
/>
)}
diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts
index 676f74e1..8df5f631 100644
--- a/src/hooks/queries/useUserTransactionsQuery.ts
+++ b/src/hooks/queries/useUserTransactionsQuery.ts
@@ -7,7 +7,6 @@ type UseUserTransactionsQueryOptions = {
/**
* When true, automatically paginates to fetch ALL transactions.
* Use for report generation when complete accuracy is needed.
- * When false (default), fetches up to 1000 transactions and returns isTruncated flag.
* Use for summary pages when speed is prioritized.
*/
paginate?: boolean;
@@ -15,11 +14,6 @@ type UseUserTransactionsQueryOptions = {
pageSize?: number;
};
-type TransactionQueryResult = TransactionResponse & {
- /** Indicates if data was truncated due to pagination limits */
- isTruncated: boolean;
-};
-
/**
* Fetches user transactions from Morpho API or Subgraph using React Query.
*
@@ -45,9 +39,6 @@ type TransactionQueryResult = TransactionResponse & {
* },
* paginate: false,
* });
- * if (data?.isTruncated) {
- * // Show warning to user
- * }
*
* // Report page (complete data)
* const { data } = useUserTransactionsQuery({
@@ -80,20 +71,10 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
queryFn: async () => {
if (!paginate) {
// Simple case: fetch once with limit
- const response = await fetchUserTransactions({
+ return await fetchUserTransactions({
...filters,
first: pageSize,
});
-
- // Check if data was truncated
- // Since we're now filtering at GraphQL level, if we get exactly pageSize items,
- // there might be more available
- const isTruncated = response.items.length >= pageSize;
-
- return {
- ...response,
- isTruncated,
- };
}
// Pagination mode: fetch all data across multiple requests
@@ -128,7 +109,6 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
countTotal: allItems.length,
},
error: null,
- isTruncated: false, // We fetched everything
};
},
enabled: enabled && filters.userAddress.length > 0,
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index 696edd59..b99b9f79 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -181,7 +181,6 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
isPositionsLoading: positionsLoading,
isEarningsLoading,
isRefetching,
- isTruncated: txData?.isTruncated ?? false,
error: positionsError,
refetch,
loadingStates,
From 9b6d0ff6b8933fe56132da11153b28e5e4e928c5 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 18:29:19 +0800
Subject: [PATCH 06/11] misc: cleanup
---
.../supplied-morpho-blue-grouped-table.tsx | 45 +++----
src/features/positions/positions-view.tsx | 77 ++----------
src/hooks/queries/useBlockTimestamps.ts | 40 ++++++
src/hooks/queries/useCurrentBlocks.ts | 31 +++++
src/hooks/queries/usePositionSnapshots.ts | 48 ++++++++
.../queries/usePositionSnapshotsQuery.ts | 60 ---------
src/hooks/queries/useUserTransactionsQuery.ts | 26 +---
src/hooks/useUserPositionsSummaryData.ts | 95 ++-------------
src/utils/blockFinder.ts | 114 ------------------
9 files changed, 159 insertions(+), 377 deletions(-)
create mode 100644 src/hooks/queries/useBlockTimestamps.ts
create mode 100644 src/hooks/queries/useCurrentBlocks.ts
create mode 100644 src/hooks/queries/usePositionSnapshots.ts
delete mode 100644 src/hooks/queries/usePositionSnapshotsQuery.ts
delete mode 100644 src/utils/blockFinder.ts
diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
index 47e071d2..fdafb935 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -19,16 +19,17 @@ import { TableContainerWithHeader } from '@/components/common/table-container-wi
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal';
import { useDisclosure } from '@/hooks/useDisclosure';
import { usePositionsPreferences } from '@/stores/usePositionsPreferences';
+import { usePositionsFilters } from '@/stores/usePositionsFilters';
import { useAppSettings } from '@/stores/useAppSettings';
import { computeMarketWarnings } from '@/hooks/useMarketWarnings';
import { useRateLabel } from '@/hooks/useRateLabel';
import { useStyledToast } from '@/hooks/useStyledToast';
-import type { EarningsPeriod } from '@/hooks/useUserPositionsSummaryData';
+import useUserPositionsSummaryData, { type EarningsPeriod } from '@/hooks/useUserPositionsSummaryData';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getNetworkImg } from '@/utils/networks';
import { getGroupedEarnings, groupPositionsByLoanAsset, processCollaterals } from '@/utils/positions';
import { convertApyToApr } from '@/utils/rateMath';
-import { type GroupedPosition, type MarketPositionWithEarnings, type WarningWithDetail, WarningCategory } from '@/utils/types';
+import { type GroupedPosition, type WarningWithDetail, WarningCategory } from '@/utils/types';
import { RiskIndicator } from '@/features/markets/components/risk-indicator';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { RebalanceModal } from './rebalance/rebalance-modal';
@@ -95,27 +96,17 @@ function AggregatedRiskIndicators({ groupedPosition }: { groupedPosition: Groupe
type SuppliedMorphoBlueGroupedTableProps = {
account: string;
- marketPositions: MarketPositionWithEarnings[];
- refetch: (onSuccess?: () => void) => void;
- isRefetching: boolean;
- isLoadingEarnings?: boolean;
- earningsPeriod: EarningsPeriod;
- setEarningsPeriod: (period: EarningsPeriod) => void;
};
-export function SuppliedMorphoBlueGroupedTable({
- marketPositions,
- refetch,
- isRefetching,
- isLoadingEarnings,
- account,
- earningsPeriod,
- setEarningsPeriod,
-}: SuppliedMorphoBlueGroupedTableProps) {
+export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGroupedTableProps) {
+ const period = usePositionsFilters((s) => s.period);
+ const setPeriod = usePositionsFilters((s) => s.setPeriod);
+
+ const { positions: marketPositions, refetch, isRefetching, isEarningsLoading } = useUserPositionsSummaryData(account, period);
+
const [expandedRows, setExpandedRows] = useState>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
const [selectedGroupedPosition, setSelectedGroupedPosition] = useState(null);
- // Positions preferences from Zustand store
const { showCollateralExposure, setShowCollateralExposure } = usePositionsPreferences();
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onOpenChange: onSettingsOpenChange } = useDisclosure();
const { address } = useConnection();
@@ -129,11 +120,11 @@ export function SuppliedMorphoBlueGroupedTable({
return account === address;
}, [account, address]);
- const periodLabels: Record = {
+ const periodLabels = {
day: '1D',
week: '7D',
month: '30D',
- };
+ } as const;
const groupedPositions = useMemo(() => groupPositionsByLoanAsset(marketPositions), [marketPositions]);
@@ -228,7 +219,7 @@ export function SuppliedMorphoBlueGroupedTable({
{rateLabel} (now)
- Interest Accrued ({earningsPeriod})
+ Interest Accrued ({period})
{formatReadable((isAprDisplay ? convertApyToApr(avgApy) : avgApy) * 100)}%
-
+
- {isLoadingEarnings ? (
+ {isEarningsLoading ? (
- {Object.entries(periodLabels).map(([period, label]) => (
+ {Object.entries(periodLabels).map(([periodKey, label]) => (
diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx
index edcf8b4a..0ff4c7c4 100644
--- a/src/features/positions/positions-view.tsx
+++ b/src/features/positions/positions-view.tsx
@@ -1,19 +1,13 @@
'use client';
-import { useMemo, useState } from 'react';
import { useParams } from 'next/navigation';
-import { Tooltip } from '@/components/ui/tooltip';
-import { IoRefreshOutline } from 'react-icons/io5';
-import { toast } from 'react-toastify';
import type { Address } from 'viem';
import { AccountIdentity } from '@/components/shared/account-identity';
-import { Button } from '@/components/ui/button';
import Header from '@/components/layout/header/Header';
import EmptyScreen from '@/components/status/empty-screen';
import LoadingScreen from '@/components/status/loading-screen';
-import { TooltipContent } from '@/components/shared/tooltip-content';
import { useProcessedMarkets } from '@/hooks/useProcessedMarkets';
-import useUserPositionsSummaryData, { type EarningsPeriod } from '@/hooks/useUserPositionsSummaryData';
+import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData';
import { usePortfolioValue } from '@/hooks/usePortfolioValue';
import { useUserVaultsV2Query } from '@/hooks/queries/useUserVaultsV2Query';
import { SuppliedMorphoBlueGroupedTable } from './components/supplied-morpho-blue-grouped-table';
@@ -21,20 +15,11 @@ import { PortfolioValueBadge } from './components/portfolio-value-badge';
import { UserVaultsTable } from './components/user-vaults-table';
export default function Positions() {
- const [earningsPeriod, setEarningsPeriod] = useState
('day');
-
const { account } = useParams<{ account: string }>();
const { loading: isMarketsLoading } = useProcessedMarkets();
- const {
- isPositionsLoading,
- isEarningsLoading,
- isRefetching,
- positions: marketPositions,
- refetch,
- loadingStates,
- } = useUserPositionsSummaryData(account, earningsPeriod);
+ const { isPositionsLoading, positions: marketPositions } = useUserPositionsSummaryData(account, 'day');
// Fetch user's auto vaults
const {
@@ -48,23 +33,12 @@ export default function Positions() {
const loading = isMarketsLoading || isPositionsLoading;
- // Generate loading message based on current state
- const loadingMessage = useMemo(() => {
- if (isMarketsLoading) return 'Loading markets...';
- if (loadingStates.positions) return 'Loading user positions...';
- if (loadingStates.snapshots) return 'Loading historical snapshots...';
- if (loadingStates.transactions) return 'Loading transaction history...';
- return 'Loading...';
- }, [isMarketsLoading, loadingStates]);
+ const loadingMessage = isMarketsLoading ? 'Loading markets...' : 'Loading user positions...';
const hasSuppliedMarkets = marketPositions && marketPositions.length > 0;
const hasVaults = vaults && vaults.length > 0;
const showEmpty = !loading && !isVaultsLoading && !hasSuppliedMarkets && !hasVaults;
- const handleRefetch = () => {
- void refetch(() => toast.info('Data refreshed', { icon: 🚀 }));
- };
-
return (
@@ -101,17 +75,7 @@ export default function Positions() {
)}
{/* Morpho Blue Positions Section */}
- {!loading && hasSuppliedMarkets && (
-
void refetch()}
- isRefetching={isRefetching}
- isLoadingEarnings={isEarningsLoading}
- earningsPeriod={earningsPeriod}
- setEarningsPeriod={setEarningsPeriod}
- />
- )}
+ {!loading && hasSuppliedMarkets && }
{/* Auto Vaults Section (progressive loading) */}
{isVaultsLoading && !loading && (
@@ -131,35 +95,10 @@ export default function Positions() {
{/* Empty state (only if both finished loading and both empty) */}
{showEmpty && (
-
-
-
- }
- >
-
-
-
-
-
-
-
-
-
+
)}
diff --git a/src/hooks/queries/useBlockTimestamps.ts b/src/hooks/queries/useBlockTimestamps.ts
new file mode 100644
index 00000000..6cb34c3c
--- /dev/null
+++ b/src/hooks/queries/useBlockTimestamps.ts
@@ -0,0 +1,40 @@
+import { useQuery } from '@tanstack/react-query';
+import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
+import type { SupportedNetworks } from '@/utils/networks';
+import { getClient } from '@/utils/rpc';
+
+/**
+ *
+ * @param snapshotBlocks { chainId: blockNumber }
+ */
+export const useBlockTimestamps = (snapshotBlocks: Record) => {
+ const { customRpcUrls } = useCustomRpcContext();
+
+ return useQuery({
+ queryKey: ['block-timestamps', snapshotBlocks],
+ queryFn: async () => {
+ const blockData: Record = {};
+
+ await Promise.all(
+ Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
+ try {
+ const client = getClient(Number(chainId) as SupportedNetworks, customRpcUrls[Number(chainId) as SupportedNetworks]);
+ const block = await client.getBlock({ blockNumber: BigInt(blockNum) });
+ blockData[Number(chainId)] = {
+ block: blockNum,
+ timestamp: Number(block.timestamp),
+ };
+ } catch (error) {
+ console.error(`Failed to get block ${blockNum} on chain ${chainId}:`, error);
+ }
+ }),
+ );
+
+ return blockData;
+ },
+ enabled: Object.keys(snapshotBlocks).length > 0,
+ staleTime: Number.POSITIVE_INFINITY,
+ gcTime: 30 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ });
+};
diff --git a/src/hooks/queries/useCurrentBlocks.ts b/src/hooks/queries/useCurrentBlocks.ts
new file mode 100644
index 00000000..3aa40440
--- /dev/null
+++ b/src/hooks/queries/useCurrentBlocks.ts
@@ -0,0 +1,31 @@
+import { useQuery } from '@tanstack/react-query';
+import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
+import type { SupportedNetworks } from '@/utils/networks';
+import { getClient } from '@/utils/rpc';
+
+export const useCurrentBlocks = (chainIds: SupportedNetworks[]) => {
+ const { customRpcUrls } = useCustomRpcContext();
+
+ return useQuery({
+ queryKey: ['current-blocks', chainIds],
+ queryFn: async () => {
+ const blocks: Record = {};
+ await Promise.all(
+ chainIds.map(async (chainId) => {
+ try {
+ const client = getClient(chainId, customRpcUrls[chainId]);
+ const blockNumber = await client.getBlockNumber();
+ blocks[chainId] = Number(blockNumber);
+ } catch (error) {
+ console.error(`Failed to get current block for chain ${chainId}:`, error);
+ }
+ }),
+ );
+ return blocks;
+ },
+ enabled: chainIds.length > 0,
+ staleTime: 2 * 60 * 1000,
+ gcTime: 5 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ });
+};
diff --git a/src/hooks/queries/usePositionSnapshots.ts b/src/hooks/queries/usePositionSnapshots.ts
new file mode 100644
index 00000000..e22a78e7
--- /dev/null
+++ b/src/hooks/queries/usePositionSnapshots.ts
@@ -0,0 +1,48 @@
+import { useQuery } from '@tanstack/react-query';
+import type { Address } from 'viem';
+import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
+import type { SupportedNetworks } from '@/utils/networks';
+import { getClient } from '@/utils/rpc';
+import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions';
+import type { MarketPosition } from '@/utils/types';
+
+type UsePositionSnapshotsOptions = {
+ positions: MarketPosition[] | undefined;
+ user: string | undefined;
+ snapshotBlocks: Record;
+};
+
+export const usePositionSnapshots = ({ positions, user, snapshotBlocks }: UsePositionSnapshotsOptions) => {
+ const { customRpcUrls } = useCustomRpcContext();
+
+ return useQuery({
+ queryKey: ['all-position-snapshots', snapshotBlocks, user, positions?.map((p) => p.market.uniqueKey)],
+ queryFn: async () => {
+ if (!positions || !user) return {};
+
+ const snapshotsByChain: Record> = {};
+
+ await Promise.all(
+ Object.entries(snapshotBlocks).map(async ([chainId, blockNum]) => {
+ const chainIdNum = Number(chainId);
+ const chainPositions = positions.filter((p) => p.market.morphoBlue.chain.id === chainIdNum);
+
+ if (chainPositions.length === 0) return;
+
+ const client = getClient(chainIdNum as SupportedNetworks, customRpcUrls[chainIdNum as SupportedNetworks]);
+ const marketIds = chainPositions.map((p) => p.market.uniqueKey);
+
+ const snapshots = await fetchPositionsSnapshots(marketIds, user as Address, chainIdNum, blockNum, client);
+
+ snapshotsByChain[chainIdNum] = snapshots;
+ }),
+ );
+
+ return snapshotsByChain;
+ },
+ enabled: !!positions && !!user && Object.keys(snapshotBlocks).length > 0,
+ staleTime: 0,
+ gcTime: 10 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ });
+};
diff --git a/src/hooks/queries/usePositionSnapshotsQuery.ts b/src/hooks/queries/usePositionSnapshotsQuery.ts
deleted file mode 100644
index ac0f2d0c..00000000
--- a/src/hooks/queries/usePositionSnapshotsQuery.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import type { Address } from 'viem';
-import { useCustomRpcContext } from '@/components/providers/CustomRpcProvider';
-import type { SupportedNetworks } from '@/utils/networks';
-import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions';
-import { getClient } from '@/utils/rpc';
-
-type PositionSnapshotsQueryOptions = {
- /** User address to fetch snapshots for */
- user: Address;
- /** Chain ID where positions exist */
- chainId: SupportedNetworks;
- /** Market unique keys to fetch snapshots for */
- marketIds: string[];
- /** Block number to fetch snapshots at */
- blockNumber: number;
- /** Whether to enable the query (default: true) */
- enabled?: boolean;
-};
-
-/**
- * Fetches position snapshots at a specific block number using React Query.
- *
- * This hook fetches historical balances for user positions at a specific block,
- * which is essential for calculating earnings over a time period.
- *
- * Cache behavior:
- * - staleTime: 5 minutes (historical snapshots don't change)
- * - Only runs when marketIds are provided and blockNumber > 0
- *
- * @example
- * ```tsx
- * const { data: snapshots, isLoading } = usePositionSnapshotsQuery({
- * user: '0x...' as Address,
- * chainId: SupportedNetworks.Mainnet,
- * marketIds: ['market1', 'market2'],
- * blockNumber: 19000000,
- * });
- *
- * const snapshot = snapshots?.get('market1');
- * const pastBalance = snapshot ? BigInt(snapshot.supplyAssets) : 0n;
- * ```
- */
-export const usePositionSnapshotsQuery = (options: PositionSnapshotsQueryOptions) => {
- const { user, chainId, marketIds, blockNumber, enabled = true } = options;
- const { customRpcUrls } = useCustomRpcContext();
-
- return useQuery
+ ) : earnings === 0n ? (
+
-
) : (
-
- {(() => {
- if (earnings === 0n) return '-';
+ {
+ const blockData = actualBlockData[groupedPosition.chainId];
+ if (!blockData) return 'Loading timestamp data...';
+
+ const startTimestamp = blockData.timestamp * 1000;
+ const endTimestamp = Date.now();
+
+ const formatDateTime = (timestamp: number) =>
+ new Date(timestamp).toLocaleString('en-US', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ });
+
return (
- formatReadable(Number(formatBalance(earnings, groupedPosition.loanAssetDecimals))) +
- ' ' +
- groupedPosition.loanAsset
+
);
})()}
-
+ >
+
+ {formatReadable(Number(formatBalance(earnings, groupedPosition.loanAssetDecimals)))}{' '}
+ {groupedPosition.loanAsset}
+
+
)}
diff --git a/src/hooks/queries/useBlockTimestamps.ts b/src/hooks/queries/useBlockTimestamps.ts
index 6cb34c3c..56b76297 100644
--- a/src/hooks/queries/useBlockTimestamps.ts
+++ b/src/hooks/queries/useBlockTimestamps.ts
@@ -4,7 +4,7 @@ import type { SupportedNetworks } from '@/utils/networks';
import { getClient } from '@/utils/rpc';
/**
- *
+ *
* @param snapshotBlocks { chainId: blockNumber }
*/
export const useBlockTimestamps = (snapshotBlocks: Record) => {
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index 34196033..d9d2256e 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -166,8 +166,6 @@ const useUserPositions = (user: string | undefined, showEmpty = false, chainIds?
marketsByChain.set(marketInfo.chainId, existing);
});
- console.log('All markets by chain', marketsByChain);
-
// Build market data map from allMarkets context (no need to fetch individually)
const marketDataMap = new Map();
allMarkets.forEach((market) => {
diff --git a/src/utils/networks.ts b/src/utils/networks.ts
index 7cda54d5..0802d9f7 100644
--- a/src/utils/networks.ts
+++ b/src/utils/networks.ts
@@ -145,7 +145,7 @@ export const networks: NetworkConfig[] = [
logo: require('../imgs/chains/arbitrum.png') as string,
name: 'Arbitrum',
defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_ARBITRUM_RPC, 'arb-mainnet'),
- blocktime: 2,
+ blocktime: 0.25,
maxBlockDelay: 2,
explorerUrl: 'https://arbiscan.io',
wrappedNativeToken: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1',
@@ -156,7 +156,7 @@ export const networks: NetworkConfig[] = [
logo: require('../imgs/chains/hyperevm.png') as string,
name: 'HyperEVM',
defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_HYPEREVM_RPC, 'hyperliquid-mainnet'),
- blocktime: 2,
+ blocktime: 1,
maxBlockDelay: 5,
nativeTokenSymbol: 'WHYPE',
wrappedNativeToken: '0x5555555555555555555555555555555555555555',
@@ -168,7 +168,7 @@ export const networks: NetworkConfig[] = [
logo: require('../imgs/chains/monad.svg') as string,
name: 'Monad',
defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_MONAD_RPC, 'monad-mainnet'),
- blocktime: 1,
+ blocktime: 0.4,
maxBlockDelay: 5,
nativeTokenSymbol: 'MON',
wrappedNativeToken: '0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A',
From 402d49a2989dd22abc0953300fffce6ffe7e6598 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 19:06:20 +0800
Subject: [PATCH 10/11] chore: clean
---
src/hooks/queries/useUserTransactionsQuery.ts | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/hooks/queries/useUserTransactionsQuery.ts b/src/hooks/queries/useUserTransactionsQuery.ts
index 6d5badcb..d48f715b 100644
--- a/src/hooks/queries/useUserTransactionsQuery.ts
+++ b/src/hooks/queries/useUserTransactionsQuery.ts
@@ -7,7 +7,6 @@ type UseUserTransactionsQueryOptions = {
/**
* When true, automatically paginates to fetch ALL transactions.
* Use for report generation when complete accuracy is needed.
- * Use for summary pages when speed is prioritized.
*/
paginate?: boolean;
/** Page size for pagination (default 1000) */
@@ -23,11 +22,6 @@ type UseUserTransactionsQueryOptions = {
* - Combines transactions from all target networks
* - Sorts by timestamp (descending)
* - Supports auto-pagination when paginate=true
- *
- * Cache behavior:
- * - staleTime: 30 seconds (transactions change moderately frequently)
- * - Refetch on window focus: enabled
- * - Only runs when userAddress is provided
* ```
*/
export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOptions) => {
@@ -92,7 +86,7 @@ export const useUserTransactionsQuery = (options: UseUserTransactionsQueryOption
};
},
enabled: enabled && filters.userAddress.length > 0,
- staleTime: 5 * 60 * 1000,
+ staleTime: 60 * 1000,
refetchOnWindowFocus: false,
});
};
From be115aceb4314d0685a3b6bd167f9e6260ae5067 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Mon, 29 Dec 2025 19:14:18 +0800
Subject: [PATCH 11/11] misc: cleanup
---
.../supplied-morpho-blue-grouped-table.tsx | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
index 0f9bfbd1..fb3f25e9 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -7,6 +7,7 @@ import { ReloadIcon } from '@radix-ui/react-icons';
import { GearIcon } from '@radix-ui/react-icons';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
+import moment from 'moment';
import { PulseLoader } from 'react-spinners';
import { useConnection } from 'wagmi';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
@@ -290,21 +291,10 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
const startTimestamp = blockData.timestamp * 1000;
const endTimestamp = Date.now();
- const formatDateTime = (timestamp: number) =>
- new Date(timestamp).toLocaleString('en-US', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false,
- });
-
return (
);
})()}