Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
30214c0
show staked balances, add fallback unstake flow
rossgalloway Feb 26, 2026
11c273a
fix double counting, better unstake and withdraw flow
rossgalloway Feb 26, 2026
d9c9508
improve balances routing behavior
rossgalloway Feb 27, 2026
7960c1a
chore(audit): add kong vault inclusion audit script
Feb 7, 2026
bfeb0a1
feat(vaults): fetch full kong list and expose catalog/all vault maps
Feb 7, 2026
1e6dce5
feat(vault-filters): include user-held vaults from allVaults
Feb 7, 2026
c44ea1f
fix(balances): restrict enso missing-token fallback to vault position…
Feb 7, 2026
ef342c8
fix(format): make toBigInt robust to scientific notation
Feb 7, 2026
e123728
Fix staking holdings detection for non-zero staking addresses
Feb 7, 2026
93985f2
feat(holdings): unify vault holdings usd calculation and consumers
Feb 7, 2026
b3bfc79
fix(vaults): restore TKong vault contracts and holdings filters
Mar 3, 2026
88a40e9
simplify and improve code
rossgalloway Feb 27, 2026
69a9190
attempt to rationalize unstaking flow
rossgalloway Feb 27, 2026
360c62c
troubleshooting withdraw flow
rossgalloway Mar 2, 2026
9f23ace
fix: correct wallet holdings loop after rebase
rossgalloway Mar 5, 2026
314d1aa
better transaction flow - still have dust and enso deposit button issues
rossgalloway Mar 5, 2026
c59d4d6
Review cleanup and misc changes incl. hidden vaults in portfolio
rossgalloway Mar 6, 2026
c87fe60
fix dust left after unstake and withdraw and final enso panel state
rossgalloway Mar 6, 2026
b820edb
simplify
rossgalloway Mar 6, 2026
a2e47a4
add non-yearn warning
rossgalloway Mar 6, 2026
356a6c8
add tag to non yearn list vault rows
rossgalloway Mar 6, 2026
ee24aa9
Merge branch 'main' into fix-staking-to-always-show-withdraw-path
rossgalloway Mar 6, 2026
711e278
chore: adjustments
0xeye Mar 9, 2026
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
107 changes: 107 additions & 0 deletions src/components/pages/portfolio/hooks/portfolioVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it } from 'vitest'
import { filterVisiblePortfolioHoldings } from './portfolioVisibility'

function makeVault(address: string, isHidden: boolean) {
return {
chainID: 1,
address,
name: `Vault ${address.slice(-4)}`,
symbol: 'yvTEST',
version: '3.0.0',
type: 'Standard',
kind: 'Single Strategy',
decimals: 18,
token: {
address,
name: 'Vault Token',
symbol: 'yvTEST',
description: '',
decimals: 18
},
tvl: {
totalAssets: 0n,
tvl: 0,
price: 0
},
apr: {
type: 'oracle',
netAPR: 0,
fees: {
performance: 0,
withdrawal: 0,
management: 0
},
extra: {
stakingRewardsAPR: 0,
gammaRewardAPR: 0
},
points: {
weekAgo: 0,
monthAgo: 0,
inception: 0
},
pricePerShare: {
today: 1,
weekAgo: 1,
monthAgo: 1
},
forwardAPR: {
type: 'oracle',
netAPR: 0,
composite: {
boost: 0,
poolAPY: 0,
boostedAPR: 0,
baseAPR: 0,
cvxAPR: 0,
rewardsAPR: 0,
v3OracleCurrentAPR: 0,
v3OracleStratRatioAPR: 0,
keepCRV: 0,
keepVELO: 0,
cvxKeepCRV: 0
}
}
},
featuringScore: 0,
strategies: [],
staking: {
address: null,
available: false,
source: '',
rewards: []
},
migration: {
available: false,
address: '0x0000000000000000000000000000000000000000',
contract: '0x0000000000000000000000000000000000000000'
},
info: {
sourceURL: '',
riskLevel: 1,
riskScore: [],
riskScoreComment: '',
uiNotice: '',
isRetired: false,
isBoosted: false,
isHighlighted: false,
isHidden
}
} as any
}

describe('filterVisiblePortfolioHoldings', () => {
it('hides hidden vaults when the persisted hidden-vault filter is off', () => {
const visible = makeVault('0x1111111111111111111111111111111111111111', false)
const hidden = makeVault('0x2222222222222222222222222222222222222222', true)

expect(filterVisiblePortfolioHoldings([visible, hidden], false)).toEqual([visible])
})

it('keeps hidden vaults when the persisted hidden-vault filter is on', () => {
const visible = makeVault('0x1111111111111111111111111111111111111111', false)
const hidden = makeVault('0x2222222222222222222222222222222222222222', true)

expect(filterVisiblePortfolioHoldings([visible, hidden], true)).toEqual([visible, hidden])
})
})
9 changes: 9 additions & 0 deletions src/components/pages/portfolio/hooks/portfolioVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getVaultInfo, type TKongVault } from '@pages/vaults/domain/kongVaultSelectors'

