Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aab8363
fix: only show validator UI when requiresValidatorSelection is true
gomesalexandre Jan 20, 2026
5ffd6ec
fix: yield modal close stays on detail page instead of navigating back
gomesalexandre Jan 20, 2026
90e9747
fix: show asset icon for yields in DeFi drawer tab
gomesalexandre Jan 20, 2026
a86d7cf
feat: handle disabled yield states and fix icon rendering
gomesalexandre Jan 20, 2026
7bdce66
test: add augment function tests for inputTokens handling
gomesalexandre Jan 20, 2026
b4c11f5
fix: filter disabled yields from related markets and fix filter options
gomesalexandre Jan 20, 2026
a908922
fix: yield UI improvements and dead translation cleanup
gomesalexandre Jan 20, 2026
8a23a6f
fix: use yield ID for default validator mapping
gomesalexandre Jan 20, 2026
a37c214
Merge remote-tracking branch 'origin/develop' into feat_last_yield_bi…
gomesalexandre Jan 20, 2026
5354672
feat: rm useless cast
gomesalexandre Jan 20, 2026
277a2e3
refactor: apply maybe prefix convention to nullable variables
gomesalexandre Jan 20, 2026
8488d01
fix: make inputAssetId required in YieldOpportunityDisplay
gomesalexandre Jan 20, 2026
7396dcd
Merge branch 'develop' into feat_last_yield_bits_fr_this_time_no_cap
gomesalexandre Jan 20, 2026
e02434e
Merge branch 'develop' into feat_last_yield_bits_fr_this_time_no_cap
gomesalexandre Jan 21, 2026
841ca76
Merge branch 'develop' into feat_last_yield_bits_fr_this_time_no_cap
NeOMakinG Jan 21, 2026
de4b5e3
feat: show provider descriptions for liquid staking yields and add mi…
gomesalexandre Jan 21, 2026
d59af9f
Merge branch 'develop' into feat_last_yield_bits_fr_this_time_no_cap
gomesalexandre Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2679,7 +2679,6 @@
"asset": "Asset",
"provider": "Provider",
"balance": "Balance",
"yourBalance": "Your Balance",
"noYields": "No yield opportunities available",
"connectWallet": "Connect a wallet to view yields",
"stats": "Stats",
Expand Down Expand Up @@ -2743,7 +2742,6 @@
"network": "Network",
"market": "market",
"markets": "markets",
"protocol": "protocol",
"chain": "chain",
"chains": "chains",
"reward": "Reward",
Expand All @@ -2761,7 +2759,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}",
Expand Down Expand Up @@ -2798,7 +2795,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.",
"learnMore": "Learn more",
"depositsDisabled": "Deposits Disabled",
"depositsDisabledDescription": "Deposits are temporarily unavailable for this yield opportunity.",
"withdrawalsDisabled": "Withdrawals Disabled",
"withdrawalsDisabledDescription": "Withdrawals are temporarily unavailable for this yield opportunity.",
"noAvailableYields": "No yield opportunities available for your assets",
"connectWalletAvailable": "Connect a wallet to see yields available for your assets",
"aboutProvider": "About %{provider}",
Expand All @@ -2813,12 +2813,17 @@
"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",
"availableToDepositTooltip": "This is the amount of %{symbol} in your wallet that you can deposit into this yield opportunity.",
"getAsset": "Get %{symbol}",
"manage": "Manage",
"potentialEarningsAmount": "%{amount}/yr at %{apy}% APY",
"depositNow": "Deposit Now",
"strategyInfo": "Strategy Info",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -123,8 +124,8 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
if (!yieldOpportunities?.length) return []
return yieldOpportunities.map(y => ({
id: y.yieldId,
assetId: y.yieldId,
underlyingAssetId: y.yieldId,
assetId: y.inputAssetId,
underlyingAssetId: y.inputAssetId,
provider: y.providerName,
apy: y.apy,
fiatAmount: y.fiatAmount,
Expand All @@ -144,23 +145,10 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
[ids, stakingOpportunities],
)

const filteredDown = useMemo<UnifiedPosition[]>(() => {
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<UnifiedPosition[]>(
() => [...yieldPositionsAsUnified, ...legacyFiltered],
[yieldPositionsAsUnified, legacyFiltered],
)

const handleClick = useCallback(
(row: RowProps, action: DefiAction) => {
Expand Down Expand Up @@ -276,14 +264,19 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
if (opp.opportunityName) subText.push(opp.opportunityName)
const isRunePool = opp.assetId === thorchainAssetId
const providerName = isRunePool ? 'RUNEPool' : opp.version ?? opp.provider
const iconElement = opp.isYield ? (
<AssetIcon assetId={opp.assetId} size='sm' />
) : (
<LazyLoadAvatar
size='sm'
bg='transparent'
src={opp.icon ?? getMetadataForProvider(opp.provider)?.icon ?? ''}
key={`provider-icon-${opp.id}`}
/>
)
return (
<Flex gap={4} alignItems='center'>
<LazyLoadAvatar
size='sm'
bg='transparent'
src={opp.icon ?? getMetadataForProvider(opp.provider)?.icon ?? ''}
key={`provider-icon-${opp.id}`}
/>
{iconElement}
<Flex flexDir='column'>
<RawText>{providerName}</RawText>
<RawText textTransform='capitalize' variant='sub-text' size='xs'>
Expand Down
14 changes: 8 additions & 6 deletions src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
20 changes: 11 additions & 9 deletions src/components/MultiHopTrade/components/Earn/EarnInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +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 { getDefaultValidatorForYield, isYieldDisabled } from '@/lib/yieldxyz/utils'
import { useYields } from '@/react-queries/queries/yieldxyz/useYields'
import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators'
import {
Expand Down Expand Up @@ -159,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) ??
Expand All @@ -170,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

Expand Down Expand Up @@ -205,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
}
Expand Down Expand Up @@ -389,7 +388,8 @@ 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 => !isYieldDisabled(y))
}, [sellAsset?.assetId, yieldsData?.byInputAssetId])

const defaultYieldForAsset = useMemo(() => {
Expand All @@ -401,11 +401,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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -98,11 +95,10 @@ 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
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
Expand Down Expand Up @@ -130,11 +126,25 @@ const YieldItem = memo(
textAlign='left'
>
<HStack spacing={3} width='full'>
<Avatar src={displayInfo.logoURI} name={displayInfo.name} size='sm' />
<Avatar
src={providerOrValidatorInfo.logoURI}
name={providerOrValidatorInfo.name}
size='sm'
/>
<VStack align='start' spacing={0} flex={1} minW={0}>
<Text fontWeight='semibold' fontSize='sm' noOfLines={1}>
{displayInfo.name}
{yieldItem.metadata.name}
</Text>
<HStack spacing={1}>
<Avatar
src={providerOrValidatorInfo.logoURI}
name={providerOrValidatorInfo.name}
size='2xs'
/>
<Text fontSize='xs' color='text.subtle' noOfLines={1}>
{providerOrValidatorInfo.name}
</Text>
</HStack>
</VStack>
<GradientApy fontSize='sm' fontWeight='bold'>
{apyDisplay}
Expand Down Expand Up @@ -260,9 +270,21 @@ export const YieldSelector = memo(
{selectedYield && selectedDisplayInfo ? (
<HStack spacing={3} flex={1}>
<Avatar src={selectedDisplayInfo.logoURI} name={selectedDisplayInfo.name} size='sm' />
<Text fontWeight='semibold' fontSize='sm'>
{selectedDisplayInfo.name}
</Text>
<VStack align='start' spacing={0} flex={1} minW={0}>
<Text fontWeight='semibold' fontSize='sm' noOfLines={1}>
{selectedYield.metadata.name}
</Text>
<HStack spacing={1}>
<Avatar
src={selectedDisplayInfo.logoURI}
name={selectedDisplayInfo.name}
size='2xs'
/>
<Text fontSize='xs' color='text.subtle' noOfLines={1}>
{selectedDisplayInfo.name}
</Text>
</HStack>
</VStack>
<Box ml='auto'>
<GradientApy fontSize='sm' fontWeight='bold'>
{selectedApyDisplay}
Expand Down
11 changes: 7 additions & 4 deletions src/components/StakingVaults/hooks/useYieldAsOpportunities.ts
Original file line number Diff line number Diff line change
@@ -1,10 +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 { 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'
Expand All @@ -13,6 +12,7 @@ export type YieldOpportunityDisplay = {
yieldId: string
providerName: string
providerIcon?: string
inputAssetId: AssetId
apy: string
fiatAmount: string
cryptoAmount: string
Expand Down Expand Up @@ -44,6 +44,9 @@ export const useYieldAsOpportunities = (
const inputAssetId = yieldItem.inputTokens?.[0]?.assetId
if (!inputAssetId) return

const hasBalance = bnOrZero(yieldBalancesData?.aggregated[yieldItem.id]?.totalUsd).gt(0)
if (isYieldDisabled(yieldItem) && !hasBalance) return

if (!aggregatedByAssetId[inputAssetId]) {
aggregatedByAssetId[inputAssetId] = {
assetId: inputAssetId,
Expand All @@ -63,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
Expand Down Expand Up @@ -103,6 +105,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(),
Expand Down
Loading