Skip to content

Commit

Permalink
Refactor EarnScene
Browse files Browse the repository at this point in the history
- Initialization of `stakePolicyMap` to be based on all supported instead of only enabled wallet `pluginIds.` This also limits the display to one card per policy instead of one card per wallet.
- Add wallet picker logic based on "Discover/Portfolio" state and number of open positions
- Only initialize `stakePolicyMap` once, regardless of if we re-navigate to the scene
  • Loading branch information
Jon-edge committed Nov 4, 2024
1 parent 51824ad commit 14be004
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 67 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- added: Add TON
- added: Log swap errors to Sentry.
- added: Tracking for unexpected fiat provider errors.
- changed: Redesign `StakingReturnsCard,` specifically for `StakeOverviewScene`
- changed: `EarnScene` shows all possible stake options, instead of only those for enabled wallets
- changed: `EarnScene` shows one card per stake option if multiple wallets have stake positions on that stake option
- changed: `EarnScene` only intializes stake options once, regardless of re-navigation to the scene
- changed: `FiatProviderError` messages now include `FiatProviderQuoteError` info.
- changed: Add explicit gas limit for Kiln staking.
- changed: Various strings updated to UK compliance spec
Expand Down
180 changes: 113 additions & 67 deletions src/components/scenes/Staking/EarnScene.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EdgeCurrencyWallet } from 'edge-core-js'
import { EdgeCurrencyInfo, EdgeCurrencyWallet } from 'edge-core-js'
import * as React from 'react'
import { ActivityIndicator } from 'react-native'

Expand All @@ -11,28 +11,36 @@ import { lstrings } from '../../../locales/strings'
import { getStakePlugins } from '../../../plugins/stake-plugins/stakePlugins'
import { StakePlugin, StakePolicy, StakePosition } from '../../../plugins/stake-plugins/types'
import { useSelector } from '../../../types/reactRedux'
import { EdgeAppSceneProps } from '../../../types/routerTypes'
import { getPluginFromPolicy, getPositionAllocations } from '../../../util/stakeUtils'
import { EdgeAppSceneProps, NavigationBase } from '../../../types/routerTypes'
import { getPositionAllocations } from '../../../util/stakeUtils'
import { zeroString } from '../../../util/utils'
import { EdgeSwitch } from '../../buttons/EdgeSwitch'
import { EarnOptionCard } from '../../cards/EarnOptionCard'
import { EdgeAnim, fadeInUp20 } from '../../common/EdgeAnim'
import { SceneWrapper } from '../../common/SceneWrapper'
import { SectionHeader } from '../../common/SectionHeader'
import { showDevError } from '../../services/AirshipInstance'
import { WalletListModal, WalletListResult } from '../../modals/WalletListModal'
import { Airship, showDevError } from '../../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext'

interface Props extends EdgeAppSceneProps<'earnScene'> {}

export interface EarnSceneParams {}

