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

Jon/fix/earn-2 #5370

Merged
merged 3 commits into from
Dec 2, 2024
Merged
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
255 changes: 191 additions & 64 deletions src/components/scenes/Staking/EarnScene.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIsFocused } from '@react-navigation/native'
import { EdgeCurrencyInfo, EdgeCurrencyWallet } from 'edge-core-js'
import * as React from 'react'
import { ActivityIndicator } from 'react-native'
Expand Down Expand Up @@ -25,136 +26,259 @@ import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext'

interface Props extends EdgeAppSceneProps<'earnScene'> {}

export interface EarnSceneParams {}

let USERNAME: string | undefined
let STAKE_POLICY_MAP: StakePolicyMap = {}
let DISCOVER_MAP: DiscoverStakeMap = {}
let PORTFOLIO_MAP: PortfolioStakeMap = {}

export interface EarnSceneParams {}
interface DiscoverStakeInfo {
stakePlugin: StakePlugin
stakePolicy: StakePolicy
}

interface PortfolioStakeInfo extends DiscoverStakeInfo {
walletStakeInfos: WalletStakeInfo[]
}

interface DiscoverStakeMap {
[stakePolicyId: string]: DiscoverStakeInfo
}

interface PortfolioStakeMap {
[stakePolicyId: string]: PortfolioStakeInfo
}

interface WalletStakeInfo {
wallet: EdgeCurrencyWallet
isPositionOpen: boolean
stakePosition: StakePosition
}

interface DisplayStakeInfo {
stakePlugin: StakePlugin
stakePolicy: StakePolicy
walletStakeInfos: WalletStakeInfo[]
}
/** Hook to ensure the UI updates on map changes, while retaining cached data
* functionality */
const useStakeMaps = () => {
const [, forceUpdate] = React.useReducer(x => x + 1, 0)

interface StakePolicyMap {
[pluginId: string]: DisplayStakeInfo[]
const updateMaps = React.useCallback((updates: () => void) => {
updates()
forceUpdate()
}, [])

return {
discoverMap: DISCOVER_MAP,
portfolioMap: PORTFOLIO_MAP,
updateMaps
}
}

export const EarnScene = (props: Props) => {
const { navigation } = props
const theme = useTheme()
const styles = getStyles(theme)

const { discoverMap, portfolioMap, updateMaps } = useStakeMaps()

const account = useSelector(state => state.core.account)
if (USERNAME !== account.username) {
// Reset local variable if user changes
USERNAME = account.username
STAKE_POLICY_MAP = {}
DISCOVER_MAP = {}
PORTFOLIO_MAP = {}
}

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

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

const [isPortfolioSelected, setIsPortfolioSelected] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true)

const [updateCounter, setUpdateCounter] = React.useState(0)
const [isLoadingDiscover, setIsLoadingDiscover] = React.useState(true)
const [isLoadingPortfolio, setIsLoadingPortfolio] = React.useState(true)
const [isPrevFocused, setIsPrevFocused] = React.useState<boolean>()

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

const isFocused = useIsFocused()

useAsyncEffect(
async () => {
for (const pluginId of Object.keys(currencyConfigMap)) {
const pluginIds = Object.keys(currencyConfigMap)

for (const pluginId of pluginIds) {
setIsLoadingDiscover(true)

const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING
if (STAKE_POLICY_MAP[pluginId] != null || !isStakingSupported) continue
if (!isStakingSupported) continue

// Initialize stake policy
const stakePlugins = await getStakePlugins(pluginId)
STAKE_POLICY_MAP[pluginId] = []

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId)
for (const stakePlugin of stakePlugins) {
const stakePolicies = stakePlugin.getPolicies({ pluginId })
updateMaps(() => {
for (const stakePlugin of stakePlugins) {
for (const stakePolicy of stakePlugin.getPolicies({ pluginId }).filter(stakePolicy => !stakePolicy.deprecated)) {
DISCOVER_MAP[stakePolicy.stakePolicyId] = {
stakePlugin,
stakePolicy
}
}
}
})

console.debug('getStakePlugins', pluginId, 'complete')
setIsLoadingDiscover(false)
}

setIsLoadingDiscover(false)
return () => {}
},
[],
'EarnScene Initialize Discover Items'
)

// Refresh stake positions when re-entering the scene or on initial load
useAsyncEffect(
async () => {
if (!isLoadingDiscover || (isFocused && !isPrevFocused)) {
setIsLoadingPortfolio(true)

const controller = new AbortController()
const signal = controller.signal

try {
const stakePolicyIds = Object.keys(discoverMap)
for (const stakePolicyId of stakePolicyIds) {
if (signal.aborted) break

const discoverInfo = discoverMap[stakePolicyId]
const { stakePlugin, stakePolicy } = discoverInfo

for (const stakePolicy of stakePolicies) {
const walletStakePositions = []
for (const wallet of matchingWallets) {
// Find matching wallets based on the first stake asset's pluginId
const pluginId = stakePolicy.stakeAssets[0].pluginId
const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId)

const walletStakeInfoPromises = matchingWallets.map(async wallet => {
if (signal.aborted) return null
try {
// Determine if a wallet matching this policy has an open position
const stakePosition = await stakePlugin.fetchStakePosition({ stakePolicyId: stakePolicy.stakePolicyId, wallet, account })
const stakePosition = await stakePlugin.fetchStakePosition({
stakePolicyId: stakePolicy.stakePolicyId,
wallet,
account
})
const allocations = getPositionAllocations(stakePosition)
const { staked, earned, unstaked } = allocations
const isPositionOpen = [...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount))

walletStakePositions.push({ wallet, isPositionOpen, stakePosition })
if (isPositionOpen) {
return { wallet, stakePosition }
}
} catch (e) {
showDevError(e)
}
}

STAKE_POLICY_MAP[pluginId].push({
stakePlugin,
stakePolicy,
walletStakeInfos: walletStakePositions
return null
})
// Trigger re-render
setUpdateCounter(prevCounter => prevCounter + 1)

if (!signal.aborted) {
const walletStakeInfos = (await Promise.all(walletStakeInfoPromises)).filter(
(info: WalletStakeInfo | null): info is WalletStakeInfo => info != null
)

updateMaps(() => {
PORTFOLIO_MAP[stakePolicyId] = {
...discoverInfo,
walletStakeInfos
}
})
}
}
} finally {
if (!signal.aborted) {
setIsLoadingPortfolio(false)
setIsPrevFocused(isFocused)
}
}

return () => {
controller.abort()
}
}
setIsLoading(false)
},
[updateCounter],
'EarnScene'
[isFocused, isLoadingDiscover, updateMaps],
'EarnScene Refresh Portfolio Data'
)

const renderStakeItems = (displayStakeInfo: DisplayStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy, walletStakeInfos } = displayStakeInfo
const renderDiscoverItem = (discoverStakeInfo: DiscoverStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy } = discoverStakeInfo

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

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
// Only one compatible wallet, auto-select it
const wallet = matchingWallets[0]
walletId = wallet.id
} 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 }))

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedAssets={allowedAssets}
headerTitle={lstrings.select_wallet}
showCreateWallet
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
}
}

