Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"featureDisabled": "This feature is temporarily disabled.",
"yes": "Yes",
"activeAccount": "Active Account",
"selectAccount": "Select Account",
"update": "Update",
"apy": "APY",
"installed": "Installed",
Expand Down Expand Up @@ -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}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PositionDetailsProps> = ({
opportunities,
forceCompactView,
yieldOpportunities,
}) => {
const [isLargerThanMd] = useMediaQuery(`(min-width: ${breakpoints['md']})`, { ssr: false })
const isCompactView = !isLargerThanMd || forceCompactView
Expand All @@ -20,9 +23,17 @@ export const PositionDetails: React.FC<PositionDetailsProps> = ({
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 (
<Flex px={flexPx} py={flexPy} flexDir='column' gap={flexGap}>
<StakingPositionsByProvider ids={opportunities.staking} forceCompactView={forceCompactView} />
<StakingPositionsByProvider
ids={opportunities.staking}
forceCompactView={forceCompactView}
yieldOpportunities={yieldOpportunities}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 = <ArrowForwardIcon />

export type RowProps = Row<StakingEarnOpportunityType>
export type RowProps = Row<UnifiedPosition>

type CalculateRewardFiatAmountArgs = {
assets: Partial<Record<AssetId, Asset>>
Expand Down Expand Up @@ -91,6 +98,7 @@ const calculateRewardFiatAmount: CalculateRewardFiatAmount = ({
export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProps> = ({
ids,
forceCompactView,
yieldOpportunities,
}) => {
const location = useLocation()
const { navigate } = useBrowserRouter()
Expand All @@ -110,24 +118,69 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
selectAggregatedEarnUserStakingOpportunitiesIncludeEmpty,
)
const isRfoxFoxEcosystemPageEnabled = useFeatureFlag('RfoxFoxEcosystemPage')
const filteredDown = useMemo(

const yieldPositionsAsUnified = useMemo<UnifiedPosition[]>(() => {
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),
),
[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 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,
Expand All @@ -136,7 +189,7 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
rewardAddress,
assetId,
highestBalanceAccountAddress,
} = opportunity
} = legacyOpp
const { assetReference, assetNamespace } = fromAssetId(assetId)

if (provider === DefiProvider.rFOX) {
Expand Down Expand Up @@ -170,7 +223,7 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
trackOpportunityEvent(
MixPanelEvent.ClickOpportunity,
{
opportunity,
opportunity: legacyOpp,
element: 'Table Row',
},
assets,
Expand Down Expand Up @@ -211,28 +264,25 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
dispatch,
],
)
const columns: Column<StakingEarnOpportunityType>[] = useMemo(
const columns: Column<UnifiedPosition>[] = 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 (
<Flex gap={4} alignItems='center'>
<LazyLoadAvatar
size='sm'
bg='transparent'
src={row.original.icon ?? getMetadataForProvider(row.original.provider)?.icon ?? ''}
key={`provider-icon-${row.original.id}`}
src={opp.icon ?? getMetadataForProvider(opp.provider)?.icon ?? ''}
key={`provider-icon-${opp.id}`}
/>
<Flex flexDir='column'>
<RawText>{providerName}</RawText>
Expand All @@ -249,20 +299,19 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
Header: translate('defi.totalValue'),
accessor: 'fiatAmount',
Cell: ({ row }: { row: RowProps }) => {
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 ? (
<Flex flexDir='column' alignItems={widthMdFlexStart}>
Expand All @@ -277,33 +326,38 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
Header: translate('defi.apy'),
accessor: 'apy',
Cell: ({ row }: { row: RowProps }) => {
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 <RfoxApy stakingAssetId={row.original.underlyingAssetId} />
if (isRfoxStaking) return <RfoxApy stakingAssetId={opp.underlyingAssetId} />

return (
<Tag colorScheme='green'>
<Amount.Percent value={row.original.apy} />
<Amount.Percent value={opp.apy} />
</Tag>
)
},
},
{
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 <RawText variant='sub-text'>-</RawText>

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 ? (
<Button
isDisabled={!hasRewardsBalance}
variant='ghost-filled'
Expand All @@ -322,30 +376,17 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
<RawText variant='sub-text'>-</RawText>
)
},
sortType: (a: RowProps, b: RowProps): number => {
const aFiatPrice = calculateRewardFiatAmount({
rewardAssetIds: a.original.rewardAssetIds,
rewardsCryptoBaseUnit: a.original.rewardsCryptoBaseUnit,
assets,
marketDataUserCurrency,
})
const bFiatPrice = calculateRewardFiatAmount({
rewardAssetIds: b.original.rewardAssetIds,
rewardsCryptoBaseUnit: b.original.rewardsCryptoBaseUnit,
assets,
marketDataUserCurrency,
})
return aFiatPrice - bFiatPrice
},
},
{
Header: () => null,
id: 'expander',
display: isCompactCols ? 'none' : undefined,
Cell: ({ row }: { row: RowProps }) => {
const url = getMetadataForProvider(row.original.provider)?.url
const opp = row.original as StakingEarnOpportunityType & UnifiedPosition
const url = getMetadataForProvider(opp.provider)?.url
const translation = (() => {
if (!row.original.isReadOnly) return 'common.manage'
if (opp.isYield) return 'common.view'
if (!opp.isReadOnly) return 'common.manage'
return url ? 'common.view' : undefined
})()
const handleOverviewClick = useCallback(
Expand All @@ -361,9 +402,7 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
size='sm'
colorScheme='blue'
width={widthMdAuto}
rightIcon={
row.original.isReadOnly && url ? <ExternalLinkIcon boxSize={3} /> : undefined
}
rightIcon={opp.isReadOnly && url ? <ExternalLinkIcon boxSize={3} /> : undefined}
onClick={handleOverviewClick}
>
{translate(translation)}
Expand Down
Loading