export function filterVisiblePortfolioHoldings(vaults: TKongVault[], showHiddenVaults: boolean): TKongVault[] {
if (showHiddenVaults) {
return vaults
}

return vaults.filter((vault) => !Boolean(getVaultInfo(vault)?.isHidden))
}
94 changes: 45 additions & 49 deletions src/components/pages/portfolio/hooks/usePortfolioModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import {
getVaultStaking,
type TKongVault
} from '@pages/vaults/domain/kongVaultSelectors'
import { getCanonicalHoldingsVaultAddress } from '@pages/vaults/domain/normalizeVault'
import { isNonYearnErc4626Vault } from '@pages/vaults/domain/vaultWarnings'
import { type TPossibleSortBy, useSortVaults } from '@pages/vaults/hooks/useSortVaults'
import { usePersistedShowHiddenVaults } from '@pages/vaults/hooks/vaultsFiltersStorage'
import { deriveListKind, isAllocatorVaultOverride } from '@pages/vaults/utils/vaultListFacets'
import { useWallet } from '@shared/contexts/useWallet'
import { useWeb3 } from '@shared/contexts/useWeb3'
import { useYearn } from '@shared/contexts/useYearn'
import { getVaultKey, isV3Vault, type TVaultFlags } from '@shared/hooks/useVaultFilterUtils'
import type { TSortDirection } from '@shared/types'
import { toAddress } from '@shared/utils'
import { isZeroAddress, toAddress } from '@shared/utils'
import { calculateVaultEstimatedAPY, calculateVaultHistoricalAPY } from '@shared/utils/vaultApy'
import { useMemo, useState } from 'react'
import { filterVisiblePortfolioHoldings } from './portfolioVisibility'

type THoldingsRow = {
key: string
Expand Down Expand Up @@ -68,30 +72,42 @@ export function usePortfolioModel(): TPortfolioModel {
cumulatedValueInV2Vaults,
cumulatedValueInV3Vaults,
isLoading: isWalletLoading,
getBalance,
getVaultHoldingsUsd,
balances
} = useWallet()
const { isActive, openLoginModal, isUserConnecting, isIdentityLoading } = useWeb3()
const { getPrice, vaults, isLoadingVaultList } = useYearn()
const { vaults, allVaults, isLoadingVaultList } = useYearn()
const showHiddenVaults = usePersistedShowHiddenVaults()
const [sortBy, setSortBy] = useState<TPossibleSortBy>('deposited')
const [sortDirection, setSortDirection] = useState<TSortDirection>('desc')

const vaultLookup = useMemo(() => {
const map = new Map<string, TKongVault>()

Object.values(vaults).forEach((vault) => {
const vaultKey = getVaultKey(vault)
map.set(vaultKey, vault)
Object.values(allVaults).forEach((vault) => {
const canonicalVaultAddress = getCanonicalHoldingsVaultAddress(getVaultAddress(vault))
const canonicalVault = allVaults[canonicalVaultAddress] ?? vault
const vaultKey = getVaultKey(canonicalVault)
if (!map.has(vaultKey)) {
map.set(vaultKey, canonicalVault)
}

const staking = getVaultStaking(vault)
if (staking?.available && staking.address) {
const stakingKey = getChainAddressKey(getVaultChainID(vault), staking.address)
map.set(stakingKey, vault)
if (!isZeroAddress(staking.address)) {
const stakingKey = getChainAddressKey(getVaultChainID(canonicalVault), staking.address)
if (!map.has(stakingKey)) {
map.set(stakingKey, canonicalVault)
}
}

const directKey = getChainAddressKey(getVaultChainID(canonicalVault), getVaultAddress(vault))
if (!map.has(directKey)) {
map.set(directKey, canonicalVault)
}
})

return map
}, [vaults])
}, [allVaults])

const holdingsVaults = useMemo(() => {
const result: TKongVault[] = []
Expand Down Expand Up @@ -122,23 +138,27 @@ export function usePortfolioModel(): TPortfolioModel {
return result
}, [balances, vaultLookup])

const visibleHoldingsVaults = useMemo(
() => filterVisiblePortfolioHoldings(holdingsVaults, showHiddenVaults),
[holdingsVaults, showHiddenVaults]
)

const vaultFlags = useMemo(() => {
const flags: Record<string, TVaultFlags> = {}

holdingsVaults.forEach((vault) => {
visibleHoldingsVaults.forEach((vault) => {
const key = getVaultKey(vault)
const info = getVaultInfo(vault)
const migration = getVaultMigration(vault)
flags[key] = {
hasHoldings: true,
isMigratable: Boolean(migration?.available),
isRetired: Boolean(info?.isRetired),
isHidden: Boolean(info?.isHidden)
isMigratable: Boolean(getVaultMigration(vault)?.available),
isRetired: Boolean(getVaultInfo(vault)?.isRetired),
isHidden: Boolean(getVaultInfo(vault)?.isHidden),
isNotYearn: isNonYearnErc4626Vault({ vault })
}
})

return flags
}, [holdingsVaults])
}, [visibleHoldingsVaults])

const isSearchingBalances =
(isActive || isUserConnecting) && (isWalletLoading || isUserConnecting || isIdentityLoading)
Expand All @@ -152,19 +172,17 @@ export function usePortfolioModel(): TPortfolioModel {
return false
}

const info = getVaultInfo(vault)
const migration = getVaultMigration(vault)
const isHidden = Boolean(info?.isHidden)
const isRetired = Boolean(info?.isRetired)
const isMigratable = Boolean(migration?.available)
const isHighlighted = Boolean(info?.isHighlighted)
const isHidden = Boolean(getVaultInfo(vault)?.isHidden)
const isRetired = Boolean(getVaultInfo(vault)?.isRetired)
const isMigratable = Boolean(getVaultMigration(vault)?.available)
const isHighlighted = Boolean(getVaultInfo(vault)?.isHighlighted)

return !isHidden && !isRetired && !isMigratable && isHighlighted
}),
[vaults]
)

