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
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export function VaultStrategiesSection({ currentVault }: { currentVault: TKongVa
variant={vaultVariant}
apr={strategy.estimatedAPY}
netApr={strategy.netAPR}
katRewardsAPR={strategy.katRewardsAPR}
fees={fees}
/>
))}
Expand Down Expand Up @@ -301,6 +302,7 @@ export function VaultStrategiesSection({ currentVault }: { currentVault: TKongVa
variant={vaultVariant}
apr={strategy.estimatedAPY}
netApr={strategy.netAPR}
katRewardsAPR={strategy.katRewardsAPR}
fees={fees}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -26,6 +27,7 @@ export function VaultsListStrategy({
variant = 'v3',
apr,
netApr,
katRewardsAPR,
fees,
totalValueUsd
}: {
Expand All @@ -40,19 +42,47 @@ export function VaultsListStrategy({
variant: 'v2' | 'v3'
apr: number | null | undefined
netApr: number | null | undefined
katRewardsAPR?: number | null
fees: TKongVaultApr['fees']
totalValueUsd: number
}): ReactElement {
const [isExpanded, setIsExpanded] = useState(false)
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 = (
<div className={'rounded-lg border border-border bg-surface-secondary p-2 text-xs text-text-primary'}>
<div>
{'Base APY: '}
{formatStrategiesApy(baseApr)}
</div>
<div className={'mt-1'}>
{'KAT Rewards APR: '}
{formatStrategiesApy(katRewardsAPR)}
</div>
</div>
)
apyContent = (
<Tooltip tooltip={tooltipContent} openDelayMs={150} align={'center'}>
<span
className={
'flex items-center gap-1 underline decoration-neutral-600/30 decoration-dotted underline-offset-4 transition-opacity hover:decoration-neutral-600'
}
>
<span aria-hidden>{'⚔️'}</span>
{formatStrategiesApy(displayApr)}
</span>
</Tooltip>
)
} else {
apyContent = formatStrategiesApy(displayApr)
}
Expand Down
135 changes: 135 additions & 0 deletions src/components/pages/vaults/domain/kongVaultSelectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
15 changes: 15 additions & 0 deletions src/components/pages/vaults/domain/kongVaultSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -773,6 +787,7 @@ const mapSnapshotComposition = (
description: '',
netAPR: resolvedApr,
estimatedAPY,
katRewardsAPR,
status,
details: {
totalDebt,
Expand Down
15 changes: 13 additions & 2 deletions src/components/shared/utils/schemas/kongVaultSnapshotSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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()
Expand Down
Loading