Skip to content

Commit

Permalink
feat(#415): Fix chunking of claims of large #s of hotspots, and add a…
Browse files Browse the repository at this point in the history
… progress bar (#418)

* feat(#415): Add progress bar to bulk claims and fix blockhash expiration

* feat(#415): Add progress bar to bulk claims and fix blockhash expiration

* feat(#415): Add progress bar to bulk claims and fix blockhash expiration

* Feature complete

* Add missing file
  • Loading branch information
ChewingGlass authored Aug 10, 2023
1 parent fed130d commit 728f827
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 147 deletions.
68 changes: 68 additions & 0 deletions src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { BoxProps } from '@shopify/restyle'
import { Theme } from '@theme/theme'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { LayoutChangeEvent, LayoutRectangle } from 'react-native'
import {
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import { ReAnimatedBox } from './AnimatedBox'
import Box from './Box'

const ProgressBar = ({
progress: progressIn,
...rest
}: BoxProps<Theme> & { progress: number }) => {
const HEIGHT = 15

const [progressRect, setProgressRect] = useState<LayoutRectangle>()

const handleLayout = useCallback((e: LayoutChangeEvent) => {
e.persist()

setProgressRect(e.nativeEvent.layout)
}, [])

const PROGRESS_WIDTH = useMemo(
() => (progressRect ? progressRect.width : 0),
[progressRect],
)

const width = useSharedValue(0)

useEffect(() => {
// withRepeat to repeat the animation
width.value = withSpring((progressIn / 100) * PROGRESS_WIDTH)
}, [PROGRESS_WIDTH, width, progressIn])

const progress = useAnimatedStyle(() => {
return {
width: width.value,
}
})

return (
<Box
onLayout={handleLayout}
{...rest}
borderRadius="round"
width="100%"
height={HEIGHT}
backgroundColor="transparent10"
overflow="hidden"
flexDirection="row"
justifyContent="flex-start"
>
<ReAnimatedBox style={progress}>
<Box
height={HEIGHT - 1}
borderRadius="round"
backgroundColor="lightGrey"
/>
</ReAnimatedBox>
</Box>
)
}

export default ProgressBar
65 changes: 59 additions & 6 deletions src/features/collectables/ClaimAllRewardsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,69 @@ import CircleLoader from '@components/CircleLoader'
import { DelayedFadeIn } from '@components/FadeInOut'
import RewardItem from '@components/RewardItem'
import Text from '@components/Text'
import {
IOT_MINT,
MOBILE_MINT,
sendAndConfirmWithRetry,
toNumber,
} from '@helium/spl-utils'
import useAlert from '@hooks/useAlert'
import { useHntSolConvert } from '@hooks/useHntSolConvert'
import useHotspots from '@hooks/useHotspots'
import useSubmitTxn from '@hooks/useSubmitTxn'
import { useNavigation } from '@react-navigation/native'
import { IOT_LAZY_KEY, MOBILE_LAZY_KEY } from '@utils/constants'
import BN from 'bn.js'
import React, { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IOT_MINT, MOBILE_MINT, toNumber } from '@helium/spl-utils'
import { CollectableNavigationProp } from './collectablesTypes'
import { useSolana } from '../../solana/SolanaProvider'
import { BalanceChange } from '../../solana/walletSignBottomSheetTypes'
import { CollectableNavigationProp } from './collectablesTypes'

const ClaimAllRewardsScreen = () => {
const { t } = useTranslation()
const navigation = useNavigation<CollectableNavigationProp>()
const [redeeming, setRedeeming] = useState(false)
const [claimError, setClaimError] = useState<string | undefined>()
const { submitClaimAllRewards } = useSubmitTxn()
const {
hntEstimateLoading,
hntSolConvertTransaction,
hntEstimate,
hasEnoughSol,
} = useHntSolConvert()
const { showOKCancelAlert } = useAlert()
const { anchorProvider } = useSolana()
const showHNTConversionAlert = useCallback(async () => {
if (!anchorProvider || !hntSolConvertTransaction) return

const decision = await showOKCancelAlert({
title: t('browserScreen.insufficientSolToPayForFees'),
message: t('browserScreen.wouldYouLikeToConvert', {
amount: hntEstimate,
ticker: 'HNT',
}),
})

if (!decision) return
const signed = await anchorProvider.wallet.signTransaction(
hntSolConvertTransaction,
)
await sendAndConfirmWithRetry(
anchorProvider.connection,
signed.serialize(),
{
skipPreflight: true,
},
'confirmed',
)
}, [
anchorProvider,
hntSolConvertTransaction,
showOKCancelAlert,
t,
hntEstimate,
])

const {
hotspots,
Expand All @@ -45,6 +91,9 @@ const ClaimAllRewardsScreen = () => {
try {
setClaimError(undefined)
setRedeeming(true)
if (!hasEnoughSol) {
await showHNTConversionAlert()
}

const balanceChanges: BalanceChange[] = []

Expand Down Expand Up @@ -78,11 +127,13 @@ const ClaimAllRewardsScreen = () => {
setRedeeming(false)
}
}, [
navigation,
hasEnoughSol,
pendingIotRewards,
pendingMobileRewards,
submitClaimAllRewards,
hotspotsWithMeta,
pendingMobileRewards,
pendingIotRewards,
navigation,
showHNTConversionAlert,
])

const addAllToAccountDisabled = useMemo(() => {
Expand Down Expand Up @@ -161,7 +212,9 @@ const ClaimAllRewardsScreen = () => {
titleColor="black"
marginHorizontal="l"
onPress={onClaimRewards}
disabled={addAllToAccountDisabled || redeeming}
disabled={
addAllToAccountDisabled || redeeming || hntEstimateLoading
}
TrailingComponent={
redeeming ? (
<CircleLoader loaderSize={20} color="white" />
Expand Down
23 changes: 22 additions & 1 deletion src/features/collectables/ClaimingRewardsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Box from '@components/Box'
import ButtonPressable from '@components/ButtonPressable'
import { DelayedFadeIn } from '@components/FadeInOut'
import IndeterminateProgressBar from '@components/IndeterminateProgressBar'
import ProgressBar from '@components/ProgressBar'
import Text from '@components/Text'
import { useSolOwnedAmount } from '@helium/helium-react-hooks'
import { useBN } from '@hooks/useBN'
Expand Down Expand Up @@ -191,7 +192,27 @@ const ClaimingRewardsScreen = () => {
{t('collectablesScreen.claimingRewardsBody')}
</Text>
<Box flexDirection="row" marginHorizontal="xxl" marginTop="m">
<IndeterminateProgressBar paddingHorizontal="l" />
{typeof solanaPayment.progress !== 'undefined' ? (
<Box
width="100%"
flexDirection="column"
alignContent="stretch"
alignItems="stretch"
>
<ProgressBar progress={solanaPayment.progress.percent} />
<Text
textAlign="center"
variant="body2"
color="secondaryText"
marginTop="s"
numberOfLines={2}
>
{solanaPayment.progress.text}
</Text>
</Box>
) : (
<IndeterminateProgressBar paddingHorizontal="l" />
)}
</Box>
</Animated.View>
)}
Expand Down
1 change: 1 addition & 0 deletions src/features/collectables/CollectablesTopTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const CollectablesTopTabs = () => {
const screenOpts = useCallback(
({ route }: { route: RouteProp<CollectablesTabParamList> }) =>
({
lazy: true,
headerShown: false,
tabBarLabelStyle: {
fontFamily: Font.medium,
Expand Down
8 changes: 4 additions & 4 deletions src/features/collectables/HotspotList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ const HotspotList = () => {
hasPressedState={false}
/>
<ListItem
key="show-all"
title={t('collectablesScreen.hotspots.all')}
key="show-1000"
title={t('collectablesScreen.hotspots.thousand')}
subtitle={t('collectablesScreen.hotspots.showAllHotspotsWarning')}
// Set an unrealistically high amount
onPress={handleSetPageAmount(10000)}
selected={pageAmount === 10000}
onPress={handleSetPageAmount(1000)}
selected={pageAmount === 1000}
hasPressedState={false}
subtitleColor="orange500"
/>
Expand Down
13 changes: 4 additions & 9 deletions src/hooks/useEntityKey.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { useEffect, useState } from 'react'
import { decodeEntityKey } from '@helium/helium-entity-manager-sdk'
import { HotspotWithPendingRewards } from '../types/solana'
import { useKeyToAsset } from './useKeyToAsset'

export const useEntityKey = (hotspot: HotspotWithPendingRewards) => {
const [entityKey, setEntityKey] = useState<string>()
const { info: kta } = useKeyToAsset(hotspot?.id)

useEffect(() => {
if (hotspot) {
setEntityKey(hotspot.content.json_uri.split('/').slice(-1)[0])
}
}, [hotspot, setEntityKey])

return entityKey
return kta ? decodeEntityKey(kta.entityKey, kta.keySerialization) : undefined
}
4 changes: 2 additions & 2 deletions src/hooks/useHntSolConvert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ export function useHntSolConvert() {
}, [baseUrl])

const hasEnoughSol = useMemo(() => {
if (!hntBalance || !solBalance || !hntEstimate) return true
if (!hntBalance || !hntEstimate) return true

if (hntBalance.lt(hntEstimate)) return true

return solBalance.gt(new BN(0.02 * LAMPORTS_PER_SOL))
return (solBalance || new BN(0)).gt(new BN(0.02 * LAMPORTS_PER_SOL))
}, [hntBalance, solBalance, hntEstimate])

const {
Expand Down
21 changes: 21 additions & 0 deletions src/hooks/useKeyToAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
mobileInfoKey,
rewardableEntityConfigKey,
} from '@helium/helium-entity-manager-sdk'
import { useAnchorAccount } from '@helium/helium-react-hooks'
import { HeliumEntityManager } from '@helium/idls/lib/types/helium_entity_manager'
import { MOBILE_SUB_DAO_KEY } from '@utils/constants'

const type = 'keyToAssetV0'

export const useKeyToAsset = (entityKey: string | undefined) => {
const [mobileConfigKey] = rewardableEntityConfigKey(
MOBILE_SUB_DAO_KEY,
'MOBILE',
)
const [mobileInfo] = mobileInfoKey(mobileConfigKey, entityKey || '')

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return useAnchorAccount<HeliumEntityManager, type>(mobileInfo, type)
}
9 changes: 4 additions & 5 deletions src/hooks/useMetaplexMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ import { useAsync } from 'react-async-hook'
const MPL_PID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s')

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache: Record<string, any> = {}
const cache: Record<string, Promise<any>> = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function getMetadata(uri: string | undefined): Promise<any | undefined> {
export function getMetadata(uri: string | undefined): Promise<any | undefined> {
if (uri) {
if (!cache[uri]) {
const res = await fetch(uri)
const json = await res.json()
cache[uri] = json
cache[uri] = fetch(uri).then((res) => res.json())
}
return cache[uri]
}
return Promise.resolve(undefined)
}

export const METADATA_PARSER: TypedAccountParser<Metadata> = (
Expand Down
26 changes: 2 additions & 24 deletions src/hooks/useSubmitTxn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,33 +310,11 @@ export default () => {
throw new Error(t('errors.account'))
}

const txns = await solUtils.claimAllRewardsTxns(
anchorProvider,
lazyDistributors,
hotspots,
)

const serializedTxs = txns.map((txn) =>
txn.serialize({
requireAllSignatures: false,
}),
)

const decision = await walletSignBottomSheetRef.show({
type: WalletStandardMessageTypes.signTransaction,
url: '',
additionalMessage: t('transactions.signClaimAllRewardsTxn'),
serializedTxs: serializedTxs.map(Buffer.from),
})

if (!decision) {
throw new Error('User rejected transaction')
}

dispatch(
claimAllRewards({
account: currentAccount,
txns,
lazyDistributors,
hotspots,
anchorProvider,
cluster,
}),
Expand Down
6 changes: 3 additions & 3 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default {
claimingRewardsBody:
'You can exit this screen while you wait. We’ll update your Wallet momentarily.',
claimComplete: 'Rewards Claimed!',
claimCompleteBody: 'We’ve added your tokens to your wallet.',
claimCompleteBody: 'Your tokens have been added to your wallet.',
claimError: 'Claim failed. Please try again later.',
transferCollectableAlertTitle:
'Are you sure you will like to transfer your collectable?',
Expand Down Expand Up @@ -219,7 +219,7 @@ export default {
'Warning: Load times may be affected when showing all hotspots per page.',
twenty: '20',
fifty: '50',
all: 'All',
thousand: '1000',
copyEccCompact: 'Copy Hotspot Key',
assertLocation: 'Assert Location',
antennaSetup: 'Antenna Setup',
Expand Down Expand Up @@ -332,7 +332,7 @@ export default {
chooseTokenToSwap: 'Choose a token to swap',
chooseTokenToReceive: 'Choose a token to receive',
swapComplete: 'Tokens swapped!',
swapCompleteBody: 'We’ve updated the tokens on your wallet.',
swapCompleteBody: 'The tokens in your wallet have been updated.',
swappingTokens: 'Swapping your tokens...',
swappingTokensBody:
'You can exit this screen while you wait. We’ll update your Wallet momentarily.',
Expand Down
1 change: 1 addition & 0 deletions src/navigation/TabBarNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ const TabBarNavigator = () => {
tabBar={(props: BottomTabBarProps) => <MyTabBar {...props} />}
screenOptions={{
headerShown: false,
lazy: true,
}}
sceneContainerStyle={{
paddingBottom: NavBarHeight + bottom,
Expand Down
Loading

0 comments on commit 728f827

Please sign in to comment.