Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make lp opportunities great #8888

Merged
merged 15 commits into from
Feb 20, 2025
1 change: 1 addition & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"needAsset": "Need some %{asset}?",
"buyNow": "Buy Now",
"manage": "Manage",
"view": "View",
"selectedAccount": "Selected Account",
"allChains": "All Chains",
"wallet": "Wallet",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExternalLinkIcon } from '@chakra-ui/icons'
import { Button, Flex, Stack } from '@chakra-ui/react'
import { Tag } from '@chakra-ui/tag'
import type { AssetId } from '@shapeshiftoss/caip'
Expand Down Expand Up @@ -75,6 +76,12 @@ export const LpPositionsByProvider: React.FC<LpPositionsByProviderProps> = ({ id
} = opportunity
const { assetReference, assetNamespace } = fromAssetId(assetId)

if (opportunity.isReadOnly) {
const url = getMetadataForProvider(opportunity.provider)?.url
url && window.open(url, '_blank')
return
}

if (!isConnected && isDemoWallet) {
dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true })
return
Expand Down Expand Up @@ -187,21 +194,34 @@ export const LpPositionsByProvider: React.FC<LpPositionsByProviderProps> = ({ id
{
Header: () => null,
id: 'expander',
Cell: ({ row }: { row: RowProps }) => (
<Flex gap={4} justifyContent='flex-end' width='full'>
<Button
variant='ghost'
width={expanderButtonWidth}
size='sm'
colorScheme='blue'
// we need to pass an arg here, so we need an anonymous function wrapper
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(row, DefiAction.Overview)}
>
{translate('common.manage')}
</Button>
</Flex>
),
Cell: ({ row }: { row: RowProps }) => {
const url = getMetadataForProvider(row.original.provider)?.url
const translation = (() => {
if (!row.original.isReadOnly) return 'common.manage'
return url ? 'common.view' : undefined
})()

return (
<Flex gap={4} justifyContent='flex-end' width='full'>
{translation && (
<Button
variant='ghost'
width={expanderButtonWidth}
size='sm'
colorScheme='blue'
rightIcon={
row.original.isReadOnly && url ? <ExternalLinkIcon boxSize={3} /> : undefined
}
// we need to pass an arg here, so we need an anonymous function wrapper
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(row, DefiAction.Overview)}
>
{translate(translation)}
</Button>
)}
</Flex>
)
},
},
],
[assetId, assets, handleClick, marketDataUserCurrency, translate],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
if (opportunity.isReadOnly) {
const url = getMetadataForProvider(opportunity.provider)?.url
url && window.open(url, '_blank')
return
}