const openStakePositions = walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen)
// User backed out of the WalletListModal
if (walletId == null) return

if (isPortfolioSelected && openStakePositions.length === 0) {
return null
navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicy,
// 'stakeOverview' scene will fetch the position if one exists.
// No need to know if a position exists at this point.
stakePosition: undefined
})
}

return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition={false} onPress={handlePress} />
</EdgeAnim>
)
}

const renderPortfolioItem = (portfolioStakeInfo: PortfolioStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy, walletStakeInfos } = portfolioStakeInfo
if (walletStakeInfos.length === 0) return null

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

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 matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last tiny change then we should get this back in QAs hands: we don't need to look at matchingWallets in renderPortfolioItem since we already have walletStakeInfos. Remove matchingWallets and replace the length check with walletStakeInfos

Copy link
Collaborator Author

@Jon-edge Jon-edge Nov 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember doing this intentionally for new positions from new wallets but I will have to check what the exact situation was and if it's still applicable

// Only one wallet with an open position, auto-select it
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 allowedPortfolioWalletIds = isPortfolioSelected
? walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen).map(walletStakePosition => walletStakePosition.wallet.id)
: undefined
// Select from wallets that have an open position
const allowedWalletIds = walletStakeInfos.map(walletStakePosition => walletStakePosition.wallet.id)

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedAssets={allowedAssets}
allowedWalletIds={!isPortfolioSelected ? undefined : allowedPortfolioWalletIds}
allowedWalletIds={allowedWalletIds}
headerTitle={lstrings.select_wallet}
// Only allow wallet creation on the Discover tab
showCreateWallet={!isPortfolioSelected}
showCreateWallet={false}
navigation={navigation as NavigationBase}
/>
))
Expand All @@ -178,7 +302,7 @@ export const EarnScene = (props: Props) => {

return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition={isPortfolioSelected} onPress={handlePress} />
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition onPress={handlePress} />
</EdgeAnim>
)
}
Expand All @@ -187,10 +311,13 @@ export const EarnScene = (props: Props) => {
<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} />
{Object.keys(STAKE_POLICY_MAP).map(pluginId =>
STAKE_POLICY_MAP[pluginId].map(displayStakeInfo => renderStakeItems(displayStakeInfo, currencyConfigMap[pluginId].currencyInfo))
{isPortfolioSelected &&
Object.values(portfolioMap).map(info => renderPortfolioItem(info, currencyConfigMap[info.stakePolicy.stakeAssets[0].pluginId].currencyInfo))}
{!isPortfolioSelected &&
Object.values(discoverMap).map(info => renderDiscoverItem(info, currencyConfigMap[info.stakePolicy.stakeAssets[0].pluginId].currencyInfo))}
{((isLoadingDiscover && !isPortfolioSelected) || (isLoadingPortfolio && isPortfolioSelected)) && (
<ActivityIndicator style={styles.loader} size="large" color={theme.primaryText} />
)}
{isLoading && <ActivityIndicator style={styles.loader} size="large" color={theme.primaryText} />}
</SceneWrapper>
)
}
Expand Down