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
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 TKongVaultInput } from '@pages/vaults/domain/kongVaultSelectors'

export function filterVisiblePortfolioHoldings<T extends TKongVaultInput>(vaults: T[], showHiddenVaults: boolean): T[] {
if (showHiddenVaults) {
return vaults
}

return vaults.filter((vault) => !Boolean(getVaultInfo(vault)?.isHidden))
}
102 changes: 51 additions & 51 deletions src/components/pages/portfolio/hooks/usePortfolioModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import {
type TKongVault,
type TKongVaultInput
} 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 { useYvUsdVaults } from '@pages/vaults/hooks/useYvUsdVaults'
import { usePersistedShowHiddenVaults } from '@pages/vaults/hooks/vaultsFiltersStorage'
import { deriveListKind, isAllocatorVaultOverride } from '@pages/vaults/utils/vaultListFacets'
import {
getWeightedYvUsdApy,
Expand All @@ -27,9 +30,10 @@ 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 { useCallback, useMemo, useState } from 'react'
import { filterVisiblePortfolioHoldings } from './portfolioVisibility'

type THoldingsRow = {
key: string
Expand Down Expand Up @@ -95,11 +99,13 @@ export function usePortfolioModel(): TPortfolioModel {
cumulatedValueInV3Vaults,
isLoading: isWalletLoading,
getBalance,
getVaultHoldingsUsd,
balances
} = useWallet()
const { isActive, openLoginModal, isUserConnecting, isIdentityLoading } = useWeb3()
const { getPrice, vaults, isLoadingVaultList } = useYearn()
const { vaults, allVaults, isLoadingVaultList } = useYearn()
const { listVault: yvUsdVault, unlockedVault: yvUsdUnlockedVault, lockedVault: yvUsdLockedVault } = useYvUsdVaults()
const showHiddenVaults = usePersistedShowHiddenVaults()
const [sortBy, setSortBy] = useState<TPossibleSortBy>('deposited')
const [sortDirection, setSortDirection] = useState<TSortDirection>('desc')

Expand Down Expand Up @@ -130,22 +136,33 @@ export function usePortfolioModel(): TPortfolioModel {
const vaultLookup = useMemo(() => {
const map = new Map<string, TKongVaultInput>()

Object.values(vaults).forEach((vault) => {
Object.values(allVaults).forEach((vault) => {
if (isYvUsdAddress(getVaultAddress(vault))) {
return
}
const vaultKey = getVaultKey(vault)
map.set(vaultKey, 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: TKongVaultInput[] = []
Expand Down Expand Up @@ -177,29 +194,37 @@ export function usePortfolioModel(): TPortfolioModel {
})

if (yvUsdVault && yvUsdPosition.hasHoldings) {
result.push(yvUsdVault)
const yvUsdKey = getVaultKey(yvUsdVault)
if (!seen.has(yvUsdKey)) {
seen.add(yvUsdKey)
result.push(yvUsdVault)
}
}

return result
}, [balances, vaultLookup, yvUsdPosition.hasHoldings, yvUsdVault])

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: isYvUsdVault(vault) ? false : isNonYearnErc4626Vault({ vault: vault as TKongVault })
}
})

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

const isSearchingBalances =
(isActive || isUserConnecting) && (isWalletLoading || isUserConnecting || isIdentityLoading)
Expand All @@ -212,19 +237,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 @@ -299,7 +322,8 @@ export function usePortfolioModel(): TPortfolioModel {
}

const apy = calculateVaultEstimatedAPY(vault)
return apy === 0 ? null : apy
const hasHistoricalNet = 'performance' in vault && Boolean(vault.performance?.historical?.net)
return apy === 0 && !hasHistoricalNet ? null : apy
},
[yvUsdPosition.blendedCurrentApy]
)
Expand All @@ -321,33 +345,9 @@ export function usePortfolioModel(): TPortfolioModel {
return yvUsdPosition.combinedValue
}

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

if (!staking?.available || !staking.address) {
return baseValue
}

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

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

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 @@ -29,7 +29,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