Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ NEXT_PUBLIC_UNICHAIN_RPC=
NEXT_PUBLIC_ARBITRUM_RPC=
NEXT_PUBLIC_HYPEREVM_RPC=
NEXT_PUBLIC_MONAD_RPC=
# Set to "false" locally to skip RPC historical-rate fallback when Morpho API rolling rates fail.
NEXT_PUBLIC_ENABLE_MARKET_RATE_RPC_FALLBACK=true


# ==================== End of RPC Settings ====================
Expand Down
34 changes: 26 additions & 8 deletions src/features/market-detail/components/charts/chart-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Dispatch, SetStateAction } from 'react';
import { CHART_COLORS, type useChartColors } from '@/constants/chartColors';
import { TIMEFRAME_CONFIG, type ChartTimeframe } from '@/stores/useMarketDetailChartState';
import type { TimeseriesOptions } from '@/utils/types';

// Derive labels from centralized config
export const TIMEFRAME_LABELS: Record<ChartTimeframe, string> = Object.fromEntries(
Expand Down Expand Up @@ -87,16 +88,27 @@ type ChartTooltipContentProps = {

export function ChartTooltipContent({ active, payload, label, formatValue }: ChartTooltipContentProps) {
if (!active || !payload) return null;
const pointMeta = payload[0]?.payload as { isStateRead?: boolean } | undefined;

return (
<div className="rounded-lg border border-border bg-background p-3 shadow-lg">
<p className="mb-2 text-xs text-secondary">
{new Date((label ?? 0) * 1000).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
<div className="mb-2 flex items-start justify-between gap-4">
<p className="text-xs text-secondary">
{new Date((label ?? 0) * 1000).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
<div className="flex items-center gap-2 text-[11px] text-secondary/70">
{pointMeta?.isStateRead ? (
<span className="rounded border border-border/60 bg-surface px-1.5 py-0.5 uppercase tracking-wide text-secondary">
State Read
</span>
) : null}
</div>
</div>
<div className="space-y-1">
{payload.map((entry: any) => (
<div
Expand Down Expand Up @@ -156,3 +168,9 @@ export const chartLegendStyle = {
iconType: 'circle' as const,
iconSize: 8,
};

export const getTimeSeriesXAxisProps = (timeRange: TimeseriesOptions) => ({
type: 'number' as const,
scale: 'linear' as const,
domain: [timeRange.startTimestamp, timeRange.endTimestamp] as [number, number],
});
95 changes: 66 additions & 29 deletions src/features/market-detail/components/charts/rate-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
createLegendClickHandler,
chartTooltipCursor,
chartLegendStyle,
getTimeSeriesXAxisProps,
} from './chart-utils';
import type { Market } from '@/utils/types';
import type { TimeseriesDataPoint } from '@/utils/types';
Expand All @@ -52,7 +53,12 @@ function RateChart({ marketId, chainId, market }: RateChartProps) {
const rateChainId = chainId as SupportedNetworks;
const customRpcUrl = customRpcUrls[rateChainId];

const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange);
const {
data: historicalData,
stateReadPoints,
isLoading,
isFetching,
} = useMarketHistoricalData(marketId, chainId, selectedTimeRange, market, selectedTimeframe);
const { data: realizedRates, isLoading: isRealizedRatesLoading } = useQuery<WindowRealizedRates>({
queryKey: ['market-realized-window-rates', chainId, market.uniqueKey, realizedWindowSeconds, customRpcUrl ?? null],
queryFn: async () => {
Expand Down Expand Up @@ -86,38 +92,62 @@ function RateChart({ marketId, chainId, market }: RateChartProps) {
});

const chartData = useMemo(() => {
if (!historicalData?.rates) return [];
const { supplyApy, borrowApy, apyAtTarget } = historicalData.rates;
const stateReadByTimestamp = new Map(stateReadPoints.map((point) => [point.targetTimestamp, point]));
const historicalPoints = historicalData?.rates
? historicalData.rates.supplyApy
.map((point: TimeseriesDataPoint, index: number) => {
const borrowValue = historicalData.rates.borrowApy[index]?.y;
const targetValue = historicalData.rates.apyAtTarget[index]?.y;

return supplyApy
.map((point: TimeseriesDataPoint, index: number) => {
const borrowValue = borrowApy[index]?.y;
const targetValue = apyAtTarget[index]?.y;
if (
point.y === null ||
borrowValue == null ||
targetValue == null ||
!Number.isFinite(point.y) ||
!Number.isFinite(borrowValue) ||
!Number.isFinite(targetValue)
) {
return null;
}

if (
point.y === null ||
borrowValue == null ||
targetValue == null ||
!Number.isFinite(point.y) ||
!Number.isFinite(borrowValue) ||
!Number.isFinite(targetValue)
) {
return null;
}
const supplyVal = isAprDisplay ? convertApyToApr(point.y) : point.y;
const borrowVal = isAprDisplay ? convertApyToApr(borrowValue) : borrowValue;
const targetVal = isAprDisplay ? convertApyToApr(targetValue) : targetValue;

return {
x: point.x,
supplyApy: supplyVal,
borrowApy: borrowVal,
apyAtTarget: targetVal,
blockNumber: stateReadByTimestamp.get(point.x)?.blockNumber,
isStateRead: stateReadByTimestamp.has(point.x),
};
})
.filter((point): point is NonNullable<typeof point> => point !== null)
: [];

const supplyVal = isAprDisplay ? convertApyToApr(point.y) : point.y;
const borrowVal = isAprDisplay ? convertApyToApr(borrowValue) : borrowValue;
const targetVal = isAprDisplay ? convertApyToApr(targetValue) : targetValue;
const nowPoint = {
x: selectedTimeRange.endTimestamp,
supplyApy: isAprDisplay ? convertApyToApr(market.state.supplyApy) : market.state.supplyApy,
borrowApy: isAprDisplay ? convertApyToApr(market.state.borrowApy) : market.state.borrowApy,
apyAtTarget: isAprDisplay ? convertApyToApr(market.state.apyAtTarget) : market.state.apyAtTarget,
};

return {
x: point.x,
supplyApy: supplyVal,
borrowApy: borrowVal,
apyAtTarget: targetVal,
};
})
.filter((point): point is NonNullable<typeof point> => point !== null);
}, [historicalData, isAprDisplay]);
const lastHistoricalPoint = historicalPoints.at(-1);
if (!lastHistoricalPoint || lastHistoricalPoint.x < nowPoint.x) {
return [...historicalPoints, nowPoint];
}

return historicalPoints;
}, [
historicalData,
isAprDisplay,
market.state.supplyApy,
market.state.borrowApy,
market.state.apyAtTarget,
selectedTimeRange.endTimestamp,
stateReadPoints,
]);

const formatPercentage = (value: number) => `${(value * 100).toFixed(2)}%`;

Expand Down Expand Up @@ -167,6 +197,12 @@ function RateChart({ marketId, chainId, market }: RateChartProps) {

{/* Controls */}
<div className="flex items-center gap-2">
{isFetching && !isLoading ? (
<div className="flex items-center gap-2 rounded-full border border-border/60 bg-surface px-2 py-1 text-[11px] text-secondary">
<Spinner size={12} />
<span>Updating</span>
</div>
) : null}
<Select
value={selectedTimeframe}
onValueChange={(value) => setTimeframe(value as '1d' | '7d' | '30d' | '3m' | '6m')}
Expand Down Expand Up @@ -212,6 +248,7 @@ function RateChart({ marketId, chainId, market }: RateChartProps) {
/>
<XAxis
dataKey="x"
{...getTimeSeriesXAxisProps(selectedTimeRange)}
axisLine={false}
tickLine={false}
tickMargin={12}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useMarketDetailChartState, calculateTimePoints } from '@/stores/useMark
import { formatSimple, formatReadable } from '@/utils/balance';
import { formatChartTime } from '@/utils/chart';
import { getSlicedAddress } from '@/utils/address';
import { chartTooltipCursor } from './chart-utils';
import { chartTooltipCursor, getTimeSeriesXAxisProps } from './chart-utils';
import type { SupportedNetworks } from '@/utils/networks';
import type { Market } from '@/utils/types';

Expand Down Expand Up @@ -45,7 +45,7 @@ function SupplierPositionsTooltip({

return (
<div className="rounded-lg border border-border bg-background p-3 shadow-lg">
<div className="mb-2 space-y-0.5">
<div className="mb-2 flex items-start justify-between gap-4">
<p className="text-xs text-secondary">
{new Date(timestamp * 1000).toLocaleString(undefined, {
month: 'short',
Expand All @@ -54,7 +54,7 @@ function SupplierPositionsTooltip({
minute: '2-digit',
})}
</p>
{blockNumber && <p className="font-mono text-xs text-secondary/70">Block #{blockNumber.toLocaleString()}</p>}
{blockNumber ? <p className="text-[11px] text-secondary/70">Block {blockNumber.toLocaleString()}</p> : null}
</div>
<div className="space-y-1">
{payload
Expand Down Expand Up @@ -251,6 +251,7 @@ export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPo
/>
<XAxis
dataKey="timestamp"
{...getTimeSeriesXAxisProps(selectedTimeRange)}
axisLine={false}
tickLine={false}
tickMargin={12}
Expand Down
25 changes: 21 additions & 4 deletions src/features/market-detail/components/charts/volume-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useMemo } from 'react';
import moment from 'moment';
import { Card } from '@/components/ui/card';
import { Tooltip as HeroTooltip } from '@/components/ui/tooltip';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
Expand All @@ -20,6 +19,7 @@ import {
createLegendClickHandler,
chartTooltipCursor,
chartLegendStyle,
getTimeSeriesXAxisProps,
} from './chart-utils';
import type { AssetTimeseriesDataPoint, Market } from '@/utils/types';

Expand Down Expand Up @@ -47,7 +47,12 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
const setTimeframe = useMarketDetailChartState((s) => s.setTimeframe);
const chartColors = useChartColors();

const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange);
const {
data: historicalData,
stateReadPoints,
isLoading,
isFetching,
} = useMarketHistoricalData(marketId, chainId, selectedTimeRange, market, selectedTimeframe);

const [visibleLines, setVisibleLines] = useState({
supply: true,
Expand All @@ -62,10 +67,11 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
};

const chartData = useMemo(() => {
const stateReadByTimestamp = new Map(stateReadPoints.map((point) => [point.targetTimestamp, point]));
if (!historicalData?.volumes) {
return [
{
x: moment().unix(),
x: selectedTimeRange.endTimestamp,
supply: convertValue(BigInt(market.state.supplyAssets ?? 0)),
borrow: convertValue(BigInt(market.state.borrowAssets ?? 0)),
liquidity: convertValue(BigInt(market.state.liquidityAssets ?? 0)),
Expand All @@ -88,12 +94,14 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
supply: convertValue(point.y),
borrow: convertValue(borrowData[index]?.y),
liquidity: convertValue(liquidityData[index]?.y),
blockNumber: stateReadByTimestamp.get(point.x)?.blockNumber,
isStateRead: stateReadByTimestamp.has(point.x),
};
})
.filter((point): point is NonNullable<typeof point> => point !== null);

const nowPoint = {
x: moment().unix(),
x: selectedTimeRange.endTimestamp,
supply: convertValue(BigInt(market.state.supplyAssets ?? 0)),
borrow: convertValue(BigInt(market.state.borrowAssets ?? 0)),
liquidity: convertValue(BigInt(market.state.liquidityAssets ?? 0)),
Expand All @@ -106,6 +114,8 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
market.state.supplyAssets,
market.state.borrowAssets,
market.state.liquidityAssets,
selectedTimeRange.endTimestamp,
stateReadPoints,
]);

const formatValue = (value: number) => {
Expand Down Expand Up @@ -200,6 +210,12 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {

{/* Controls */}
<div className="flex gap-2">
{isFetching && !isLoading ? (
<div className="flex items-center gap-2 rounded-full border border-border/60 bg-surface px-2 py-1 text-[11px] text-secondary">
<Spinner size={12} />
<span>Updating</span>
</div>
) : null}
<Select
value={selectedTimeframe}
onValueChange={(value) => setTimeframe(value as '1d' | '7d' | '30d' | '3m' | '6m')}
Expand Down Expand Up @@ -245,6 +261,7 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
/>
<XAxis
dataKey="x"
{...getTimeSeriesXAxisProps(selectedTimeRange)}
axisLine={false}
tickLine={false}
tickMargin={12}
Expand Down
Loading