From 362cab3ff5129de1dfe92793bbd27f04be063f65 Mon Sep 17 00:00:00 2001 From: JuniorDevBot Date: Thu, 19 Mar 2026 18:04:46 +0000 Subject: [PATCH 1/5] Add strategy-level KAT rewards APR display - Schema: make estimated apr/apy optional (Kong hydration may set only apr for strategy-addressed rows), add katRewardsAPR component - Selector: fall back to estimated.apr for katana strategies when estimated.apy is missing, extract katRewardsAPR from estimated type - Strategy UI: show sword emoji indicator when strategy has KAT rewards - Tests: cover katana strategy APR fallback and non-katana isolation --- .../detail/VaultStrategiesSection.tsx | 2 + .../components/detail/VaultsListStrategy.tsx | 10 +++ .../vaults/domain/kongVaultSelectors.test.ts | 76 +++++++++++++++++++ .../pages/vaults/domain/kongVaultSelectors.ts | 18 +++++ .../utils/schemas/kongVaultSnapshotSchema.ts | 15 +++- 5 files changed, 119 insertions(+), 2 deletions(-) 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..7524eee2c 100644 --- a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx +++ b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx @@ -26,6 +26,7 @@ export function VaultsListStrategy({ variant = 'v3', apr, netApr, + katRewardsAPR, fees, totalValueUsd }: { @@ -40,6 +41,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 +49,20 @@ export function VaultsListStrategy({ const isInactive = status === 'not_active' const isUnallocated = status === 'unallocated' const shouldShowPlaceholders = isInactive || isUnallocated + const hasKatRewards = typeof katRewardsAPR === 'number' && katRewardsAPR > 0 const displayApr = apr ?? netApr ?? 0 const lastReportTime = details?.lastReport ? formatDuration(details.lastReport * 1000 - Date.now(), true) : 'N/A' let apyContent: ReactElement | string = '-' if (shouldShowPlaceholders) { apyContent = '-' + } else if (hasKatRewards) { + 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..6b2f8bd82 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts @@ -205,4 +205,80 @@ describe('getVaultStrategies', () => { expect(strategies[0]?.estimatedAPY).toBeUndefined() }) + + it('falls back to estimated apr for katana strategies when estimated apy is missing', () => { + 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).toBe(0.0028) + expect(strategies[0]?.katRewardsAPR).toBe(0.0028) + }) + + 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..e7092508d 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,25 @@ const mapSnapshotComposition = ( if (estimatedApy !== null) { return estimatedApy } + // Katana strategy webhook emits APR-named components (e.g. katRewardsAPR), + // so Kong hydration sets estimated.apr without estimated.apy + const estimatedType = entry.performance?.estimated?.type ?? '' + if (estimatedType.includes('katana')) { + const estimatedApr = pickNumberOrNull(entry.performance?.estimated?.apr) + if (estimatedApr !== null) { + return estimatedApr + } + } 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?.apr) ?? undefined + })() const resolvedApr = hasAllocation ? pickNumberOrNull( entry.performance?.historical?.net, @@ -773,6 +790,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() From 21db7f8d9114c96f2628e29d88ff18399d06c54e Mon Sep 17 00:00:00 2001 From: JuniorDevBot Date: Thu, 19 Mar 2026 18:30:49 +0000 Subject: [PATCH 2/5] fix: make KAT rewards additive on top of oracle APY with tooltip breakdown - Selector: estimatedAPY now always falls through to oracle.apy for Katana strategies instead of using estimated.apr (which is KAT rewards) - UI: strategy row shows combined APY (oracle + KAT rewards) - UI: hover tooltip breaks down Base APY and KAT Rewards APR - Tooltip follows existing pattern (rounded-lg border surface-secondary) - Tests updated to reflect additive behavior --- .../components/detail/VaultsListStrategy.tsx | 20 ++++++++--- .../vaults/domain/kongVaultSelectors.test.ts | 33 +++++++++++++++++-- .../pages/vaults/domain/kongVaultSelectors.ts | 12 ++----- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx index 7524eee2c..df28283ce 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' @@ -50,18 +51,27 @@ export function VaultsListStrategy({ const isUnallocated = status === 'unallocated' const shouldShowPlaceholders = isInactive || isUnallocated const hasKatRewards = typeof katRewardsAPR === 'number' && katRewardsAPR > 0 - const displayApr = apr ?? netApr ?? 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)} - + + + {'⚔️'} + {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 6b2f8bd82..2208e1314 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts @@ -206,7 +206,7 @@ describe('getVaultStrategies', () => { expect(strategies[0]?.estimatedAPY).toBeUndefined() }) - it('falls back to estimated apr for katana strategies when estimated apy is missing', () => { + it('uses oracle apy as base for katana strategies — estimated apr is KAT rewards only', () => { const strategies = getVaultStrategies(vault, { totalAssets: '1000000', composition: [ @@ -220,13 +220,42 @@ describe('getVaultStrategies', () => { estimated: { apr: 0.0028, type: 'katana-estimated-apr' + }, + oracle: { + apr: 0.03, + apy: 0.04 } } } ] } as any) - expect(strategies[0]?.estimatedAPY).toBe(0.0028) + // 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) }) diff --git a/src/components/pages/vaults/domain/kongVaultSelectors.ts b/src/components/pages/vaults/domain/kongVaultSelectors.ts index e7092508d..9bd5bc797 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.ts @@ -758,15 +758,9 @@ const mapSnapshotComposition = ( if (estimatedApy !== null) { return estimatedApy } - // Katana strategy webhook emits APR-named components (e.g. katRewardsAPR), - // so Kong hydration sets estimated.apr without estimated.apy - const estimatedType = entry.performance?.estimated?.type ?? '' - if (estimatedType.includes('katana')) { - const estimatedApr = pickNumberOrNull(entry.performance?.estimated?.apr) - if (estimatedApr !== null) { - return estimatedApr - } - } + // 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 })() From d01c7e21f45c7a66765a642736d864ecc2908d27 Mon Sep 17 00:00:00 2001 From: JuniorDevBot Date: Thu, 19 Mar 2026 18:47:57 +0000 Subject: [PATCH 3/5] fix: remove sword emoji from KAT rewards tooltip --- .../pages/vaults/components/detail/VaultsListStrategy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx index df28283ce..b011d7437 100644 --- a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx +++ b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx @@ -62,7 +62,7 @@ export function VaultsListStrategy({ const tooltipContent = (
{'Base APY: '}{formatStrategiesApy(baseApr)}
-
{'⚔️ KAT Rewards APR: '}{formatStrategiesApy(katRewardsAPR)}
+
{'KAT Rewards APR: '}{formatStrategiesApy(katRewardsAPR)}
) apyContent = ( From 37323914e0655641039718cd2983ce408a249721 Mon Sep 17 00:00:00 2001 From: JuniorDevBot Date: Thu, 19 Mar 2026 18:53:10 +0000 Subject: [PATCH 4/5] style: fix biome formatting --- .../components/detail/VaultsListStrategy.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx index b011d7437..c768465ec 100644 --- a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx +++ b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx @@ -61,13 +61,23 @@ export function VaultsListStrategy({ } else if (hasKatRewards) { const tooltipContent = (
-
{'Base APY: '}{formatStrategiesApy(baseApr)}
-
{'KAT Rewards APR: '}{formatStrategiesApy(katRewardsAPR)}
+
+ {'Base APY: '} + {formatStrategiesApy(baseApr)} +
+
+ {'KAT Rewards APR: '} + {formatStrategiesApy(katRewardsAPR)} +
) apyContent = ( - + {'⚔️'} {formatStrategiesApy(displayApr)} From 7e8443c17ba6f4406043eb59a4eff7a487df384b Mon Sep 17 00:00:00 2001 From: Ross Date: Fri, 20 Mar 2026 16:16:37 -0400 Subject: [PATCH 5/5] fix kong selector --- .../vaults/domain/kongVaultSelectors.test.ts | 30 +++++++++++++++++++ .../pages/vaults/domain/kongVaultSelectors.ts | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts index 2208e1314..9e299a524 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.test.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.test.ts @@ -259,6 +259,36 @@ describe('getVaultStrategies', () => { 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', diff --git a/src/components/pages/vaults/domain/kongVaultSelectors.ts b/src/components/pages/vaults/domain/kongVaultSelectors.ts index 9bd5bc797..ddd178526 100644 --- a/src/components/pages/vaults/domain/kongVaultSelectors.ts +++ b/src/components/pages/vaults/domain/kongVaultSelectors.ts @@ -769,7 +769,10 @@ const mapSnapshotComposition = ( if (!estimatedType.includes('katana')) { return undefined } - return pickNumberOrNull(entry.performance?.estimated?.apr) ?? undefined + return ( + pickNumberOrNull(entry.performance?.estimated?.components?.katRewardsAPR, entry.performance?.estimated?.apr) ?? + undefined + ) })() const resolvedApr = hasAllocation ? pickNumberOrNull(