interface StakePolicyPosition {
stakePolicy: StakePolicy
interface WalletStakeInfo {
wallet: EdgeCurrencyWallet
isPositionOpen: boolean
stakePosition: StakePosition
}

interface DisplayStakeInfo {
stakePlugin: StakePlugin
stakePolicy: StakePolicy
walletStakeInfos: WalletStakeInfo[]
}

interface StakePolicyMap {
[walletId: string]: { stakePolicyPositions: StakePolicyPosition[]; stakePlugins: StakePlugin[] }
[pluginId: string]: DisplayStakeInfo[]
}

export const EarnScene = (props: Props) => {
Expand All @@ -41,101 +49,139 @@ export const EarnScene = (props: Props) => {
const styles = getStyles(theme)

const account = useSelector(state => state.core.account)

const currencyConfigMap = useSelector(state => state.core.account.currencyConfig)

const currencyWallets = useWatch(account, 'currencyWallets')
const wallets = Object.values(currencyWallets)

const [stakePolicyMap, setStakePolicyMap] = React.useState<StakePolicyMap>()
const [positionWallets, setPositionWallets] = React.useState<EdgeCurrencyWallet[]>([])
const [isPortfolioSelected, setIsPortfolioSelected] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true)

// Filter wallets based on isPortfolioSelected
const displayWallets = !stakePolicyMap ? [] : isPortfolioSelected ? positionWallets : wallets
// Store `stakePolicyMap` in a ref and manage re-renders manually to avoid
// re-initializing it every time we enter the scene.
const [updateCounter, setUpdateCounter] = React.useState(0)
const stakePolicyMapRef = React.useRef<StakePolicyMap>({})
const stakePolicyMap = stakePolicyMapRef.current

const handleSelectEarn = useHandler(() => setIsPortfolioSelected(false))
const handleSelectPortfolio = useHandler(() => setIsPortfolioSelected(true))

useAsyncEffect(
async () => {
if (stakePolicyMap != null) return

const positionWallets = []
const policyMap: StakePolicyMap = {}

for (const wallet of wallets) {
// Get all available stake policies
const { pluginId } = wallet.currencyInfo
if (SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING) {
// For each wallet
const stakePolicyPositions: StakePolicyPosition[] = []

try {
const stakePlugins = await getStakePlugins(pluginId)
for (const stakePlugin of stakePlugins) {
const stakePolicies = stakePlugin.getPolicies({ wallet, currencyCode: wallet.currencyInfo.currencyCode })

// Check if there's open positions
for (const stakePolicy of stakePolicies) {
for (const pluginId of Object.keys(currencyConfigMap)) {
const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING
if (stakePolicyMap[pluginId] != null || !isStakingSupported) continue

// Initialize stake policy
try {
const stakePlugins = await getStakePlugins(pluginId)
stakePolicyMap[pluginId] = []

for (const stakePlugin of stakePlugins) {
const stakePolicies = stakePlugin.getPolicies({ currencyCode: currencyConfigMap[pluginId].currencyInfo.currencyCode })
const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId)

for (const stakePolicy of stakePolicies) {
const walletStakePositions = []
for (const wallet of matchingWallets) {
// Determine if a wallet matching this policy has an open position
const stakePosition = await stakePlugin.fetchStakePosition({ stakePolicyId: stakePolicy.stakePolicyId, wallet, account })
stakePolicyPositions.push({ stakePolicy, stakePosition })

const allocations = getPositionAllocations(stakePosition)
const { staked, earned, unstaked } = allocations
if ([...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount))) {
positionWallets.push(wallet)
}
const isPositionOpen = [...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount))

walletStakePositions.push({ wallet, isPositionOpen, stakePosition })
}
}

policyMap[wallet.id] = { stakePolicyPositions, stakePlugins }
setStakePolicyMap({ ...policyMap })
setPositionWallets(positionWallets)
} catch (e) {
showDevError(e)
stakePolicyMap[pluginId].push({
stakePlugin,
stakePolicy,
walletStakeInfos: walletStakePositions
})
// Trigger re-render
setUpdateCounter(prevCounter => prevCounter + 1)
}
}
} catch (e) {
showDevError(e)
}
}
setIsLoading(false)
},
[],
[updateCounter],
'EarnScene'
)

const renderStakeItems = (wallet: EdgeCurrencyWallet) => {
if (stakePolicyMap == null) return null
const renderStakeItems = (displayStakeInfo: DisplayStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy, walletStakeInfos } = displayStakeInfo

const handlePress = async () => {
let walletId: string | undefined
let stakePosition

const openStakePositions = walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen)

if (walletStakeInfos.length === 1 || (isPortfolioSelected && openStakePositions.length === 1)) {
// Only one compatible wallet if on "Discover", or only one open
// position on "Portfolio." Auto-select the wallet.
const { wallet, stakePosition: existingStakePosition } = walletStakeInfos[0]

walletId = wallet.id
stakePosition = existingStakePosition
} else {
// Select an existing wallet that matches this policy or create a new one
const allowedAssets = stakePolicy.stakeAssets.map(stakeAsset => ({ pluginId: stakeAsset.pluginId, tokenId: null }))

// Filter for wallets that have an open position if "Portfolio" is
// selected
const allowedWalletIds = isPortfolioSelected
? walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen).map(walletStakePosition => walletStakePosition.wallet.id)
: undefined

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedAssets={allowedAssets}
allowedWalletIds={allowedWalletIds}
headerTitle={lstrings.select_wallet}
// Only allow wallet creation on the Discover tab
showCreateWallet={isPortfolioSelected}
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
stakePosition = walletStakeInfos.find(walletStakeInfo => walletStakeInfo.wallet.id === result.walletId)?.stakePosition
}
}

// User backed out of the WalletListModal
if (walletId == null) return

const { stakePolicyPositions, stakePlugins } = stakePolicyMap[wallet.id] ?? { stakePolicyPositions: [], stakePlugins: [] }
navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicy,
stakePosition
})
}

return (
<>
{stakePolicyPositions.map((stakePolicyPosition: { stakePolicy: StakePolicy; stakePosition: StakePosition }, index: number) => {
const { stakePolicy, stakePosition } = stakePolicyPosition

if (stakePolicy == null) return null
const stakePlugin = getPluginFromPolicy(stakePlugins, stakePolicy)

const handlePress =
stakePlugin == null ? undefined : () => navigation.push('stakeOverview', { stakePlugin, walletId: wallet.id, stakePolicy, stakePosition })

return (
<EdgeAnim key={`${wallet.id}-${index}`} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={wallet.currencyInfo} stakePolicy={stakePolicy} isOpenPosition={isPortfolioSelected} onPress={handlePress} />
</EdgeAnim>
)
})}
</>
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition={isPortfolioSelected} onPress={handlePress} />
</EdgeAnim>
)
}

return (
// TODO: Address "VirtualizedLists should never be nested inside plain
// ScrollViews with the same orientation because it can break windowing and
// other functionality - use another VirtualizedList-backed container
// instead." somehow, while retaining the bottom loader positioning...
<SceneWrapper scroll padding={theme.rem(0.5)}>
<EdgeSwitch labelA={lstrings.staking_discover} labelB={lstrings.staking_portfolio} onSelectA={handleSelectEarn} onSelectB={handleSelectPortfolio} />
<SectionHeader leftTitle={lstrings.staking_earning_pools} />
{displayWallets.map(wallet => renderStakeItems(wallet))}
{Object.keys(stakePolicyMap).map(pluginId =>
stakePolicyMap[pluginId].map(displayStakeInfo => renderStakeItems(displayStakeInfo, currencyConfigMap[pluginId].currencyInfo))
)}
{isLoading && <ActivityIndicator style={styles.loader} size="large" color={theme.primaryText} />}
</SceneWrapper>
)
Expand Down

0 comments on commit 14be004

Please sign in to comment.