diff --git a/.env.development b/.env.development index 7ee5b06ee99..218aad21ac8 100644 --- a/.env.development +++ b/.env.development @@ -99,3 +99,4 @@ VITE_FEATURE_KATANA=true VITE_FEATURE_YIELD_XYZ=true VITE_FEATURE_YIELDS_PAGE=true VITE_FEATURE_EARN_TAB=true +VITE_FEATURE_YIELD_MULTI_ACCOUNT=true diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index a926e3a4742..7fb7c526a96 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -180,6 +180,7 @@ "featureDisabled": "This feature is temporarily disabled.", "yes": "Yes", "activeAccount": "Active Account", + "selectAccount": "Select Account", "update": "Update", "apy": "APY", "installed": "Installed", @@ -2769,6 +2770,7 @@ "recommendedForYou": "Recommended for you", "earn": "Earn", "myBalance": "My Balance", + "balanceByAccount": "Balance by Account", "providers": "Providers", "successEnter": "You successfully entered %{amount} %{symbol}", "successExit": "You successfully exited %{amount} %{symbol}", diff --git a/src/components/EarnDashboard/components/PositionDetails/PositionDetails.tsx b/src/components/EarnDashboard/components/PositionDetails/PositionDetails.tsx index b2e812a1f91..3c4400a0582 100644 --- a/src/components/EarnDashboard/components/PositionDetails/PositionDetails.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/PositionDetails.tsx @@ -2,16 +2,19 @@ import { Flex, useMediaQuery } from '@chakra-ui/react' import { StakingPositionsByProvider } from './StakingPositionsByProvider' +import type { YieldOpportunityDisplay } from '@/components/StakingVaults/hooks/useYieldAsOpportunities' import type { AggregatedOpportunitiesByAssetIdReturn } from '@/state/slices/opportunitiesSlice/types' import { breakpoints } from '@/theme/theme' type PositionDetailsProps = AggregatedOpportunitiesByAssetIdReturn & { forceCompactView?: boolean + yieldOpportunities?: YieldOpportunityDisplay[] } export const PositionDetails: React.FC = ({ opportunities, forceCompactView, + yieldOpportunities, }) => { const [isLargerThanMd] = useMediaQuery(`(min-width: ${breakpoints['md']})`, { ssr: false }) const isCompactView = !isLargerThanMd || forceCompactView @@ -20,9 +23,17 @@ export const PositionDetails: React.FC = ({ const flexPy = isCompactView ? 2 : 8 const flexGap = isCompactView ? 2 : 6 + const hasPositions = opportunities.staking.length > 0 || (yieldOpportunities?.length ?? 0) > 0 + + if (!hasPositions) return null + return ( - + ) } diff --git a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx index 5e412568f22..2845224ce49 100644 --- a/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx +++ b/src/components/EarnDashboard/components/PositionDetails/StakingPositionsByProvider.tsx @@ -12,6 +12,7 @@ import type { Column, Row } from 'react-table' import { Amount } from '@/components/Amount/Amount' import { LazyLoadAvatar } from '@/components/LazyLoadAvatar' import { ReactTable } from '@/components/ReactTable/ReactTable' +import type { YieldOpportunityDisplay } from '@/components/StakingVaults/hooks/useYieldAsOpportunities' import { RawText } from '@/components/Text' import { WalletActions } from '@/context/WalletProvider/actions' import { DefiAction } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' @@ -38,14 +39,20 @@ import { import { useAppSelector } from '@/state/store' import { breakpoints } from '@/theme/theme' +type UnifiedPosition = StakingEarnOpportunityType & { + isYield?: boolean + yieldId?: string +} + type StakingPositionsByProviderProps = { ids: OpportunityId[] forceCompactView?: boolean + yieldOpportunities?: YieldOpportunityDisplay[] } const arrowForwardIcon = -export type RowProps = Row +export type RowProps = Row type CalculateRewardFiatAmountArgs = { assets: Partial> @@ -91,6 +98,7 @@ const calculateRewardFiatAmount: CalculateRewardFiatAmount = ({ export const StakingPositionsByProvider: React.FC = ({ ids, forceCompactView, + yieldOpportunities, }) => { const location = useLocation() const { navigate } = useBrowserRouter() @@ -110,7 +118,25 @@ export const StakingPositionsByProvider: React.FC(() => { + if (!yieldOpportunities?.length) return [] + return yieldOpportunities.map(y => ({ + id: y.yieldId, + assetId: y.yieldId, + underlyingAssetId: y.yieldId, + provider: y.providerName, + apy: y.apy, + fiatAmount: y.fiatAmount, + icon: y.providerIcon, + isYield: true, + yieldId: y.yieldId, + isReadOnly: true, + opportunityName: undefined, + })) as unknown as UnifiedPosition[] + }, [yieldOpportunities]) + + const legacyFiltered = useMemo( () => stakingOpportunities.filter( e => ids.includes(e.assetId as OpportunityId) || ids.includes(e.id as OpportunityId), @@ -118,16 +144,43 @@ 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 handleClick = useCallback( (row: RowProps, action: DefiAction) => { const { original: opportunity } = row + if (opportunity.isYield && opportunity.yieldId) { + if (walletDrawer.isOpen) { + walletDrawer.close() + } + navigate(`/yields/${opportunity.yieldId}`) + return + } + if (opportunity.isReadOnly) { const url = getMetadataForProvider(opportunity.provider)?.url url && window.open(url, '_blank') return } + const legacyOpp = opportunity as StakingEarnOpportunityType const { type, provider, @@ -136,7 +189,7 @@ export const StakingPositionsByProvider: React.FC[] = useMemo( + const columns: Column[] = useMemo( () => [ { Header: translate('defi.stakingPosition'), accessor: 'assetId', Cell: ({ row }: { row: RowProps }) => { - // Version or Provider - // Opportunity Name + const opp = row.original as StakingEarnOpportunityType & UnifiedPosition const subText = [] - if (row.original.version) subText.push(row.original.provider) - if (row.original.opportunityName) subText.push(row.original.opportunityName) - const isRunePool = row.original.assetId === thorchainAssetId - const providerName = isRunePool - ? 'RUNEPool' - : row.original.version ?? row.original.provider + if (opp.version) subText.push(opp.provider) + if (opp.opportunityName) subText.push(opp.opportunityName) + const isRunePool = opp.assetId === thorchainAssetId + const providerName = isRunePool ? 'RUNEPool' : opp.version ?? opp.provider return ( {providerName} @@ -249,20 +299,19 @@ export const StakingPositionsByProvider: React.FC { - const opportunity = row.original - - const fiatRewardsAmount = calculateRewardFiatAmount({ - rewardAssetIds: row.original.rewardAssetIds, - rewardsCryptoBaseUnit: row.original.rewardsCryptoBaseUnit, - assets, - marketDataUserCurrency, - }) + const opp = row.original as StakingEarnOpportunityType & UnifiedPosition - const hasValue = - bnOrZero(opportunity.fiatAmount).gt(0) || bnOrZero(fiatRewardsAmount).gt(0) + const fiatRewardsAmount = opp.isYield + ? 0 + : calculateRewardFiatAmount({ + rewardAssetIds: opp.rewardAssetIds, + rewardsCryptoBaseUnit: opp.rewardsCryptoBaseUnit, + assets, + marketDataUserCurrency, + }) - // Note, this already includes rewards. Let's not double-count them - const totalFiatAmount = bnOrZero(row.original.fiatAmount).toFixed(2) + const hasValue = bnOrZero(opp.fiatAmount).gt(0) || bnOrZero(fiatRewardsAmount).gt(0) + const totalFiatAmount = bnOrZero(opp.fiatAmount).toFixed(2) return hasValue ? ( @@ -277,33 +326,38 @@ export const StakingPositionsByProvider: React.FC { - const isRfoxStaking = RFOX_STAKING_ASSET_IDS.includes(row.original.underlyingAssetId) + const opp = row.original as StakingEarnOpportunityType & UnifiedPosition + const isRfoxStaking = + !opp.isYield && RFOX_STAKING_ASSET_IDS.includes(opp.underlyingAssetId) - if (isRfoxStaking) return + if (isRfoxStaking) return return ( - + ) }, }, { Header: translate('defi.claimableRewards'), - accessor: 'rewardsCryptoBaseUnit', + accessor: 'id', display: isCompactCols ? 'none' : undefined, Cell: ({ row }: { row: RowProps }) => { + const opp = row.original as StakingEarnOpportunityType & UnifiedPosition + const handleClaimClick = useCallback(() => handleClick(row, DefiAction.Claim), [row]) + + if (opp.isYield) return - + const fiatAmount = calculateRewardFiatAmount({ - rewardAssetIds: row.original.rewardAssetIds, - rewardsCryptoBaseUnit: row.original.rewardsCryptoBaseUnit, + rewardAssetIds: opp.rewardAssetIds, + rewardsCryptoBaseUnit: opp.rewardsCryptoBaseUnit, assets, marketDataUserCurrency, }) const hasRewardsBalance = bnOrZero(fiatAmount).gt(0) - const handleClaimClick = useCallback(() => handleClick(row, DefiAction.Claim), [row]) - - return hasRewardsBalance && row.original.isClaimableRewards ? ( + return hasRewardsBalance && opp.isClaimableRewards ? (