From f3d1dfa2640ffeb27bab8c8c306d72f2269f9922 Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Wed, 24 Jun 2026 11:18:10 +0200 Subject: [PATCH] fix(widget): add liquid_staking yield type support Map liquid_staking to the stake dashboard category and treat unknown future API yield types as "unknown" so filters and labels stay safe when the server adds types before the widget is updated. --- packages/widget/src/domain/types/yields.ts | 72 ++++++++++++----- packages/widget/src/generated/api/legacy.ts | 13 +++- packages/widget/src/generated/api/yield.ts | 10 ++- .../widget/src/hooks/use-yield-meta-info.tsx | 1 + .../src/translation/English/translations.json | 21 ++++- .../src/translation/French/translations.json | 21 ++++- .../dashboard-yield-category-types.test.ts | 77 +++++++++++++++---- .../tests/hooks/activity-actions.test.ts | 6 +- 8 files changed, 174 insertions(+), 47 deletions(-) diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts index ac286ce5..93976645 100644 --- a/packages/widget/src/domain/types/yields.ts +++ b/packages/widget/src/domain/types/yields.ts @@ -36,8 +36,23 @@ export type YieldMetadata = Pick< > & { provider?: YieldProviderDetails; }; + +const knownApiYieldTypes = [ + "staking", + "restaking", + "lending", + "vault", + "fixed_yield", + "real_world_asset", + "concentrated_liquidity_pool", + "liquidity_pool", + "liquid_staking", +] as const satisfies ReadonlyArray; + +type KnownApiYieldType = (typeof knownApiYieldTypes)[number]; type LocallyDerivedYieldType = "native_staking" | "pooled_staking"; -export type ExtendedYieldType = ApiYieldType | LocallyDerivedYieldType; +type KnownExtendedYieldType = KnownApiYieldType | LocallyDerivedYieldType; +export type ExtendedYieldType = KnownExtendedYieldType | "unknown"; type YieldActionType = "enter" | "exit"; type YieldArgumentName = ArgumentFieldDto["name"]; @@ -77,29 +92,28 @@ export const dashboardYieldCategories = [ ] as const satisfies ReadonlyArray; /** - * Maps every API `YieldType` to exactly one dashboard category. The - * `satisfies Record` guarantees exhaustiveness: a new server - * yield type fails the build here until it is assigned a category. This mirrors - * `getDashboardYieldCategory` (which classifies hydrated yields) but is keyed by - * the API `type` so it can drive `types[]` query filters. + * Maps locally known API yield types to dashboard categories. Unknown future + * API types are intentionally not included in filtered queries because the app + * cannot infer which dashboard category they belong to. */ const apiYieldTypeToDashboardCategory = { staking: "stake", restaking: "stake", + liquid_staking: "stake", lending: "defi", vault: "defi", fixed_yield: "defi", concentrated_liquidity_pool: "defi", liquidity_pool: "defi", real_world_asset: "rwa", -} as const satisfies Record; +} as const satisfies Record; export const getApiYieldTypesForDashboardCategory = ( category: DashboardYieldCategory -): ApiYieldType[] => +): KnownApiYieldType[] => ( Object.entries(apiYieldTypeToDashboardCategory) as [ - ApiYieldType, + KnownApiYieldType, DashboardYieldCategory, ][] ) @@ -306,6 +320,11 @@ export const getYieldFeePercent = (yieldDto: Yield): number | null => { export const getYieldLockupPeriod = (yieldDto: Yield) => secondsToDays(yieldDto.mechanics.lockupPeriod?.seconds); +const knownApiYieldTypeValues = new Set(knownApiYieldTypes); + +const isKnownApiYieldType = (type: string): type is KnownApiYieldType => + knownApiYieldTypeValues.has(type); + export const getExtendedYieldType = ( yieldDto: YieldBase ): ExtendedYieldType => { @@ -317,7 +336,9 @@ export const getExtendedYieldType = ( return "pooled_staking"; } - return yieldDto.mechanics.type; + const type = yieldDto.mechanics.type as string; + + return isKnownApiYieldType(type) ? type : "unknown"; }; export const getYieldOutputToken = (yieldDto: YieldBase) => @@ -340,6 +361,7 @@ export const hasYieldBearingOutputToken = (yieldDto: YieldBase) => const isStakingYieldType = (yieldType: ExtendedYieldType) => yieldType === "staking" || + yieldType === "liquid_staking" || yieldType === "native_staking" || yieldType === "pooled_staking"; @@ -383,6 +405,12 @@ export const getYieldTypeLabels = ( review: t("yield_types.restaking.review"), cta: t("yield_types.restaking.cta"), }, + liquid_staking: { + type: "liquid_staking", + title: t("yield_types.liquid-staking.title"), + review: t("yield_types.liquid-staking.review"), + cta: t("yield_types.liquid-staking.cta"), + }, fixed_yield: { type: "fixed_yield", title: t("yield_types.fixed_yield.title"), @@ -419,6 +447,12 @@ export const getYieldTypeLabels = ( review: t("yield_types.pooled_staking.review"), cta: t("yield_types.pooled_staking.cta"), }, + unknown: { + type: "unknown", + title: "Yield", + review: "Earn", + cta: "Earn", + }, } satisfies YieldTypeLabelsMap; return map[getExtendedYieldType(yieldDto)]; @@ -427,14 +461,16 @@ export const getYieldTypeLabels = ( const yieldTypesSortRank: { [Key in ExtendedYieldType]: number } = { real_world_asset: 1, staking: 2, - native_staking: 3, - pooled_staking: 4, - restaking: 5, - lending: 6, - vault: 7, - fixed_yield: 8, - liquidity_pool: 9, - concentrated_liquidity_pool: 10, + liquid_staking: 3, + native_staking: 4, + pooled_staking: 5, + restaking: 6, + lending: 7, + vault: 8, + fixed_yield: 9, + liquidity_pool: 10, + concentrated_liquidity_pool: 11, + unknown: 12, }; export const getYieldTypesSortRank = (yieldDto: YieldBase) => diff --git a/packages/widget/src/generated/api/legacy.ts b/packages/widget/src/generated/api/legacy.ts index dbbd7095..293deadf 100644 --- a/packages/widget/src/generated/api/legacy.ts +++ b/packages/widget/src/generated/api/legacy.ts @@ -383,6 +383,7 @@ export type Team = { readonly oavEnabled: boolean; readonly isMfaEnforced: boolean; readonly hyperliquidVerifyByClientOrderId: boolean; + readonly unifiedAccountModeEnabled: boolean; readonly referredBy: string | null; readonly referralCode: string | null; }; @@ -787,6 +788,7 @@ export type YieldProviders = | "lista" | "dolomite" | "midas" + | "concrete" | "dinari" | "ondo" | "superstate" @@ -855,7 +857,8 @@ export type PerpActionTypes = | "approveAgent" | "approveBuilderFee" | "updateMargin" - | "setTpAndSl"; + | "setTpAndSl" + | "setUnifiedAccount"; export type ProgrammaticPerpReportingTransactionDto = { readonly id: string; readonly type: @@ -873,7 +876,8 @@ export type ProgrammaticPerpReportingTransactionDto = { | "ENABLE_DEX_ABSTRACTION" | "APPROVE_AGENT" | "UPDATE_MARGIN" - | "SET_TP_AND_SL"; + | "SET_TP_AND_SL" + | "SET_USER_ABSTRACTION"; readonly status: | "CREATED" | "QUEUED" @@ -3305,6 +3309,7 @@ export type ActionDto = { }; export type ActionGasEstimateDto = { readonly amount: string | null; + readonly entryReserveEstimate?: string; readonly token: TokenDto; readonly gasLimit?: string; readonly transactions: ReadonlyArray; @@ -4139,7 +4144,8 @@ export type ProgrammaticReportingControllerGetPerpActions200 = { | "approveAgent" | "approveBuilderFee" | "updateMargin" - | "setTpAndSl"; + | "setTpAndSl" + | "setUnifiedAccount"; readonly status: | "CANCELED" | "CREATED" @@ -5168,6 +5174,7 @@ export type YieldV2ControllerYieldsParams = { | "lista" | "dolomite" | "midas" + | "concrete" | "dinari" | "ondo" | "superstate" diff --git a/packages/widget/src/generated/api/yield.ts b/packages/widget/src/generated/api/yield.ts index 7c8e5dfc..a0fd7658 100644 --- a/packages/widget/src/generated/api/yield.ts +++ b/packages/widget/src/generated/api/yield.ts @@ -234,6 +234,7 @@ export type RewardDto = { }; readonly yieldSource: | "staking" + | "liquid_staking" | "restaking" | "protocol_incentive" | "campaign_incentive" @@ -257,7 +258,8 @@ export type YieldType = | "fixed_yield" | "real_world_asset" | "concentrated_liquidity_pool" - | "liquidity_pool"; + | "liquidity_pool" + | "liquid_staking"; export type RewardSchedule = | "block" | "hour" @@ -3971,7 +3973,8 @@ export type YieldsControllerGetYieldsParams = { | "fixed_yield" | "real_world_asset" | "concentrated_liquidity_pool" - | "liquidity_pool"; + | "liquidity_pool" + | "liquid_staking"; readonly types?: ReadonlyArray< | "staking" | "restaking" @@ -3981,6 +3984,7 @@ export type YieldsControllerGetYieldsParams = { | "real_world_asset" | "concentrated_liquidity_pool" | "liquidity_pool" + | "liquid_staking" >; readonly hasCooldownPeriod?: boolean; readonly hasWarmupPeriod?: boolean; @@ -4428,6 +4432,7 @@ export type TokensControllerGetTokensParams = { | "real_world_asset" | "concentrated_liquidity_pool" | "liquidity_pool" + | "liquid_staking" >; readonly offset?: number; readonly limit?: number; @@ -4514,6 +4519,7 @@ export type ActionsControllerGetActionsParams = { | "real_world_asset" | "concentrated_liquidity_pool" | "liquidity_pool" + | "liquid_staking" >; readonly network?: | "ethereum" diff --git a/packages/widget/src/hooks/use-yield-meta-info.tsx b/packages/widget/src/hooks/use-yield-meta-info.tsx index 02d05144..fc822438 100644 --- a/packages/widget/src/hooks/use-yield-meta-info.tsx +++ b/packages/widget/src/hooks/use-yield-meta-info.tsx @@ -130,6 +130,7 @@ export const useYieldMetaInfo = ({ switch (yieldType) { case "staking": + case "liquid_staking": case "native_staking": case "pooled_staking": { return { diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json index 35f795ac..cac12c71 100644 --- a/packages/widget/src/translation/English/translations.json +++ b/packages/widget/src/translation/English/translations.json @@ -466,6 +466,7 @@ "staking": "staked", "staking_ethena_usde": "deposited", "liquid-staking": "staked", + "liquid_staking": "staked", "vault": "deposited", "lending": "supplied", "restaking": "restaked", @@ -474,12 +475,14 @@ "fixed_yield": "deposited", "real_world_asset": "deposited", "concentrated_liquidity_pool": "deposited", - "liquidity_pool": "deposited" + "liquidity_pool": "deposited", + "unknown": "earned" }, "unstake": { "staking": "unstaked", "staking_ethena_usde": "withdrawn", "liquid-staking": "unstaked", + "liquid_staking": "unstaked", "vault": "withdrawn", "lending": "withdrawn", "restaking": "unstaked", @@ -488,7 +491,8 @@ "fixed_yield": "withdrawn", "real_world_asset": "withdrawn", "concentrated_liquidity_pool": "withdrawn", - "liquidity_pool": "withdrawn" + "liquidity_pool": "withdrawn", + "unknown": "withdrawn" }, "pending_action": { "stake": "staked", @@ -606,6 +610,11 @@ "review": "Liquid Staking", "cta": "Stake" }, + "liquid_staking": { + "title": "Liquid Staking", + "review": "Liquid Staking", + "cta": "Stake" + }, "vault": { "title": "Vault", "review": "Deposit", @@ -645,26 +654,30 @@ "native_staking": "Staked", "pooled_staking": "Staked", "liquid-staking": "Liquid staked", + "liquid_staking": "Liquid staked", "lending": "Deposited", "vault": "Deposited", "restaking": "Restaked", "fixed_yield": "Deposited", "real_world_asset": "Deposited", "concentrated_liquidity_pool": "Deposited", - "liquidity_pool": "Deposited" + "liquidity_pool": "Deposited", + "unknown": "Yield" }, "unstake_label": { "staking": "Unstake", "native_staking": "Unstake", "pooled_staking": "Unstake", "liquid-staking": "Unstake", + "liquid_staking": "Unstake", "lending": "Withdraw", "vault": "Withdraw", "restaking": "Unstake", "fixed_yield": "Withdraw", "real_world_asset": "Withdraw", "concentrated_liquidity_pool": "Withdraw", - "liquidity_pool": "Withdraw" + "liquidity_pool": "Withdraw", + "unknown": "Manage" }, "balance_type": { "active": "Active", diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json index b62baf28..a508d49a 100644 --- a/packages/widget/src/translation/French/translations.json +++ b/packages/widget/src/translation/French/translations.json @@ -339,6 +339,7 @@ "staking": "staké", "staking_ethena_usde": "déposé", "liquid-staking": "staké", + "liquid_staking": "staké", "vault": "déposé", "lending": "fourni", "restaking": "restaké", @@ -347,12 +348,14 @@ "fixed_yield": "déposé", "real_world_asset": "déposé", "concentrated_liquidity_pool": "déposé", - "liquidity_pool": "déposé" + "liquidity_pool": "déposé", + "unknown": "gagné" }, "unstake": { "staking": "déstaké", "staking_ethena_usde": "retiré", "liquid-staking": "déstaké", + "liquid_staking": "déstaké", "vault": "retiré", "lending": "retiré", "restaking": "déstaké", @@ -361,7 +364,8 @@ "fixed_yield": "retiré", "real_world_asset": "retiré", "concentrated_liquidity_pool": "retiré", - "liquidity_pool": "retiré" + "liquidity_pool": "retiré", + "unknown": "retiré" }, "pending_action": { "stake": "staké", @@ -479,6 +483,11 @@ "review": "Staker", "cta": "Staker" }, + "liquid_staking": { + "title": "Staking Liquide", + "review": "Staker", + "cta": "Staker" + }, "vault": { "title": "Vault", "review": "Déposer", @@ -518,26 +527,30 @@ "native_staking": "Staké", "pooled_staking": "Staké", "liquid-staking": "Staké", + "liquid_staking": "Staké", "lending": "Déposé", "vault": "Déposé", "restaking": "Restaké", "fixed_yield": "Déposé", "real_world_asset": "Déposé", "concentrated_liquidity_pool": "Déposé", - "liquidity_pool": "Déposé" + "liquidity_pool": "Déposé", + "unknown": "Rendement" }, "unstake_label": { "staking": "Déstaker", "native_staking": "Déstaker", "pooled_staking": "Déstaker", "liquid-staking": "Déstaker", + "liquid_staking": "Déstaker", "lending": "Retirer", "vault": "Retirer", "restaking": "Déstaker", "fixed_yield": "Retirer", "real_world_asset": "Retirer", "concentrated_liquidity_pool": "Retirer", - "liquidity_pool": "Retirer" + "liquidity_pool": "Retirer", + "unknown": "Gérer" }, "balance_type": { "active": "Actif", diff --git a/packages/widget/tests/domain/dashboard-yield-category-types.test.ts b/packages/widget/tests/domain/dashboard-yield-category-types.test.ts index b6547e45..7f33cb4a 100644 --- a/packages/widget/tests/domain/dashboard-yield-category-types.test.ts +++ b/packages/widget/tests/domain/dashboard-yield-category-types.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest"; import { dashboardYieldCategories, getApiYieldTypesForDashboardCategory, + getDashboardYieldCategory, + getYieldTypeLabels, getYieldTypesSortRank, type YieldBase, } from "../../src/domain/types/yields"; @@ -15,12 +17,34 @@ const allApiYieldTypes = [ "real_world_asset", "concentrated_liquidity_pool", "liquidity_pool", + "liquid_staking", ] as const; +const makeYield = (type: string): YieldBase => + ({ + mechanics: { + type, + }, + token: { + network: "ethereum", + symbol: "USDC", + }, + }) as YieldBase; + +const t = ((key: string) => { + const values: Record = { + "yield_types.liquid-staking.title": "Liquid Staking", + "yield_types.liquid-staking.review": "Liquid Staking", + "yield_types.liquid-staking.cta": "Stake", + }; + + return values[key] ?? key; +}) as Parameters[1]; + describe("getApiYieldTypesForDashboardCategory", () => { - it("maps stake to staking + restaking", () => { + it("maps stake to staking + restaking + liquid staking", () => { expect(getApiYieldTypesForDashboardCategory("stake").sort()).toEqual( - ["restaking", "staking"].sort() + ["liquid_staking", "restaking", "staking"].sort() ); }); @@ -54,17 +78,6 @@ describe("getApiYieldTypesForDashboardCategory", () => { }); describe("getYieldTypesSortRank", () => { - const makeYield = (type: YieldBase["mechanics"]["type"]): YieldBase => - ({ - mechanics: { - type, - }, - token: { - network: "ethereum", - symbol: "USDC", - }, - }) as YieldBase; - it("ranks RWA yields before other API yield types", () => { const rwaRank = getYieldTypesSortRank(makeYield("real_world_asset")); const otherRanks = allApiYieldTypes @@ -73,4 +86,42 @@ describe("getYieldTypesSortRank", () => { expect(rwaRank).toBeLessThan(Math.min(...otherRanks)); }); + + it("assigns unknown runtime yield types a valid last-place rank", () => { + const unknownRank = getYieldTypesSortRank(makeYield("future_yield_type")); + const knownRanks = allApiYieldTypes.map((type) => + getYieldTypesSortRank(makeYield(type)) + ); + + expect(Number.isFinite(unknownRank)).toBe(true); + expect(unknownRank).toBeGreaterThan(Math.max(...knownRanks)); + }); +}); + +describe("getDashboardYieldCategory", () => { + it("does not assign unknown runtime yield types to a dashboard category", () => { + expect( + getDashboardYieldCategory(makeYield("future_yield_type")) + ).toBeNull(); + }); +}); + +describe("getYieldTypeLabels", () => { + it("uses liquid staking copy for liquid_staking", () => { + expect(getYieldTypeLabels(makeYield("liquid_staking"), t)).toEqual({ + type: "liquid_staking", + title: "Liquid Staking", + review: "Liquid Staking", + cta: "Stake", + }); + }); + + it("uses generic copy for unknown runtime yield types", () => { + expect(getYieldTypeLabels(makeYield("future_yield_type"), t)).toEqual({ + type: "unknown", + title: "Yield", + review: "Earn", + cta: "Earn", + }); + }); }); diff --git a/packages/widget/tests/hooks/activity-actions.test.ts b/packages/widget/tests/hooks/activity-actions.test.ts index af855713..2ca9caed 100644 --- a/packages/widget/tests/hooks/activity-actions.test.ts +++ b/packages/widget/tests/hooks/activity-actions.test.ts @@ -18,7 +18,7 @@ const yieldTypeKey = (values?: ReadonlyArray) => const filterByYieldTypes = new Map([ ["", "all"], - [yieldTypeKey(["staking", "restaking"]), "stake"], + [yieldTypeKey(["staking", "restaking", "liquid_staking"]), "stake"], [ yieldTypeKey([ "lending", @@ -90,7 +90,7 @@ describe("activity action request params", () => { }); it.each([ - ["stake", ["staking", "restaking"]], + ["stake", ["staking", "restaking", "liquid_staking"]], [ "defi", [ @@ -131,7 +131,7 @@ describe("activity action request params", () => { expect(allKey).not.toEqual(stakeKey); expect(stakeKey[1]).toMatchObject({ filter: "stake", - yieldTypes: ["staking", "restaking"], + yieldTypes: ["staking", "restaking", "liquid_staking"], }); }); });