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
1 change: 1 addition & 0 deletions src/data-sources/subgraph/v2-vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type UserVaultV2 = VaultV2Details & {
networkId: SupportedNetworks;
balance?: bigint; // User's redeemable assets (from previewRedeem)
adapter?: Address; // MorphoMarketV1Adapter address
actualApy?: number; // Historical APY for the selected period
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,80 +21,19 @@ import { usePositionsPreferences } from '@/stores/usePositionsPreferences';
import { usePositionsFilters } from '@/stores/usePositionsFilters';
import { useAppSettings } from '@/stores/useAppSettings';
import { useModalStore } from '@/stores/useModalStore';
import { computeMarketWarnings } from '@/hooks/useMarketWarnings';
import { useRateLabel } from '@/hooks/useRateLabel';
import { useStyledToast } from '@/hooks/useStyledToast';
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 WarningWithDetail, WarningCategory } from '@/utils/types';
import { RiskIndicator } from '@/features/markets/components/risk-indicator';
import { useTokenPrices } from '@/hooks/useTokenPrices';
import { getTokenPriceKey } from '@/data-sources/morpho-api/prices';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { SuppliedMarketsDetail } from './supplied-markets-detail';
import { CollateralIconsDisplay } from './collateral-icons-display';

// Component to compute and display aggregated risk indicators for a group of positions
function AggregatedRiskIndicators({ groupedPosition }: { groupedPosition: GroupedPosition }) {
// Compute warnings for all markets in the group
const allWarnings: WarningWithDetail[] = [];

for (const position of groupedPosition.markets) {
const marketWarnings = computeMarketWarnings(position.market, true);
allWarnings.push(...marketWarnings);
}

// Remove duplicates based on warning code
const uniqueWarnings = allWarnings.filter((warning, index, array) => array.findIndex((w) => w.code === warning.code) === index);

// Helper to get warnings by category and determine risk level
const getWarningIndicator = (category: WarningCategory, greenDesc: string, yellowDesc: string, redDesc: string) => {
const categoryWarnings = uniqueWarnings.filter((w) => w.category === category);

if (categoryWarnings.length === 0) {
return (
<RiskIndicator
level="green"
description={greenDesc}
mode="complex"
/>
);
}

if (categoryWarnings.some((w) => w.level === 'alert')) {
const alertWarning = categoryWarnings.find((w) => w.level === 'alert');
return (
<RiskIndicator
level="red"
description={`One or more markets have: ${redDesc}`}
mode="complex"
warningDetail={alertWarning}
/>
);
}

return (
<RiskIndicator
level="yellow"
description={`One or more markets have: ${yellowDesc}`}
mode="complex"
warningDetail={categoryWarnings[0]}
/>
);
};

return (
<>
{getWarningIndicator(WarningCategory.asset, 'Recognized asset', 'Asset with warning', 'High-risk asset')}
{getWarningIndicator(WarningCategory.oracle, 'Recognized oracles', 'Oracle warning', 'Oracle warning')}
{getWarningIndicator(WarningCategory.debt, 'No bad debt', 'Bad debt has occurred', 'Bad debt higher than 1% of supply')}
</>
);
}

type SuppliedMorphoBlueGroupedTableProps = {
account: string;
};
Expand Down Expand Up @@ -222,9 +161,11 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
<TableHead className="w-10">Network</TableHead>
<TableHead>Size</TableHead>
<TableHead>{rateLabel} (now)</TableHead>
<TableHead>Interest Accrued ({period})</TableHead>
<TableHead>
{rateLabel} ({periodLabels[period]})
</TableHead>
<TableHead>Interest Accrued ({periodLabels[period]})</TableHead>
<TableHead>Collateral</TableHead>
<TableHead>Risk Tiers</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
Expand Down Expand Up @@ -275,6 +216,35 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
</div>
</TableCell>

{/* Actual APY for period */}
<TableCell data-label={`${rateLabel} (${periodLabels[period]})`}>
<div className="flex items-center justify-center">
{isEarningsLoading ? (
<PulseLoader
size={4}
color="#f45f2d"
margin={3}
/>
) : (
<Tooltip
content={
<TooltipContent
title={`Historical ${rateLabel}`}
detail={`Annualized yield derived from your actual interest earned over the last ${periodLabels[period]}.`}
/>
}
>
<span className="cursor-help font-medium">
{formatReadable(
(isAprDisplay ? convertApyToApr(groupedPosition.actualApy) : groupedPosition.actualApy) * 100,
)}
%
</span>
</Tooltip>
)}
</div>
</TableCell>

{/* Accrued interest */}
<TableCell data-label={`Interest Accrued (${period})`}>
<div className="flex items-center justify-center gap-2">
Expand Down Expand Up @@ -339,16 +309,6 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
/>
</TableCell>

{/* Risk indicators */}
<TableCell
data-label="Risk Tiers"
className="align-middle"
>
<div className="flex items-center justify-center gap-1">
<AggregatedRiskIndicators groupedPosition={groupedPosition} />
</div>
</TableCell>

