diff --git a/src/components/pages/vaults/components/detail/VaultStrategiesSection.tsx b/src/components/pages/vaults/components/detail/VaultStrategiesSection.tsx index 1df8e4912..b9a365bb9 100644 --- a/src/components/pages/vaults/components/detail/VaultStrategiesSection.tsx +++ b/src/components/pages/vaults/components/detail/VaultStrategiesSection.tsx @@ -243,6 +243,7 @@ export function VaultStrategiesSection({ currentVault }: { currentVault: TKongVa variant={vaultVariant} apr={strategy.estimatedAPY} netApr={strategy.netAPR} + katRewardsAPR={strategy.katRewardsAPR} fees={fees} /> ))} @@ -301,6 +302,7 @@ export function VaultStrategiesSection({ currentVault }: { currentVault: TKongVa variant={vaultVariant} apr={strategy.estimatedAPY} netApr={strategy.netAPR} + katRewardsAPR={strategy.katRewardsAPR} fees={fees} /> ))} diff --git a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx index 033065edb..c768465ec 100644 --- a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx +++ b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx @@ -1,5 +1,6 @@ import type { TKongVaultApr, TKongVaultStrategy } from '@pages/vaults/domain/kongVaultSelectors' import { TokenLogo } from '@shared/components/TokenLogo' +import { Tooltip } from '@shared/components/Tooltip' import { IconChevron } from '@shared/icons/IconChevron' import { IconCopy } from '@shared/icons/IconCopy' import { IconLinkOut } from '@shared/icons/IconLinkOut' @@ -26,6 +27,7 @@ export function VaultsListStrategy({ variant = 'v3', apr, netApr, + katRewardsAPR, fees, totalValueUsd }: { @@ -40,6 +42,7 @@ export function VaultsListStrategy({ variant: 'v2' | 'v3' apr: number | null | undefined netApr: number | null | undefined + katRewardsAPR?: number | null fees: TKongVaultApr['fees'] totalValueUsd: number }): ReactElement { @@ -47,12 +50,39 @@ export function VaultsListStrategy({ const isInactive = status === 'not_active' const isUnallocated = status === 'unallocated' const shouldShowPlaceholders = isInactive || isUnallocated - const displayApr = apr ?? netApr ?? 0 + const hasKatRewards = typeof katRewardsAPR === 'number' && katRewardsAPR > 0 + const baseApr = apr ?? netApr ?? 0 + const displayApr = hasKatRewards ? baseApr + (katRewardsAPR ?? 0) : baseApr const lastReportTime = details?.lastReport ? formatDuration(details.lastReport * 1000 - Date.now(), true) : 'N/A' let apyContent: ReactElement | string = '-' if (shouldShowPlaceholders) { apyContent = '-' + } else if (hasKatRewards) { + const tooltipContent = ( +
+
+ {'Base APY: '} + {formatStrategiesApy(baseApr)} +
+
+ {'KAT Rewards APR: '} + {formatStrategiesApy(katRewardsAPR)} +
+
+ ) + apyContent = ( + + + {'⚔️'} + {formatStrategiesApy(displayApr)} + + + ) } else { apyContent = formatStrategiesApy(displayApr) } diff --git a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts index 787a499d9..9e299a524 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts @@ -205,4 +205,139 @@ describe('getVaultStrategies', () => { expect(strategies[0]?.estimatedAPY).toBeUndefined() }) + + it('uses oracle apy as base for katana strategies — estimated apr is KAT rewards only', () => { + const strategies = getVaultStrategies(vault, { + totalAssets: '1000000', + composition: [ + { + address: '0x8888888888888888888888888888888888888888', + name: 'Morpho Strategy', + status: 'active', + totalDebt: '500000', + currentDebt: '500000', + performance: { + estimated: { + apr: 0.0028, + type: 'katana-estimated-apr' + }, + oracle: { + apr: 0.03, + apy: 0.04 + } + } + } + ] + } as any) + + // estimatedAPY should be oracle.apy (base yield), not estimated.apr (KAT rewards) + expect(strategies[0]?.estimatedAPY).toBe(0.04) + expect(strategies[0]?.katRewardsAPR).toBe(0.0028) + }) + + it('leaves estimatedAPY undefined for katana strategies when neither estimated.apy nor oracle.apy exists', () => { + const strategies = getVaultStrategies(vault, { + totalAssets: '1000000', + composition: [ + { + address: '0x8888888888888888888888888888888888888888', + name: 'Morpho Strategy', + status: 'active', + totalDebt: '500000', + currentDebt: '500000', + performance: { + estimated: { + apr: 0.0028, + type: 'katana-estimated-apr' + } + } + } + ] + } as any) + + expect(strategies[0]?.estimatedAPY).toBeUndefined() + expect(strategies[0]?.katRewardsAPR).toBe(0.0028) + }) + + it('reads katRewardsAPR from estimated components when apr is omitted', () => { + const strategies = getVaultStrategies(vault, { + totalAssets: '1000000', + composition: [ + { + address: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + name: 'Katana Strategy with Components', + status: 'active', + totalDebt: '500000', + currentDebt: '500000', + performance: { + estimated: { + type: 'katana-estimated-apr', + components: { + katRewardsAPR: 0.002978698024448475 + } + }, + oracle: { + apr: 0.013945609013431531, + apy: 0.014041406702504533 + } + } + } + ] + } as any) + + expect(strategies[0]?.estimatedAPY).toBe(0.014041406702504533) + expect(strategies[0]?.katRewardsAPR).toBe(0.002978698024448475) + }) + + it('does not set katRewardsAPR for non-katana strategies', () => { + const strategies = getVaultStrategies(vault, { + totalAssets: '1000000', + composition: [ + { + address: '0x9999999999999999999999999999999999999999', + name: 'Regular Strategy', + status: 'active', + totalDebt: '500000', + currentDebt: '500000', + performance: { + estimated: { + apr: 0.05, + apy: 0.06, + type: 'yvusd-estimated-apr', + components: {} + } + } + } + ] + } as any) + + expect(strategies[0]?.estimatedAPY).toBe(0.06) + expect(strategies[0]?.katRewardsAPR).toBeUndefined() + }) + + it('prefers estimated apy over estimated apr even for katana strategies', () => { + const strategies = getVaultStrategies(vault, { + totalAssets: '1000000', + composition: [ + { + address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + name: 'Katana Strategy with APY', + status: 'active', + totalDebt: '500000', + currentDebt: '500000', + performance: { + estimated: { + apr: 0.003, + apy: 0.05, + type: 'katana-estimated-apr', + components: {} + } + } + } + ] + } as any) + + expect(strategies[0]?.estimatedAPY).toBe(0.05) + expect(strategies[0]?.katRewardsAPR).toBe(0.003) + }) }) diff --git a/src/components/pages/vaults/domain/kongVaultSelectors.ts b/src/components/pages/vaults/domain/kongVaultSelectors.ts index 74d638145..ddd178526 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.ts @@ -333,6 +333,7 @@ export type TKongVaultStrategy = { description: string netAPR: number | null estimatedAPY?: number | null + katRewardsAPR?: number | null status: 'active' | 'not_active' | 'unallocated' details?: { totalDebt: string @@ -757,9 +758,22 @@ const mapSnapshotComposition = ( if (estimatedApy !== null) { return estimatedApy } + // For Katana strategies, estimated.apr is KAT rewards (additive incentive), + // NOT the base yield — so skip straight to oracle.apy for the base value. + // KAT rewards are captured separately in katRewardsAPR below. const oracleApy = pickNumberOrNull(entry.performance?.oracle?.apy) return oracleApy === null ? undefined : oracleApy })() + const katRewardsAPR = (() => { + const estimatedType = entry.performance?.estimated?.type ?? '' + if (!estimatedType.includes('katana')) { + return undefined + } + return ( + pickNumberOrNull(entry.performance?.estimated?.components?.katRewardsAPR, entry.performance?.estimated?.apr) ?? + undefined + ) + })() const resolvedApr = hasAllocation ? pickNumberOrNull( entry.performance?.historical?.net, @@ -773,6 +787,7 @@ const mapSnapshotComposition = ( description: '', netAPR: resolvedApr, estimatedAPY, + katRewardsAPR, status, details: { totalDebt, diff --git a/src/components/shared/utils/schemas/kongVaultSnapshotSchema.ts b/src/components/shared/utils/schemas/kongVaultSnapshotSchema.ts index 2514e8165..3b6c56193 100644 --- a/src/components/shared/utils/schemas/kongVaultSnapshotSchema.ts +++ b/src/components/shared/utils/schemas/kongVaultSnapshotSchema.ts @@ -88,8 +88,18 @@ const snapshotRiskSchema = z .default(null) const estimatedAprSchema = z.object({ - apr: z.union([z.number(), z.string()]).transform((value) => Number(value)), - apy: z.union([z.number(), z.string()]).transform((value) => Number(value)), + apr: z + .union([z.number(), z.string()]) + .transform((value) => Number(value)) + .optional() + .nullable() + .catch(null), + apy: z + .union([z.number(), z.string()]) + .transform((value) => Number(value)) + .optional() + .nullable() + .catch(null), type: z.string().optional().default('').catch(''), components: z .object({ @@ -107,6 +117,7 @@ const estimatedAprSchema = z.object({ katanaBonusAPY: nullableNumberSchema.nullish(), katanaNativeYield: nullableNumberSchema.nullish(), katanaAppRewardsAPR: nullableNumberSchema.nullish(), + katRewardsAPR: nullableNumberSchema.nullish(), steerPointsPerDollar: nullableNumberSchema.nullish(), fixedRateKatanaRewards: nullableNumberSchema.nullish(), FixedRateKatanaRewards: nullableNumberSchema.nullish()