diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0041bd4a..bd143681 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -28,7 +28,8 @@ "Bash(identify:*)", "Bash(sips:*)", "Bash(awk:*)", - "Bash(pnpm dlx:*)" + "Bash(pnpm dlx:*)", + "Bash(oracle ask:*)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index d4091397..4672f335 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,6 @@ next-env.d.ts .cursor -CLAUDE.md \ No newline at end of file +CLAUDE.md +FULLAUTO_CONTEXT.md +.claude/settings.local.json diff --git a/app/autovault/[chainId]/[vaultAddress]/components/allocations/MarketView.tsx b/app/autovault/[chainId]/[vaultAddress]/components/allocations/MarketView.tsx index e927f732..6ee70f1d 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/allocations/MarketView.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/allocations/MarketView.tsx @@ -1,7 +1,10 @@ import { MarketIdentity, MarketIdentityFocus } from '@/components/MarketIdentity'; +import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { MarketAllocation } from '@/types/vaultAllocations'; import { formatBalance, formatReadable } from '@/utils/balance'; import { SupportedNetworks } from '@/utils/networks'; +import { convertApyToApr } from '@/utils/rateMath'; import { formatAllocationAmount, calculateAllocationPercent } from '@/utils/vaultAllocation'; import { AllocationPieChart } from './AllocationPieChart'; @@ -20,6 +23,9 @@ export function MarketView({ vaultAssetDecimals, chainId, }: MarketViewProps) { + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + // Sort by allocation amount (most to least) const sortedItems = [...allocations].sort((a, b) => { if (a.allocation > b.allocation) return -1; @@ -33,7 +39,7 @@ export function MarketView({ Market - APY + {rateLabel} Total Supply Liquidity Amount @@ -46,7 +52,8 @@ export function MarketView({ const { market, allocation } = item; const percentage = totalAllocation > 0n ? parseFloat(calculateAllocationPercent(allocation, totalAllocation)) : 0; - const supplyApy = (market.state.supplyApy * 100).toFixed(2); + const displayRate = (isAprDisplay ? convertApyToApr(market.state.supplyApy) : market.state.supplyApy); + const supplyRate = (displayRate * 100).toFixed(2); const hasAllocation = allocation > 0n; const totalSupply = formatReadable( formatBalance(BigInt(market.state.supplyAssets || 0), market.loanAsset.decimals).toString() @@ -70,9 +77,9 @@ export function MarketView({ /> - {/* APY */} + {/* APY/APR */} - {supplyApy}% + {supplyRate}% {/* Total Supply */} diff --git a/app/autovault/components/VaultListV2.tsx b/app/autovault/components/VaultListV2.tsx index ad18065a..7c3f6483 100644 --- a/app/autovault/components/VaultListV2.tsx +++ b/app/autovault/components/VaultListV2.tsx @@ -6,9 +6,12 @@ import { Spinner } from '@/components/common/Spinner'; import { useTokens } from '@/components/providers/TokenProvider'; import { TokenIcon } from '@/components/TokenIcon'; import { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; +import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; import { parseCapIdParams } from '@/utils/morpho'; import { SupportedNetworks, getNetworkImg } from '@/utils/networks'; +import { convertApyToApr } from '@/utils/rateMath'; type VaultListV2Props = { vaults: UserVaultV2[]; @@ -17,6 +20,8 @@ type VaultListV2Props = { export function VaultListV2({ vaults, loading }: VaultListV2Props) { const { findToken } = useTokens(); + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); if (loading) { return ( @@ -53,7 +58,7 @@ export function VaultListV2({ vaults, loading }: VaultListV2Props) { ID Asset - APY + {rateLabel} Collaterals Action @@ -95,10 +100,12 @@ export function VaultListV2({ vaults, loading }: VaultListV2Props) { - {/* APY */} - + {/* APY/APR */} + - {vault.avgApy && (vault.avgApy * 100).toFixed(2) + '%'} + {vault.avgApy != null + ? ((isAprDisplay ? convertApyToApr(vault.avgApy) : vault.avgApy) * 100).toFixed(2) + '%' + : '—'} diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx index 65a5604e..130c8943 100644 --- a/app/market/[chainId]/[marketid]/RateChart.tsx +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unstable-nested-components */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card, CardHeader, CardBody } from '@heroui/react'; import { Progress } from '@heroui/react'; import { @@ -16,6 +16,9 @@ import { import ButtonGroup from '@/components/ButtonGroup'; import { Spinner } from '@/components/common/Spinner'; import { CHART_COLORS } from '@/constants/chartColors'; +import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; +import { convertApyToApr } from '@/utils/rateMath'; import { MarketRates } from '@/utils/types'; import { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; @@ -36,50 +39,64 @@ function RateChart({ selectedTimeRange, handleTimeframeChange, }: RateChartProps) { + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + const [visibleLines, setVisibleLines] = useState({ supplyApy: true, borrowApy: true, apyAtTarget: true, }); - const getChartData = () => { + const getChartData = useMemo(() => { if (!historicalData) return []; const { supplyApy, borrowApy, apyAtTarget } = historicalData; - return supplyApy.map((point: TimeseriesDataPoint, index: number) => ({ - x: point.x, - supplyApy: point.y, - borrowApy: borrowApy[index]?.y || 0, - apyAtTarget: apyAtTarget[index]?.y || 0, - })); - }; + return supplyApy.map((point: TimeseriesDataPoint, index: number) => { + // Convert values to APR if display mode is enabled + const supplyVal = isAprDisplay ? convertApyToApr(point.y) : point.y; + const borrowVal = isAprDisplay ? convertApyToApr(borrowApy[index]?.y || 0) : (borrowApy[index]?.y || 0); + const targetVal = isAprDisplay ? convertApyToApr(apyAtTarget[index]?.y || 0) : (apyAtTarget[index]?.y || 0); + + return { + x: point.x, + supplyApy: supplyVal, + borrowApy: borrowVal, + apyAtTarget: targetVal, + }; + }); + }, [historicalData, isAprDisplay]); const formatPercentage = (value: number) => `${(value * 100).toFixed(2)}%`; const getCurrentApyValue = (type: 'supply' | 'borrow') => { - return type === 'supply' ? market.state.supplyApy : market.state.borrowApy; + const apy = type === 'supply' ? market.state.supplyApy : market.state.borrowApy; + return isAprDisplay ? convertApyToApr(apy) : apy; }; const getAverageApyValue = (type: 'supply' | 'borrow') => { if (!historicalData) return 0; const data = type === 'supply' ? historicalData.supplyApy : historicalData.borrowApy; - return data.length > 0 + const avgApy = data.length > 0 ? data.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / data.length : 0; + return isAprDisplay ? convertApyToApr(avgApy) : avgApy; }; const getCurrentapyAtTargetValue = () => { - return market.state.apyAtTarget; + const apy = market.state.apyAtTarget; + return isAprDisplay ? convertApyToApr(apy) : apy; }; const getAverageapyAtTargetValue = () => { if (!historicalData?.apyAtTarget || historicalData.apyAtTarget.length === 0) return 0; - return ( + const avgApy = ( historicalData.apyAtTarget.reduce( (sum: number, point: TimeseriesDataPoint) => sum + point.y, 0, ) / historicalData.apyAtTarget.length ); + return isAprDisplay ? convertApyToApr(avgApy) : avgApy; }; const getCurrentUtilizationRate = () => { @@ -131,7 +148,7 @@ function RateChart({ ) : ( - + sum + campaign.apr, 0) : 0; - const fullSupplyAPY = baseSupplyAPY + extraRewards; - const displaySupplyAPY = showFullRewardAPY && hasActiveRewards ? fullSupplyAPY : baseSupplyAPY; - - const borrowAPY = market.state.borrowApy * 100; + const fullSupplyRate = baseSupplyRate + extraRewards; + const displaySupplyRate = showFullRewardAPY && hasActiveRewards ? fullSupplyRate : baseSupplyRate; return (
@@ -195,16 +203,15 @@ export function PositionStats({
- Supply APY: + {rateLabel}:
{hasActiveRewards ? ( - {baseSupplyAPY.toFixed(2)}% + {baseSupplyRate.toFixed(2)}% {' '} (+{extraRewards.toFixed(2)}%) @@ -212,14 +219,14 @@ export function PositionStats({ ) : ( - {displaySupplyAPY.toFixed(2)}% + {displaySupplyRate.toFixed(2)}% )}
- Borrow APY: + {borrowRateLabel}:
- {borrowAPY.toFixed(2)}% + {baseBorrowRate.toFixed(2)}%
diff --git a/app/markets/components/APYBreakdownTooltip.tsx b/app/markets/components/APYBreakdownTooltip.tsx index f40b70f6..c08113b9 100644 --- a/app/markets/components/APYBreakdownTooltip.tsx +++ b/app/markets/components/APYBreakdownTooltip.tsx @@ -3,13 +3,14 @@ import { Tooltip } from '@heroui/react'; import { TokenIcon } from '@/components/TokenIcon'; import { useMarketCampaigns } from '@/hooks/useMarketCampaigns'; import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { SimplifiedCampaign } from '@/utils/merklTypes'; +import { convertApyToApr } from '@/utils/rateMath'; import { Market } from '@/utils/types'; type APYBreakdownTooltipProps = { baseAPY: number; activeCampaigns: SimplifiedCampaign[]; - fullAPY: number; children: React.ReactNode; }; @@ -20,16 +21,26 @@ type APYCellProps = { export function APYBreakdownTooltip({ baseAPY, activeCampaigns, - fullAPY, children, }: APYBreakdownTooltipProps) { + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + + // Convert base rate if APR display is enabled + // Note: baseAPY is already a percentage (not decimal), so we need to convert it + const baseRateValue = isAprDisplay ? convertApyToApr(baseAPY / 100) * 100 : baseAPY; + + // Calculate total: base (converted if needed) + rewards (already APR) + const rewardTotal = activeCampaigns.reduce((sum, campaign) => sum + campaign.apr, 0); + const totalRate = baseRateValue + rewardTotal; + const content = (
-
APY Breakdown
+
{rateLabel} Breakdown
- Base APY - {baseAPY.toFixed(2)}% + Base {rateLabel} + {baseRateValue.toFixed(2)}%
{activeCampaigns.map((campaign, index) => (
@@ -49,7 +60,7 @@ export function APYBreakdownTooltip({
Total - {fullAPY.toFixed(2)}% + {totalRate.toFixed(2)}%
@@ -70,7 +81,7 @@ export function APYBreakdownTooltip({ } export function APYCell({ market }: APYCellProps) { - const { showFullRewardAPY } = useMarkets(); + const { showFullRewardAPY, isAprDisplay } = useMarkets(); const { activeCampaigns, hasActiveRewards } = useMarketCampaigns({ marketId: market.uniqueKey, loanTokenAddress: market.loanAsset.address, @@ -82,17 +93,22 @@ export function APYCell({ market }: APYCellProps) { const extraRewards = hasActiveRewards ? activeCampaigns.reduce((sum, campaign) => sum + campaign.apr, 0) : 0; - const fullAPY = baseAPY + extraRewards; - const displayAPY = showFullRewardAPY && hasActiveRewards ? fullAPY : baseAPY; + // Convert base rate if APR display is enabled + const baseRate = isAprDisplay ? convertApyToApr(market.state.supplyApy) * 100 : baseAPY; + + // Full rate includes base (converted if needed) + rewards + const fullRate = baseRate + extraRewards; + + const displayRate = showFullRewardAPY && hasActiveRewards ? fullRate : baseRate; if (hasActiveRewards) { return ( - - {displayAPY.toFixed(2)}% + + {displayRate.toFixed(2)}% ); } - return {displayAPY.toFixed(2)}%; + return {displayRate.toFixed(2)}%; } diff --git a/app/markets/components/MarketTableBody.tsx b/app/markets/components/MarketTableBody.tsx index ceea24ae..cc3fd254 100644 --- a/app/markets/components/MarketTableBody.tsx +++ b/app/markets/components/MarketTableBody.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { GoStarFill, GoStar } from 'react-icons/go'; +import { RateFormatted } from '@/components/common/RateFormatted'; import { MarketIdBadge } from '@/components/MarketIdBadge'; import { MarketIndicators } from '@/components/MarketIndicators'; import OracleVendorBadge from '@/components/OracleVendorBadge'; import { TrustedByCell } from '@/components/vaults/TrustedVaultBadges'; import { getVaultKey, type TrustedVault } from '@/constants/vaults/known_vaults'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { Market } from '@/utils/types'; import { APYCell } from './APYBreakdownTooltip'; import { ColumnVisibility } from './columnVisibility'; @@ -45,6 +47,9 @@ export function MarketTableBody({ addBlacklistedMarket, isBlacklisted, }: MarketTableBodyProps) { + const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' }); + const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' }); + // Calculate colspan for expanded row based on visible columns const visibleColumnsCount = 9 + // Base columns: Star, ID, Loan, Collateral, Oracle, LLTV, Risk, Indicators, Actions @@ -199,21 +204,21 @@ export function MarketTableBody({ /> )} {columnVisibility.supplyAPY && ( - + )} {columnVisibility.borrowAPY && ( - +

- {item.state.borrowApy ? `${(item.state.borrowApy * 100).toFixed(2)}%` : '—'} + {item.state.borrowApy ? : '—'}

)} {columnVisibility.rateAtTarget && (

- {item.state.apyAtTarget ? `${(item.state.apyAtTarget * 100).toFixed(2)}%` : '—'} + {item.state.apyAtTarget ? : '—'}

)} diff --git a/app/markets/components/marketsTable.tsx b/app/markets/components/marketsTable.tsx index 1b37ea1e..801dfec2 100644 --- a/app/markets/components/marketsTable.tsx +++ b/app/markets/components/marketsTable.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { FaRegStar, FaStar } from 'react-icons/fa'; import { TablePagination } from '@/components/common/TablePagination'; import { type TrustedVault } from '@/constants/vaults/known_vaults'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { Market } from '@/utils/types'; import { buildTrustedVaultMap } from '@/utils/vaults'; import { ColumnVisibility } from './columnVisibility'; @@ -55,6 +56,8 @@ function MarketsTable({ isBlacklisted, }: MarketsTableProps) { const [expandedRowId, setExpandedRowId] = useState(null); + const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' }); + const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' }); const trustedVaultMap = useMemo(() => buildTrustedVaultMap(trustedVaults), [trustedVaults]); @@ -149,7 +152,7 @@ function MarketsTable({ )} {columnVisibility.supplyAPY && ( ; + const { short: rateLabel } = useRateLabel(); + return ; } diff --git a/app/positions/components/FromMarketsTable.tsx b/app/positions/components/FromMarketsTable.tsx index 3a73ce47..82ec991d 100644 --- a/app/positions/components/FromMarketsTable.tsx +++ b/app/positions/components/FromMarketsTable.tsx @@ -2,8 +2,11 @@ import { useState } from 'react'; import { Pagination } from '@heroui/react'; import { Button } from '@/components/common/Button'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/components/MarketIdentity'; +import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; import { previewMarketState } from '@/utils/morpho'; +import { convertApyToApr } from '@/utils/rateMath'; import { MarketPosition } from '@/utils/types'; type PositionWithPendingDelta = MarketPosition & { pendingDelta: number }; @@ -24,6 +27,8 @@ export function FromMarketsTable({ onSelectMax, }: FromMarketsTableProps) { const [currentPage, setCurrentPage] = useState(1); + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); const totalPages = Math.ceil(positions.length / PER_PAGE); const paginatedPositions = positions.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); @@ -58,7 +63,7 @@ export function FromMarketsTable({ Market - APY + {rateLabel} Util Supplied Amount @@ -106,14 +111,26 @@ export function FromMarketsTable({ {apyPreview ? ( - {formatReadable(position.market.state.supplyApy * 100)}% + {formatReadable( + (isAprDisplay + ? convertApyToApr(position.market.state.supplyApy) + : position.market.state.supplyApy) * 100 + )}% {' → '} - {formatReadable(apyPreview.supplyApy * 100)}% + + {formatReadable( + (isAprDisplay ? convertApyToApr(apyPreview.supplyApy) : apyPreview.supplyApy) * 100 + )}% + ) : ( - {formatReadable(position.market.state.supplyApy * 100)}% + {formatReadable( + (isAprDisplay + ? convertApyToApr(position.market.state.supplyApy) + : position.market.state.supplyApy) * 100 + )}% )} diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 972168c7..b8eb46a4 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -22,7 +22,9 @@ import { Button } from '@/components/common/Button'; import { TokenIcon } from '@/components/TokenIcon'; import { TooltipContent } from '@/components/TooltipContent'; import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useMarkets } from '@/hooks/useMarkets'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { useStyledToast } from '@/hooks/useStyledToast'; import { EarningsPeriod } from '@/hooks/useUserPositionsSummaryData'; import { formatReadable, formatBalance } from '@/utils/balance'; @@ -32,6 +34,7 @@ import { groupPositionsByLoanAsset, processCollaterals, } from '@/utils/positions'; +import { convertApyToApr } from '@/utils/rateMath'; import { PositionsShowEmptyKey, PositionsShowCollateralExposureKey } from '@/utils/storageKeys'; import { MarketPosition, @@ -157,6 +160,8 @@ export function PositionsSummaryTable({ true, ); const { address } = useAccount(); + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); const toast = useStyledToast(); @@ -300,7 +305,7 @@ export function PositionsSummaryTable({ Network Size - APY (now) + {rateLabel} (now) Interest Accrued ({earningsPeriod}) @@ -364,9 +369,11 @@ export function PositionsSummaryTable({ />
- +
- {formatReadable(avgApy * 100)}% + + {formatReadable((isAprDisplay ? convertApyToApr(avgApy) : avgApy) * 100)}% +
diff --git a/app/positions/components/RebalanceActionRow.tsx b/app/positions/components/RebalanceActionRow.tsx index d6de3fcb..54005209 100644 --- a/app/positions/components/RebalanceActionRow.tsx +++ b/app/positions/components/RebalanceActionRow.tsx @@ -4,6 +4,7 @@ import { formatUnits, parseUnits } from 'viem'; import { Button } from '@/components/common'; import { MarketIdentity, MarketIdentityMode } from '@/components/MarketIdentity'; import { TokenIcon } from '@/components/TokenIcon'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { previewMarketState } from '@/utils/morpho'; import { GroupedPosition, Market } from '@/utils/types'; import { ApyPreview } from './ApyPreview'; @@ -53,6 +54,8 @@ export function RebalanceActionRow({ isAddDisabled = false, onRemoveAction, }: RebalanceActionRowProps) { + const { short: rateLabel } = useRateLabel(); + // Calculate preview state for the "to" market const previewState = useMemo(() => { if (!toMarket || !amount) { @@ -158,11 +161,11 @@ export function RebalanceActionRow({
- {/* Column 2: APY & Utilization Preview - 25% */} + {/* Column 2: APY/APR & Utilization Preview - 25% */}
- {/* Market APY */} + {/* Market APY/APR */}
- APY + {rateLabel} {toMarket ? ( void; setShowSupplyModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; + rateLabel: string; }) { const suppliedAmount = Number( @@ -55,8 +59,8 @@ function MarketRow({ wide /> - - {formatReadable(position.market.state.supplyApy * 100)}% + + {formatReadable(suppliedAmount)} {position.market.loanAsset.symbol} @@ -111,6 +115,8 @@ export function SuppliedMarketsDetail({ showEmptyPositions, showCollateralExposure, }: SuppliedMarketsDetailProps) { + const { short: rateLabel } = useRateLabel(); + // Sort active markets by size first const sortedMarkets = [...groupedPosition.markets].sort( (a, b) => @@ -186,7 +192,7 @@ export function SuppliedMarketsDetail({ Market Collateral & Parameters - APY + {rateLabel} Supplied % of Portfolio Indicators @@ -202,6 +208,7 @@ export function SuppliedMarketsDetail({ setShowWithdrawModal={setShowWithdrawModal} setShowSupplyModal={setShowSupplyModal} setSelectedPosition={setSelectedPosition} + rateLabel={rateLabel} /> ))} diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx index bc32cd0a..c73a2c8b 100644 --- a/app/positions/components/onboarding/SetupPositions.tsx +++ b/app/positions/components/onboarding/SetupPositions.tsx @@ -10,6 +10,7 @@ import { SupplyProcessModal } from '@/components/SupplyProcessModal'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { useStyledToast } from '@/hooks/useStyledToast'; import { formatBalance } from '@/utils/balance'; import { SupportedNetworks } from '@/utils/networks'; @@ -18,6 +19,7 @@ import { useOnboarding } from './OnboardingContext'; export function SetupPositions() { const toast = useStyledToast(); + const { short: rateLabel } = useRateLabel(); const { selectedToken, selectedMarkets, goToNextStep, goToPrevStep, balances } = useOnboarding(); const [useEth] = useLocalStorage('useEth', false); const [usePermit2Setting] = useLocalStorage('usePermit2', true); @@ -291,7 +293,7 @@ export function SetupPositions() { Market - APY + {rateLabel} Distribution @@ -316,8 +318,8 @@ export function SetupPositions() { /> - {/* APY */} - + {/* APY/APR */} + diff --git a/app/positions/report/components/ReportTable.tsx b/app/positions/report/components/ReportTable.tsx index 94fa2d3e..bec63cf7 100644 --- a/app/positions/report/components/ReportTable.tsx +++ b/app/positions/report/components/ReportTable.tsx @@ -13,11 +13,14 @@ import { Badge } from '@/components/common/Badge'; import { NetworkIcon } from '@/components/common/NetworkIcon'; import OracleVendorBadge from '@/components/OracleVendorBadge'; import { TokenIcon } from '@/components/TokenIcon'; +import { useMarkets } from '@/hooks/useMarkets'; import { ReportSummary } from '@/hooks/usePositionReport'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; import { getExplorerTxURL } from '@/utils/external'; import { actionTypeToText } from '@/utils/morpho'; import { getNetworkName } from '@/utils/networks'; +import { convertApyToApr } from '@/utils/rateMath'; import { Market } from '@/utils/types'; @@ -68,6 +71,12 @@ function MarketSummaryBlock({ apy, hasActivePosition, }: MarketInfoBlockProps) { + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + + // Convert to APR if display mode is enabled + const displayRate = isAprDisplay ? convertApyToApr(apy) : apy; + return (
@@ -103,8 +112,8 @@ function MarketSummaryBlock({
-
{(apy * 100).toFixed(2)}%
-
APY
+
{(displayRate * 100).toFixed(2)}%
+
{rateLabel}
>(new Set()); + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + + // Convert APY to APR if display mode is enabled + const displayGroupedRate = isAprDisplay + ? convertApyToApr(report.groupedEarnings.apy) + : report.groupedEarnings.apy; + const formatter = useDateFormatter({ dateStyle: 'long' }); const toggleMarket = (marketKey: string) => { @@ -183,8 +200,8 @@ export function ReportTable({ report, asset, startDate, endDate, chainId }: Repo

-

APY

-

{(report.groupedEarnings.apy * 100).toFixed(2)}%

+

{rateLabel}

+

{(displayGroupedRate * 100).toFixed(2)}%

diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 099f6137..add9ae68 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -25,7 +25,7 @@ export default function SettingsPage() { defaultTrustedVaults, ); - const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets } = useMarkets(); + const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay, setIsAprDisplay } = useMarkets(); const [isTrustedVaultsModalOpen, setIsTrustedVaultsModalOpen] = React.useState(false); const [isBlacklistedMarketsModalOpen, setIsBlacklistedMarketsModalOpen] = React.useState(false); @@ -87,6 +87,35 @@ export default function SettingsPage() {
+ {/* Display Settings Section */} +
+

Display Settings

+ +
+
+
+

Show APR Instead of APY

+

+ Display Annual Percentage Rate (APR) instead of Annual Percentage Yield (APY). + APR represents the simple annualized rate, while APY accounts for continuous compounding. +

+

+ APR is calculated as ln(1 + APY) and represents the underlying per-second rate + annualized without compounding effects. This affects all rate displays including + tables, charts, and statistics. +

+
+ +
+
+
+ {/* Filter Settings Section */}

Filter Settings

diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx index 588e3ab9..3c388ed5 100644 --- a/src/components/common/MarketDetailsBlock.tsx +++ b/src/components/common/MarketDetailsBlock.tsx @@ -3,9 +3,12 @@ import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from '@radix-ui/reac import { motion, AnimatePresence } from 'framer-motion'; import { formatUnits } from 'viem'; import { useMarketCampaigns } from '@/hooks/useMarketCampaigns'; +import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getIRMTitle, previewMarketState } from '@/utils/morpho'; import { getTruncatedAssetName } from '@/utils/oracle'; +import { convertApyToApr } from '@/utils/rateMath'; import { Market } from '@/utils/types'; import OracleVendorBadge from '../OracleVendorBadge'; import { TokenIcon } from '../TokenIcon'; @@ -33,6 +36,9 @@ export function MarketDetailsBlock({ }: MarketDetailsBlockProps): JSX.Element { const [isExpanded, setIsExpanded] = useState(!defaultCollapsed && !disableExpansion); + const { isAprDisplay } = useMarkets(); + const { short: rateLabel } = useRateLabel(); + const { activeCampaigns, hasActiveRewards } = useMarketCampaigns({ marketId: market.uniqueKey, loanTokenAddress: market.loanAsset.address, @@ -55,16 +61,19 @@ export function MarketDetailsBlock({ return null; }, [market, supplyDelta, borrowDelta, mode]); - // Helper to format APY based on mode - const getAPY = () => { + // Helper to format rate based on mode + const getRate = () => { const apy = mode === 'supply' ? market.state.supplyApy : market.state.borrowApy; - return (apy * 100).toFixed(2); + const rate = isAprDisplay ? convertApyToApr(apy) : apy; + return (rate * 100).toFixed(2); }; - const getPreviewAPY = () => { + const getPreviewRate = () => { if (!previewState) return null; const apy = mode === 'supply' ? previewState.supplyApy : previewState.borrowApy; - return apy ? (apy * 100).toFixed(2) : null; + if (!apy) return null; + const rate = isAprDisplay ? convertApyToApr(apy) : apy; + return (rate * 100).toFixed(2); }; return ( @@ -135,15 +144,15 @@ export function MarketDetailsBlock({ · {previewState !== null ? ( - {getAPY()}% + {getRate()}% {' → '} - {getPreviewAPY()}% + {getPreviewRate()}% - {' APY'} + {' '}{rateLabel} ) : ( - {getAPY()}% APY + {getRate()}% {rateLabel} )} · {(Number(market.lltv) / 1e16).toFixed(0)}% LLTV @@ -190,18 +199,18 @@ export function MarketDetailsBlock({

- {mode === 'supply' ? 'Supply' : 'Borrow'} APY: + {mode === 'supply' ? 'Supply' : 'Borrow'} {rateLabel}:

{previewState !== null ? (

- {getAPY()}% + {getRate()}% {' → '} - {getPreviewAPY()}% + {getPreviewRate()}%

) : ( -

{getAPY()}%

+

{getRate()}%

)}
{showRewards && hasActiveRewards && ( diff --git a/src/components/common/MarketsTableWithSameLoanAsset.tsx b/src/components/common/MarketsTableWithSameLoanAsset.tsx index 44eb7b52..f44be822 100644 --- a/src/components/common/MarketsTableWithSameLoanAsset.tsx +++ b/src/components/common/MarketsTableWithSameLoanAsset.tsx @@ -17,11 +17,13 @@ import { defaultTrustedVaults, getVaultKey, type TrustedVault } from '@/constant import { useFreshMarketsState } from '@/hooks/useFreshMarketsState'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarkets } from '@/hooks/useMarkets'; +import { useRateLabel } from '@/hooks/useRateLabel'; import { formatBalance, formatReadable } from '@/utils/balance'; import { filterMarkets, sortMarkets, createPropertySort } from '@/utils/marketFilters'; import { parseNumericThreshold } from '@/utils/markets'; import { getViemChain } from '@/utils/networks'; import { parsePriceFeedVendors, PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { convertApyToApr } from '@/utils/rateMath'; import * as keys from "@/utils/storageKeys" import { ERC20Token, UnknownERC20Token, infoToKey } from '@/utils/tokens'; import { Market } from '@/utils/types'; @@ -427,6 +429,9 @@ function MarketRow({ showSelectColumn, columnVisibility, trustedVaultMap, + supplyRateLabel, + borrowRateLabel, + isAprDisplay, }: { marketWithSelection: MarketWithSelection; onToggle: () => void; @@ -434,6 +439,9 @@ function MarketRow({ showSelectColumn: boolean; columnVisibility: ColumnVisibility; trustedVaultMap: Map; + supplyRateLabel: string; + borrowRateLabel: string; + isAprDisplay: boolean; }) { const { market, isSelected } = marketWithSelection; const trustedVaults = useMemo(() => { @@ -511,26 +519,32 @@ function MarketRow({ )} {columnVisibility.supplyAPY && ( - +

- {market.state.supplyApy ? `${(market.state.supplyApy * 100).toFixed(2)}` : '—'} + {market.state.supplyApy + ? `${((isAprDisplay ? convertApyToApr(market.state.supplyApy) : market.state.supplyApy) * 100).toFixed(2)}` + : '—'}

{market.state.supplyApy && % }
)} {columnVisibility.borrowAPY && ( - +

- {market.state.borrowApy ? `${(market.state.borrowApy * 100).toFixed(2)}%` : '—'} + {market.state.borrowApy + ? `${((isAprDisplay ? convertApyToApr(market.state.borrowApy) : market.state.borrowApy) * 100).toFixed(2)}%` + : '—'}

)} {columnVisibility.rateAtTarget && (

- {market.state.apyAtTarget ? `${(market.state.apyAtTarget * 100).toFixed(2)}%` : '—'} + {market.state.apyAtTarget + ? `${((isAprDisplay ? convertApyToApr(market.state.apyAtTarget) : market.state.apyAtTarget) * 100).toFixed(2)}%` + : '—'}

)} @@ -559,8 +573,10 @@ export function MarketsTableWithSameLoanAsset({ showSettings = true, }: MarketsTableWithSameLoanAssetProps): JSX.Element { // Get global market settings - const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets } = useMarkets(); + const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay } = useMarkets(); const { findToken } = useTokens(); + const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' }); + const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' }); // Extract just the Market objects for fresh fetching const marketsList = useMemo(() => markets.map((m) => m.market), [markets]); @@ -1012,7 +1028,7 @@ export function MarketsTableWithSameLoanAsset({ )} {columnVisibility.supplyAPY && ( )) )} diff --git a/src/components/common/RateFormatted.tsx b/src/components/common/RateFormatted.tsx new file mode 100644 index 00000000..c10c5c97 --- /dev/null +++ b/src/components/common/RateFormatted.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useMarkets } from '@/hooks/useMarkets'; +import { convertApyToApr } from '@/utils/rateMath'; + +type RateFormattedProps = { + /** + * The rate value as a decimal (e.g., 0.05 for 5%) + */ + value: number; + /** + * Whether to append the "APR" or "APY" label + */ + showLabel?: boolean; + /** + * Number of decimal places to show (default: 2) + */ + precision?: number; + /** + * Additional CSS classes + */ + className?: string; +}; + +/** + * A component that displays a rate value (APY or APR) based on the global setting. + * + * When the user has enabled APR display mode, this component automatically + * converts APY values to APR using the formula: APR = ln(1 + APY) + * + * @example + * // Shows "5.00%" in APY mode or "4.88%" in APR mode + * + * + * @example + * // Shows "5.00% APY" or "4.88% APR" based on setting + * + */ +export function RateFormatted({ + value, + showLabel = false, + precision = 2, + className = '', +}: RateFormattedProps) { + const { isAprDisplay } = useMarkets(); + + // Convert APY to APR if the user has enabled APR display mode + const displayValue = isAprDisplay ? convertApyToApr(value) : value; + + // Format as percentage + const formattedValue = `${(displayValue * 100).toFixed(precision)}%`; + + // Append label if requested + const label = showLabel ? ` ${isAprDisplay ? 'APR' : 'APY'}` : ''; + + return {formattedValue}{label}; +} diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index dcbf3583..34a16ada 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -34,6 +34,8 @@ export type MarketsContextType = { setShowUnwhitelistedMarkets: (value: boolean) => void; showFullRewardAPY: boolean; setShowFullRewardAPY: (value: boolean) => void; + isAprDisplay: boolean; + setIsAprDisplay: (value: boolean) => void; isBlacklisted: (uniqueKey: string) => boolean; addBlacklistedMarket: (uniqueKey: string, chainId: number, reason?: string) => boolean; removeBlacklistedMarket: (uniqueKey: string) => void; @@ -64,6 +66,9 @@ export function MarketsProvider({ children }: MarketsProviderProps) { // Global setting for showing full reward APY (base + external rewards) const [showFullRewardAPY, setShowFullRewardAPY] = useLocalStorage('showFullRewardAPY', false); + // Global setting for showing APR instead of APY + const [isAprDisplay, setIsAprDisplay] = useLocalStorage('settings-apr-display', false); + // Blacklisted markets management const { allBlacklistedMarketKeys, @@ -328,6 +333,8 @@ export function MarketsProvider({ children }: MarketsProviderProps) { setShowUnwhitelistedMarkets, showFullRewardAPY, setShowFullRewardAPY, + isAprDisplay, + setIsAprDisplay, isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, @@ -347,6 +354,8 @@ export function MarketsProvider({ children }: MarketsProviderProps) { setShowUnwhitelistedMarkets, showFullRewardAPY, setShowFullRewardAPY, + isAprDisplay, + setIsAprDisplay, isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, diff --git a/src/hooks/useRateLabel.ts b/src/hooks/useRateLabel.ts new file mode 100644 index 00000000..7081f24c --- /dev/null +++ b/src/hooks/useRateLabel.ts @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; +import { useMarkets } from './useMarkets'; + +type RateLabelOptions = { + /** + * Optional prefix for the label (e.g., "Supply", "Borrow", "Net") + */ + prefix?: string; + /** + * Optional suffix for the label (e.g., "Rate", "Breakdown") + */ + suffix?: string; +}; + +type RateLabelReturn = { + /** + * The label text (e.g., "APY", "APR", "Supply APY", "Borrow APR") + */ + label: string; + /** + * The short form (e.g., "APY" or "APR") + */ + short: string; + /** + * A description of what the label means + */ + description: string; +}; + +/** + * Hook that returns the appropriate rate label based on the global APR/APY setting + * + * @param options - Optional configuration for prefix/suffix + * @returns An object with label, short, and description + * + * @example + * const { label } = useRateLabel({ prefix: 'Supply' }); + * // Returns "Supply APY" or "Supply APR" based on setting + * + * @example + * const { short, description } = useRateLabel(); + * // short: "APR" or "APY" + * // description: explains the rate type + */ +export function useRateLabel(options: RateLabelOptions = {}): RateLabelReturn { + const { prefix, suffix } = options; + const { isAprDisplay } = useMarkets(); + + return useMemo(() => { + const short = isAprDisplay ? 'APR' : 'APY'; + const description = isAprDisplay + ? 'APR (Annual Percentage Rate) uses continuous compounding to match per-second interest accrual. Calculated as ln(1 + APY).' + : 'APY (Annual Percentage Yield) represents the annualized rate including compound interest effects.'; + + // Build the full label + const parts = [prefix, short, suffix].filter(Boolean); + const label = parts.join(' '); + + return { + label, + short, + description, + }; + }, [isAprDisplay, prefix, suffix]); +} diff --git a/src/utils/rateMath.ts b/src/utils/rateMath.ts new file mode 100644 index 00000000..6880bdc2 --- /dev/null +++ b/src/utils/rateMath.ts @@ -0,0 +1,40 @@ +/** + * Converts APY (Annual Percentage Yield) to APR (Annual Percentage Rate) + * using continuous compounding formula: APR = ln(1 + APY) + * + * This matches per-second interest accrual semantics used in DeFi protocols. + * + * @param apy - The APY value as a decimal (e.g., 0.05 for 5%) + * @returns The APR value as a decimal + * + * @example + * convertApyToApr(0.05) // Returns ~0.04879 (4.879% APR from 5% APY) + * convertApyToApr(0.1) // Returns ~0.09531 (9.531% APR from 10% APY) + * convertApyToApr(0) // Returns 0 (0% APR from 0% APY) + */ +export function convertApyToApr(apy: number): number { + // Handle edge case: APY <= -1 would result in ln(0) or ln(negative) + // which is undefined/infinity. Clamp to -1 (representing -100% APR) + if (apy <= -1) { + return -1; + } + + // Handle edge case: APY = 0 returns APR = 0 (optimization) + if (apy === 0) { + return 0; + } + + // Standard conversion: APR = ln(1 + APY) + return Math.log(1 + apy); +} + +/** + * Formats a rate value as a percentage string + * + * @param rate - The rate value as a decimal (e.g., 0.05 for 5%) + * @param precision - Number of decimal places (default: 2) + * @returns Formatted percentage string (e.g., "5.00%") + */ +export function formatRateAsPercentage(rate: number, precision = 2): string { + return `${(rate * 100).toFixed(precision)}%`; +}