{/* Actions button */}
<TableCell
data-label="Actions"
Expand Down Expand Up @@ -379,7 +339,7 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr
{expandedRows.has(rowKey) && (
<TableRow className="bg-surface [&:hover]:border-transparent [&:hover]:bg-surface">
<TableCell
colSpan={10}
colSpan={7}
className="bg-surface"
>
<motion.div
Expand Down
106 changes: 67 additions & 39 deletions src/features/positions/components/user-vaults-table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Fragment, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import Image from 'next/image';
import { PulseLoader } from 'react-spinners';
import { RefetchIcon } from '@/components/ui/refetch-icon';
import { formatUnits } from 'viem';
import { Tooltip } from '@/components/ui/tooltip';
Expand All @@ -12,6 +13,7 @@ import { TableContainerWithHeader } from '@/components/common/table-container-wi
import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults';
import { useTokensQuery } from '@/hooks/queries/useTokensQuery';
import { useAppSettings } from '@/stores/useAppSettings';
import type { EarningsPeriod } from '@/stores/usePositionsFilters';
import { useRateLabel } from '@/hooks/useRateLabel';
import { formatReadable } from '@/utils/balance';
import { getNetworkImg } from '@/utils/networks';
Expand All @@ -20,16 +22,36 @@ import { convertApyToApr } from '@/utils/rateMath';
import { VaultAllocationDetail } from './vault-allocation-detail';
import { CollateralIconsDisplay } from './collateral-icons-display';
import { VaultActionsDropdown } from './vault-actions-dropdown';
import { AggregatedVaultRiskIndicators } from './vault-risk-indicators';

const periodLabels = {
day: '1D',
week: '7D',
month: '30D',
} as const;

const formatRate = (rate: number | null | undefined, isApr: boolean): string => {
if (rate === null || rate === undefined) return '-';
const displayRate = isApr ? convertApyToApr(rate) : rate;
return `${formatReadable((displayRate * 100).toString())}%`;
};

type UserVaultsTableProps = {
vaults: UserVaultV2[];
account: string;
period: EarningsPeriod;
isEarningsLoading?: boolean;
refetch?: () => void;
isRefetching?: boolean;
};

export function UserVaultsTable({ vaults, account, refetch, isRefetching = false }: UserVaultsTableProps) {
export function UserVaultsTable({
vaults,
account,
period,
isEarningsLoading = false,
refetch,
isRefetching = false,
}: UserVaultsTableProps) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const { findToken } = useTokensQuery();
const { isAprDisplay } = useAppSettings();
Expand Down Expand Up @@ -90,9 +112,11 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false
<TableHead className="w-10">Network</TableHead>
<TableHead>Size</TableHead>
<TableHead>{rateLabel} (now)</TableHead>
<TableHead>Interest Accrued</TableHead>
<TableHead>
{rateLabel} ({periodLabels[period]})
</TableHead>
<TableHead>Interest Accrued ({periodLabels[period]})</TableHead>
<TableHead>Collateral</TableHead>
<TableHead>Risk Tiers</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
Expand All @@ -112,27 +136,19 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false
const token = findToken(vault.asset, vault.networkId);
const networkImg = getNetworkImg(vault.networkId);

// Extract unique collateral addresses from caps
const collateralAddresses = vault.caps
.map((cap) => parseCapIdParams(cap.idParams).collateralToken)
.filter((collat) => collat !== undefined);

const uniqueCollateralAddresses = Array.from(new Set(collateralAddresses));
// Extract unique collateral addresses from caps and transform for display
const uniqueCollateralAddresses = [
...new Set(vault.caps.map((cap) => parseCapIdParams(cap.idParams).collateralToken).filter((addr) => addr !== undefined)),
];

// Transform to format expected by CollateralIconsDisplay
const collaterals = uniqueCollateralAddresses
.map((address) => {
const collateralToken = findToken(address, vault.networkId);
return {
address,
symbol: collateralToken?.symbol ?? 'Unknown',
amount: 1, // Use 1 as placeholder since we're just showing presence
};
})
.filter((c) => c !== null);
const collaterals = uniqueCollateralAddresses.map((address) => ({
address,
symbol: findToken(address, vault.networkId)?.symbol ?? 'Unknown',
amount: 1, // Placeholder - we're just showing presence
}));

const avgApy = vault.avgApy;
const displayRate = avgApy !== null && avgApy !== undefined && isAprDisplay ? convertApyToApr(avgApy) : avgApy;
const currentRateDisplay = formatRate(vault.avgApy, isAprDisplay);
const historicalRateDisplay = formatRate(vault.actualApy, isAprDisplay);

return (
<Fragment key={rowKey}>
Expand Down Expand Up @@ -170,17 +186,39 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false
</div>
</TableCell>

{/* APY/APR */}
{/* APY/APR (now) */}
<TableCell data-label={`${rateLabel} (now)`}>
<div className="flex items-center justify-center">
<span className="font-medium">
{displayRate !== null && displayRate !== undefined ? `${(displayRate * 100).toFixed(2)}%` : '-'}
</span>
<span className="font-medium">{currentRateDisplay}</span>
</div>
</TableCell>

{/* Historical APY/APR */}
<TableCell data-label={`${rateLabel} (${periodLabels[period]})`}>
<div className="flex items-center justify-center">
{isEarningsLoading ? (
<PulseLoader
size={4}
color="#f45f2d"
margin={3}
/>
) : (
<Tooltip
content={
<TooltipContent
title={`Historical ${rateLabel}`}
detail={`Annualized yield derived from share price change over the last ${periodLabels[period]}.`}
/>
}
>
<span className="cursor-help font-medium">{historicalRateDisplay}</span>
</Tooltip>
)}
</div>
</TableCell>

{/* Interest Accrued - TODO: implement vault earnings calculation */}
<TableCell data-label="Interest Accrued">
{/* Interest Accrued */}
<TableCell data-label={`Interest Accrued (${periodLabels[period]})`}>
<div className="flex items-center justify-center">
<span className="font-medium">-</span>
</div>
Expand All @@ -196,16 +234,6 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false
/>
</TableCell>

{/* Risk Tiers */}
<TableCell
data-label="Risk Tiers"
className="align-middle"
>
<div className="flex items-center justify-center gap-1">
<AggregatedVaultRiskIndicators vault={vault} />
</div>
</TableCell>

{/* Actions */}
<TableCell data-label="Actions">
<div className="flex justify-center">
Expand Down
Loading