diff --git a/src/features/markets/components/table/market-table-body.tsx b/src/features/markets/components/table/market-table-body.tsx
index 8d79e8fa..7cd86017 100644
--- a/src/features/markets/components/table/market-table-body.tsx
+++ b/src/features/markets/components/table/market-table-body.tsx
@@ -241,9 +241,7 @@ export function MarketTableBody({ currentEntries, expandedRowId, setExpandedRowI
className="z-50 text-center"
style={{ minWidth: '85px', paddingLeft: 3, paddingRight: 3 }}
>
-
- {item.state.dailySupplyApy != null ? : '—'}
-
+ {item.state.dailySupplyApy != null ? : '—'}
)}
{columnVisibility.dailyBorrowAPY && (
@@ -252,9 +250,7 @@ export function MarketTableBody({ currentEntries, expandedRowId, setExpandedRowI
className="z-50 text-center"
style={{ minWidth: '85px', paddingLeft: 3, paddingRight: 3 }}
>
-
- {item.state.dailyBorrowApy != null ? : '—'}
-
+ {item.state.dailyBorrowApy != null ? : '—'}
)}
{columnVisibility.weeklySupplyAPY && (
diff --git a/src/features/positions/components/supplied-markets-detail.tsx b/src/features/positions/components/supplied-markets-detail.tsx
index d59b35d9..9c370d9b 100644
--- a/src/features/positions/components/supplied-markets-detail.tsx
+++ b/src/features/positions/components/supplied-markets-detail.tsx
@@ -8,14 +8,18 @@ import { MarketRiskIndicators } from '@/features/markets/components/market-risk-
import { APYCell } from '@/features/markets/components/apy-breakdown-tooltip';
import { useRateLabel } from '@/hooks/useRateLabel';
import { useModal } from '@/hooks/useModal';
-import { formatReadable, formatBalance } from '@/utils/balance';
+import { formatBalance } from '@/utils/balance';
import type { MarketPosition, GroupedPosition } from '@/utils/types';
-import { getCollateralColorFromPalette, OTHER_COLOR } from '@/features/positions/utils/colors';
-import { useChartColors } from '@/constants/chartColors';
import { AllocationCell } from './allocation-cell';
+import { UserPositionsChart } from './user-positions-chart';
+import type { UserTransaction } from '@/utils/types';
+import type { PositionSnapshot } from '@/utils/positions';
+
type SuppliedMarketsDetailProps = {
groupedPosition: GroupedPosition;
- showCollateralExposure: boolean;
+ transactions: UserTransaction[];
+ snapshotsByChain: Record>;
+ chainBlockData: Record;
};
function MarketRow({ position, totalSupply, rateLabel }: { position: MarketPosition; totalSupply: number; rateLabel: string }) {
@@ -106,9 +110,8 @@ function MarketRow({ position, totalSupply, rateLabel }: { position: MarketPosit
}
// shared similar style with @vault-allocation-detail.tsx
-export function SuppliedMarketsDetail({ groupedPosition, showCollateralExposure }: SuppliedMarketsDetailProps) {
+export function SuppliedMarketsDetail({ groupedPosition, transactions, snapshotsByChain, chainBlockData }: SuppliedMarketsDetailProps) {
const { short: rateLabel } = useRateLabel();
- const { pie: pieColors } = useChartColors();
// Sort markets by size
const sortedMarkets = [...groupedPosition.markets].sort(
@@ -128,45 +131,14 @@ export function SuppliedMarketsDetail({ groupedPosition, showCollateralExposure
className="overflow-hidden"
>
- {/* Conditionally render collateral exposure section */}
- {showCollateralExposure && (
-
-
-
Collateral Exposure
-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
-
- ))}
-
-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
-
-
- ■
- {' '}
- {collateral.symbol}: {formatReadable(collateral.percentage)}%
-
- ))}
-
-
-
- )}
+ {/* Position History Chart with synchronized pie */}
+
{/* Markets Table - Always visible */}
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 252599b0..71107ceb 100644
--- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
+++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
@@ -109,10 +109,12 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
isRefetching,
isEarningsLoading,
actualBlockData,
+ transactions,
+ snapshotsByChain,
} = useUserPositionsSummaryData(account, period);
const [expandedRows, setExpandedRows] = useState>(new Set());
- const { showCollateralExposure, setShowCollateralExposure, showEarningsInUsd, setShowEarningsInUsd } = usePositionsPreferences();
+ const { showEarningsInUsd, setShowEarningsInUsd } = usePositionsPreferences();
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onOpenChange: onSettingsOpenChange } = useDisclosure();
const { address } = useConnection();
const { isAprDisplay } = useAppSettings();
@@ -389,7 +391,9 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
>
@@ -451,16 +455,6 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
title="Display Options"
helper="Customize what information is shown in the table"
>
-
-
-
>;
+ chainBlockData: Record;
+};
+
+// Props for standalone usage (history page)
+type StandaloneChartProps = BaseChartProps & {
+ variant: 'standalone';
+ chainId: SupportedNetworks;
+ loanAssetAddress: Address;
+ loanAssetSymbol: string;
+ loanAssetDecimals: number;
+ startTimestamp: number;
+ endTimestamp: number;
+ markets: {
+ uniqueKey: string;
+ collateralSymbol: string;
+ collateralAddress: string;
+ currentSupplyAssets: string;
+ }[];
+ transactions: UserTransaction[];
+ snapshots: Map | undefined;
+};
+
+export type UserPositionsChartProps = GroupedPositionChartProps | StandaloneChartProps;
+
+// Pie chart data type
+type PieDataPoint = {
+ key: string; // unique market key
+ name: string;
+ value: number;
+ color: string;
+ percentage: number;
+};
+
+function ChartContent({
+ dataPoints,
+ markets,
+ loanAssetSymbol,
+ height,
+}: {
+ dataPoints: PositionHistoryDataPoint[];
+ markets: MarketInfo[];
+ loanAssetSymbol: string;
+ height: number;
+}) {
+ const chartColors = useChartColors();
+
+ // Track which data point is being hovered (for synced pie chart)
+ const [hoveredIndex, setHoveredIndex] = useState(null);
+
+ // Build a map of market uniqueKey -> index for consistent color assignment
+ // Note: markets from hook already have lowercase uniqueKeys
+ const marketColorMap = useMemo(() => {
+ const map: Record = {};
+ markets.forEach((market, index) => {
+ map[market.uniqueKey] = index;
+ });
+ return map;
+ }, [markets]);
+
+ // Get color for a market based on its index in the markets array
+ const getMarketColor = useCallback(
+ (marketKey: string): string => {
+ const index = marketColorMap[marketKey] ?? 0;
+ return chartColors.pie[index % chartColors.pie.length];
+ },
+ [marketColorMap, chartColors.pie],
+ );
+
+ // Detect duplicate collateral symbols and create display names
+ const marketDisplayNames = useMemo(() => {
+ const symbolCounts: Record = {};
+
+ // First pass: count occurrences
+ markets.forEach((market) => {
+ symbolCounts[market.collateralSymbol] = (symbolCounts[market.collateralSymbol] || 0) + 1;
+ });
+
+ // Second pass: create display names
+ const names: Record = {};
+ markets.forEach((market) => {
+ if (symbolCounts[market.collateralSymbol] > 1) {
+ // Duplicate symbol - add market key prefix (first 8 chars)
+ const keyPrefix = market.uniqueKey.slice(0, 8);
+ names[market.uniqueKey] = `${market.collateralSymbol} (${keyPrefix}...)`;
+ } else {
+ names[market.uniqueKey] = market.collateralSymbol;
+ }
+ });
+
+ return names;
+ }, [markets]);
+
+ // Current data point for pie chart (hovered or latest)
+ const currentDataPoint = useMemo(() => {
+ if (hoveredIndex !== null && dataPoints[hoveredIndex]) {
+ return dataPoints[hoveredIndex];
+ }
+ return dataPoints.at(-1);
+ }, [hoveredIndex, dataPoints]);
+
+ // Pie chart data derived from current data point
+ // Keep all markets for consistent height, sort by value descending
+ const pieData = useMemo((): PieDataPoint[] => {
+ if (!currentDataPoint) return [];
+
+ const total = currentDataPoint.total || 0;
+ return markets
+ .map((market) => {
+ const value = Number(currentDataPoint[market.uniqueKey] ?? 0);
+ return {
+ key: market.uniqueKey,
+ name: marketDisplayNames[market.uniqueKey] || market.collateralSymbol,
+ value,
+ color: getMarketColor(market.uniqueKey),
+ percentage: total > 0 && value > 0 ? (value / total) * 100 : 0,
+ };
+ })
+ .sort((a, b) => b.value - a.value);
+ }, [currentDataPoint, markets, getMarketColor, marketDisplayNames]);
+
+ // Format date for display
+ const formatDate = (timestamp: number) => {
+ return new Date(timestamp * 1000).toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ });
+ };
+
+ // Don't render if no meaningful data
+ if (dataPoints.length <= 1 || markets.length === 0) {
+ return null;
+ }
+
+ const currentTimestamp = currentDataPoint?.timestamp;
+ const isHistorical = hoveredIndex !== null && hoveredIndex < dataPoints.length - 1;
+
+ return (
+
+ {/* Responsive: stack vertically on mobile, side-by-side on larger screens */}
+
+ {/* Stacked Area Chart - Left side */}
+
+
+
Position History
+
+
+
+ {
+ if (state?.activeTooltipIndex !== undefined) {
+ setHoveredIndex(state.activeTooltipIndex);
+ }
+ }}
+ onMouseLeave={() => setHoveredIndex(null)}
+ >
+
+
+ new Date(time * 1000).toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ })
+ }
+ tick={{ fontSize: 10, fill: 'var(--color-text-secondary)' }}
+ />
+ formatReadable(v)}
+ tick={{ fontSize: 10, fill: 'var(--color-text-secondary)' }}
+ width={55}
+ domain={[0, 'auto']}
+ />
+ {
+ if (!active || !payload || payload.length === 0) return null;
+
+ const dataPoint = dataPoints.find((dp) => dp.timestamp === Number(label));
+ const total = dataPoint?.total ?? 0;
+
+ return (
+
+
+ {new Date((label ?? 0) * 1000).toLocaleString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+ {/* Sort by value descending, show only non-zero */}
+ {pieData
+ .filter((entry) => entry.value > 0)
+ .map((entry) => (
+
+
+
+ {entry.name}
+
+
+ {formatReadable(entry.value)} ({entry.percentage.toFixed(1)}%)
+
+
+ ))}
+
+
+ Total
+
+ {formatReadable(total)} {loanAssetSymbol}
+
+
+
+ );
+ }}
+ />
+ {markets.map((market) => {
+ const key = market.uniqueKey;
+ const color = getMarketColor(key);
+ const displayName = marketDisplayNames[key] || market.collateralSymbol;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* Synchronized Pie Chart - Right side on desktop, below on mobile */}
+
+ {/* Mobile: horizontal layout with pie on left, legend on right */}
+ {/* Desktop: vertical layout */}
+
+
+
{isHistorical ? 'Historical' : 'Current'}
+ {currentTimestamp &&
{formatDate(currentTimestamp)}
}
+
+
+
+
+ {pieData.map((entry) => (
+ |
+ ))}
+
+
+
+
+ {/* Legend - show all markets, dim zero values */}
+
+
+ {pieData.slice(0, 5).map((entry) => {
+ const isZero = entry.value === 0;
+ return (
+
+
+
+ {entry.name}
+
+
{isZero ? '—' : `${entry.percentage.toFixed(1)}%`}
+
+ );
+ })}
+ {pieData.length > 5 &&
+{pieData.length - 5} more
}
+
+
+
+
+
+ );
+}
+
+// Extract params from props to normalize between variants
+function useChartParams(props: UserPositionsChartProps) {
+ return useMemo(() => {
+ if (props.variant === 'grouped') {
+ const { groupedPosition, transactions, snapshotsByChain, chainBlockData } = props;
+ const chainId = groupedPosition.chainId;
+ const chainSnapshots = snapshotsByChain[chainId];
+ const blockData = chainBlockData[chainId];
+
+ if (!blockData) {
+ return null;
+ }
+
+ const startTimestamp = blockData.timestamp;
+ const endTimestamp = Math.floor(Date.now() / 1000);
+
+ const markets = groupedPosition.markets.map((position) => ({
+ uniqueKey: position.market.uniqueKey,
+ collateralSymbol: position.market.collateralAsset?.symbol ?? 'Unknown',
+ collateralAddress: position.market.collateralAsset?.address ?? '',
+ currentSupplyAssets: position.state.supplyAssets,
+ }));
+
+ return {
+ markets,
+ loanAssetDecimals: groupedPosition.loanAssetDecimals,
+ loanAssetSymbol: groupedPosition.loanAssetSymbol,
+ chainId: chainId as SupportedNetworks,
+ startTimestamp,
+ endTimestamp,
+ transactions,
+ snapshots: chainSnapshots,
+ };
+ }
+
+ // Standalone variant
+ return {
+ markets: props.markets,
+ loanAssetDecimals: props.loanAssetDecimals,
+ loanAssetSymbol: props.loanAssetSymbol,
+ chainId: props.chainId,
+ startTimestamp: props.startTimestamp,
+ endTimestamp: props.endTimestamp,
+ transactions: props.transactions,
+ snapshots: props.snapshots,
+ };
+ }, [props]);
+}
+
+export function UserPositionsChart(props: UserPositionsChartProps) {
+ const height = props.height ?? 180;
+ const debug = props.debug ?? false;
+
+ const chartParams = useChartParams(props);
+
+ const { dataPoints, markets: marketInfoList } = usePositionHistoryChart({
+ markets: chartParams?.markets ?? [],
+ loanAssetDecimals: chartParams?.loanAssetDecimals ?? 18,
+ chainId: chartParams?.chainId ?? (1 as SupportedNetworks),
+ startTimestamp: chartParams?.startTimestamp ?? 0,
+ endTimestamp: chartParams?.endTimestamp ?? 0,
+ transactions: chartParams?.transactions ?? [],
+ snapshots: chartParams?.snapshots,
+ debug,
+ });
+
+ if (!chartParams) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/hooks/usePositionHistoryChart.ts b/src/hooks/usePositionHistoryChart.ts
new file mode 100644
index 00000000..f9d6434c
--- /dev/null
+++ b/src/hooks/usePositionHistoryChart.ts
@@ -0,0 +1,308 @@
+import { useMemo } from 'react';
+import type { UserTransaction } from '@/utils/types';
+import type { PositionSnapshot } from '@/utils/positions';
+import { UserTxTypes } from '@/utils/types';
+import { formatBalance } from '@/utils/balance';
+import type { SupportedNetworks } from '@/utils/networks';
+
+// Maximum number of data points before batching kicks in
+const MAX_DATA_POINTS = 50;
+
+export type PositionHistoryDataPoint = {
+ timestamp: number;
+ total: number;
+ eventType?: 'supply' | 'withdraw' | 'batch';
+ eventAmount?: number;
+ eventMarketKey?: string;
+ batchCount?: number; // Number of events in this batch
+ [marketKey: string]: number | string | undefined;
+};
+
+export type MarketInfo = {
+ uniqueKey: string;
+ collateralSymbol: string;
+ collateralAddress: string;
+};
+
+export type PositionHistoryDebugInfo = {
+ inputTransactionsCount: number;
+ relevantTransactionsCount: number;
+ filteredByMarket: number;
+ filteredByType: number;
+ filteredByTime: number;
+ dataPointsCount: number;
+ startTimestamp: number;
+ endTimestamp: number;
+ marketsCount: number;
+ snapshotFound: boolean;
+ batchingApplied: boolean;
+ batchWindowSeconds: number;
+};
+
+type UsePositionHistoryChartOptions = {
+ // Market data
+ markets: {
+ uniqueKey: string;
+ collateralSymbol: string;
+ collateralAddress: string;
+ currentSupplyAssets: string;
+ }[];
+ loanAssetDecimals: number;
+ chainId: SupportedNetworks;
+
+ // Time range
+ startTimestamp: number;
+ endTimestamp: number;
+
+ // Data inputs
+ transactions: UserTransaction[];
+ snapshots: Map | undefined;
+
+ // Debug
+ debug?: boolean;
+};
+
+export type PositionHistoryChartData = {
+ dataPoints: PositionHistoryDataPoint[];
+ markets: MarketInfo[];
+ debugInfo: PositionHistoryDebugInfo | null;
+};
+
+export function usePositionHistoryChart({
+ markets,
+ loanAssetDecimals,
+ chainId,
+ startTimestamp,
+ endTimestamp,
+ transactions,
+ snapshots,
+ debug = false,
+}: UsePositionHistoryChartOptions): PositionHistoryChartData {
+ return useMemo(() => {
+ const decimals = loanAssetDecimals;
+
+ // Build market info list - use lowercase keys consistently
+ const marketInfoList: MarketInfo[] = markets.map((m) => ({
+ uniqueKey: m.uniqueKey.toLowerCase(),
+ collateralSymbol: m.collateralSymbol,
+ collateralAddress: m.collateralAddress,
+ }));
+
+ const marketKeys = marketInfoList.map((m) => m.uniqueKey);
+
+ // Debug counters
+ let filteredByMarket = 0;
+ let filteredByType = 0;
+ let filteredByTime = 0;
+
+ // Filter transactions step by step for debugging
+ const txsWithMarket = transactions.filter((tx) => {
+ const txMarketKey = tx.data?.market?.uniqueKey?.toLowerCase();
+ const matches = txMarketKey && marketKeys.includes(txMarketKey);
+ if (!matches) filteredByMarket++;
+ return matches;
+ });
+
+ const txsWithCorrectType = txsWithMarket.filter((tx) => {
+ const isSupplyOrWithdraw = tx.type === UserTxTypes.MarketSupply || tx.type === UserTxTypes.MarketWithdraw;
+ if (!isSupplyOrWithdraw) filteredByType++;
+ return isSupplyOrWithdraw;
+ });
+
+ const txsInTimeRange = txsWithCorrectType.filter((tx) => {
+ const inRange = Number(tx.timestamp) >= startTimestamp && Number(tx.timestamp) <= endTimestamp;
+ if (!inRange) filteredByTime++;
+ return inRange;
+ });
+
+ const relevantTxs = txsInTimeRange.sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
+
+ // Initialize positions from "before" snapshot - use lowercase keys
+ const marketPositions: Record = {};
+ let snapshotFound = false;
+
+ for (const market of markets) {
+ const key = market.uniqueKey.toLowerCase();
+ const snapshot = snapshots?.get(key);
+ if (snapshot) snapshotFound = true;
+ marketPositions[key] = snapshot ? BigInt(snapshot.supplyAssets) : 0n;
+ }
+
+ // Helper to calculate total and create data point
+ const createDataPoint = (
+ timestamp: number,
+ eventType?: 'supply' | 'withdraw' | 'batch',
+ eventAmount?: number,
+ eventMarketKey?: string,
+ batchCount?: number,
+ ): PositionHistoryDataPoint => {
+ const point: PositionHistoryDataPoint = { timestamp, total: 0 };
+ if (eventType) point.eventType = eventType;
+ if (eventAmount !== undefined) point.eventAmount = eventAmount;
+ if (eventMarketKey) point.eventMarketKey = eventMarketKey;
+ if (batchCount !== undefined) point.batchCount = batchCount;
+
+ let total = 0;
+ for (const market of marketInfoList) {
+ const positionBigInt = marketPositions[market.uniqueKey] ?? 0n;
+ const value = Number(formatBalance(positionBigInt, decimals));
+ point[market.uniqueKey] = value;
+ total += value;
+ }
+ point.total = total;
+ return point;
+ };
+
+ // Group transactions by timestamp (same block = same timestamp)
+ const txsByTimestamp = new Map();
+ for (const tx of relevantTxs) {
+ const ts = Number(tx.timestamp);
+ if (!txsByTimestamp.has(ts)) {
+ txsByTimestamp.set(ts, []);
+ }
+ txsByTimestamp.get(ts)!.push(tx);
+ }
+
+ const uniqueTimestamps = Array.from(txsByTimestamp.keys()).sort((a, b) => a - b);
+
+ // Determine if we need batching (only when unique timestamps exceed MAX_DATA_POINTS)
+ const needsBatching = uniqueTimestamps.length > MAX_DATA_POINTS;
+ const timeRange = endTimestamp - startTimestamp;
+ // If batching needed, calculate window size to get ~MAX_DATA_POINTS buckets
+ const batchWindowSeconds = needsBatching ? Math.ceil(timeRange / MAX_DATA_POINTS) : 0;
+
+ const dataPoints: PositionHistoryDataPoint[] = [];
+
+ // Add starting data point
+ dataPoints.push(createDataPoint(startTimestamp));
+
+ // Helper to apply transaction to positions
+ const applyTransaction = (tx: UserTransaction) => {
+ const key = tx.data.market.uniqueKey.toLowerCase();
+ const assets = BigInt(tx.data?.assets ?? '0');
+
+ if (tx.type === UserTxTypes.MarketSupply) {
+ marketPositions[key] = (marketPositions[key] ?? 0n) + assets;
+ } else if (tx.type === UserTxTypes.MarketWithdraw) {
+ const newValue = (marketPositions[key] ?? 0n) - assets;
+ // Never allow negative positions - clamp to 0
+ marketPositions[key] = newValue < 0n ? 0n : newValue;
+ }
+ };
+
+ if (needsBatching) {
+ // Batch timestamps into time windows
+ let currentWindowEnd = startTimestamp + batchWindowSeconds;
+ let batchTxs: UserTransaction[] = [];
+ let batchTimestamp = 0;
+
+ const processBatch = () => {
+ if (batchTxs.length === 0) return;
+
+ // Apply all transactions in the batch to update positions
+ let totalSupplied = 0n;
+ let totalWithdrawn = 0n;
+
+ for (const tx of batchTxs) {
+ applyTransaction(tx);
+ const assets = BigInt(tx.data?.assets ?? '0');
+ if (tx.type === UserTxTypes.MarketSupply) {
+ totalSupplied += assets;
+ } else {
+ totalWithdrawn += assets;
+ }
+ }
+
+ // Create a single data point for the batch at the last timestamp in the batch
+ const netChange = totalSupplied - totalWithdrawn;
+ const eventAmount = Number(formatBalance(netChange >= 0n ? netChange : -netChange, decimals));
+
+ dataPoints.push(createDataPoint(batchTimestamp, 'batch', eventAmount, undefined, batchTxs.length));
+ };
+
+ for (const ts of uniqueTimestamps) {
+ // Move to the correct window
+ while (ts >= currentWindowEnd) {
+ processBatch();
+ batchTxs = [];
+ currentWindowEnd += batchWindowSeconds;
+ }
+
+ const txsAtTimestamp = txsByTimestamp.get(ts) ?? [];
+ batchTxs.push(...txsAtTimestamp);
+ batchTimestamp = ts; // Use the last timestamp in the batch
+ }
+
+ // Process remaining batch
+ processBatch();
+ } else {
+ // Process each timestamp group as one data point
+ for (const ts of uniqueTimestamps) {
+ const txsAtTimestamp = txsByTimestamp.get(ts) ?? [];
+
+ // Track event info for single-tx timestamps
+ let totalSupplied = 0n;
+ let totalWithdrawn = 0n;
+ let lastMarketKey: string | undefined;
+
+ for (const tx of txsAtTimestamp) {
+ applyTransaction(tx);
+ const assets = BigInt(tx.data?.assets ?? '0');
+ lastMarketKey = tx.data.market.uniqueKey.toLowerCase();
+ if (tx.type === UserTxTypes.MarketSupply) {
+ totalSupplied += assets;
+ } else {
+ totalWithdrawn += assets;
+ }
+ }
+
+ // Determine event type and amount
+ if (txsAtTimestamp.length === 1) {
+ const tx = txsAtTimestamp[0];
+ const eventType = tx.type === UserTxTypes.MarketSupply ? 'supply' : 'withdraw';
+ const eventAmount = Number(formatBalance(BigInt(tx.data?.assets ?? '0'), decimals));
+ dataPoints.push(createDataPoint(ts, eventType, eventAmount, lastMarketKey));
+ } else {
+ // Multiple txs at same timestamp - treat as batch
+ const netChange = totalSupplied - totalWithdrawn;
+ const eventAmount = Number(formatBalance(netChange >= 0n ? netChange : -netChange, decimals));
+ dataPoints.push(createDataPoint(ts, 'batch', eventAmount, undefined, txsAtTimestamp.length));
+ }
+ }
+ }
+
+ // Add final data point with current positions
+ // Reset to current values
+ for (const market of markets) {
+ const key = market.uniqueKey.toLowerCase();
+ marketPositions[key] = BigInt(market.currentSupplyAssets);
+ }
+ dataPoints.push(createDataPoint(endTimestamp));
+
+ const debugInfo: PositionHistoryDebugInfo | null = debug
+ ? {
+ inputTransactionsCount: transactions.length,
+ relevantTransactionsCount: relevantTxs.length,
+ filteredByMarket,
+ filteredByType,
+ filteredByTime,
+ dataPointsCount: dataPoints.length,
+ startTimestamp,
+ endTimestamp,
+ marketsCount: markets.length,
+ snapshotFound,
+ batchingApplied: needsBatching,
+ batchWindowSeconds,
+ }
+ : null;
+
+ if (debug) {
+ console.log('[PositionHistoryChart] Debug Info:', debugInfo);
+ console.log('[PositionHistoryChart] Data Points:', dataPoints);
+ console.log('[PositionHistoryChart] Markets:', marketInfoList);
+ console.log('[PositionHistoryChart] Relevant Transactions:', relevantTxs.length);
+ }
+
+ return { dataPoints, markets: marketInfoList, debugInfo };
+ }, [markets, loanAssetDecimals, startTimestamp, endTimestamp, transactions, snapshots, debug]);
+}
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index fb550767..74d31531 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -112,6 +112,8 @@ const useUserPositionsSummaryData = (user: string | undefined, period: EarningsP
refetch,
loadingStates,
actualBlockData: actualBlockData ?? {},
+ transactions: txData?.items ?? [],
+ snapshotsByChain: allSnapshots ?? {},
};
};
diff --git a/src/utils/blockEstimation.ts b/src/utils/blockEstimation.ts
index 059eba1d..559bbd6d 100644
--- a/src/utils/blockEstimation.ts
+++ b/src/utils/blockEstimation.ts
@@ -60,9 +60,7 @@ export async function fetchBlocksWithTimestamps(
currentTimestamp: number,
): Promise {
// First, estimate all block numbers
- const estimatedBlocks = targetTimestamps.map((ts) =>
- estimateBlockAtTimestamp(chainId, ts, currentBlock, currentTimestamp),
- );
+ const estimatedBlocks = targetTimestamps.map((ts) => estimateBlockAtTimestamp(chainId, ts, currentBlock, currentTimestamp));
// Fetch all block timestamps in parallel
const blockPromises = estimatedBlocks.map(async (blockNum, index) => {