diff --git a/src/data-sources/subgraph/v2-vaults.ts b/src/data-sources/subgraph/v2-vaults.ts index 937e2339..e1270e27 100644 --- a/src/data-sources/subgraph/v2-vaults.ts +++ b/src/data-sources/subgraph/v2-vaults.ts @@ -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 }; /** 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 71107ceb..dfc18537 100644 --- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx +++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx @@ -21,7 +21,6 @@ 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'; @@ -29,72 +28,12 @@ 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 ( - - ); - } - - if (categoryWarnings.some((w) => w.level === 'alert')) { - const alertWarning = categoryWarnings.find((w) => w.level === 'alert'); - return ( - - ); - } - - return ( - - ); - }; - - 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; }; @@ -222,9 +161,11 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr Network Size {rateLabel} (now) - Interest Accrued ({period}) + + {rateLabel} ({periodLabels[period]}) + + Interest Accrued ({periodLabels[period]}) Collateral - Risk Tiers Actions @@ -275,6 +216,35 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr + {/* Actual APY for period */} + +
+ {isEarningsLoading ? ( + + ) : ( + + } + > + + {formatReadable( + (isAprDisplay ? convertApyToApr(groupedPosition.actualApy) : groupedPosition.actualApy) * 100, + )} + % + + + )} +
+
+ {/* Accrued interest */}
@@ -339,16 +309,6 @@ export function SuppliedMorphoBlueGroupedTable({ account }: SuppliedMorphoBlueGr /> - {/* Risk indicators */} - -
- -
-
- {/* Actions button */} { + 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>(new Set()); const { findToken } = useTokensQuery(); const { isAprDisplay } = useAppSettings(); @@ -90,9 +112,11 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false Network Size {rateLabel} (now) - Interest Accrued + + {rateLabel} ({periodLabels[period]}) + + Interest Accrued ({periodLabels[period]}) Collateral - Risk Tiers Actions @@ -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 ( @@ -170,17 +186,39 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false
- {/* APY/APR */} + {/* APY/APR (now) */}
- - {displayRate !== null && displayRate !== undefined ? `${(displayRate * 100).toFixed(2)}%` : '-'} - + {currentRateDisplay} +
+
+ + {/* Historical APY/APR */} + +
+ {isEarningsLoading ? ( + + ) : ( + + } + > + {historicalRateDisplay} + + )}
- {/* Interest Accrued - TODO: implement vault earnings calculation */} - + {/* Interest Accrued */} +
-
@@ -196,16 +234,6 @@ export function UserVaultsTable({ vaults, account, refetch, isRefetching = false />
- {/* Risk Tiers */} - -
- -
-
- {/* Actions */}
diff --git a/src/features/positions/components/vault-risk-indicators.tsx b/src/features/positions/components/vault-risk-indicators.tsx deleted file mode 100644 index 6ec2b089..00000000 --- a/src/features/positions/components/vault-risk-indicators.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useMemo } from 'react'; -import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; -import { RiskIndicator } from '@/features/markets/components/risk-indicator'; -import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; -import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; -import { parseCapIdParams } from '@/utils/morpho'; -import { type WarningWithDetail, WarningCategory } from '@/utils/types'; - -type AggregatedVaultRiskIndicatorsProps = { - vault: UserVaultV2; -}; - -/** - * Aggregates risk indicators from all markets allocated in a vault. - * Similar to AggregatedRiskIndicators but works with vault data structure. - */ -export function AggregatedVaultRiskIndicators({ vault }: AggregatedVaultRiskIndicatorsProps) { - const { allMarkets } = useProcessedMarkets(); - - // Aggregate warnings from all markets in the vault - const uniqueWarnings = useMemo((): WarningWithDetail[] => { - const allWarnings: WarningWithDetail[] = []; - - vault.caps.forEach((cap) => { - const params = parseCapIdParams(cap.idParams); - - // Only process market caps (not collateral caps) - if (params.type === 'market' && params.marketId) { - const market = allMarkets.find((m) => m.uniqueKey.toLowerCase() === params.marketId?.toLowerCase()); - - if (market) { - const marketWarnings = computeMarketWarnings(market, true); - allWarnings.push(...marketWarnings); - } - } - }); - - // Remove duplicates based on warning code - return allWarnings.filter((warning, index, array) => array.findIndex((w) => w.code === warning.code) === index); - }, [vault.caps, allMarkets]); - - // 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 ( - - ); - } - - if (categoryWarnings.some((w) => w.level === 'alert')) { - const alertWarning = categoryWarnings.find((w) => w.level === 'alert'); - return ( - - ); - } - - return ( - - ); - }; - - 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')} - - ); -} diff --git a/src/features/positions/positions-view.tsx b/src/features/positions/positions-view.tsx index a8e5ebd9..8e78d613 100644 --- a/src/features/positions/positions-view.tsx +++ b/src/features/positions/positions-view.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { IoIosSwap } from 'react-icons/io'; import { GoHistory } from 'react-icons/go'; @@ -14,7 +14,9 @@ import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData'; import { usePortfolioValue } from '@/hooks/usePortfolioValue'; import { useUserVaultsV2Query } from '@/hooks/queries/useUserVaultsV2Query'; +import { useVaultHistoricalApy } from '@/hooks/useVaultHistoricalApy'; import { useModal } from '@/hooks/useModal'; +import { usePositionsFilters } from '@/stores/usePositionsFilters'; import { SuppliedMorphoBlueGroupedTable } from './components/supplied-morpho-blue-grouped-table'; import { PortfolioValueBadge } from './components/portfolio-value-badge'; import { UserVaultsTable } from './components/user-vaults-table'; @@ -25,6 +27,7 @@ export default function Positions() { const { account } = useParams<{ account: string }>(); const { open } = useModal(); const { chainId } = useConnection(); + const period = usePositionsFilters((s) => s.period); const { loading: isMarketsLoading } = useProcessedMarkets(); @@ -38,6 +41,18 @@ export default function Positions() { refetch: refetchVaults, } = useUserVaultsV2Query({ userAddress: account as Address }); + // Fetch historical APY for vaults + const { data: vaultApyData, isLoading: isVaultApyLoading } = useVaultHistoricalApy(vaults, period); + + // Merge APY data into vaults + const vaultsWithApy = useMemo(() => { + if (!vaultApyData) return vaults; + return vaults.map((vault) => ({ + ...vault, + actualApy: vaultApyData.get(vault.address.toLowerCase())?.actualApy, + })); + }, [vaults, vaultApyData]); + const router = useRouter(); // Calculate portfolio value from positions and vaults @@ -63,8 +78,6 @@ export default function Positions() {

Portfolio

- {' '} - {/* aligned with portfolio */}