Skip to content

Commit

Permalink
feat(#517): Add option to repair un-onboarded hotspots (#518)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChewingGlass authored Oct 18, 2023
1 parent 61741da commit 2a20fd3
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 19 deletions.
139 changes: 137 additions & 2 deletions src/features/collectables/HotspotDetailsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,42 @@ import BackScreen from '@components/BackScreen'
import BlurActionSheet from '@components/BlurActionSheet'
import Box from '@components/Box'
import ButtonPressable from '@components/ButtonPressable'
import CircleLoader from '@components/CircleLoader'
import { DelayedFadeIn } from '@components/FadeInOut'
import ImageBox from '@components/ImageBox'
import ListItem from '@components/ListItem'
import SafeAreaBox from '@components/SafeAreaBox'
import Text from '@components/Text'
import { toNumber } from '@helium/spl-utils'
import { useOnboarding } from '@helium/react-native-sdk'
import { sendAndConfirmWithRetry, toNumber } from '@helium/spl-utils'
import useCopyText from '@hooks/useCopyText'
import { useEntityKey } from '@hooks/useEntityKey'
import { getExplorerUrl, useExplorer } from '@hooks/useExplorer'
import useHaptic from '@hooks/useHaptic'
import { useHotspotAddress } from '@hooks/useHotspotAddress'
import { useIotInfo } from '@hooks/useIotInfo'
import { useMobileInfo } from '@hooks/useMobileInfo'
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
import { Transaction } from '@solana/web3.js'
import { useSpacing } from '@theme/themeHooks'
import { ellipsizeAddress } from '@utils/accountUtils'
import { Explorer } from '@utils/walletApiV2'
import BN from 'bn.js'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useAsyncCallback } from 'react-async-hook'
import { useTranslation } from 'react-i18next'
import { Linking, ScrollView } from 'react-native'
import { Alert, AlertButton, Linking, ScrollView } from 'react-native'
import { FadeIn } from 'react-native-reanimated'
import { Edge } from 'react-native-safe-area-context'
import { SvgUri } from 'react-native-svg'
import 'text-encoding-polyfill'
import { useSolana } from '../../solana/SolanaProvider'
import { useWalletSign } from '../../solana/WalletSignProvider'
import { WalletStandardMessageTypes } from '../../solana/walletSignBottomSheetTypes'
import { Mints } from '../../utils/constants'
import { removeDashAndCapitalize } from '../../utils/hotspotNftsUtils'
import { ww } from '../../utils/layout'
import { isInsufficientBal } from '../../utils/solanaUtils'
import {
CollectableNavigationProp,
CollectableStackParamList,
Expand All @@ -44,6 +53,9 @@ const HotspotDetailsScreen = () => {
const safeEdges = useMemo(() => ['bottom'] as Edge[], [])
const backEdges = useMemo(() => ['top'] as Edge[], [])
const [optionsOpen, setOptionsOpen] = useState(false)
const { getOnboardTransactions } = useOnboarding()
const { walletSignBottomSheetRef } = useWalletSign()
const { anchorProvider } = useSolana()

const { t } = useTranslation()
const { triggerImpact } = useHaptic()
Expand All @@ -52,6 +64,7 @@ const HotspotDetailsScreen = () => {
const { collectable } = route.params
const entityKey = useEntityKey(collectable)
const iotInfoAcc = useIotInfo(entityKey)
const mobileInfoAcc = useMobileInfo(entityKey)
const streetAddress = useHotspotAddress(collectable)

const pendingIotRewards =
Expand Down Expand Up @@ -147,6 +160,109 @@ const HotspotDetailsScreen = () => {
setSelectExplorerOpen(false)
}, [copyText, collectable, triggerImpact])

const {
execute: handleOnboard,
loading,
error,
} = useAsyncCallback(async () => {
if (!anchorProvider || !entityKey) {
return
}
setOptionsOpen(false)
const hotspotType: 'iot' | 'mobile' | undefined = await new Promise(
(resolve) => {
const options: AlertButton[] = []
if (!iotInfoAcc?.info) {
options.push({
text: 'IOT',
onPress: () => {
resolve('iot')
},
})
}
if (!mobileInfoAcc?.info) {
options.push({
text: 'MOBILE',
onPress: () => {
resolve('mobile')
},
})
}
options.push({
text: t('generic.cancel'),
style: 'destructive',
onPress: () => {
resolve(undefined)
},
})
Alert.alert(
t('collectablesScreen.hotspots.onboard.title'),
t('collectablesScreen.hotspots.onboard.which'),
options,
)
},
)
if (!hotspotType) {
return
}

const { solanaTransactions } = await getOnboardTransactions({
hotspotAddress: entityKey,
networkDetails: [
{
hotspotType,
},
],
})
const serializedTxs = solanaTransactions?.map((txn) =>
Buffer.from(txn, 'base64'),
)

if ((serializedTxs?.length || 0) > 0 && walletSignBottomSheetRef) {
const decision = await walletSignBottomSheetRef.show({
type: WalletStandardMessageTypes.signTransaction,
url: '',
additionalMessage: t('transactions.signAssertLocationTxn'),
serializedTxs,
})

if (!decision) {
throw new Error('User rejected transaction')
}
const signedTxns =
serializedTxs &&
(await anchorProvider.wallet.signAllTransactions(
serializedTxs.map((ser) => Transaction.from(ser)),
))

try {
// eslint-disable-next-line no-restricted-syntax
for (const txn of signedTxns || []) {
// eslint-disable-next-line no-await-in-loop
await sendAndConfirmWithRetry(
anchorProvider.connection,
txn.serialize(),
{
skipPreflight: true,
},
'confirmed',
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (isInsufficientBal(e)) {
throw new Error(
'Manufacturer does not have enough SOL or Data Credits to assert location. Please contact the manufacturer of this hotspot to resolve this issue.',
)
}
if (e.InstructionError) {
throw new Error(`Program Error: ${JSON.stringify(e)}`)
}
throw e
}
}
})

const handleConfirmExplorer = useCallback(
async (selectedExplorer: string) => {
await updateExplorer(selectedExplorer)
Expand Down Expand Up @@ -247,6 +363,13 @@ const HotspotDetailsScreen = () => {
selected={false}
hasPressedState={false}
/>
<ListItem
key="onboard"
title={t('collectablesScreen.hotspots.onboard.title')}
onPress={handleOnboard}
selected={false}
hasPressedState={false}
/>
</>
)
}, [
Expand All @@ -261,6 +384,7 @@ const HotspotDetailsScreen = () => {
available,
explorer?.value,
handleConfirmExplorer,
handleOnboard,
])

return (
Expand Down Expand Up @@ -353,6 +477,11 @@ const HotspotDetailsScreen = () => {
</Box>
</Box>
<Box>
{error && (
<Text mb="s" variant="body2Medium" color="red500">
{error.toString()}
</Text>
)}
<ButtonPressable
height={65}
flexGrow={1}
Expand All @@ -362,10 +491,16 @@ const HotspotDetailsScreen = () => {
borderWidth={2}
borderColor="white"
backgroundColorOpacityPressed={0.7}
disabled={loading}
title={t('collectablesScreen.hotspots.manage')}
titleColor="white"
titleColorPressed="black"
onPress={toggleFiltersOpen(true)}
TrailingComponent={
loading ? (
<CircleLoader loaderSize={20} color="black" />
) : undefined
}
/>
<Box paddingVertical="s" />
<ButtonPressable
Expand Down
23 changes: 20 additions & 3 deletions src/features/collectables/NftMetadataScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,44 @@ import { CollectableStackParamList } from './collectablesTypes'

type Route = RouteProp<CollectableStackParamList, 'NftMetadataScreen'>

function stringify(
s: boolean | string | string[] | undefined,
): string | undefined {
if (Array.isArray(s)) {
if (s.length === 0) {
return 'None'
}
return s.join(', ')
}

return s?.toString()
}

const NftMetadataScreen = () => {
const route = useRoute<Route>()
const { t } = useTranslation()
const { metadata } = route.params

const renderProperty = useCallback(
(traitType: string | undefined, traitValue: string | undefined) => (
(
traitType: string | undefined,
traitValue: boolean | string | string[] | undefined,
) => (
<Box
padding="s"
paddingHorizontal="m"
borderRadius="round"
backgroundColor="transparent10"
margin="s"
key={`${traitType}+${traitValue}`}
key={`${traitType}+${stringify(traitValue)}`}
>
<Text variant="subtitle4" color="grey600">
{traitType?.toUpperCase() ||
t('collectablesScreen.collectables.noTraitType')}
</Text>
<Text variant="body1" color="white" textAlign="center">
{traitValue || t('collectablesScreen.collectables.noTraitValue')}
{stringify(traitValue) ||
t('collectablesScreen.collectables.noTraitValue')}
</Text>
</Box>
),
Expand Down
8 changes: 2 additions & 6 deletions src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useNavigation } from '@react-navigation/native'
import { LAMPORTS_PER_SOL, PublicKey, Transaction } from '@solana/web3.js'
import { useAccountStorage } from '@storage/AccountStorageProvider'
import { DAO_KEY, IOT_SUB_DAO_KEY } from '@utils/constants'
import { getHotspotWithRewards } from '@utils/solanaUtils'
import { getHotspotWithRewards, isInsufficientBal } from '@utils/solanaUtils'
import { Buffer } from 'buffer'
import React from 'react'
import { useAsyncCallback } from 'react-async-hook'
Expand Down Expand Up @@ -102,11 +102,7 @@ const AddGatewayBle = () => {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function wrapProgramError(e: any) {
if (
e.toString().includes('Insufficient Balance') ||
e.toString().includes('"Custom":1') ||
e.InstructionError[1].Custom === 1
) {
if (isInsufficientBal(e)) {
throw new Error(
t('hotspotOnboarding.onboarding.manufacturerMissingDcOrSol', {
name: onboardRecord?.maker.name,
Expand Down
4 changes: 1 addition & 3 deletions src/hooks/useSimulatedTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export function useSimulatedTransaction(
if (
!connection ||
!transaction ||
!anchorProvider ||
!wallet ||
!tokenAccounts ||
hntEstimateLoading
Expand Down Expand Up @@ -337,13 +336,12 @@ export function useSimulatedTransaction(
tokenAccounts,
hasEnoughSol,
hasEnoughHNTForSol,
anchorProvider,
wallet,
hntEstimateLoading,
])

return {
loading: loadingBal || loadingFee,
loading: loadingBal || loadingFee || hntEstimateLoading,
simulationError,
insufficientFunds,
balanceChanges: estimatedBalanceChanges,
Expand Down
6 changes: 1 addition & 5 deletions src/hooks/useSubmitTxn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,7 @@ export default () => {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (
e.toString().includes('Insufficient Balance') ||
e.toString().includes('"Custom":1') ||
e.InstructionError[1].Custom === 1
) {
if (solUtils.isInsufficientBal(e)) {
if (data.isFree) {
throw new Error(
`Manufacturer ${data?.maker?.name} does not have enough SOL or Data Credits to assert location. Please contact the manufacturer of this hotspot to resolve this issue.`,
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ export default {
fifty: '50',
thousand: '1000',
copyEccCompact: 'Copy Hotspot Key',
onboard: {
title: 'Repair Onboarding',
which:
'On rare occasions, onboarding a hotspot can fail. Use this utility to repair the hotspot. Which Sub Network does the hotspot use? If the network is not in this list, your hotspot is properly onboarded.',
},
viewInExplorer: 'View in Explorer',
assertLocation: 'Assert Location',
antennaSetup: 'Antenna Setup',
Expand Down
9 changes: 9 additions & 0 deletions src/utils/solanaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ import { decimalSeparator, groupSeparator } from './i18n'
import * as Logger from './logger'
import sleep from './sleep'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isInsufficientBal(e: any) {
return (
e.toString().includes('Insufficient Balance') ||
e.toString().includes('"Custom":1') ||
e.InstructionError?.[1]?.Custom === 1
)
}

export const isVersionedTransaction = (
tx: Transaction | VersionedTransaction,
): tx is VersionedTransaction => {
Expand Down

0 comments on commit 2a20fd3

Please sign in to comment.