const sortedHoldings = useSortVaults(holdingsVaults, sortBy, sortDirection)
const sortedHoldings = useSortVaults(visibleHoldingsVaults, sortBy, sortDirection)
const sortedCandidates = useSortVaults(suggestedVaultCandidates, 'tvl', 'desc')

const holdingsKeySet = useMemo(() => new Set(sortedHoldings.map((vault) => getVaultKey(vault))), [sortedHoldings])
Expand Down Expand Up @@ -200,7 +218,7 @@ export function usePortfolioModel(): TPortfolioModel {
() =>
(vault: (typeof holdingsVaults)[number]): number | null => {
const apy = calculateVaultEstimatedAPY(vault)
return apy === 0 ? null : apy
return apy === 0 && !vault.performance?.historical?.net ? null : apy
},
[]
)
Expand All @@ -216,31 +234,9 @@ export function usePortfolioModel(): TPortfolioModel {
const getVaultValue = useMemo(
() =>
(vault: (typeof holdingsVaults)[number]): number => {
const chainID = getVaultChainID(vault)
const address = getVaultAddress(vault)
const staking = getVaultStaking(vault)

const shareBalance = getBalance({
address,
chainID
})
const price = getPrice({
address,
chainID
})
const baseValue = shareBalance.normalized * price.normalized

const stakingValue =
staking?.available && staking.address
? getBalance({
address: staking.address,
chainID
}).normalized * price.normalized
: 0

return baseValue + stakingValue
return getVaultHoldingsUsd(vault)
},
[getBalance, getPrice]
[getVaultHoldingsUsd]
)

const blendedMetrics = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type TKongVaultStaking
} from '@pages/vaults/domain/kongVaultSelectors'
import { useVaultSnapshot } from '@pages/vaults/hooks/useVaultSnapshot'
import { isZeroAddress } from '@shared/utils'

type UseVaultWithStakingRewardsReturn = {
vault: TKongVault
Expand All @@ -20,7 +21,7 @@ export function useVaultWithStakingRewards(
const baseStaking = getVaultStaking(originalVault)
const chainId = getVaultChainID(originalVault)
const address = getVaultAddress(originalVault)
const needsFetch = enabled && baseStaking.available
const needsFetch = enabled && !isZeroAddress(baseStaking.address)

const { data: snapshot, isLoading } = useVaultSnapshot({
chainId: needsFetch ? chainId : undefined,
Expand Down
6 changes: 3 additions & 3 deletions src/components/pages/portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { useYearn } from '@shared/contexts/useYearn'
import { getVaultKey } from '@shared/hooks/useVaultFilterUtils'
import { IconSpinner } from '@shared/icons/IconSpinner'
import type { TSortDirection } from '@shared/types'
import { cl, formatPercent, SUPPORTED_NETWORKS } from '@shared/utils'
import { cl, formatPercent, isZeroAddress, SUPPORTED_NETWORKS } from '@shared/utils'
import { formatUSD } from '@shared/utils/format'
import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible'
import type { CSSProperties, ReactElement } from 'react'
Expand Down Expand Up @@ -337,7 +337,7 @@ function ChainStakingRewardsFetcher({
}): null {
const { vault, staking, isLoading: isLoadingVault } = useVaultWithStakingRewards(originalVault, isActive)

const stakingAddress = staking.available ? staking.address : undefined
const stakingAddress = !isZeroAddress(staking.address) ? staking.address : undefined
const rewardTokens = useMemo(
() =>
(staking.rewards ?? []).map((reward) => ({
Expand Down Expand Up @@ -420,7 +420,7 @@ function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioCl
const { vaults } = useYearn()
const trackEvent = usePlausible()
const stakingVaults = useMemo(
() => Object.values(vaults).filter((vault) => getVaultStaking(vault).available),
() => Object.values(vaults).filter((vault) => !isZeroAddress(getVaultStaking(vault).address)),
[vaults]
)
const [selectedChainId, setSelectedChainId] = useState<number | null>(null)
Expand Down
Loading
Loading