const {
Expand Down Expand Up @@ -279,22 +280,34 @@ export const StakingPositionsByProvider: React.FC<StakingPositionsByProviderProp
{
Header: () => null,
id: 'expander',
Cell: ({ row }: { row: RowProps }) => (
<Flex justifyContent='flex-end' width='full'>
<Button
variant='ghost'
size='sm'
colorScheme='blue'
width={widthMdAuto}
rightIcon={row.original.isReadOnly ? <ExternalLinkIcon boxSize={3} /> : undefined}
// we need to pass an arg here, so we need an anonymous function wrapper
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(row, DefiAction.Overview)}
>
{translate('common.manage')}
</Button>
</Flex>
),
Cell: ({ row }: { row: RowProps }) => {
const url = getMetadataForProvider(row.original.provider)?.url
const translation = (() => {
if (!row.original.isReadOnly) return 'common.manage'
return url ? 'common.view' : undefined
})()

return (
<Flex justifyContent='flex-end' width='full'>
{translation && (
<Button
variant='ghost'
size='sm'
colorScheme='blue'
width={widthMdAuto}
rightIcon={
row.original.isReadOnly && url ? <ExternalLinkIcon boxSize={3} /> : undefined
}
// we need to pass an arg here, so we need an anonymous function wrapper
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(row, DefiAction.Overview)}
>
{translate(translation)}
</Button>
)}
</Flex>
)
},
},
],
[assetId, assets, handleClick, marketDataUserCurrency, translate],
Expand Down
13 changes: 12 additions & 1 deletion src/state/apis/portals/portalsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,19 @@ export const portals = createApi({
type: DefiType.Staking,
}

const lpMetadataUpsertPayload: GetOpportunityMetadataOutput = {
byId: {},
type: DefiType.LiquidityPool,
}

// Populate read only metadata payload
for (const id in readOnlyMetadata) {
stakingMetadataUpsertPayload.byId[id] = readOnlyMetadata[id]
const metadata = readOnlyMetadata[id]
if (metadata.type === DefiType.LiquidityPool) {
lpMetadataUpsertPayload.byId[id] = metadata
} else {
stakingMetadataUpsertPayload.byId[id] = metadata
}
}

// Populate read only userData payload
Expand Down Expand Up @@ -751,6 +761,7 @@ export const portals = createApi({

// Make all dispatches at the end
dispatch(opportunities.actions.upsertOpportunitiesMetadata(stakingMetadataUpsertPayload))
dispatch(opportunities.actions.upsertOpportunitiesMetadata(lpMetadataUpsertPayload))
dispatch(opportunities.actions.upsertOpportunityAccounts(accountUpsertPayload))
dispatch(opportunities.actions.upsertUserStakingOpportunities(userStakingUpsertPayload))

Expand Down
1 change: 1 addition & 0 deletions src/state/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const clearTxHistoryMigrations = {
export const clearOpportunitiesMigrations = {
0: clearOpportunities,
1: clearOpportunities,
2: clearOpportunities,
} as unknown as Omit<MigrationManifest, '_persist'>

export const clearPortfolioMigrations = {
Expand Down
2 changes: 1 addition & 1 deletion src/state/slices/opportunitiesSlice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ export const DEFI_PROVIDER_TO_METADATA: Record<DefiProvider, DefiProviderMetadat
provider: DefiProvider.ThorchainSavers,
icon: 'https://rawcdn.githack.com/trustwallet/assets/32e51d582a890b3dd3135fe3ee7c20c2fd699a6d/blockchains/thorchain/info/logo.png',
color: '#0CDBE0',
url: 'https://app.shapeshift.com',
url: 'https://x.com/thorchain',
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('opportunitiesSlice', () => {
...initialState,
_persist: {
rehydrated: true,
version: 1,
version: 2,
},
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const thorchainSaversStakingOpportunitiesMetadataResolver = async ({
saversMaxSupplyFiat: saversMaxSupplyUserCurrency,
isFull: thorchainPool.synth_mint_paused,
isClaimableRewards: false,
isReadOnly: true,
}
}

Expand Down Expand Up @@ -288,6 +289,8 @@ export const thorchainSaversStakingOpportunitiesMetadataResolver = async ({
saversMaxSupplyFiat: undefined,
isFull: false,
isClaimableRewards: false,
// RUNEPool is not *yet* dead at protocol-level per se and can still be managed
isReadOnly: false,
}
}

Expand Down
42 changes: 39 additions & 3 deletions src/state/slices/opportunitiesSlice/selectors/combined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
OpportunityId,
StakingEarnOpportunityType,
} from '../types'
import { DefiType } from '../types'
import { DefiProvider, DefiType } from '../types'
import { getOpportunityAccessor, getUnderlyingAssetIdsBalances } from '../utils'
import { selectAssets } from './../../assetsSlice/selectors'
import { selectMarketDataUserCurrency } from './../../marketDataSlice/selectors'
Expand Down Expand Up @@ -241,8 +241,44 @@ export const selectAggregatedEarnOpportunitiesByAssetId = createDeepEqualOutputS

const sortedOpportunitiesByFiatAmountAndApy = activeOpportunities.concat(inactiveOpportunities)

const filterThorchainSaversZeroBalance = (
opportunities: AggregatedOpportunitiesByAssetIdReturn[],
) => {
return (
opportunities
.map(opportunity => {
// Individual opportunities - all but savers
const filteredStakingOpportunities = opportunity.opportunities.staking.filter(
opportunityId => {
const maybeOpportunity = combined.find(opp => opp.id === opportunityId)
if (!maybeOpportunity) return true
if (maybeOpportunity.provider !== DefiProvider.ThorchainSavers) return true
return !bnOrZero(maybeOpportunity.fiatAmount).isZero()
},
)

return {
...opportunity,
opportunities: {
// LP stays as-is, savers are OpportunityType.Staking
...opportunity.opportunities,
staking: filteredStakingOpportunities,
},
}
})
// Second pass on the actual aggregate. These are *not* indivial opportunities but an aggregation of many.
// We want to filter savers out, but keep the rest
.filter(aggregate => {
// Filter out the entire aggregate if it has no opportunities left after filtering savers out
return (
aggregate.opportunities.staking.length > 0 || aggregate.opportunities.lp.length > 0
)
})
)
}

if (!includeEarnBalances && !includeRewardsBalances)
return sortedOpportunitiesByFiatAmountAndApy
return filterThorchainSaversZeroBalance(sortedOpportunitiesByFiatAmountAndApy)

const withEarnBalances = aggregatedEarnOpportunitiesByAssetId.filter(opportunity =>
Boolean(includeEarnBalances && !bnOrZero(opportunity.fiatAmount).isZero()),
Expand All @@ -251,7 +287,7 @@ export const selectAggregatedEarnOpportunitiesByAssetId = createDeepEqualOutputS
Boolean(includeRewardsBalances && bnOrZero(opportunity.fiatRewardsAmount).gt(0)),
)

return withEarnBalances.concat(withRewardsBalances)
return filterThorchainSaversZeroBalance(withEarnBalances.concat(withRewardsBalances))
},
)

Expand Down
11 changes: 6 additions & 5 deletions src/state/slices/opportunitiesSlice/selectors/lpSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,16 @@ export const selectAggregatedEarnUserLpOpportunities = createDeepEqualOutputSele

if (!portfolioAssetBalancesById) return opportunities

for (const [lpId, opportunityMetadata] of Object.entries(lpOpportunitiesById)) {
for (const opportunityMetadata of Object.values(lpOpportunitiesById)) {
if (!opportunityMetadata) continue
const underlyingAssetId = opportunityMetadata.underlyingAssetId

const lpAsset = assets[lpId as AssetId]
const lpAsset = assets[underlyingAssetId]

if (!lpAsset) continue

const marketDataPrice = marketDataUserCurrency[lpId as AssetId]?.price
const aggregatedLpAssetBalance = portfolioAssetBalancesById[lpId]
const marketDataPrice = marketDataUserCurrency[underlyingAssetId]?.price
const aggregatedLpAssetBalance = portfolioAssetBalancesById[underlyingAssetId]

/* Get amounts of each underlying token (in base units, eg. wei) */
/* TODO:(pastaghost) Generalize this (and LpEarnOpportunityType) so that number of underlying assets is not assumed to be 2. */
Expand All @@ -218,7 +219,7 @@ export const selectAggregatedEarnUserLpOpportunities = createDeepEqualOutputSele
const opportunity: LpEarnOpportunityType = {
...opportunityMetadata,
isLoaded: true,
chainId: fromAssetId(lpId as AssetId).chainId,
chainId: fromAssetId(underlyingAssetId).chainId,
underlyingToken0AmountCryptoBaseUnit,
underlyingToken1AmountCryptoBaseUnit,
cryptoAmountPrecision: bnOrZero(aggregatedLpAssetBalance)
Expand Down