From aab836356a8082547cdc606d4066703a6bb5c613 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:34:30 +0100 Subject: [PATCH 01/12] fix: only show validator UI when requiresValidatorSelection is true Yields like solana-sol-lido-staking have requiresValidatorSelection: false but still require validatorAddress in the API request. This fix: - UI: Only shows validator info when requiresValidatorSelection is true - API: Still passes validatorAddress when the field exists in args This prevents showing "Figment" validator in UI for yields that don't actually require validator selection, while still satisfying API requirements. Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/YieldDetail.tsx | 32 +++++++++---------- src/pages/Yields/components/YieldForm.tsx | 7 ++-- src/pages/Yields/components/YieldManager.tsx | 26 ++++++--------- .../Yields/hooks/useYieldTransactionFlow.ts | 3 +- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index 884871fcd87..1deb198f897 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -11,7 +11,6 @@ import { Display } from '@/components/Display' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { bnOrZero } from '@/lib/bignumber/bignumber' import { - COSMOS_ATOM_NATIVE_STAKING_YIELD_ID, DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, FIGMENT_SOLANA_VALIDATOR_ADDRESS, FIGMENT_VALIDATOR_LOGO, @@ -19,7 +18,6 @@ import { SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, SHAPESHIFT_VALIDATOR_LOGO, SHAPESHIFT_VALIDATOR_NAME, - SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID, } from '@/lib/yieldxyz/constants' import { getYieldDisplayName } from '@/lib/yieldxyz/getYieldDisplayName' import { YieldBalanceType } from '@/lib/yieldxyz/types' @@ -104,24 +102,19 @@ export const YieldDetail = memo(() => { ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] : undefined + const isStaking = yieldItem?.mechanics.type === 'staking' + const requiresValidatorSelection = yieldItem?.mechanics.requiresValidatorSelection ?? false + const shouldFetchValidators = isStaking && requiresValidatorSelection + const selectedValidatorAddress = useMemo(() => { - if ( - yieldId === COSMOS_ATOM_NATIVE_STAKING_YIELD_ID || - yieldId === SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID || - (yieldId?.includes('solana') && yieldId?.includes('native')) - ) { - return defaultValidator - } + if (!requiresValidatorSelection) return undefined return validatorParam || defaultValidator - }, [yieldId, validatorParam, defaultValidator]) - - const isStaking = yieldItem?.mechanics.type === 'staking' - const shouldFetchValidators = isStaking && yieldItem?.mechanics.requiresValidatorSelection + }, [requiresValidatorSelection, validatorParam, defaultValidator]) const { data: validators } = useYieldValidators(yieldItem?.id ?? '', shouldFetchValidators) const { data: yieldProviders } = useYieldProviders() const validatorOrProvider = useMemo(() => { - if (isStaking && selectedValidatorAddress) { + if (isStaking && requiresValidatorSelection && selectedValidatorAddress) { const found = validators?.find(v => v.address === selectedValidatorAddress) if (found) return { name: found.name, logoURI: found.logoURI } if (selectedValidatorAddress === SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS) { @@ -131,7 +124,7 @@ export const YieldDetail = memo(() => { return { name: FIGMENT_VALIDATOR_NAME, logoURI: FIGMENT_VALIDATOR_LOGO } } } - if (!isStaking && yieldItem) { + if (yieldItem) { const provider = yieldProviders?.[yieldItem.providerId] if (provider) { return { @@ -143,7 +136,14 @@ export const YieldDetail = memo(() => { } } return null - }, [isStaking, selectedValidatorAddress, validators, yieldItem, yieldProviders]) + }, [ + isStaking, + requiresValidatorSelection, + selectedValidatorAddress, + validators, + yieldItem, + yieldProviders, + ]) const titleOverride = useMemo(() => { if (!yieldItem) return undefined diff --git a/src/pages/Yields/components/YieldForm.tsx b/src/pages/Yields/components/YieldForm.tsx index 33019235efc..de04048cf0c 100644 --- a/src/pages/Yields/components/YieldForm.tsx +++ b/src/pages/Yields/components/YieldForm.tsx @@ -191,19 +191,20 @@ export const YieldForm = memo( ) const selectedValidatorAddress = useMemo(() => { + if (!shouldFetchValidators) return undefined if (validatorAddress) return validatorAddress if (chainId && DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId]) { return DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] } return validators?.[0]?.address - }, [chainId, validators, validatorAddress]) + }, [shouldFetchValidators, chainId, validators, validatorAddress]) const { data: providers } = useYieldProviders() const isStaking = isStakingYieldType(yieldItem.mechanics.type) const selectedValidatorMetadata = useMemo(() => { - if (!isStaking || !selectedValidatorAddress) return null + if (!shouldFetchValidators || !selectedValidatorAddress) return null const found = validators?.find(v => v.address === selectedValidatorAddress) if (found) return found if (selectedValidatorAddress === SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS) { @@ -214,7 +215,7 @@ export const YieldForm = memo( } } return null - }, [isStaking, selectedValidatorAddress, validators]) + }, [shouldFetchValidators, selectedValidatorAddress, validators]) const providerMetadata = useMemo(() => { if (!providers) return null diff --git a/src/pages/Yields/components/YieldManager.tsx b/src/pages/Yields/components/YieldManager.tsx index c6b5792c391..ffe4bff3dd9 100644 --- a/src/pages/Yields/components/YieldManager.tsx +++ b/src/pages/Yields/components/YieldManager.tsx @@ -9,11 +9,7 @@ import { DialogBody } from '@/components/Modal/components/DialogBody' import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' import { DialogHeader } from '@/components/Modal/components/DialogHeader' import { DialogTitle } from '@/components/Modal/components/DialogTitle' -import { - COSMOS_ATOM_NATIVE_STAKING_YIELD_ID, - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, - SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID, -} from '@/lib/yieldxyz/constants' +import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' import { YieldBalanceType } from '@/lib/yieldxyz/types' import { getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' @@ -31,19 +27,15 @@ export const YieldManager = () => { const { data: yieldItem } = useYield(yieldId ?? '') + const requiresValidatorSelection = yieldItem?.mechanics.requiresValidatorSelection ?? false + const validatorAddress = useMemo(() => { - // For native staking with hardcoded defaults, always use the default validator (ignore URL param) - if ( - yieldId === COSMOS_ATOM_NATIVE_STAKING_YIELD_ID || - yieldId === SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID || - (yieldId?.includes('solana') && yieldId?.includes('native')) - ) { - return yieldItem?.chainId - ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] - : undefined - } - return validatorParam - }, [yieldId, yieldItem?.chainId, validatorParam]) + if (!requiresValidatorSelection) return undefined + return ( + validatorParam || + (yieldItem?.chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] : undefined) + ) + }, [requiresValidatorSelection, validatorParam, yieldItem?.chainId]) const { accountId, accountNumber } = useYieldAccount() const { data: allBalancesData } = useAllYieldBalances() const balances = yieldItem?.id ? allBalancesData?.normalized[yieldItem.id] : undefined diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 624c02092fc..2e151465820 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -237,7 +237,8 @@ export const useYieldTransactionFlow = ({ args.receiverAddress = userAddress } - if (fieldNames.has('validatorAddress') && yieldChainId) { + const validatorField = fields.find(f => f.name === 'validatorAddress') + if (validatorField && yieldChainId) { args.validatorAddress = validatorAddress || DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldChainId] } From 5ffd6ecd3c104b71377d83ea4ab61fbf55671c8d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:36:04 +0100 Subject: [PATCH 02/12] fix: yield modal close stays on detail page instead of navigating back When closing the yield deposit modal on a yield-specific route (e.g., /yields/solana-sol-lido-staking?modal=yield), the modal now removes the modal search params instead of using navigate(-1). This keeps the user on the yield detail page instead of unexpectedly navigating back to the yields list. Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/components/YieldManager.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/pages/Yields/components/YieldManager.tsx b/src/pages/Yields/components/YieldManager.tsx index ffe4bff3dd9..48bdefcb427 100644 --- a/src/pages/Yields/components/YieldManager.tsx +++ b/src/pages/Yields/components/YieldManager.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' @@ -58,17 +58,24 @@ export const YieldManager = () => { return translate('yieldXYZ.manage') }, [action, yieldItem, translate, inputTokenSymbol, claimableTokenSymbol]) + const handleClose = useCallback(() => { + const newParams = new URLSearchParams(searchParams) + newParams.delete('modal') + newParams.delete('action') + navigate({ search: newParams.toString() }, { replace: true }) + }, [navigate, searchParams]) + if (!yieldItem) return null return ( - navigate(-1)} isFullScreen> + {null} {title} - navigate(-1)} /> + @@ -79,8 +86,8 @@ export const YieldManager = () => { validatorAddress={validatorAddress} accountId={accountId} accountNumber={accountNumber} - onClose={() => navigate(-1)} - onDone={() => navigate(-1)} + onClose={handleClose} + onDone={handleClose} /> From 90e974748e0f89b74577ca9d2d36bb85e11e7522 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:46:39 +0100 Subject: [PATCH 03/12] fix: show asset icon for yields in DeFi drawer tab Previously, yield opportunities in the DeFi drawer tab (e.g., "Drift CASH Lending", "Kamino CASH Lending") showed no icon because: 1. The yieldId was used as assetId (invalid for icon lookup) 2. The provider icon was used but often empty/missing Now: - Added inputAssetId to YieldOpportunityDisplay type - Pass inputAssetId when creating yield opportunity items - Use AssetIcon component for yield items in position details This ensures yields show the proper underlying asset icon (e.g., CASH icon for CASH lending opportunities). Co-Authored-By: Claude Opus 4.5 --- .../StakingPositionsByProvider.tsx | 22 ++++++++++++------- .../hooks/useYieldAsOpportunities.ts | 2 ++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx index 2845224ce49..d75ed9c7396 100644 --- a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx @@ -10,6 +10,7 @@ import { useLocation } from 'react-router-dom' import type { Column, Row } from 'react-table' import { Amount } from '@/components/Amount/Amount' +import { AssetIcon } from '@/components/AssetIcon' import { LazyLoadAvatar } from '@/components/LazyLoadAvatar' import { ReactTable } from '@/components/ReactTable/ReactTable' import type { YieldOpportunityDisplay } from '@/components/StakingVaults/hooks/useYieldAsOpportunities' @@ -123,8 +124,8 @@ export const StakingPositionsByProvider: React.FC ({ id: y.yieldId, - assetId: y.yieldId, - underlyingAssetId: y.yieldId, + assetId: y.inputAssetId ?? y.yieldId, + underlyingAssetId: y.inputAssetId ?? y.yieldId, provider: y.providerName, apy: y.apy, fiatAmount: y.fiatAmount, @@ -276,14 +277,19 @@ export const StakingPositionsByProvider: React.FC + ) : ( + + ) return ( - + {iconElement} {providerName} diff --git a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts index 60d6f0462ef..d21882257ab 100644 --- a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts +++ b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts @@ -13,6 +13,7 @@ export type YieldOpportunityDisplay = { yieldId: string providerName: string providerIcon?: string + inputAssetId?: AssetId apy: string fiatAmount: string cryptoAmount: string @@ -103,6 +104,7 @@ export const useYieldAsOpportunities = ( yieldId: yieldItem.id, providerName: yieldItem.metadata.name || yieldItem.providerId, providerIcon: yieldItem.metadata.logoURI, + inputAssetId, apy: yieldItem.rewardRate.total.toString(), fiatAmount: bnOrZero(totalUsd).toFixed(2), cryptoAmount: bnOrZero(totalCrypto).toString(), From a86d7cf8f63aefd7445741645a4f86e1442f4747 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:06:35 +0100 Subject: [PATCH 04/12] feat: handle disabled yield states and fix icon rendering - Add UI indicators for yields with status.enter=false - Disable enter/exit buttons when status is false - Show warning alerts in modals for disabled actions - Filter disabled yields from lists unless user has balance - Don't auto-select disabled yields as default - Fix CASH icon not loading by handling null inputTokens array - Add translations for depositsDisabled and withdrawalsDisabled Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 4 ++ .../StakingPositionsByProvider.tsx | 21 ++-------- .../components/Earn/EarnInput.tsx | 9 ++++- .../hooks/useYieldAsOpportunities.ts | 7 ++++ src/lib/yieldxyz/augment.ts | 9 ++++- src/pages/Yields/YieldAssetDetails.tsx | 10 ++++- src/pages/Yields/components/YieldForm.tsx | 38 ++++++++++++++++++- src/pages/Yields/components/YieldHero.tsx | 11 ++++++ src/pages/Yields/components/YieldInfoCard.tsx | 11 ++++++ src/pages/Yields/components/YieldItem.tsx | 12 +++++- .../Yields/components/YieldPositionCard.tsx | 12 ++++++ src/pages/Yields/components/YieldsList.tsx | 8 +++- 12 files changed, 127 insertions(+), 25 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index c324d311341..2626400f439 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2799,6 +2799,10 @@ "underMaintenanceDescription": "This yield opportunity is currently under maintenance. Deposits may be unavailable.", "deprecated": "Deprecated", "deprecatedDescription": "This yield opportunity has been deprecated and may be discontinued soon.", + "depositsDisabled": "Deposits Disabled", + "depositsDisabledDescription": "Deposits are temporarily unavailable for this yield opportunity.", + "withdrawalsDisabled": "Withdrawals Disabled", + "withdrawalsDisabledDescription": "Withdrawals are temporarily unavailable for this yield opportunity.", "learnMore": "Learn more", "noAvailableYields": "No yield opportunities available for your assets", "connectWalletAvailable": "Connect a wallet to see yields available for your assets", diff --git a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx index d75ed9c7396..534d1694783 100644 --- a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx @@ -145,23 +145,10 @@ export const StakingPositionsByProvider: React.FC(() => { - const result = [...yieldPositionsAsUnified, ...legacyFiltered] - console.debug( - '[StakingPositionsByProvider] filteredDown:', - JSON.stringify( - result.map(r => ({ - id: r.id, - provider: r.provider, - isYield: (r as UnifiedPosition).isYield, - fiatAmount: r.fiatAmount, - })), - null, - 2, - ), - ) - return result - }, [yieldPositionsAsUnified, legacyFiltered]) + const filteredDown = useMemo( + () => [...yieldPositionsAsUnified, ...legacyFiltered], + [yieldPositionsAsUnified, legacyFiltered], + ) const handleClick = useCallback( (row: RowProps, action: DefiAction) => { diff --git a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx index 55699db4b77..3bbc520f9b9 100644 --- a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx +++ b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx @@ -389,7 +389,10 @@ export const EarnInput = memo( const yieldsForAsset = useMemo(() => { if (!sellAsset?.assetId || !yieldsData?.byInputAssetId) return [] - return yieldsData.byInputAssetId[sellAsset.assetId] ?? [] + const allYields = yieldsData.byInputAssetId[sellAsset.assetId] ?? [] + return allYields.filter( + y => y.status.enter && !y.metadata.underMaintenance && !y.metadata.deprecated, + ) }, [sellAsset?.assetId, yieldsData?.byInputAssetId]) const defaultYieldForAsset = useMemo(() => { @@ -401,11 +404,13 @@ export const EarnInput = memo( const userBalance = bnOrZero(sellAssetBalanceCryptoPrecision) const actionableYield = sortedByApy.find(y => { + if (!y.status.enter) return false const minDepositAmount = bnOrZero(y.mechanics?.entryLimits?.minimum) return minDepositAmount.lte(0) || userBalance.gte(minDepositAmount) }) - return actionableYield ?? sortedByApy[0] + const enabledYield = actionableYield ?? sortedByApy.find(y => y.status.enter) + return enabledYield ?? sortedByApy[0] }, [yieldsForAsset, sellAssetBalanceCryptoPrecision]) useEffect(() => { diff --git a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts index d21882257ab..9995c03ee7f 100644 --- a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts +++ b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts @@ -45,6 +45,13 @@ export const useYieldAsOpportunities = ( const inputAssetId = yieldItem.inputTokens?.[0]?.assetId if (!inputAssetId) return + const hasBalance = bnOrZero(yieldBalancesData?.aggregated[yieldItem.id]?.totalUsd).gt(0) + const isDisabled = + !yieldItem.status.enter || + yieldItem.metadata.underMaintenance || + yieldItem.metadata.deprecated + if (isDisabled && !hasBalance) return + if (!aggregatedByAssetId[inputAssetId]) { aggregatedByAssetId[inputAssetId] = { assetId: inputAssetId, diff --git a/src/lib/yieldxyz/augment.ts b/src/lib/yieldxyz/augment.ts index 99b7df42f6a..de9ea6a671f 100644 --- a/src/lib/yieldxyz/augment.ts +++ b/src/lib/yieldxyz/augment.ts @@ -129,12 +129,17 @@ export const augmentYield = (yieldDto: YieldDto): AugmentedYieldDto => { const chainId = chainIdFromYieldDto(yieldDto) const evmNetworkId = parseEvmNetworkId(yieldDto.chainId) + const augmentedToken = augmentYieldToken(yieldDto.token, chainId) + const inputTokens = yieldDto.inputTokens?.length + ? yieldDto.inputTokens.map(t => augmentYieldToken(t, chainId)) + : [augmentedToken] + return { ...yieldDto, chainId, evmNetworkId, - token: augmentYieldToken(yieldDto.token, chainId), - inputTokens: yieldDto.inputTokens.map(t => augmentYieldToken(t, chainId)), + token: augmentedToken, + inputTokens, outputToken: yieldDto.outputToken ? augmentYieldToken(yieldDto.outputToken, chainId) : undefined, diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index fe2c39fbf79..33e39b450db 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -201,12 +201,20 @@ export const YieldAssetDetails = memo(() => { const filteredYields = useMemo( () => assetYields.filter(y => { + const isDisabled = !y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated + if (isDisabled) { + const balances = allBalances?.[y.id] + const hasBalance = + balances && + balances.reduce((sum, b) => sum.plus(bnOrZero(b.amountUsd)), bnOrZero(0)).gt(0) + if (!hasBalance) return false + } if (selectedNetwork && y.network !== selectedNetwork) return false if (selectedProvider && y.providerId !== selectedProvider) return false if (selectedType && y.mechanics.type !== selectedType) return false return true }), - [assetYields, selectedNetwork, selectedProvider, selectedType], + [assetYields, allBalances, selectedNetwork, selectedProvider, selectedType], ) const columns = useMemo[]>( diff --git a/src/pages/Yields/components/YieldForm.tsx b/src/pages/Yields/components/YieldForm.tsx index de04048cf0c..6d9d8fdec71 100644 --- a/src/pages/Yields/components/YieldForm.tsx +++ b/src/pages/Yields/components/YieldForm.tsx @@ -1,4 +1,17 @@ -import { Avatar, Box, Button, Flex, HStack, Icon, Input, Skeleton, Text } from '@chakra-ui/react' +import { + Alert, + AlertDescription, + AlertIcon, + Avatar, + Box, + Button, + Flex, + HStack, + Icon, + Input, + Skeleton, + Text, +} from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import type { ChangeEvent } from 'react' @@ -412,9 +425,16 @@ export const YieldForm = memo( return null }, [isStaking, selectedValidatorMetadata, providerMetadata]) + const isActionDisabled = useMemo(() => { + if (action === 'enter') return !yieldItem.status.enter + if (action === 'exit') return !yieldItem.status.exit + return false + }, [action, yieldItem.status.enter, yieldItem.status.exit]) + const buttonDisabled = useMemo(() => { if (!isConnected) return false if (isLoading) return true + if (isActionDisabled) return true if (isClaimAction) { return !claimAction || !claimableAmount || bnOrZero(claimableAmount).lte(0) } @@ -422,6 +442,7 @@ export const YieldForm = memo( }, [ isConnected, isLoading, + isActionDisabled, isClaimAction, claimAction, claimableAmount, @@ -698,6 +719,20 @@ export const YieldForm = memo( const stepsToShow = activeStepIndex >= 0 ? transactionSteps : displaySteps + const actionDisabledAlert = useMemo(() => { + if (!isActionDisabled) return null + const descriptionKey = + action === 'enter' + ? 'yieldXYZ.depositsDisabledDescription' + : 'yieldXYZ.withdrawalsDisabledDescription' + return ( + + + {translate(descriptionKey)} + + ) + }, [isActionDisabled, action, translate]) + // If Success, render YieldSuccess if (isSuccess) { const successAmount = isClaimAction ? claimableAmount : cryptoAmount @@ -726,6 +761,7 @@ export const YieldForm = memo( return ( + {actionDisabledAlert} {inputContent} {!isClaimAction && percentButtons} {!isClaimAction && inputTokenAssetId && accountId && ( diff --git a/src/pages/Yields/components/YieldHero.tsx b/src/pages/Yields/components/YieldHero.tsx index 0c4c4d4244b..47902fdf15b 100644 --- a/src/pages/Yields/components/YieldHero.tsx +++ b/src/pages/Yields/components/YieldHero.tsx @@ -144,6 +144,17 @@ export const YieldHero = memo( )} + {!yieldItem.status.enter && + !yieldItem.metadata.deprecated && + !yieldItem.metadata.underMaintenance && ( + + + + {translate('yieldXYZ.depositsDisabledDescription')} + + + )} + {iconSource.assetId ? ( diff --git a/src/pages/Yields/components/YieldInfoCard.tsx b/src/pages/Yields/components/YieldInfoCard.tsx index 760c704f18e..53abf8f820c 100644 --- a/src/pages/Yields/components/YieldInfoCard.tsx +++ b/src/pages/Yields/components/YieldInfoCard.tsx @@ -108,6 +108,17 @@ export const YieldInfoCard = memo( )} + {!yieldItem.status.enter && + !yieldItem.metadata.deprecated && + !yieldItem.metadata.underMaintenance && ( + + + + {translate('yieldXYZ.depositsDisabledDescription')} + + + )} + {stackedIconElement} diff --git a/src/pages/Yields/components/YieldItem.tsx b/src/pages/Yields/components/YieldItem.tsx index 1a239a34d29..a404684e4c6 100644 --- a/src/pages/Yields/components/YieldItem.tsx +++ b/src/pages/Yields/components/YieldItem.tsx @@ -161,6 +161,7 @@ export const YieldItem = memo( const underMaintenance = isSingle ? data.yieldItem.metadata.underMaintenance : undefined const deprecated = isSingle ? data.yieldItem.metadata.deprecated : undefined + const depositsDisabled = isSingle ? !data.yieldItem.status.enter : false const statusBadge = useMemo(() => { if (!isSingle) return null @@ -182,8 +183,17 @@ export const YieldItem = memo( ) } + if (depositsDisabled) { + return ( + + + {translate('yieldXYZ.depositsDisabled')} + + + ) + } return null - }, [isSingle, deprecated, underMaintenance, translate]) + }, [isSingle, deprecated, underMaintenance, depositsDisabled, translate]) const showAvailable = isSingle && hasAvailable && !hasBalance diff --git a/src/pages/Yields/components/YieldPositionCard.tsx b/src/pages/Yields/components/YieldPositionCard.tsx index 624d66bbe30..c9c742c9fb4 100644 --- a/src/pages/Yields/components/YieldPositionCard.tsx +++ b/src/pages/Yields/components/YieldPositionCard.tsx @@ -477,6 +477,12 @@ export const YieldPositionCard = memo( onClick={handleEnter} flex={1} fontWeight='bold' + isDisabled={!yieldItem.status.enter} + title={ + !yieldItem.status.enter + ? translate('yieldXYZ.depositsDisabledDescription') + : undefined + } > {enterLabel} @@ -490,6 +496,12 @@ export const YieldPositionCard = memo( onClick={handleExit} flex={1} fontWeight='bold' + isDisabled={!yieldItem.status.exit} + title={ + !yieldItem.status.exit + ? translate('yieldXYZ.withdrawalsDisabledDescription') + : undefined + } > {exitLabel} diff --git a/src/pages/Yields/components/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index 16f5a947c20..2d4bdefab5c 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -251,6 +251,7 @@ export const YieldsList = memo(() => { if (balance.lte(0)) continue const eligibleYields = yieldsForAsset.filter(y => { + if (!y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated) return false const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum) if (minDeposit.gt(0)) { const asset = assets[assetId] @@ -316,7 +317,12 @@ export const YieldsList = memo(() => { return yields.assetGroups .map(group => { - let filteredYields = group.yields + let filteredYields = group.yields.filter(y => { + const isDisabled = !y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated + if (!isDisabled) return true + const hasBalance = bnOrZero(getYieldPositionBalanceUsd(y.id)).gt(0) + return hasBalance + }) if (selectedNetwork) filteredYields = filteredYields.filter(y => y.network === selectedNetwork) if (selectedProvider) From 7bdce667b8441379f4762a982e31c1c1ac7475cc Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:11:33 +0100 Subject: [PATCH 05/12] test: add augment function tests for inputTokens handling - Test augmenting inputTokens when array is provided - Test fallback to token when inputTokens is null (CASH fix) - Test fallback to token when inputTokens is empty array - Test status preservation (enter/exit flags) - Test metadata preservation (underMaintenance, deprecated) Co-Authored-By: Claude Opus 4.5 --- src/lib/yieldxyz/augment.test.ts | 217 +++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/lib/yieldxyz/augment.test.ts diff --git a/src/lib/yieldxyz/augment.test.ts b/src/lib/yieldxyz/augment.test.ts new file mode 100644 index 00000000000..b44c63ee8de --- /dev/null +++ b/src/lib/yieldxyz/augment.test.ts @@ -0,0 +1,217 @@ +import { solanaChainId } from '@shapeshiftoss/caip' +import { describe, expect, it, vi } from 'vitest' + +import { augmentYield } from './augment' +import type { YieldDto } from './types' +import { YieldNetwork } from './types' + +vi.mock('@/context/PluginProvider/chainAdapterSingleton', () => ({ + getChainAdapterManager: () => ({ + get: (chainId: string) => { + if (chainId === solanaChainId) { + return { + getFeeAssetId: () => 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', + } + } + return undefined + }, + }), +})) + +const createMockYieldDto = (overrides: Partial = {}): YieldDto => + ({ + id: 'test-yield-id', + network: YieldNetwork.Solana, + chainId: '', + token: { + name: 'Test Token', + symbol: 'TEST', + decimals: 6, + network: YieldNetwork.Solana, + address: 'TestTokenAddress123', + logoURI: 'https://example.com/token.png', + }, + inputTokens: [ + { + name: 'Input Token', + symbol: 'INPUT', + decimals: 6, + network: YieldNetwork.Solana, + address: 'InputTokenAddress123', + logoURI: 'https://example.com/input.png', + }, + ], + outputToken: undefined, + status: { enter: true, exit: true }, + rewardRate: { + total: 0.05, + rateType: 'APY', + components: [], + }, + metadata: { + name: 'Test Yield', + logoURI: 'https://example.com/yield.png', + description: 'A test yield', + underMaintenance: false, + deprecated: false, + }, + statistics: { + tvlUsd: '1000000', + }, + mechanics: { + type: 'lending', + requiresValidatorSelection: false, + gasFeeToken: { + name: 'Solana', + symbol: 'SOL', + decimals: 9, + network: YieldNetwork.Solana, + logoURI: 'https://example.com/sol.png', + }, + arguments: { + enter: { fields: [] }, + exit: { fields: [] }, + }, + entryLimits: { minimum: '0' }, + exitLimits: { minimum: '0' }, + }, + providerId: 'test-provider', + ...overrides, + }) as YieldDto + +describe('augmentYield', () => { + describe('inputTokens handling', () => { + it('should augment inputTokens when array is provided', () => { + const yieldDto = createMockYieldDto({ + inputTokens: [ + { + name: 'Input Token', + symbol: 'INPUT', + decimals: 6, + network: YieldNetwork.Solana, + address: 'InputTokenAddress123', + logoURI: 'https://example.com/input.png', + }, + ], + }) + + const result = augmentYield(yieldDto) + + expect(result.inputTokens).toHaveLength(1) + expect(result.inputTokens[0].symbol).toBe('INPUT') + expect(result.inputTokens[0].assetId).toBe( + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:InputTokenAddress123', + ) + }) + + it('should use token as inputToken when inputTokens is null', () => { + const yieldDto = createMockYieldDto({ + inputTokens: null as unknown as YieldDto['inputTokens'], + token: { + name: 'CASH', + symbol: 'CASH', + decimals: 6, + network: YieldNetwork.Solana, + address: 'CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH', + logoURI: 'https://example.com/cash.png', + }, + }) + + const result = augmentYield(yieldDto) + + expect(result.inputTokens).toHaveLength(1) + expect(result.inputTokens[0].symbol).toBe('CASH') + expect(result.inputTokens[0].assetId).toBe( + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH', + ) + }) + + it('should use token as inputToken when inputTokens is empty array', () => { + const yieldDto = createMockYieldDto({ + inputTokens: [], + token: { + name: 'CASH', + symbol: 'CASH', + decimals: 6, + network: YieldNetwork.Solana, + address: 'CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH', + logoURI: 'https://example.com/cash.png', + }, + }) + + const result = augmentYield(yieldDto) + + expect(result.inputTokens).toHaveLength(1) + expect(result.inputTokens[0].symbol).toBe('CASH') + }) + }) + + describe('status preservation', () => { + it('should preserve status.enter=true', () => { + const yieldDto = createMockYieldDto({ + status: { enter: true, exit: true }, + }) + + const result = augmentYield(yieldDto) + + expect(result.status.enter).toBe(true) + expect(result.status.exit).toBe(true) + }) + + it('should preserve status.enter=false', () => { + const yieldDto = createMockYieldDto({ + status: { enter: false, exit: true }, + }) + + const result = augmentYield(yieldDto) + + expect(result.status.enter).toBe(false) + expect(result.status.exit).toBe(true) + }) + + it('should preserve status.exit=false', () => { + const yieldDto = createMockYieldDto({ + status: { enter: true, exit: false }, + }) + + const result = augmentYield(yieldDto) + + expect(result.status.enter).toBe(true) + expect(result.status.exit).toBe(false) + }) + }) + + describe('metadata preservation', () => { + it('should preserve underMaintenance flag', () => { + const yieldDto = createMockYieldDto({ + metadata: { + name: 'Test', + logoURI: '', + description: '', + underMaintenance: true, + deprecated: false, + }, + }) + + const result = augmentYield(yieldDto) + + expect(result.metadata.underMaintenance).toBe(true) + }) + + it('should preserve deprecated flag', () => { + const yieldDto = createMockYieldDto({ + metadata: { + name: 'Test', + logoURI: '', + description: '', + underMaintenance: false, + deprecated: true, + }, + }) + + const result = augmentYield(yieldDto) + + expect(result.metadata.deprecated).toBe(true) + }) + }) +}) From b4c11f57195f45738bdb22e4146f4084be20fdc4 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:31:24 +0100 Subject: [PATCH 06/12] fix: filter disabled yields from related markets and fix filter options - Filter disabled yields from "Other X Yields" related markets section - Fix "Available to Earn" filter options to use unfiltered data so options don't shrink when a filter is applied - Unify balance terminology to "Balance" across all yield components - Extract isYieldDisabled utility for code reuse Co-Authored-By: Claude Opus 4.5 --- .../components/Earn/EarnInput.tsx | 5 +-- .../hooks/useYieldAsOpportunities.ts | 7 +--- src/lib/yieldxyz/utils.ts | 5 +++ src/pages/Yields/YieldAssetDetails.tsx | 7 ++-- .../Yields/components/YieldRelatedMarkets.tsx | 2 + .../Yields/components/YieldViewHelpers.tsx | 2 +- src/pages/Yields/components/YieldsList.tsx | 38 +++++++++++++------ 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx index 3bbc520f9b9..5ab1559ebc0 100644 --- a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx +++ b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx @@ -27,6 +27,7 @@ import { bnOrZero, positiveOrZero } from '@/lib/bignumber/bignumber' import { fromBaseUnit } from '@/lib/math' import { enterYield } from '@/lib/yieldxyz/api' import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' +import { isYieldDisabled } from '@/lib/yieldxyz/utils' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' import { @@ -390,9 +391,7 @@ export const EarnInput = memo( const yieldsForAsset = useMemo(() => { if (!sellAsset?.assetId || !yieldsData?.byInputAssetId) return [] const allYields = yieldsData.byInputAssetId[sellAsset.assetId] ?? [] - return allYields.filter( - y => y.status.enter && !y.metadata.underMaintenance && !y.metadata.deprecated, - ) + return allYields.filter(y => !isYieldDisabled(y)) }, [sellAsset?.assetId, yieldsData?.byInputAssetId]) const defaultYieldForAsset = useMemo(() => { diff --git a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts index 9995c03ee7f..16154764675 100644 --- a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts +++ b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts @@ -5,6 +5,7 @@ import { useMemo } from 'react' import { bnOrZero } from '@/lib/bignumber/bignumber' import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { isYieldDisabled } from '@/lib/yieldxyz/utils' import { useAllYieldBalances } from '@/react-queries/queries/yieldxyz/useAllYieldBalances' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' import type { AggregatedOpportunitiesByAssetIdReturn } from '@/state/slices/opportunitiesSlice/types' @@ -46,11 +47,7 @@ export const useYieldAsOpportunities = ( if (!inputAssetId) return const hasBalance = bnOrZero(yieldBalancesData?.aggregated[yieldItem.id]?.totalUsd).gt(0) - const isDisabled = - !yieldItem.status.enter || - yieldItem.metadata.underMaintenance || - yieldItem.metadata.deprecated - if (isDisabled && !hasBalance) return + if (isYieldDisabled(yieldItem) && !hasBalance) return if (!aggregatedByAssetId[inputAssetId]) { aggregatedByAssetId[inputAssetId] = { diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index ec55769eae3..7952afbb67f 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -337,3 +337,8 @@ export const getYieldSuccessMessageKey = ( return assertNever(yieldType) } } + +export const isYieldDisabled = ( + yieldItem: Pick, +): boolean => + !yieldItem.status.enter || yieldItem.metadata.underMaintenance || yieldItem.metadata.deprecated diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index 33e39b450db..c28d28394ee 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -35,7 +35,7 @@ import { YIELD_NETWORK_TO_CHAIN_ID, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto, YieldNetwork } from '@/lib/yieldxyz/types' -import { resolveYieldInputAssetIcon } from '@/lib/yieldxyz/utils' +import { isYieldDisabled, resolveYieldInputAssetIcon } from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { YieldFilters } from '@/pages/Yields/components/YieldFilters' import { YieldItem, YieldItemSkeleton } from '@/pages/Yields/components/YieldItem' @@ -201,8 +201,7 @@ export const YieldAssetDetails = memo(() => { const filteredYields = useMemo( () => assetYields.filter(y => { - const isDisabled = !y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated - if (isDisabled) { + if (isYieldDisabled(y)) { const balances = allBalances?.[y.id] const hasBalance = balances && @@ -330,7 +329,7 @@ export const YieldAssetDetails = memo(() => { meta: { display: { base: 'none', md: 'table-cell' } }, }, { - header: translate('yieldXYZ.yourBalance'), + header: translate('yieldXYZ.balance'), id: 'balance', accessorFn: row => { const balances = allBalances?.[row.id] diff --git a/src/pages/Yields/components/YieldRelatedMarkets.tsx b/src/pages/Yields/components/YieldRelatedMarkets.tsx index a4fa1da5b94..fc536021662 100644 --- a/src/pages/Yields/components/YieldRelatedMarkets.tsx +++ b/src/pages/Yields/components/YieldRelatedMarkets.tsx @@ -3,6 +3,7 @@ import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' +import { isYieldDisabled } from '@/lib/yieldxyz/utils' import { YieldItem } from '@/pages/Yields/components/YieldItem' import { useYieldProviders } from '@/react-queries/queries/yieldxyz/useYieldProviders' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' @@ -26,6 +27,7 @@ export const YieldRelatedMarkets = memo( return yields.all .filter(y => { if (y.id === currentYieldId) return false + if (isYieldDisabled(y)) return false const inputSymbol = y.inputTokens?.[0]?.symbol || y.token.symbol return inputSymbol === tokenSymbol }) diff --git a/src/pages/Yields/components/YieldViewHelpers.tsx b/src/pages/Yields/components/YieldViewHelpers.tsx index 98fbad04f78..5ebcebf2fe8 100644 --- a/src/pages/Yields/components/YieldViewHelpers.tsx +++ b/src/pages/Yields/components/YieldViewHelpers.tsx @@ -54,7 +54,7 @@ export const ListHeader = memo(({ isGroup = true }: ListHeaderProps) => { [translate, isGroup], ) const tvlText = useMemo(() => translate('yieldXYZ.tvl'), [translate]) - const balanceText = useMemo(() => translate('yieldXYZ.myBalance'), [translate]) + const balanceText = useMemo(() => translate('yieldXYZ.balance'), [translate]) const providerText = useMemo( () => (isGroup ? translate('yieldXYZ.providers') : translate('yieldXYZ.provider')), [translate, isGroup], diff --git a/src/pages/Yields/components/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index 2d4bdefab5c..a48aa85698e 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -45,7 +45,12 @@ import { YIELD_NETWORK_TO_CHAIN_ID, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto, YieldNetwork } from '@/lib/yieldxyz/types' -import { isStakingYieldType, resolveYieldInputAssetIcon, searchYields } from '@/lib/yieldxyz/utils' +import { + isStakingYieldType, + isYieldDisabled, + resolveYieldInputAssetIcon, + searchYields, +} from '@/lib/yieldxyz/utils' import { YieldFilters } from '@/pages/Yields/components/YieldFilters' import { YieldItem, YieldItemSkeleton } from '@/pages/Yields/components/YieldItem' import { YieldOpportunityStats } from '@/pages/Yields/components/YieldOpportunityStats' @@ -237,21 +242,32 @@ export const YieldsList = memo(() => { [], ) + const unfilteredByInputAssetId = useMemo(() => { + if (!yields?.unfiltered) return {} + return yields.unfiltered.reduce>((acc, item) => { + const assetId = item.inputTokens?.[0]?.assetId + if (assetId) { + if (!acc[assetId]) acc[assetId] = [] + acc[assetId].push(item) + } + return acc + }, {}) + }, [yields?.unfiltered]) + const unfilteredAvailableYields = useMemo(() => { - if (!isConnected || !yields?.byInputAssetId || !userCurrencyBalances || !assetBalancesBaseUnit) - return [] + if (!isConnected || !userCurrencyBalances || !assetBalancesBaseUnit) return [] const available: AugmentedYieldDto[] = [] for (const [assetId, balanceFiat] of Object.entries(userCurrencyBalances)) { - const yieldsForAsset = yields.byInputAssetId[assetId] + const yieldsForAsset = unfilteredByInputAssetId[assetId] if (!yieldsForAsset?.length) continue const balance = bnOrZero(balanceFiat) if (balance.lte(0)) continue const eligibleYields = yieldsForAsset.filter(y => { - if (!y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated) return false + if (isYieldDisabled(y)) return false const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum) if (minDeposit.gt(0)) { const asset = assets[assetId] @@ -267,7 +283,7 @@ export const YieldsList = memo(() => { } return available - }, [isConnected, yields?.byInputAssetId, userCurrencyBalances, assetBalancesBaseUnit, assets]) + }, [isConnected, unfilteredByInputAssetId, userCurrencyBalances, assetBalancesBaseUnit, assets]) const filterSourceYields = useMemo( () => @@ -318,10 +334,8 @@ export const YieldsList = memo(() => { return yields.assetGroups .map(group => { let filteredYields = group.yields.filter(y => { - const isDisabled = !y.status.enter || y.metadata.underMaintenance || y.metadata.deprecated - if (!isDisabled) return true - const hasBalance = bnOrZero(getYieldPositionBalanceUsd(y.id)).gt(0) - return hasBalance + if (!isYieldDisabled(y)) return true + return bnOrZero(getYieldPositionBalanceUsd(y.id)).gt(0) }) if (selectedNetwork) filteredYields = filteredYields.filter(y => y.network === selectedNetwork) @@ -664,7 +678,7 @@ export const YieldsList = memo(() => { meta: { display: { base: 'none', md: 'table-cell' } }, }, { - header: translate('yieldXYZ.yourBalance'), + header: translate('yieldXYZ.balance'), id: 'balance', accessorFn: row => { const balance = getYieldPositionBalanceUsd(row.id) @@ -861,7 +875,7 @@ export const YieldsList = memo(() => { - {translate('yieldXYZ.myBalance')} + {translate('yieldXYZ.balance')} From a908922ed87df72471e8a27df58f11376267068e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:40:31 +0100 Subject: [PATCH 07/12] fix: yield UI improvements and dead translation cleanup - Show provider/validator icon and name in Earn tab yield selector - Add missing yieldXYZ.manage translation - Remove dead translation keys: learnMore, myBalance, protocol, yourBalance Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 7 +--- .../Earn/components/YieldSelector.tsx | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 2626400f439..6d12826fec3 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2680,7 +2680,6 @@ "asset": "Asset", "provider": "Provider", "balance": "Balance", - "yourBalance": "Your Balance", "noYields": "No yield opportunities available", "connectWallet": "Connect a wallet to view yields", "stats": "Stats", @@ -2744,7 +2743,6 @@ "network": "Network", "market": "market", "markets": "markets", - "protocol": "protocol", "chain": "chain", "chains": "chains", "reward": "Reward", @@ -2762,7 +2760,6 @@ "earningsPerYear": "%{amount} %{symbol} /yr", "recommendedForYou": "Recommended for you", "earn": "Earn", - "myBalance": "My Balance", "providers": "Providers", "successStaked": "You successfully staked %{amount} %{symbol}", "successUnstaked": "You successfully unstaked %{amount} %{symbol}", @@ -2803,7 +2800,6 @@ "depositsDisabledDescription": "Deposits are temporarily unavailable for this yield opportunity.", "withdrawalsDisabled": "Withdrawals Disabled", "withdrawalsDisabledDescription": "Withdrawals are temporarily unavailable for this yield opportunity.", - "learnMore": "Learn more", "noAvailableYields": "No yield opportunities available for your assets", "connectWalletAvailable": "Connect a wallet to see yields available for your assets", "aboutProvider": "About %{provider}", @@ -2823,7 +2819,8 @@ "otherYields": "Other %{symbol} Yields", "availableToDeposit": "Available to Deposit", "availableToDepositTooltip": "This is the amount of %{symbol} in your wallet that you can deposit into this yield opportunity.", - "getAsset": "Get %{symbol}" + "getAsset": "Get %{symbol}", + "manage": "Manage" }, "earn": { "enterFrom": "Enter from", diff --git a/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx b/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx index f2f9445e730..a9a72d231d9 100644 --- a/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx +++ b/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx @@ -98,7 +98,7 @@ const YieldItem = memo( return `${apy.toFixed(2)}%` }, [yieldItem.rewardRate?.total]) - const displayInfo = useMemo(() => { + const providerOrValidatorInfo = useMemo(() => { // For staking yields with validators, show the default validator if (requiresValidator && validators?.length) { const chainId = yieldItem.chainId @@ -130,11 +130,25 @@ const YieldItem = memo( textAlign='left' > - + - {displayInfo.name} + {yieldItem.metadata.name} + + + + {providerOrValidatorInfo.name} + + {apyDisplay} @@ -260,9 +274,21 @@ export const YieldSelector = memo( {selectedYield && selectedDisplayInfo ? ( - - {selectedDisplayInfo.name} - + + + {selectedYield.metadata.name} + + + + + {selectedDisplayInfo.name} + + + {selectedApyDisplay} From 8a23a6f6daa6ff1b2c182e9807bb4dbbb4e4f122 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:13:17 +0100 Subject: [PATCH 08/12] fix: use yield ID for default validator mapping - Replace DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID with DEFAULT_VALIDATOR_BY_YIELD_ID - Add getDefaultValidatorForYield utility function - Remove user's highest balance validator fallback from navigation - Default validators are now determined by yield ID, not chain ID: - cosmos-atom-native-staking: ShapeShift DAO - solana-sol-native-multivalidator-staking: Figment - Update all yield components to use the new yield ID based approach - Add unit tests for getDefaultValidatorForYield Co-Authored-By: Claude Opus 4.5 --- .../components/Earn/EarnConfirm.tsx | 14 ++++++----- .../components/Earn/EarnInput.tsx | 14 +++++------ .../Earn/components/YieldSelector.tsx | 10 +++----- .../hooks/useYieldAsOpportunities.ts | 7 ++---- src/lib/yieldxyz/constants.ts | 9 +++++--- src/lib/yieldxyz/utils.test.ts | 23 ++++++++++++++++++- src/lib/yieldxyz/utils.ts | 8 +++++++ src/pages/Yields/YieldAssetDetails.tsx | 15 ++++++------ src/pages/Yields/YieldDetail.tsx | 6 ++--- .../Yields/components/YieldEnterModal.tsx | 9 ++++---- src/pages/Yields/components/YieldForm.tsx | 9 ++++---- src/pages/Yields/components/YieldManager.tsx | 12 ++++------ src/pages/Yields/components/YieldStats.tsx | 23 ++++--------------- src/pages/Yields/components/YieldsList.tsx | 10 ++++---- .../Yields/hooks/useYieldTransactionFlow.ts | 14 ++++------- 15 files changed, 91 insertions(+), 92 deletions(-) diff --git a/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx b/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx index e82fb192ffa..74fc903b872 100644 --- a/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx +++ b/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx @@ -8,8 +8,11 @@ import { EarnRoutePaths } from './types' import { Amount } from '@/components/Amount/Amount' import { bnOrZero } from '@/lib/bignumber/bignumber' -import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' -import { getTransactionButtonText, getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' +import { + getDefaultValidatorForYield, + getTransactionButtonText, + getYieldActionLabelKeys, +} from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { TransactionStepsList } from '@/pages/Yields/components/TransactionStepsList' import { YieldAssetFlow } from '@/pages/Yields/components/YieldAssetFlow' @@ -73,16 +76,15 @@ export const EarnConfirm = memo(() => { const { data: providers } = useYieldProviders() const selectedValidatorAddress = useMemo(() => { - if (!requiresValidatorSelection || !validators?.length) return undefined - const chainId = selectedYield?.chainId - const defaultAddress = chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] : undefined + if (!requiresValidatorSelection || !validators?.length || !selectedYield) return undefined + const defaultAddress = getDefaultValidatorForYield(selectedYield.id) if (defaultAddress) { const defaultValidator = validators.find(v => v.address === defaultAddress) if (defaultValidator) return defaultValidator.address } const preferred = validators.find(v => v.preferred) return preferred?.address ?? validators[0]?.address - }, [requiresValidatorSelection, validators, selectedYield?.chainId]) + }, [requiresValidatorSelection, validators, selectedYield]) const selectedValidator = useMemo(() => { if (!selectedValidatorAddress || !validators?.length) return undefined diff --git a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx index 5ab1559ebc0..6980ffa86f4 100644 --- a/src/components/MultiHopTrade/components/Earn/EarnInput.tsx +++ b/src/components/MultiHopTrade/components/Earn/EarnInput.tsx @@ -26,8 +26,7 @@ import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero, positiveOrZero } from '@/lib/bignumber/bignumber' import { fromBaseUnit } from '@/lib/math' import { enterYield } from '@/lib/yieldxyz/api' -import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' -import { isYieldDisabled } from '@/lib/yieldxyz/utils' +import { getDefaultValidatorForYield, isYieldDisabled } from '@/lib/yieldxyz/utils' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' import { @@ -160,9 +159,8 @@ export const EarnInput = memo( ) const selectedValidator = useMemo(() => { - if (!requiresValidatorSelection || !validators?.length) return undefined - const chainId = selectedYield?.chainId - const defaultAddress = chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] : undefined + if (!requiresValidatorSelection || !validators?.length || !selectedYield) return undefined + const defaultAddress = getDefaultValidatorForYield(selectedYield.id) if (defaultAddress) { return ( validators.find(v => v.address === defaultAddress) ?? @@ -171,7 +169,7 @@ export const EarnInput = memo( ) } return validators.find(v => v.preferred) ?? validators[0] - }, [requiresValidatorSelection, validators, selectedYield?.chainId]) + }, [requiresValidatorSelection, validators, selectedYield]) const selectedValidatorAddress = selectedValidator?.address @@ -206,9 +204,9 @@ export const EarnInput = memo( args.receiverAddress = userAddress } - if (fieldNames.has('validatorAddress') && yieldChainId) { + if (fieldNames.has('validatorAddress') && selectedYield) { const validatorAddress = - selectedValidatorAddress ?? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldChainId] + selectedValidatorAddress ?? getDefaultValidatorForYield(selectedYield.id) if (validatorAddress) { args.validatorAddress = validatorAddress } diff --git a/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx b/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx index a9a72d231d9..db10a658821 100644 --- a/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx +++ b/src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx @@ -22,12 +22,9 @@ import { DialogBody } from '@/components/Modal/components/DialogBody' import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' import { DialogHeader } from '@/components/Modal/components/DialogHeader' import { DialogTitle } from '@/components/Modal/components/DialogTitle' -import { - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, - SHAPESHIFT_VALIDATOR_LOGO, - SHAPESHIFT_VALIDATOR_NAME, -} from '@/lib/yieldxyz/constants' +import { SHAPESHIFT_VALIDATOR_LOGO, SHAPESHIFT_VALIDATOR_NAME } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto, ProviderDto, ValidatorDto } from '@/lib/yieldxyz/types' +import { getDefaultValidatorForYield } from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { useYieldProviders } from '@/react-queries/queries/yieldxyz/useYieldProviders' import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' @@ -101,8 +98,7 @@ const YieldItem = memo( const providerOrValidatorInfo = useMemo(() => { // For staking yields with validators, show the default validator if (requiresValidator && validators?.length) { - const chainId = yieldItem.chainId - const defaultAddress = chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] : undefined + const defaultAddress = getDefaultValidatorForYield(yieldItem.id) const defaultValidator = defaultAddress ? validators.find(v => v.address === defaultAddress) : undefined diff --git a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts index 16154764675..f673ba0d21c 100644 --- a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts +++ b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts @@ -1,11 +1,9 @@ import type { AssetId } from '@shapeshiftoss/caip' -import { fromAssetId } from '@shapeshiftoss/caip' import { useMemo } from 'react' import { bnOrZero } from '@/lib/bignumber/bignumber' -import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' -import { isYieldDisabled } from '@/lib/yieldxyz/utils' +import { getDefaultValidatorForYield, isYieldDisabled } from '@/lib/yieldxyz/utils' import { useAllYieldBalances } from '@/react-queries/queries/yieldxyz/useAllYieldBalances' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' import type { AggregatedOpportunitiesByAssetIdReturn } from '@/state/slices/opportunitiesSlice/types' @@ -68,13 +66,12 @@ export const useYieldAsOpportunities = ( } const balancesForYield = yieldBalancesData?.aggregated[yieldItem.id] - const { chainId } = fromAssetId(inputAssetId) let totalUsd: string let totalCrypto: string if (balancesForYield?.hasValidators) { - const defaultValidatorAddress = DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] + const defaultValidatorAddress = getDefaultValidatorForYield(yieldItem.id) const validatorAddresses = Object.keys(balancesForYield.byValidator) const selectedValidatorAddress = defaultValidatorAddress ?? validatorAddresses[0] const validatorBalance = selectedValidatorAddress diff --git a/src/lib/yieldxyz/constants.ts b/src/lib/yieldxyz/constants.ts index a7690c61975..5dd1c0ca798 100644 --- a/src/lib/yieldxyz/constants.ts +++ b/src/lib/yieldxyz/constants.ts @@ -98,7 +98,10 @@ export const FIGMENT_SOLANA_VALIDATOR_ADDRESS = 'CcaHc2L43ZWjwCHART3oZoJvHLAe9hz export const FIGMENT_VALIDATOR_NAME = 'Figment' export const FIGMENT_VALIDATOR_LOGO = 'https://assets.stakek.it/validators/figment.png' -export const DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID: Partial> = { - [cosmosChainId]: SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, - [solanaChainId]: FIGMENT_SOLANA_VALIDATOR_ADDRESS, +// Default validators by yield ID - used for yields that require validator selection +export const DEFAULT_VALIDATOR_BY_YIELD_ID: Record = { + // ShapeShift DAO + [COSMOS_ATOM_NATIVE_STAKING_YIELD_ID]: SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, + // Figment + [SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID]: FIGMENT_SOLANA_VALIDATOR_ADDRESS, } diff --git a/src/lib/yieldxyz/utils.test.ts b/src/lib/yieldxyz/utils.test.ts index 037b94343e6..3dac14a8313 100644 --- a/src/lib/yieldxyz/utils.test.ts +++ b/src/lib/yieldxyz/utils.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from 'vitest' -import { SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS } from './constants' +import { FIGMENT_SOLANA_VALIDATOR_ADDRESS, SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS } from './constants' import type { AugmentedYieldDto, ValidatorDto } from './types' import { ensureValidatorApr, formatYieldTxTitle, + getDefaultValidatorForYield, getTransactionButtonText, getYieldActionLabelKeys, getYieldSuccessMessageKey, @@ -363,3 +364,23 @@ describe('getYieldSuccessMessageKey', () => { expect(getYieldSuccessMessageKey('vault', 'manage')).toBe('successClaim') }) }) + +describe('getDefaultValidatorForYield', () => { + it('should return ShapeShift DAO validator for cosmos-atom-native-staking', () => { + expect(getDefaultValidatorForYield('cosmos-atom-native-staking')).toBe( + SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, + ) + }) + + it('should return Figment validator for solana-sol-native-multivalidator-staking', () => { + expect(getDefaultValidatorForYield('solana-sol-native-multivalidator-staking')).toBe( + FIGMENT_SOLANA_VALIDATOR_ADDRESS, + ) + }) + + it('should return undefined for yields without enforced validators', () => { + expect(getDefaultValidatorForYield('ethereum-eth-lido-staking')).toBeUndefined() + expect(getDefaultValidatorForYield('solana-sol-lido-staking')).toBeUndefined() + expect(getDefaultValidatorForYield('some-random-yield')).toBeUndefined() + }) +}) diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index 7952afbb67f..7b20aee6e15 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -2,6 +2,7 @@ import type { ChainId } from '@shapeshiftoss/caip' import { COSMOS_NETWORK_FALLBACK_APR, + DEFAULT_VALIDATOR_BY_YIELD_ID, isSupportedYieldNetwork, SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, YIELD_NETWORK_TO_CHAIN_ID, @@ -13,6 +14,13 @@ export const yieldNetworkToChainId = (network: string): ChainId | undefined => { return YIELD_NETWORK_TO_CHAIN_ID[network] } +/** + * Get the default validator address for a yield ID. + * Returns the enforced validator for yields that require validator selection. + */ +export const getDefaultValidatorForYield = (yieldId: string): string | undefined => + DEFAULT_VALIDATOR_BY_YIELD_ID[yieldId] + type TxTitlePattern = { pattern: RegExp staking: string diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index c28d28394ee..ce6f6c63a61 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -35,7 +35,11 @@ import { YIELD_NETWORK_TO_CHAIN_ID, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto, YieldNetwork } from '@/lib/yieldxyz/types' -import { isYieldDisabled, resolveYieldInputAssetIcon } from '@/lib/yieldxyz/utils' +import { + getDefaultValidatorForYield, + isYieldDisabled, + resolveYieldInputAssetIcon, +} from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { YieldFilters } from '@/pages/Yields/components/YieldFilters' import { YieldItem, YieldItemSkeleton } from '@/pages/Yields/components/YieldItem' @@ -386,14 +390,11 @@ export const YieldAssetDetails = memo(() => { const handleYieldClick = useCallback( (yieldId: string) => { - const balances = allBalances?.[yieldId] - const highestAmountValidator = balances?.[0]?.highestAmountUsdValidator - const url = highestAmountValidator - ? `/yields/${yieldId}?validator=${highestAmountValidator}` - : `/yields/${yieldId}` + const validator = getDefaultValidatorForYield(yieldId) + const url = validator ? `/yields/${yieldId}?validator=${validator}` : `/yields/${yieldId}` navigate(url) }, - [allBalances, navigate], + [navigate], ) const handleRowClick = useCallback( diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index 1deb198f897..e1eaef9defa 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -11,7 +11,6 @@ import { Display } from '@/components/Display' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { bnOrZero } from '@/lib/bignumber/bignumber' import { - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, FIGMENT_SOLANA_VALIDATOR_ADDRESS, FIGMENT_VALIDATOR_LOGO, FIGMENT_VALIDATOR_NAME, @@ -21,6 +20,7 @@ import { } from '@/lib/yieldxyz/constants' import { getYieldDisplayName } from '@/lib/yieldxyz/getYieldDisplayName' import { YieldBalanceType } from '@/lib/yieldxyz/types' +import { getDefaultValidatorForYield } from '@/lib/yieldxyz/utils' import { YieldAvailableToDeposit } from '@/pages/Yields/components/YieldAvailableToDeposit' import { YieldHero } from '@/pages/Yields/components/YieldHero' import { YieldInfoCard } from '@/pages/Yields/components/YieldInfoCard' @@ -98,9 +98,7 @@ export const YieldDetail = memo(() => { const isBalancesLoading = !allBalancesData && isBalancesFetching const validatorParam = searchParams.get('validator') - const defaultValidator = yieldItem?.chainId - ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] - : undefined + const defaultValidator = yieldItem?.id ? getDefaultValidatorForYield(yieldItem.id) : undefined const isStaking = yieldItem?.mechanics.type === 'staking' const requiresValidatorSelection = yieldItem?.mechanics.requiresValidatorSelection ?? false diff --git a/src/pages/Yields/components/YieldEnterModal.tsx b/src/pages/Yields/components/YieldEnterModal.tsx index 5ac534b1401..7f3f0774577 100644 --- a/src/pages/Yields/components/YieldEnterModal.tsx +++ b/src/pages/Yields/components/YieldEnterModal.tsx @@ -23,13 +23,13 @@ import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatte import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import { - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, SHAPESHIFT_VALIDATOR_LOGO, SHAPESHIFT_VALIDATOR_NAME, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { + getDefaultValidatorForYield, getTransactionButtonText, getYieldActionLabelKeys, getYieldHeadingKeys, @@ -179,11 +179,10 @@ export const YieldEnterModal = memo( ) const selectedValidatorAddress = useMemo(() => { - if (chainId && DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId]) { - return DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] - } + const defaultValidator = getDefaultValidatorForYield(yieldItem.id) + if (defaultValidator) return defaultValidator return validators?.[0]?.address - }, [chainId, validators]) + }, [validators, yieldItem.id]) const { data: providers } = useYieldProviders() diff --git a/src/pages/Yields/components/YieldForm.tsx b/src/pages/Yields/components/YieldForm.tsx index 6d9d8fdec71..dead5d254b2 100644 --- a/src/pages/Yields/components/YieldForm.tsx +++ b/src/pages/Yields/components/YieldForm.tsx @@ -30,7 +30,6 @@ import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatte import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import { - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, SHAPESHIFT_VALIDATOR_LOGO, SHAPESHIFT_VALIDATOR_NAME, @@ -38,6 +37,7 @@ import { import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { YieldBalanceType } from '@/lib/yieldxyz/types' import { + getDefaultValidatorForYield, getTransactionButtonText, getYieldActionLabelKeys, getYieldMinAmountKey, @@ -206,11 +206,10 @@ export const YieldForm = memo( const selectedValidatorAddress = useMemo(() => { if (!shouldFetchValidators) return undefined if (validatorAddress) return validatorAddress - if (chainId && DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId]) { - return DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] - } + const defaultValidator = getDefaultValidatorForYield(yieldItem.id) + if (defaultValidator) return defaultValidator return validators?.[0]?.address - }, [shouldFetchValidators, chainId, validators, validatorAddress]) + }, [shouldFetchValidators, validators, validatorAddress, yieldItem.id]) const { data: providers } = useYieldProviders() diff --git a/src/pages/Yields/components/YieldManager.tsx b/src/pages/Yields/components/YieldManager.tsx index 48bdefcb427..9c6c7a932dc 100644 --- a/src/pages/Yields/components/YieldManager.tsx +++ b/src/pages/Yields/components/YieldManager.tsx @@ -9,9 +9,8 @@ import { DialogBody } from '@/components/Modal/components/DialogBody' import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' import { DialogHeader } from '@/components/Modal/components/DialogHeader' import { DialogTitle } from '@/components/Modal/components/DialogTitle' -import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' import { YieldBalanceType } from '@/lib/yieldxyz/types' -import { getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' +import { getDefaultValidatorForYield, getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' import { useAllYieldBalances } from '@/react-queries/queries/yieldxyz/useAllYieldBalances' import { useYield } from '@/react-queries/queries/yieldxyz/useYield' @@ -30,12 +29,9 @@ export const YieldManager = () => { const requiresValidatorSelection = yieldItem?.mechanics.requiresValidatorSelection ?? false const validatorAddress = useMemo(() => { - if (!requiresValidatorSelection) return undefined - return ( - validatorParam || - (yieldItem?.chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] : undefined) - ) - }, [requiresValidatorSelection, validatorParam, yieldItem?.chainId]) + if (!requiresValidatorSelection || !yieldItem) return undefined + return validatorParam || getDefaultValidatorForYield(yieldItem.id) + }, [requiresValidatorSelection, validatorParam, yieldItem]) const { accountId, accountNumber } = useYieldAccount() const { data: allBalancesData } = useAllYieldBalances() const balances = yieldItem?.id ? allBalancesData?.normalized[yieldItem.id] : undefined diff --git a/src/pages/Yields/components/YieldStats.tsx b/src/pages/Yields/components/YieldStats.tsx index ff6693763b5..773e26a9c2d 100644 --- a/src/pages/Yields/components/YieldStats.tsx +++ b/src/pages/Yields/components/YieldStats.tsx @@ -5,13 +5,8 @@ import { useSearchParams } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' import { bnOrZero } from '@/lib/bignumber/bignumber' -import { - COSMOS_ATOM_NATIVE_STAKING_YIELD_ID, - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, - SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID, -} from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' -import { isStakingYieldType } from '@/lib/yieldxyz/utils' +import { getDefaultValidatorForYield, isStakingYieldType } from '@/lib/yieldxyz/utils' import type { NormalizedYieldBalances } from '@/react-queries/queries/yieldxyz/useAllYieldBalances' import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' import { @@ -41,21 +36,13 @@ export const YieldStats = memo(({ yieldItem, balances }: YieldStatsProps) => { const { data: validators } = useYieldValidators(yieldItem.id, shouldFetchValidators) const defaultValidator = useMemo( - () => - yieldItem.chainId && DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] - ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldItem.chainId] - : validators?.[0]?.address, - [yieldItem.chainId, validators], + () => getDefaultValidatorForYield(yieldItem.id) ?? validators?.[0]?.address, + [yieldItem.id, validators], ) const selectedValidatorAddress = useMemo(() => { - if ( - yieldItem.id === COSMOS_ATOM_NATIVE_STAKING_YIELD_ID || - yieldItem.id === SOLANA_SOL_NATIVE_MULTIVALIDATOR_STAKING_YIELD_ID || - (yieldItem.id.includes('solana') && yieldItem.id.includes('native')) - ) { - return defaultValidator - } + const enforced = getDefaultValidatorForYield(yieldItem.id) + if (enforced) return enforced return validatorParam || defaultValidator }, [yieldItem.id, validatorParam, defaultValidator]) diff --git a/src/pages/Yields/components/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index a48aa85698e..93124222c02 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -46,6 +46,7 @@ import { } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto, YieldNetwork } from '@/lib/yieldxyz/types' import { + getDefaultValidatorForYield, isStakingYieldType, isYieldDisabled, resolveYieldInputAssetIcon, @@ -556,14 +557,11 @@ export const YieldsList = memo(() => { const handleYieldClick = useCallback( (yieldId: string) => { - const balances = allBalances?.[yieldId] - const highestAmountValidator = balances?.[0]?.highestAmountUsdValidator - const url = highestAmountValidator - ? `/yields/${yieldId}?validator=${highestAmountValidator}` - : `/yields/${yieldId}` + const validator = getDefaultValidatorForYield(yieldId) + const url = validator ? `/yields/${yieldId}?validator=${validator}` : `/yields/${yieldId}` navigate(url) }, - [navigate, allBalances], + [navigate], ) const handleRowClick = useCallback( diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 2e151465820..5f75bce2d6c 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -15,16 +15,12 @@ import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import { toBaseUnit } from '@/lib/math' import { enterYield, exitYield, fetchAction, manageYield } from '@/lib/yieldxyz/api' -import { - DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID, - YIELD_MAX_POLL_ATTEMPTS, - YIELD_POLL_INTERVAL_MS, -} from '@/lib/yieldxyz/constants' +import { YIELD_MAX_POLL_ATTEMPTS, YIELD_POLL_INTERVAL_MS } from '@/lib/yieldxyz/constants' import type { CosmosStakeArgs } from '@/lib/yieldxyz/executeTransaction' import { executeTransaction } from '@/lib/yieldxyz/executeTransaction' import type { ActionDto, AugmentedYieldDto, TransactionDto } from '@/lib/yieldxyz/types' import { ActionStatus as YieldActionStatus, TransactionStatus } from '@/lib/yieldxyz/types' -import { formatYieldTxTitle } from '@/lib/yieldxyz/utils' +import { formatYieldTxTitle, getDefaultValidatorForYield } from '@/lib/yieldxyz/utils' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' import { reactQueries } from '@/react-queries' import { useAllowance } from '@/react-queries/hooks/useAllowance' @@ -238,8 +234,8 @@ export const useYieldTransactionFlow = ({ } const validatorField = fields.find(f => f.name === 'validatorAddress') - if (validatorField && yieldChainId) { - args.validatorAddress = validatorAddress || DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[yieldChainId] + if (validatorField && yieldItem) { + args.validatorAddress = validatorAddress || getDefaultValidatorForYield(yieldItem.id) } if (fieldNames.has('cosmosPubKey') && yieldChainId === cosmosChainId) { @@ -465,7 +461,7 @@ export const useYieldTransactionFlow = ({ const buildCosmosStakeArgs = useCallback((): CosmosStakeArgs | undefined => { if (yieldChainId !== cosmosChainId || !yieldItem) return undefined - const validator = validatorAddress || DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[cosmosChainId] + const validator = validatorAddress || getDefaultValidatorForYield(yieldItem.id) if (!validator) return undefined const inputTokenDecimals = From 5354672fab30aabcb5aec9bf8c89c4d216183d8c Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:50:29 +0100 Subject: [PATCH 09/12] feat: rm useless cast --- .../components/PositionDetails/StakingPositionsByProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx index 534d1694783..5fed62e52d0 100644 --- a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx @@ -265,7 +265,7 @@ export const StakingPositionsByProvider: React.FC + ) : ( Date: Tue, 20 Jan 2026 18:00:01 +0100 Subject: [PATCH 10/12] refactor: apply maybe prefix convention to nullable variables Variables that early return null now use the maybe prefix for clarity. Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/YieldDetail.tsx | 22 +++++------ src/pages/Yields/components/YieldForm.tsx | 48 ++++++++++++----------- src/pages/Yields/components/YieldHero.tsx | 4 +- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index e1eaef9defa..9d5a8708325 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -111,7 +111,7 @@ export const YieldDetail = memo(() => { const { data: validators } = useYieldValidators(yieldItem?.id ?? '', shouldFetchValidators) const { data: yieldProviders } = useYieldProviders() - const validatorOrProvider = useMemo(() => { + const maybeValidatorOrProvider = useMemo(() => { if (isStaking && requiresValidatorSelection && selectedValidatorAddress) { const found = validators?.find(v => v.address === selectedValidatorAddress) if (found) return { name: found.name, logoURI: found.logoURI } @@ -263,7 +263,7 @@ export const YieldDetail = memo(() => { yieldItem={yieldItem} userBalanceUsd={userBalances.userCurrency} userBalanceCrypto={userBalances.crypto} - validatorOrProvider={validatorOrProvider} + validatorOrProvider={maybeValidatorOrProvider} titleOverride={titleOverride} /> @@ -278,12 +278,12 @@ export const YieldDetail = memo(() => { inputTokenMarketData={inputTokenMarketData} /> - {!isStaking && validatorOrProvider && ( + {!isStaking && maybeValidatorOrProvider && ( )} { - {!isStaking && validatorOrProvider && ( + {!isStaking && maybeValidatorOrProvider && ( )} { + const maybeSelectedValidatorMetadata = useMemo(() => { if (!shouldFetchValidators || !selectedValidatorAddress) return null const found = validators?.find(v => v.address === selectedValidatorAddress) if (found) return found @@ -229,7 +229,7 @@ export const YieldForm = memo( return null }, [shouldFetchValidators, selectedValidatorAddress, validators]) - const providerMetadata = useMemo(() => { + const maybeProviderMetadata = useMemo(() => { if (!providers) return null return providers[yieldItem.providerId] }, [providers, yieldItem.providerId]) @@ -408,21 +408,21 @@ export const YieldForm = memo( } }, [step]) - const successProviderInfo = useMemo(() => { - if (isStaking && selectedValidatorMetadata) { + const maybeSuccessProviderInfo = useMemo(() => { + if (isStaking && maybeSelectedValidatorMetadata) { return { - name: selectedValidatorMetadata.name, - logoURI: selectedValidatorMetadata.logoURI, + name: maybeSelectedValidatorMetadata.name, + logoURI: maybeSelectedValidatorMetadata.logoURI, } } - if (providerMetadata) { + if (maybeProviderMetadata) { return { - name: providerMetadata.name, - logoURI: providerMetadata.logoURI, + name: maybeProviderMetadata.name, + logoURI: maybeProviderMetadata.logoURI, } } return null - }, [isStaking, selectedValidatorMetadata, providerMetadata]) + }, [isStaking, maybeSelectedValidatorMetadata, maybeProviderMetadata]) const isActionDisabled = useMemo(() => { if (action === 'enter') return !yieldItem.status.enter @@ -570,7 +570,7 @@ export const YieldForm = memo( )} - {isStaking && selectedValidatorMetadata && ( + {isStaking && maybeSelectedValidatorMetadata && ( {translate('yieldXYZ.validator')} @@ -578,24 +578,28 @@ export const YieldForm = memo( - {selectedValidatorMetadata.name} + {maybeSelectedValidatorMetadata.name} )} - {!isStaking && providerMetadata && ( + {!isStaking && maybeProviderMetadata && ( {translate('yieldXYZ.provider')} - + - {providerMetadata.name} + {maybeProviderMetadata.name} @@ -624,8 +628,8 @@ export const YieldForm = memo( inputTokenAsset?.symbol, estimatedYearlyEarningsFiat, isStaking, - selectedValidatorMetadata, - providerMetadata, + maybeSelectedValidatorMetadata, + maybeProviderMetadata, minDeposit, isBelowMinimum, action, @@ -718,7 +722,7 @@ export const YieldForm = memo( const stepsToShow = activeStepIndex >= 0 ? transactionSteps : displaySteps - const actionDisabledAlert = useMemo(() => { + const maybeActionDisabledAlert = useMemo(() => { if (!isActionDisabled) return null const descriptionKey = action === 'enter' @@ -747,7 +751,7 @@ export const YieldForm = memo( - {actionDisabledAlert} + {maybeActionDisabledAlert} {inputContent} {!isClaimAction && percentButtons} {!isClaimAction && inputTokenAssetId && accountId && ( diff --git a/src/pages/Yields/components/YieldHero.tsx b/src/pages/Yields/components/YieldHero.tsx index 47902fdf15b..2b488e12fc9 100644 --- a/src/pages/Yields/components/YieldHero.tsx +++ b/src/pages/Yields/components/YieldHero.tsx @@ -84,7 +84,7 @@ export const YieldHero = memo( const yieldTitle = titleOverride ?? yieldItem.metadata.name ?? yieldItem.token.symbol - const descriptionSection = useMemo(() => { + const maybeDescriptionSection = useMemo(() => { const docUrl = validatorOrProvider?.documentation ?? yieldItem.metadata.documentation const description = validatorOrProvider?.description ?? yieldItem.metadata.description if (!description && !docUrl) return null @@ -202,7 +202,7 @@ export const YieldHero = memo( {apy}% {translate('common.apy')} - {descriptionSection} + {maybeDescriptionSection} From 8488d01822ea025cc4b95f6ee69999784c9f569a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:16:38 +0100 Subject: [PATCH 11/12] fix: make inputAssetId required in YieldOpportunityDisplay useYieldAsOpportunities filters out yields without inputAssetId, so the fallback to yieldId was unreachable. Type now reflects the actual guarantee. Co-Authored-By: Claude Opus 4.5 --- .../components/PositionDetails/StakingPositionsByProvider.tsx | 4 ++-- src/components/StakingVaults/hooks/useYieldAsOpportunities.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx index 5fed62e52d0..f7550a58c4a 100644 --- a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx @@ -124,8 +124,8 @@ export const StakingPositionsByProvider: React.FC ({ id: y.yieldId, - assetId: y.inputAssetId ?? y.yieldId, - underlyingAssetId: y.inputAssetId ?? y.yieldId, + assetId: y.inputAssetId, + underlyingAssetId: y.inputAssetId, provider: y.providerName, apy: y.apy, fiatAmount: y.fiatAmount, diff --git a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts index f673ba0d21c..2618f0d5f6c 100644 --- a/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts +++ b/src/components/StakingVaults/hooks/useYieldAsOpportunities.ts @@ -12,7 +12,7 @@ export type YieldOpportunityDisplay = { yieldId: string providerName: string providerIcon?: string - inputAssetId?: AssetId + inputAssetId: AssetId apy: string fiatAmount: string cryptoAmount: string From de4b5e3773d109a823ea60beba98d609c4def7bf Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:11:57 +0100 Subject: [PATCH 12/12] feat: show provider descriptions for liquid staking yields and add missing providers - Fix YieldProviderInfo not showing for liquid staking yields like Lido (changed condition from !isStaking to !isStaking || !requiresValidatorSelection) - Add fallback for providers not in API (e.g., Drift) to still show provider info - Add provider descriptions for yearn, spark, rocket-pool, drift Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 6 +++++- src/pages/Yields/YieldDetail.tsx | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 55b280928fd..9f9ef048e58 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2813,7 +2813,11 @@ "kamino": "Kamino is a Solana DeFi suite unifying lending, liquidity, and leverage into one platform. Runs an Immunefi program with up to $1.5M maximum bounty.", "fluid": "Fluid is a liquidity layer built by the Instadapp team, connecting lending, DEX, borrowing, and stablecoin markets into one efficient system. Multiply audited by top-tier security firms, live since 2024, with $0.5M in bug bounty incentives.", "venus": "Venus is a lending and borrowing protocol focused on BNB Chain. Emphasizes security through third-party audits and an ongoing bug bounty program.", - "gearbox": "Gearbox is a composable leverage protocol enabling credit accounts that plug into DeFi strategies. Multiply audited by top-tier security firms, live since 2021, with $0.2M in bug bounty incentives." + "gearbox": "Gearbox is a composable leverage protocol enabling credit accounts that plug into DeFi strategies. Multiply audited by top-tier security firms, live since 2021, with $0.2M in bug bounty incentives.", + "yearn": "Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets. Multiply audited by top-tier security firms, live since 2020, with $0.2M in bug bounty incentives.", + "spark": "Spark is a decentralized lending protocol powered by the Sky (formerly MakerDAO) ecosystem, allowing users to borrow DAI and other stablecoins at competitive rates. Runs an Immunefi program with up to $5M maximum bounty.", + "rocket-pool": "Rocket Pool is a decentralized Ethereum liquid staking protocol that issues rETH. Multiply audited by top-tier security firms, live since 2021, with $0.5M in bug bounty incentives.", + "drift": "Drift is a decentralized perpetuals exchange and lending platform built on Solana. Runs an Immunefi program with up to $0.5M maximum bounty." }, "otherYields": "Other %{symbol} Yields", "availableToDeposit": "Available to Deposit", diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index 9d5a8708325..2264dd99ac7 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -132,6 +132,13 @@ export const YieldDetail = memo(() => { documentation: provider.references?.[0] ?? provider.website, } } + // Fallback for providers not in the API (e.g., drift) + // NOTE: This shouldn't happen and is a bug upstream, currently happens w/ Drift. + // Report to Yield if you see some other provider missing in /providers in the future. + return { + name: yieldItem.providerId.charAt(0).toUpperCase() + yieldItem.providerId.slice(1), + logoURI: yieldItem.metadata.logoURI, + } } return null }, [ @@ -278,7 +285,7 @@ export const YieldDetail = memo(() => { inputTokenMarketData={inputTokenMarketData} /> - {!isStaking && maybeValidatorOrProvider && ( + {(!isStaking || !requiresValidatorSelection) && maybeValidatorOrProvider && ( { titleOverride={titleOverride} /> - {!isStaking && maybeValidatorOrProvider && ( + {(!isStaking || !requiresValidatorSelection) && maybeValidatorOrProvider && (