diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index 98515123a..b278339ce 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -1,11 +1,11 @@ -arguments= +arguments=--init-script /var/folders/ck/r25tcygs77n6hv8d515p1w_c0000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/ck/r25tcygs77n6hv8d515p1w_c0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle auto.sync=false build.scans.enabled=false connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home +java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home jvm.arguments= offline.mode=false override.workspace.settings=true diff --git a/ios/HeliumWallet.xcodeproj/project.pbxproj b/ios/HeliumWallet.xcodeproj/project.pbxproj index bae96ed6a..cd15b69e5 100644 --- a/ios/HeliumWallet.xcodeproj/project.pbxproj +++ b/ios/HeliumWallet.xcodeproj/project.pbxproj @@ -997,7 +997,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", @@ -1035,7 +1035,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1203,7 +1203,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -1248,7 +1248,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.helium.wallet.app.OneSignalNotificationServiceExtension; @@ -1296,7 +1296,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -1345,7 +1345,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.helium.wallet.app.HeliumWalletWidget; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b761b4cf7..109a1773f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -841,7 +841,7 @@ SPEC CHECKSUMS: BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e boost: 57d2868c099736d80fcd648bf211b4431e51a558 BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44 - Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 + Charts: 354f86803d11d9c35de280587fef50d1af063978 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXBarCodeScanner: 8e23fae8d267dbef9f04817833a494200f1fce35 diff --git a/package.json b/package.json index 7347870c3..2a84d3419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helium-wallet", - "version": "2.1.1", + "version": "2.2.0", "private": true, "scripts": { "postinstall": "patch-package && ./node_modules/.bin/rn-nodeify --hack --install && npx jetify", @@ -39,24 +39,25 @@ "@coral-xyz/anchor": "0.26.0", "@gorhom/bottom-sheet": "4.4.6", "@gorhom/portal": "1.0.14", - "@helium/account-fetch-cache": "^0.2.5", - "@helium/address": "4.6.2", + "@helium/account-fetch-cache": "^0.2.17", + "@helium/account-fetch-cache-hooks": "^0.2.17", + "@helium/address": "4.10.2", + "@helium/circuit-breaker-sdk": "^0.2.17", "@helium/crypto-react-native": "4.8.0", - "@helium/currency": "4.11.1", "@helium/currency-utils": "0.1.1", - "@helium/data-credits-sdk": "0.1.2", - "@helium/distributor-oracle": "^0.2.6", - "@helium/fanout-sdk": "0.1.2", - "@helium/helium-entity-manager-sdk": "^0.2.6", - "@helium/helium-react-hooks": "0.1.2", - "@helium/helium-sub-daos-sdk": "0.1.2", + "@helium/data-credits-sdk": "^0.2.17", + "@helium/distributor-oracle": "^0.2.17", + "@helium/fanout-sdk": "^0.2.17", + "@helium/helium-entity-manager-sdk": "^0.2.17", + "@helium/helium-react-hooks": "^0.2.17", + "@helium/helium-sub-daos-sdk": "^0.2.17", "@helium/http": "4.7.5", - "@helium/idls": "^0.2.5", - "@helium/lazy-distributor-sdk": "0.1.2", + "@helium/idls": "^0.2.17", + "@helium/lazy-distributor-sdk": "^0.2.17", "@helium/onboarding": "4.9.0", "@helium/proto-ble": "4.0.0", "@helium/react-native-sdk": "1.0.0", - "@helium/spl-utils": "^0.2.6", + "@helium/spl-utils": "^0.2.17", "@helium/transactions": "4.8.1", "@helium/treasury-management-sdk": "0.1.2", "@helium/voter-stake-registry-sdk": "0.1.2", @@ -83,6 +84,7 @@ "@shopify/restyle": "1.8.0", "@solana/spl-account-compression": "0.1.4", "@solana/spl-token": "0.3.6", + "@solana/wallet-adapter-react": "^0.15.33", "@solana/wallet-standard-features": "1.0.0", "@solana/web3.js": "1.64.0", "@tradle/react-native-http": "2.0.1", diff --git a/src/App.tsx b/src/App.tsx index 6d0046446..48a9fc476 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,36 +1,37 @@ -import './polyfill' +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' +import { PortalHost, PortalProvider } from '@gorhom/portal' +import { AccountContext } from '@helium/account-fetch-cache-hooks' +import { DarkTheme, NavigationContainer } from '@react-navigation/native' +import MapboxGL from '@rnmapbox/maps' +import { ThemeProvider } from '@shopify/restyle' +import TokensProvider from '@storage/TokensProvider' +import globalStyles from '@theme/globalStyles' +import { darkThemeColors, lightThemeColors, theme } from '@theme/theme' +import { useColorScheme } from '@theme/themeHooks' +import * as SplashLib from 'expo-splash-screen' import React, { useMemo } from 'react' import { LogBox, Platform } from 'react-native' -import { ThemeProvider } from '@shopify/restyle' -import { DarkTheme, NavigationContainer } from '@react-navigation/native' import useAppState from 'react-native-appstate-hook' -import OneSignal, { OpenedEvent } from 'react-native-onesignal' import Config from 'react-native-config' -import { SafeAreaProvider } from 'react-native-safe-area-context' import { GestureHandlerRootView } from 'react-native-gesture-handler' -import { PortalHost, PortalProvider } from '@gorhom/portal' -import * as SplashLib from 'expo-splash-screen' -import { AccountProvider } from '@helium/helium-react-hooks' -import { theme, darkThemeColors, lightThemeColors } from '@theme/theme' -import { useColorScheme } from '@theme/themeHooks' -import globalStyles from '@theme/globalStyles' -import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' -import MapboxGL from '@rnmapbox/maps' -import useMount from './hooks/useMount' -import RootNavigator from './navigation/RootNavigator' -import { useAccountStorage } from './storage/AccountStorageProvider' -import LockScreen from './features/lock/LockScreen' -import SecurityScreen from './features/security/SecurityScreen' -import OnboardingProvider from './features/onboarding/OnboardingProvider' -import { BalanceProvider } from './utils/Balance' -import { useDeepLinking } from './utils/linking' -import { useNotificationStorage } from './storage/NotificationStorageProvider' +import OneSignal, { OpenedEvent } from 'react-native-onesignal' +import { SafeAreaProvider } from 'react-native-safe-area-context' import NetworkAwareStatusBar from './components/NetworkAwareStatusBar' +import SplashScreen from './components/SplashScreen' import WalletConnectProvider from './features/dappLogin/WalletConnectProvider' +import LockScreen from './features/lock/LockScreen' +import OnboardingProvider from './features/onboarding/OnboardingProvider' +import SecurityScreen from './features/security/SecurityScreen' +import useMount from './hooks/useMount' import { navigationRef } from './navigation/NavigationHelper' -import SplashScreen from './components/SplashScreen' +import RootNavigator from './navigation/RootNavigator' +import './polyfill' import { useSolana } from './solana/SolanaProvider' import WalletSignProvider from './solana/WalletSignProvider' +import { useAccountStorage } from './storage/AccountStorageProvider' +import { useNotificationStorage } from './storage/NotificationStorageProvider' +import { BalanceProvider } from './utils/Balance' +import { useDeepLinking } from './utils/linking' SplashLib.preventAutoHideAsync().catch(() => { /* reloading the app might trigger some race conditions, ignore them */ @@ -55,7 +56,7 @@ const App = () => { const { appState } = useAppState() const { restored: accountsRestored } = useAccountStorage() - const { connection } = useSolana() + const { cache } = useSolana() const { setOpenedNotification } = useNotificationStorage() const linking = useDeepLinking() @@ -112,14 +113,10 @@ const App = () => { - {connection && ( - - - + + {cache && ( + + {accountsRestored && ( <> { ref={navigationRef} > - - - - + + + + + + { /> )} - - - - )} + + + )} + diff --git a/src/assets/images/config.svg b/src/assets/images/config.svg new file mode 100644 index 000000000..92c1b8df0 --- /dev/null +++ b/src/assets/images/config.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/HNTKeyboard.tsx b/src/components/HNTKeyboard.tsx index 2ba8315d5..3d52b6d34 100644 --- a/src/components/HNTKeyboard.tsx +++ b/src/components/HNTKeyboard.tsx @@ -1,55 +1,53 @@ +import PaymentArrow from '@assets/images/paymentArrow.svg' +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetModalProvider, +} from '@gorhom/bottom-sheet' +import { Portal } from '@gorhom/portal' +import { useMint, useOwnedAmount } from '@helium/helium-react-hooks' +import useBackHandler from '@hooks/useBackHandler' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { BoxProps } from '@shopify/restyle' +import { NATIVE_MINT } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' +import { Theme } from '@theme/theme' +import { useOpacity, useSafeTopPaddingStyle } from '@theme/themeHooks' +import BN from 'bn.js' import React, { - forwardRef, - memo, ReactNode, Ref, + forwardRef, + memo, useCallback, useImperativeHandle, useMemo, useRef, useState, } from 'react' -import { - BottomSheetBackdrop, - BottomSheetModal, - BottomSheetModalProvider, -} from '@gorhom/bottom-sheet' -import Balance, { - CurrencyType, - NetworkTokens, - SolTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' import { useTranslation } from 'react-i18next' -import PaymentArrow from '@assets/images/paymentArrow.svg' import { LayoutChangeEvent } from 'react-native' import { Edge } from 'react-native-safe-area-context' -import { BoxProps } from '@shopify/restyle' -import { floor } from 'lodash' -import { Portal } from '@gorhom/portal' -import { useOpacity, useSafeTopPaddingStyle } from '@theme/themeHooks' -import { Theme } from '@theme/theme' -import useBackHandler from '@hooks/useBackHandler' -import Keypad from './Keypad' -import Box from './Box' -import Text from './Text' -import { balanceToString, useBalance } from '../utils/Balance' -import TouchableOpacityBox from './TouchableOpacityBox' -import SafeAreaBox from './SafeAreaBox' +import { Payment } from '../features/payment/PaymentItem' +import { CSAccount } from '../storage/cloudStorage' +import { decimalSeparator, groupSeparator } from '../utils/i18n' +import { humanReadable } from '../utils/solanaUtils' import AccountIcon from './AccountIcon' -import { KeypadInput } from './KeypadButton' -import { decimalSeparator, groupSeparator, locale } from '../utils/i18n' import BackgroundFill from './BackgroundFill' +import Box from './Box' import HandleBasic from './HandleBasic' -import { CSAccount } from '../storage/cloudStorage' -import { Payment } from '../features/payment/PaymentItem' +import Keypad from './Keypad' +import { KeypadInput } from './KeypadButton' +import SafeAreaBox from './SafeAreaBox' +import Text from './Text' +import TouchableOpacityBox from './TouchableOpacityBox' type ShowOptions = { payer?: CSAccount | null payee?: CSAccount | string | null containerHeight?: number - balance?: Balance + balance?: BN index?: number payments?: Payment[] } @@ -60,12 +58,12 @@ export type HNTKeyboardRef = { } type Props = { - ticker: Ticker - networkFee?: Balance + mint?: PublicKey + networkFee?: BN children: ReactNode handleVisible?: (visible: boolean) => void onConfirmBalance: (opts: { - balance: Balance + balance: BN payee?: string index?: number }) => void @@ -77,7 +75,7 @@ const HNTKeyboardSelector = forwardRef( children, onConfirmBalance, handleVisible, - ticker, + mint, networkFee, usePortal = false, ...boxProps @@ -85,8 +83,10 @@ const HNTKeyboardSelector = forwardRef( ref: Ref, ) => { useImperativeHandle(ref, () => ({ show, hide })) + const decimals = useMint(mint)?.info?.decimals const { t } = useTranslation() const bottomSheetModalRef = useRef(null) + const { symbol, loading: loadingMeta } = useMetaplexMetadata(mint) const { backgroundStyle } = useOpacity('surfaceSecondary', 1) const [value, setValue] = useState('0') const [originalValue, setOriginalValue] = useState('') @@ -98,42 +98,9 @@ const HNTKeyboardSelector = forwardRef( const [headerHeight, setHeaderHeight] = useState(0) const containerStyle = useSafeTopPaddingStyle('android') const { handleDismiss, setIsShowing } = useBackHandler(bottomSheetModalRef) + const wallet = useCurrentWallet() - const { - floatToBalance, - hntBalance, - mobileBalance, - iotBalance, - dcBalance, - bonesToBalance, - solBalance, - } = useBalance() - - const getHeliumBalance = useMemo(() => { - switch (ticker) { - case 'HNT': - return hntBalance - case 'SOL': - return solBalance - case 'MOBILE': - return mobileBalance - case 'IOT': - return iotBalance - case 'DC': - return dcBalance - default: - return hntBalance - } - }, [dcBalance, iotBalance, mobileBalance, hntBalance, ticker, solBalance]) - - const isDntToken = useMemo(() => { - return ticker === 'IOT' || ticker === 'MOBILE' - }, [ticker]) - - const balanceForTicker = useMemo( - () => (ticker === 'HNT' ? hntBalance : getHeliumBalance), - [getHeliumBalance, hntBalance, ticker], - ) + const { amount: balanceForMint } = useOwnedAmount(wallet, mint) const snapPoints = useMemo(() => { const sheetHeight = containerHeight - headerHeight @@ -150,31 +117,24 @@ const HNTKeyboardSelector = forwardRef( }, [payee]) const valueAsBalance = useMemo(() => { - const stripped = value - .replaceAll(groupSeparator, '') - .replaceAll(decimalSeparator, '.') - const numberVal = parseFloat(stripped) - - if (ticker === 'DC') { - return new Balance(numberVal, CurrencyType.dataCredit) - } + if (!value || typeof decimals === 'undefined') return undefined + const [whole, dec] = value.split(decimalSeparator) + const decPart = (dec || '').padEnd(decimals, '0').slice(0, decimals) + const fullStr = `${whole.replaceAll(groupSeparator, '')}${decPart}` - return floatToBalance(numberVal, ticker) - }, [floatToBalance, ticker, value]) + return new BN(fullStr) + }, [value, decimals]) const hasMaxDecimals = useMemo(() => { - if (!valueAsBalance) return false + if (!valueAsBalance || typeof decimals === 'undefined') return false const valueString = value .replaceAll(groupSeparator, '') .replaceAll(decimalSeparator, '.') if (!valueString.includes('.')) return false - const [, decimals] = valueString.split('.') - return ( - decimals.length >= - (isDntToken ? 6 : valueAsBalance?.type.decimalPlaces.toNumber()) - ) - }, [value, valueAsBalance, isDntToken]) + const [, dec] = valueString.split('.') + return dec.length >= decimals + }, [value, valueAsBalance, decimals]) const getNextPayments = useCallback(() => { if (payments && paymentIndex !== undefined) { @@ -198,15 +158,16 @@ const HNTKeyboardSelector = forwardRef( setPayments(opts.payments) setContainerHeight(opts.containerHeight || 0) - const val = opts.balance?.floatBalance - .toLocaleString(locale, { maximumFractionDigits: 10 }) - .replaceAll(groupSeparator, '') + const val = + opts.balance && typeof decimals !== 'undefined' + ? humanReadable(opts.balance, decimals) + : undefined setValue(val || '0') bottomSheetModalRef.current?.present() setIsShowing(true) }, - [handleVisible, setIsShowing], + [handleVisible, setIsShowing, decimals], ) const hide = useCallback(() => { @@ -222,55 +183,37 @@ const HNTKeyboardSelector = forwardRef( const [maxEnabled, setMaxEnabled] = useState(false) const handleSetMax = useCallback(() => { - if (!solBalance || !getHeliumBalance || !networkFee) return + if (!valueAsBalance || !networkFee) return const currentAmount = getNextPayments() - .filter((_v, index) => index !== paymentIndex || 0) // Remove the payment being updated - .reduce( - (prev, current) => { - if (!current.amount) { - return prev - } - return prev.plus(current.amount) - }, - ticker === 'DC' - ? new Balance(0, CurrencyType.dataCredit) - : bonesToBalance(0, ticker), - ) - - let maxBalance: Balance | undefined - if (ticker === 'SOL') { - maxBalance = solBalance.minus(currentAmount).minus(networkFee) - } else { - maxBalance = getHeliumBalance.minus(currentAmount) - } - - if (maxBalance.integerBalance < 0 && ticker !== 'DC') { - maxBalance = bonesToBalance(0, ticker) + .filter((_v, index) => index !== (paymentIndex || 0)) // Remove the payment being updated + .reduce((prev, current) => { + if (!current.amount) { + return prev + } + return prev.add(current.amount) + }, new BN(0)) + + let maxBalance: BN | undefined = balanceForMint + ? new BN(balanceForMint.toString()).sub(currentAmount) + : undefined + if (mint?.equals(NATIVE_MINT)) { + maxBalance = networkFee ? maxBalance?.sub(networkFee) : maxBalance } - const decimalPlaces = isDntToken - ? 6 - : maxBalance.type.decimalPlaces.toNumber() - - const val = floor(maxBalance.floatBalance, decimalPlaces) - .toLocaleString(locale, { - maximumFractionDigits: decimalPlaces, - }) - .replaceAll(groupSeparator, '') + const val = humanReadable(maxBalance, decimals) || '0' setValue(maxEnabled ? '0' : val) setMaxEnabled((m) => !m) }, [ - isDntToken, - getHeliumBalance, + valueAsBalance, networkFee, getNextPayments, - bonesToBalance, - ticker, + balanceForMint, + mint, + decimals, maxEnabled, paymentIndex, - solBalance, ]) const BackdropWrapper = useCallback( @@ -303,11 +246,13 @@ const HNTKeyboardSelector = forwardRef( > - - {t('hntKeyboard.enterAmount', { - ticker: valueAsBalance?.type.ticker, - })} - + {!loadingMeta && ( + + {t('hntKeyboard.enterAmount', { + ticker: symbol || '', + })} + + )} - {payer + {payer && balanceForMint && typeof decimals !== 'undefined' ? t('hntKeyboard.hntAvailable', { - amount: balanceToString(balanceForTicker, { - maxDecimalPlaces: 4, - }), + amount: + decimals && + humanReadable( + new BN(balanceForMint.toString()), + decimals, + ), }) : ''} @@ -347,13 +295,15 @@ const HNTKeyboardSelector = forwardRef( ), [ - balanceForTicker, + BackdropWrapper, handleHeaderLayout, - payeeAddress, - payer, + loadingMeta, t, - valueAsBalance, - BackdropWrapper, + symbol, + payer, + payeeAddress, + balanceForMint, + decimals, ], ) @@ -398,27 +348,19 @@ const HNTKeyboardSelector = forwardRef( const hasSufficientBalance = useMemo(() => { if (!payer) return true - if (!networkFee || !valueAsBalance || !hntBalance || !getHeliumBalance) { + if (!networkFee || !valueAsBalance || !balanceForMint) { return false } - if (ticker !== 'HNT') { - return getHeliumBalance.minus(valueAsBalance).integerBalance >= 0 - } - return hntBalance.minus(valueAsBalance).integerBalance >= 0 - }, [ - getHeliumBalance, - hntBalance, - networkFee, - payer, - ticker, - valueAsBalance, - ]) + return new BN(balanceForMint.toString()) + .sub(valueAsBalance) + .gte(new BN(0)) + }, [networkFee, payer, valueAsBalance, balanceForMint]) const handleConfirm = useCallback(() => { bottomSheetModalRef.current?.dismiss() - if (!valueAsBalance) return + if (!valueAsBalance || typeof decimals === 'undefined') return onConfirmBalance({ balance: valueAsBalance, @@ -426,7 +368,7 @@ const HNTKeyboardSelector = forwardRef( index: paymentIndex, }) bottomSheetModalRef.current?.dismiss() - }, [payeeAddress, valueAsBalance, onConfirmBalance, paymentIndex]) + }, [valueAsBalance, decimals, onConfirmBalance, payeeAddress, paymentIndex]) const handleCancel = useCallback(() => { setValue(originalValue) @@ -523,7 +465,7 @@ const HNTKeyboardSelector = forwardRef( numberOfLines={1} adjustsFontSizeToFit > - {`${value || '0'} ${valueAsBalance?.type.ticker}`} + {`${value || '0'} ${symbol || ''}`} {payer && networkFee && ( {t('hntKeyboard.fee', { - value: balanceToString(networkFee, { - maxDecimalPlaces: 4, - }), + value: networkFee && humanReadable(networkFee, 9), })} )} diff --git a/src/components/LedgerBurnModal.tsx b/src/components/LedgerBurnModal.tsx deleted file mode 100644 index b0915ba5c..000000000 --- a/src/components/LedgerBurnModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { - forwardRef, - memo, - ReactNode, - Ref, - useCallback, - useImperativeHandle, - useMemo, - useRef, -} from 'react' -import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' -import { Edge } from 'react-native-safe-area-context' -import { TokenBurnV1 } from '@helium/transactions' -import Ledger from '@assets/images/ledger.svg' -import { useTranslation } from 'react-i18next' -import { useColors, useOpacity } from '@theme/themeHooks' -import useAlert from '@hooks/useAlert' -import useBackHandler from '@hooks/useBackHandler' -import useLedger from '@hooks/useLedger' -import { signLedgerBurn } from '../utils/heliumLedger' -import { LedgerDevice } from '../storage/cloudStorage' -import HandleBasic from './HandleBasic' -import SafeAreaBox from './SafeAreaBox' -import Box from './Box' -import Text from './Text' -import * as Logger from '../utils/logger' - -type ShowOptions = { - ledgerDevice: LedgerDevice - unsignedTxn: TokenBurnV1 - txnJson: string - accountIndex: number -} - -export type LedgerBurnModalRef = { - show: (opts: ShowOptions) => void - hide: () => void -} - -type Props = { - children: ReactNode - onConfirm: (opts: { txn: TokenBurnV1; txnJson: string }) => void - onError: (error: Error) => void - title: string - subtitle: string -} -const LedgerBurnModal = forwardRef( - ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { children, onConfirm, onError, title, subtitle }: Props, - ref: Ref, - ) => { - useImperativeHandle(ref, () => ({ show, hide })) - const bottomSheetModalRef = useRef(null) - const { backgroundStyle } = useOpacity('surfaceSecondary', 1) - const { showOKAlert } = useAlert() - const { t } = useTranslation() - const { primaryText } = useColors() - const { getTransport } = useLedger() - const snapPoints = useMemo(() => { - return [600] - }, []) - const { handleDismiss, setIsShowing } = useBackHandler(bottomSheetModalRef) - - const show = useCallback( - async (opts: ShowOptions) => { - bottomSheetModalRef.current?.present() - setIsShowing(true) - try { - const nextTransport = await getTransport( - opts.ledgerDevice.id, - opts.ledgerDevice.type, - ) - if (!nextTransport) { - showOKAlert({ - title: t('ledger.deviceNotFound.title'), - message: t('addressBook.deviceNotFound.message'), - }) - return - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const payment = await signLedgerBurn( - nextTransport, - opts.unsignedTxn, - opts.accountIndex, - ) - // onConfirm({ txn: payment.txn, txnJson: opts.txnJson }) - bottomSheetModalRef.current?.dismiss() - } catch (error) { - // in this case, user is likely not on Helium app - Logger.error(error) - onError(error as Error) - bottomSheetModalRef.current?.dismiss() - } - }, - [ - getTransport, - // onConfirm, - onError, - setIsShowing, - showOKAlert, - t, - ], - ) - - const hide = useCallback(() => { - bottomSheetModalRef.current?.dismiss() - }, []) - - const renderBackdrop = useCallback( - (props) => ( - - ), - [], - ) - - const renderHandle = useCallback(() => { - return - }, []) - - const safeEdges = useMemo(() => ['bottom'] as Edge[], []) - - return ( - <> - - - - - - - {title} - - - {subtitle} - - - - {children} - - ) - }, -) - -export default memo(LedgerBurnModal) diff --git a/src/components/LedgerPayment.tsx b/src/components/LedgerPayment.tsx deleted file mode 100644 index 5828f523c..000000000 --- a/src/components/LedgerPayment.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { - forwardRef, - memo, - ReactNode, - Ref, - useCallback, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' -import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' -import { useTranslation } from 'react-i18next' -import { Edge } from 'react-native-safe-area-context' -import { PaymentV2 } from '@helium/transactions' -import Ledger from '@assets/images/ledger.svg' -import { Ticker } from '@helium/currency' -import { useColors, useOpacity } from '@theme/themeHooks' -import useAlert from '@hooks/useAlert' -import useBackHandler from '@hooks/useBackHandler' -import useLedger from '@hooks/useLedger' -import SafeAreaBox from './SafeAreaBox' -import HandleBasic from './HandleBasic' -import Text from './Text' -import Box from './Box' -import { LedgerDevice } from '../storage/cloudStorage' -import { SendDetails } from '../utils/linking' - -type ShowOptions = { - payments: SendDetails[] - ledgerDevice: LedgerDevice - address: string - accountIndex: number - speculativeNonce: number -} - -export type LedgerPaymentRef = { - show: (opts: ShowOptions) => void - hide: () => void -} - -type Props = { - children: ReactNode - onConfirm: (opts: { txn: PaymentV2; txnJson: string }) => void - onError: (error: Error) => void - ticker: Ticker -} -const LedgerPaymentSelector = forwardRef( - ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { children, onConfirm, onError, ticker }: Props, - ref: Ref, - ) => { - useImperativeHandle(ref, () => ({ show, hide })) - const { showOKAlert } = useAlert() - const { t } = useTranslation() - const bottomSheetModalRef = useRef(null) - const { backgroundStyle } = useOpacity('surfaceSecondary', 1) - const { primaryText } = useColors() - const [options, setOptions] = useState() - const { getTransport } = useLedger() - const snapPoints = useMemo(() => { - return [600] - }, []) - const { handleDismiss, setIsShowing } = useBackHandler(bottomSheetModalRef) - - const show = useCallback( - async (opts: ShowOptions) => { - setOptions(opts) - bottomSheetModalRef.current?.present() - setIsShowing(true) - try { - const nextTransport = await getTransport( - opts.ledgerDevice.id, - opts.ledgerDevice.type, - ) - if (!nextTransport) { - showOKAlert({ - title: t('ledger.deviceNotFound.title'), - message: t('addressBook.deviceNotFound.message'), - }) - return - } - - // TODO: Implement Solana Ledger Payment - - // const payment = await signLedgerPayment( - // nextTransport, - // unsignedTxn, - // opts.accountIndex, - // ) - // onConfirm({ txn: payment, txnJson }) - bottomSheetModalRef.current?.dismiss() - } catch (error) { - // in this case, user is likely not on Helium app - console.error(error) - onError(error as Error) - bottomSheetModalRef.current?.dismiss() - } - }, - [getTransport, onError, setIsShowing, showOKAlert, t], - ) - - const hide = useCallback(() => { - bottomSheetModalRef.current?.dismiss() - }, []) - - const renderBackdrop = useCallback( - (props) => ( - - ), - [], - ) - - const renderHandle = useCallback(() => { - return - }, []) - - const safeEdges = useMemo(() => ['bottom'] as Edge[], []) - - return ( - <> - - - - - - - {t('ledger.payment.title')} - - - {t('ledger.payment.subtitle', { - name: options?.ledgerDevice.name, - })} - - - - {children} - - ) - }, -) - -export default memo(LedgerPaymentSelector) diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 000000000..bdc2f4710 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -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 & { progress: number }) => { + const HEIGHT = 15 + + const [progressRect, setProgressRect] = useState() + + 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 ( + + + + + + ) +} + +export default ProgressBar diff --git a/src/components/RewardItem.tsx b/src/components/RewardItem.tsx index 012f621aa..3d81c0d0d 100644 --- a/src/components/RewardItem.tsx +++ b/src/components/RewardItem.tsx @@ -1,31 +1,26 @@ -import { Ticker } from '@helium/currency' +import RewardBG from '@assets/images/rewardBg.svg' +import { useMint } from '@helium/helium-react-hooks' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' import { BoxProps } from '@shopify/restyle' +import { PublicKey } from '@solana/web3.js' +import { Theme } from '@theme/theme' +import { humanReadable } from '@utils/solanaUtils' import BN from 'bn.js' import React, { memo, useMemo } from 'react' -import RewardBG from '@assets/images/rewardBg.svg' -import { Theme } from '@theme/theme' -import { IOT_MINT, MOBILE_MINT, toNumber } from '@helium/spl-utils' -import { useMint } from '@helium/helium-react-hooks' -import { formatLargeNumber } from '@utils/accountUtils' -import BigNumber from 'bignumber.js' import Box from './Box' -import TokenIcon from './TokenIcon' import Text from './Text' +import TokenIcon from './TokenIcon' -type RewardItemProps = { ticker: Ticker; amount: BN } & BoxProps - -const RewardItem = ({ ticker, amount, ...rest }: RewardItemProps) => { - const { info: iotMint } = useMint(IOT_MINT) - const { info: mobileMint } = useMint(MOBILE_MINT) +type RewardItemProps = { mint: PublicKey; amount: BN } & BoxProps +const RewardItem = ({ mint, amount, ...rest }: RewardItemProps) => { + const decimals = useMint(mint)?.info?.decimals + const { json, symbol } = useMetaplexMetadata(mint) const pendingRewardsString = useMemo(() => { if (!amount) return - const decimals = - ticker === 'MOBILE' ? mobileMint?.info.decimals : iotMint?.info.decimals - const num = toNumber(amount, decimals || 6) - return formatLargeNumber(new BigNumber(num)) - }, [mobileMint, iotMint, amount, ticker]) + return humanReadable(amount, decimals || 6) + }, [amount, decimals]) return ( { - + { {pendingRewardsString} - {ticker} + {symbol} ) diff --git a/src/components/TokenButton.tsx b/src/components/TokenButton.tsx index c92112d53..a7cec4148 100644 --- a/src/components/TokenButton.tsx +++ b/src/components/TokenButton.tsx @@ -1,39 +1,22 @@ -import React, { memo, useCallback, useMemo } from 'react' import ChevronDown from '@assets/images/chevronDown.svg' -import { Keyboard, StyleSheet } from 'react-native' +import useHaptic from '@hooks/useHaptic' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' import { BoxProps } from '@shopify/restyle' -import TokenSOL from '@assets/images/tokenSOL.svg' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' -import TokenHNT from '@assets/images/tokenHNT.svg' -import { Ticker } from '@helium/currency' -import { useColors, useHitSlop } from '@theme/themeHooks' +import { PublicKey } from '@solana/web3.js' import { Color, Theme } from '@theme/theme' -import useHaptic from '@hooks/useHaptic' +import { useColors, useHitSlop } from '@theme/themeHooks' +import React, { memo, useCallback, useMemo } from 'react' +import { Keyboard, StyleSheet } from 'react-native' import Box from './Box' import Text from './Text' +import TokenIcon from './TokenIcon' import TouchableOpacityBox from './TouchableOpacityBox' -const TokenItem = ({ ticker }: { ticker: Ticker }) => { - const colors = useColors() - const color = useMemo(() => { - return ticker === 'MOBILE' ? 'blueBright500' : 'white' - }, [ticker]) - +const TokenItem = ({ mint }: { mint?: PublicKey }) => { + const { json } = useMetaplexMetadata(mint) return ( - {ticker === 'SOL' && ( - - )} - {ticker === 'HNT' && ( - - )} - - {ticker === 'MOBILE' && ( - - )} - - {ticker === 'IOT' && } + ) } @@ -45,7 +28,7 @@ type Props = { subtitle?: string showBubbleArrow?: boolean innerBoxProps?: BoxProps - ticker: Ticker + mint?: PublicKey } & BoxProps const TokenButton = ({ @@ -55,7 +38,7 @@ const TokenButton = ({ subtitle, showBubbleArrow, innerBoxProps, - ticker, + mint, backgroundColor: backgroundColorProps, ...boxProps }: Props) => { @@ -90,7 +73,7 @@ const TokenButton = ({ paddingVertical={innerBoxProps?.paddingVertical || 'm'} {...innerBoxProps} > - + {title} diff --git a/src/components/TokenIcon.tsx b/src/components/TokenIcon.tsx index b473e0f8b..bb562664e 100644 --- a/src/components/TokenIcon.tsx +++ b/src/components/TokenIcon.tsx @@ -1,66 +1,33 @@ import React from 'react' -import TokenHNT from '@assets/images/tokenHNT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' -import TokenDC from '@assets/images/tokenDC.svg' -import TokenSOL from '@assets/images/tokenSolana.svg' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenSolWhite from '@assets/images/tokenSOL.svg' -import { Ticker } from '@helium/currency' -import { useColors } from '@theme/themeHooks' +import { Image } from 'react-native' import Box from './Box' -import BackgroundFill from './BackgroundFill' type Props = { - ticker: Ticker size?: number - white?: boolean + img?: string } -const TokenIcon = ({ ticker, size = 40, white }: Props) => { - const colors = useColors() - - switch (ticker) { - default: - case 'HNT': - return - case 'MOBILE': - return - case 'IOT': - return - case 'DC': - return - case 'HST': - return - case 'SOL': - if (white) { - return ( - - - - ) - } - return ( - - - - - ) +const TokenIcon = ({ size = 40, img }: Props) => { + if (img) { + return ( + + ) } + + return ( + + ) } export default TokenIcon diff --git a/src/components/TokenSelector.tsx b/src/components/TokenSelector.tsx index 2ba7ba8be..655732bde 100644 --- a/src/components/TokenSelector.tsx +++ b/src/components/TokenSelector.tsx @@ -1,33 +1,55 @@ +import TokenIcon from '@components/TokenIcon' +import { + BottomSheetBackdrop, + BottomSheetFlatList, + BottomSheetModal, + BottomSheetModalProvider, +} from '@gorhom/bottom-sheet' +import useBackHandler from '@hooks/useBackHandler' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { BoxProps } from '@shopify/restyle' +import { PublicKey } from '@solana/web3.js' +import { Theme } from '@theme/theme' +import { useColors, useOpacity } from '@theme/themeHooks' import React, { - forwardRef, - memo, ReactNode, Ref, + forwardRef, + memo, useCallback, useImperativeHandle, useMemo, useRef, } from 'react' -import { - BottomSheetBackdrop, - BottomSheetFlatList, - BottomSheetModal, - BottomSheetModalProvider, -} from '@gorhom/bottom-sheet' -import { BoxProps } from '@shopify/restyle' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Ticker } from '@helium/currency' -import { useColors, useOpacity } from '@theme/themeHooks' -import { Theme } from '@theme/theme' -import useBackHandler from '@hooks/useBackHandler' import Box from './Box' import ListItem, { LIST_ITEM_HEIGHT } from './ListItem' export type TokenListItem = { - label: string - icon: ReactNode - value: Ticker + mint: PublicKey + selected: boolean +} + +const ProvidedListItem = ({ + mint, + onPress, + selected, +}: { + mint: PublicKey + onPress: () => void selected: boolean +}) => { + const { symbol, json } = useMetaplexMetadata(mint) + return ( + : undefined} + onPress={onPress} + selected={selected} + paddingStart="l" + hasDivider + /> + ) } export type TokenSelectorRef = { @@ -35,7 +57,7 @@ export type TokenSelectorRef = { } type Props = { children: ReactNode - onTokenSelected: (type: Ticker) => void + onTokenSelected: (mint: PublicKey) => void tokenData: TokenListItem[] } & BoxProps const TokenSelector = forwardRef( @@ -68,27 +90,25 @@ const TokenSelector = forwardRef( ) const handleTokenPress = useCallback( - (token: string) => () => { + (token: PublicKey) => { bottomSheetModalRef.current?.dismiss() - onTokenSelected(token as Ticker) + onTokenSelected(token) }, [onTokenSelected], ) const keyExtractor = useCallback((item: TokenListItem) => { - return item.value + return item.mint.toBase58() }, []) const renderFlatlistItem = useCallback( ({ item }: { item: TokenListItem; index: number }) => { return ( - handleTokenPress(item.mint)} + mint={item.mint} /> ) }, diff --git a/src/features/account/AccountActionBar.tsx b/src/features/account/AccountActionBar.tsx index 14f1cdae0..032d404f0 100644 --- a/src/features/account/AccountActionBar.tsx +++ b/src/features/account/AccountActionBar.tsx @@ -1,14 +1,14 @@ -import React, { useCallback, useMemo } from 'react' -import { useNavigation } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' -import { LayoutChangeEvent } from 'react-native' -import { Ticker } from '@helium/currency' import Box from '@components/Box' import FabButton from '@components/FabButton' import Text from '@components/Text' +import { useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import React, { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { LayoutChangeEvent } from 'react-native' +import { useAccountStorage } from '../../storage/AccountStorageProvider' import { useAppStorage } from '../../storage/AppStorageProvider' import { HomeNavigationProp } from '../home/homeTypes' -import { useAccountStorage } from '../../storage/AccountStorageProvider' export type Action = | 'send' @@ -21,7 +21,7 @@ export type Action = | 'airdrop' type Props = { - ticker?: Ticker + mint?: PublicKey onLayout?: (event: LayoutChangeEvent) => void compact?: boolean maxCompact?: boolean @@ -43,7 +43,7 @@ const AccountActionBar = ({ hasDelegate, hasSwaps, hasAirdrop, - ticker, + mint, }: Props) => { const navigation = useNavigation() const { t } = useTranslation() @@ -58,7 +58,7 @@ const AccountActionBar = ({ navigation.navigate('ConfirmPin', { action: 'payment' }) } else { navigation.navigate('PaymentScreen', { - defaultTokenType: ticker, + mint: mint?.toBase58(), }) } break @@ -72,7 +72,9 @@ const AccountActionBar = ({ break } case 'airdrop': { - navigation.navigate('AirdropScreen', { ticker: ticker || 'HNT' }) + if (mint) { + navigation.navigate('AirdropScreen', { mint: mint?.toBase58() }) + } break } case '5G': { @@ -93,7 +95,7 @@ const AccountActionBar = ({ } } }, - [navigation, pin, requirePinForPayment, ticker], + [pin?.status, requirePinForPayment, navigation, mint], ) const fabMargin = useMemo(() => { diff --git a/src/features/account/AccountManageTokenListScreen.tsx b/src/features/account/AccountManageTokenListScreen.tsx new file mode 100644 index 000000000..f286f7b47 --- /dev/null +++ b/src/features/account/AccountManageTokenListScreen.tsx @@ -0,0 +1,174 @@ +import Close from '@assets/images/close.svg' +import Box from '@components/Box' +import IconPressedContainer from '@components/IconPressedContainer' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TokenIcon from '@components/TokenIcon' +import TouchableContainer from '@components/TouchableContainer' +import { useOwnedAmount } from '@helium/helium-react-hooks' +import { DC_MINT } from '@helium/spl-utils' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import CheckBox from '@react-native-community/checkbox' +import { useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import { useVisibleTokens } from '@storage/TokensProvider' +import { useColors, useHitSlop } from '@theme/themeHooks' +import { useBalance } from '@utils/Balance' +import { humanReadable } from '@utils/solanaUtils' +import BN from 'bn.js' +import React, { memo, useCallback, useMemo } from 'react' +import { FlatList } from 'react-native-gesture-handler' +import { Edge } from 'react-native-safe-area-context' +import { HomeNavigationProp } from '../home/homeTypes' +import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' +import { getSortValue } from './AccountTokenList' + +const CheckableTokenListItem = ({ + bottomBorder, + mint: token, + checked, + onUpdateTokens, +}: { + bottomBorder: boolean + mint: string + checked: boolean + onUpdateTokens: (_token: PublicKey, _value: boolean) => void +}) => { + const mint = usePublicKey(token) + const wallet = useCurrentWallet() + const { amount, decimals } = useOwnedAmount(wallet, mint) + const { json, symbol } = useMetaplexMetadata(mint) + const balanceToDisplay = useMemo(() => { + return amount && typeof decimals !== 'undefined' + ? humanReadable(new BN(amount.toString()), decimals) + : '' + }, [amount, decimals]) + const colors = useColors() + + return ( + {}} + flexDirection="row" + minHeight={72} + alignItems="center" + paddingHorizontal="m" + paddingVertical="m" + borderBottomColor="primaryBackground" + borderBottomWidth={bottomBorder ? 0 : 1} + disabled + > + + + + + {`${balanceToDisplay} `} + + + {symbol} + + + {symbol && ( + + )} + + + mint && onUpdateTokens(mint, !checked)} + /> + + + ) +} + +const AccountManageTokenListScreen: React.FC = () => { + const navigation = useNavigation() + const { primaryText } = useColors() + const hitSlop = useHitSlop('l') + const { visibleTokens, setVisibleTokens } = useVisibleTokens() + const { tokenAccounts } = useBalance() + const mints = useMemo(() => { + return tokenAccounts + ?.filter( + (ta) => + ta.balance > 0 && (ta.decimals > 0 || ta.mint === DC_MINT.toBase58()), + ) + .map((ta) => ta.mint) + .sort((a, b) => { + return getSortValue(b) - getSortValue(a) + }) + }, [tokenAccounts]) + + const renderItem = useCallback( + // eslint-disable-next-line react/no-unused-prop-types + ({ index, item: token }: { index: number; item: string }) => { + return ( + + ) + }, + [mints?.length, visibleTokens, setVisibleTokens], + ) + + const keyExtractor = useCallback((item: string) => { + return item + }, []) + const safeEdges = useMemo(() => ['top'] as Edge[], []) + + return ( + + + + + + + + + + + + ) +} + +export default memo(AccountManageTokenListScreen) diff --git a/src/features/account/AccountTokenBalance.tsx b/src/features/account/AccountTokenBalance.tsx index 8533c53a9..0ba6ab27f 100644 --- a/src/features/account/AccountTokenBalance.tsx +++ b/src/features/account/AccountTokenBalance.tsx @@ -1,81 +1,97 @@ -import { Ticker } from '@helium/currency' -import { BoxProps } from '@shopify/restyle' -import React, { memo, useMemo } from 'react' +import Box from '@components/Box' import Text from '@components/Text' import TextTransform from '@components/TextTransform' -import Box from '@components/Box' +import { useOwnedAmount, useTokenAccount } from '@helium/helium-react-hooks' +import { DC_MINT } from '@helium/spl-utils' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { BoxProps } from '@shopify/restyle' +import { PublicKey } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' import { Theme } from '@theme/theme' +import { IOT_SUB_DAO_KEY, MOBILE_SUB_DAO_KEY } from '@utils/constants' +import { getEscrowTokenAccount, humanReadable } from '@utils/solanaUtils' +import BN from 'bn.js' +import React, { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useBalance } from '../../utils/Balance' type Props = { - ticker: Ticker + mint: PublicKey textVariant?: 'h0' | 'h1' | 'h2' | 'h2Medium' showTicker?: boolean } & BoxProps +const EscrowDetails = () => { + const { t } = useTranslation() + const { currentAccount } = useAccountStorage() + + const iotEscrow = getEscrowTokenAccount( + currentAccount?.solanaAddress, + IOT_SUB_DAO_KEY, + ) + const mobileEscrow = getEscrowTokenAccount( + currentAccount?.solanaAddress, + MOBILE_SUB_DAO_KEY, + ) + const { info: iotEscrowAcct } = useTokenAccount(iotEscrow) + const { info: mobileEscrowAcct } = useTokenAccount(mobileEscrow) + + return ( + + + {t('accountsScreen.receivedBalance', { + amount: humanReadable( + new BN(iotEscrowAcct?.amount?.toString() || '0').add( + new BN(mobileEscrowAcct?.amount?.toString() || '0'), + ), + 6, + ), + })} + + + ) +} + const AccountTokenBalance = ({ - ticker, + mint, textVariant, showTicker = true, ...boxProps }: Props) => { + const wallet = useCurrentWallet() const { - dcBalance, - mobileBalance, - iotBalance, - solBalance, - hntBalance, - dcEscrowBalance, - } = useBalance() - const { t } = useTranslation() - - const balance = useMemo(() => { - switch (ticker) { - default: - case 'HNT': { - return hntBalance - } - case 'MOBILE': - return mobileBalance - case 'IOT': - return iotBalance - case 'SOL': - return solBalance - case 'DC': - return dcBalance - } - }, [dcBalance, mobileBalance, hntBalance, solBalance, iotBalance, ticker]) + amount: balance, + decimals, + loading: loadingOwned, + } = useOwnedAmount(wallet, mint) + const balanceStr = + typeof decimals !== 'undefined' && balance + ? humanReadable(new BN(balance?.toString() || '0'), decimals) + : undefined + const { symbol } = useMetaplexMetadata(mint) const tokenDetails = useMemo(() => { - if (ticker !== 'DC' || !showTicker) return + if (!mint.equals(DC_MINT) || !showTicker) return - return ( - - - {t('accountsScreen.receivedBalance', { - amount: dcEscrowBalance?.toString(2, { showTicker: false }), - })} - - - ) - }, [ticker, showTicker, t, dcEscrowBalance]) + return + }, [mint, showTicker]) return ( - {!showTicker && ( - - {typeof balance === 'number' - ? balance - : `${balance?.toString(2, { showTicker: false })}`} - - )} + {!showTicker && + (loadingOwned ? ( + + ) : ( + + {balanceStr} + + ))} {showTicker && ( )} diff --git a/src/features/account/AccountTokenCurrencyBalance.tsx b/src/features/account/AccountTokenCurrencyBalance.tsx index 4615fe7fa..a6eeffb21 100644 --- a/src/features/account/AccountTokenCurrencyBalance.tsx +++ b/src/features/account/AccountTokenCurrencyBalance.tsx @@ -1,10 +1,9 @@ import React, { useMemo } from 'react' -import { Ticker } from '@helium/currency' import Text, { TextProps } from '@components/Text' import { useBalance } from '../../utils/Balance' type Props = { - ticker: Ticker | 'ALL' + ticker: string } & TextProps const AccountTokenCurrencyBalance = ({ ticker, ...textProps }: Props) => { diff --git a/src/features/account/AccountTokenList.tsx b/src/features/account/AccountTokenList.tsx index 3265b3f73..c8169c06d 100644 --- a/src/features/account/AccountTokenList.tsx +++ b/src/features/account/AccountTokenList.tsx @@ -1,51 +1,69 @@ -import React, { useCallback, useMemo } from 'react' -import Balance, { AnyCurrencyType, Ticker } from '@helium/currency' -import { times, without } from 'lodash' -import { useSafeAreaInsets } from 'react-native-safe-area-context' +import Config from '@assets/images/config.svg' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import { BottomSheetFlatListProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetScrollable/types' +import { DC_MINT, HNT_MINT, IOT_MINT, MOBILE_MINT } from '@helium/spl-utils' +import { useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import { useVisibleTokens, DEFAULT_TOKENS } from '@storage/TokensProvider' import { useBalance } from '@utils/Balance' +import { times } from 'lodash' +import React, { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { HomeNavigationProp } from '../home/homeTypes' import TokenListItem, { TokenSkeleton } from './TokenListItem' -type Token = { - type: Ticker - balance: Balance - staked: boolean +type Props = { + onLayout?: BottomSheetFlatListProps['onLayout'] } -type Props = { - onLayout?: BottomSheetFlatListProps['onLayout'] +const sortValues: Record = { + [HNT_MINT.toBase58()]: 10, + [IOT_MINT.toBase58()]: 9, + [MOBILE_MINT.toBase58()]: 8, + [DC_MINT.toBase58()]: 7, +} +export function getSortValue(mint: string): number { + return sortValues[mint] || 0 } const AccountTokenList = ({ onLayout }: Props) => { - const { solBalance, hntBalance, mobileBalance, dcBalance, iotBalance } = - useBalance() + const navigation = useNavigation() + const { t } = useTranslation() + const { visibleTokens } = useVisibleTokens() + + const onManageTokenList = useCallback(() => { + navigation.navigate('AccountManageTokenListScreen') + }, [navigation]) + const { tokenAccounts } = useBalance() const { bottom } = useSafeAreaInsets() + const mints = useMemo(() => { + const taMints = tokenAccounts + ?.filter( + (ta) => + visibleTokens.has(ta.mint) && + ta.balance > 0 && + (ta.decimals > 0 || ta.mint === DC_MINT.toBase58()), + ) + .map((ta) => ta.mint) - const bottomSpace = useMemo(() => bottom * 2, [bottom]) + const all = [...new Set([...DEFAULT_TOKENS, ...(taMints || [])])] + .sort((a, b) => { + return getSortValue(b) - getSortValue(a) + }) + .map((mint) => new PublicKey(mint)) - const tokens = useMemo(() => { - const allTokens = [ - hntBalance, - mobileBalance, - iotBalance, - dcBalance, - solBalance, - ] - return without(allTokens, undefined) as Balance[] - }, [dcBalance, hntBalance, iotBalance, mobileBalance, solBalance]) + return all + }, [tokenAccounts, visibleTokens]) - const renderItem = useCallback( - ({ - item: token, - }: { - // eslint-disable-next-line react/no-unused-prop-types - item: Balance - }) => { - return - }, - [], - ) + const bottomSpace = useMemo(() => bottom * 2, [bottom]) + + // eslint-disable-next-line react/no-unused-prop-types + const renderItem = useCallback(({ item }: { item: PublicKey }) => { + return + }, []) const renderEmptyComponent = useCallback(() => { return ( @@ -57,8 +75,24 @@ const AccountTokenList = ({ onLayout }: Props) => { ) }, []) - const keyExtractor = useCallback((item: Balance) => { - return item.type.ticker + const renderFooterComponent = useCallback(() => { + return ( + + + + {t('accountTokenList.manage')} + + + ) + }, [onManageTokenList, t]) + + const keyExtractor = useCallback((mint: PublicKey) => { + return mint.toBase58() }, []) const contentContainerStyle = useMemo( @@ -70,7 +104,8 @@ const AccountTokenList = ({ onLayout }: Props) => { return ( { contentContainerStyle={contentContainerStyle} renderItem={renderItem} ListEmptyComponent={renderEmptyComponent} + ListFooterComponent={renderFooterComponent} keyExtractor={keyExtractor} - onLayout={onLayout} /> ) } diff --git a/src/features/account/AccountTokenScreen.tsx b/src/features/account/AccountTokenScreen.tsx index 76cc2885d..66889159a 100644 --- a/src/features/account/AccountTokenScreen.tsx +++ b/src/features/account/AccountTokenScreen.tsx @@ -1,45 +1,48 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react' -import { RouteProp, useRoute } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' +import ActivityIndicator from '@components/ActivityIndicator' +import { ReAnimatedBox } from '@components/AnimatedBox' +import BackScreen from '@components/BackScreen' +import BlurActionSheet from '@components/BlurActionSheet' +import Box from '@components/Box' +import FadeInOut, { DelayedFadeIn } from '@components/FadeInOut' +import ListItem from '@components/ListItem' +import { NavBarHeight } from '@components/NavBar' +import Text from '@components/Text' +import TokenIcon from '@components/TokenIcon' +import TouchableOpacityBox from '@components/TouchableOpacityBox' import BottomSheet, { BottomSheetFlatList, WINDOW_HEIGHT, } from '@gorhom/bottom-sheet' +import { DC_MINT, HNT_MINT, IOT_MINT, MOBILE_MINT } from '@helium/spl-utils' +import useLayoutHeight from '@hooks/useLayoutHeight' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import { RouteProp, useRoute } from '@react-navigation/native' +import { NATIVE_MINT } from '@solana/spl-token' +import globalStyles from '@theme/globalStyles' +import { useColors } from '@theme/themeHooks' +import React, { useCallback, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Platform, View } from 'react-native' import Animated, { FadeIn, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' -import { Platform, View } from 'react-native' -import { Ticker } from '@helium/currency' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import BackScreen from '@components/BackScreen' -import Box from '@components/Box' -import Text from '@components/Text' -import ListItem from '@components/ListItem' -import TokenIcon from '@components/TokenIcon' -import BlurActionSheet from '@components/BlurActionSheet' -import useLayoutHeight from '@hooks/useLayoutHeight' -import FadeInOut, { DelayedFadeIn } from '@components/FadeInOut' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import ActivityIndicator from '@components/ActivityIndicator' -import { ReAnimatedBox } from '@components/AnimatedBox' -import globalStyles from '@theme/globalStyles' -import { useColors } from '@theme/themeHooks' -import { NavBarHeight } from '@components/NavBar' +import { useSolana } from '../../solana/SolanaProvider' import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { Activity } from '../../types/activity' +import { HomeStackParamList } from '../home/homeTypes' import AccountActionBar from './AccountActionBar' import { FilterType, useActivityFilter } from './AccountActivityFilter' -import TxnListItem from './TxnListItem' +import AccountTokenBalance from './AccountTokenBalance' +import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' import { useTransactionDetail, withTransactionDetail, } from './TransactionDetail' -import { HomeStackParamList } from '../home/homeTypes' -import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' -import AccountTokenBalance from './AccountTokenBalance' -import { Activity } from '../../types/activity' -import { useSolana } from '../../solana/SolanaProvider' +import TxnListItem from './TxnListItem' import useSolanaActivityList from './useSolanaActivityList' const delayedAnimation = FadeIn.delay(300) @@ -69,10 +72,11 @@ const AccountTokenScreen = () => { setOnEndReachedCalledDuringMomentum, ] = useState(true) - const routeTicker = useMemo( - () => route.params.tokenType?.toUpperCase() as Ticker, - [route.params.tokenType], - ) + const mintStr = useMemo(() => route.params.mint, [route.params.mint]) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mint = usePublicKey(mintStr)! + + const { json, symbol } = useMetaplexMetadata(mint) const toggleFiltersOpen = useCallback( (open) => () => { @@ -97,7 +101,8 @@ const AccountTokenScreen = () => { } = useSolanaActivityList({ account: currentAccount, filter: filterState.filter, - ticker: routeTicker, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mint: mint!, }) const handleOnFetchMoreActivity = useCallback(() => { @@ -171,22 +176,23 @@ const AccountTokenScreen = () => { showTxnDetail({ item, accountAddress: currentAccount?.address || '', + mint, }) }, - [currentAccount, showTxnDetail], + [currentAccount, showTxnDetail, mint], ) const hasAirdrop = useMemo(() => { if (cluster === 'devnet') { return ( - routeTicker === 'SOL' || - routeTicker === 'HNT' || - routeTicker === 'IOT' || - routeTicker === 'MOBILE' + mint.equals(NATIVE_MINT) || + mint.equals(HNT_MINT) || + mint.equals(IOT_MINT) || + mint.equals(MOBILE_MINT) ) } return false - }, [routeTicker, cluster]) + }, [mint, cluster]) const renderHeader = useCallback(() => { const filterName = t(`accountsScreen.filterTypes.${filterState.filter}`) @@ -244,6 +250,7 @@ const AccountTokenScreen = () => { }} > { ) }, - [activityData, bottomScreenHeaderHeight, now, showTransactionDetail], + [ + activityData?.length, + bottomScreenHeaderHeight, + mint, + now, + showTransactionDetail, + ], ) const renderFooter = useCallback(() => { @@ -306,7 +319,7 @@ const AccountTokenScreen = () => { const filters = useCallback( () => ( <> - {routeTicker !== 'DC' && ( + {!mint.equals(DC_MINT) && ( <> { /> )} - {routeTicker === 'DC' && ( + {mint.equals(DC_MINT) && ( <> { )} ), - [filterState.filter, routeTicker, setFilter, t], + [filterState.filter, mint, setFilter, t], ) const backgroundComponent = useCallback( @@ -388,7 +401,7 @@ const AccountTokenScreen = () => { hasBottomTitle: true, } - if (routeTicker === 'DC') { + if (mint.equals(DC_MINT)) { options = { hasSend: false, hasRequest: false, @@ -399,14 +412,14 @@ const AccountTokenScreen = () => { } return options - }, [routeTicker]) + }, [mint]) return ( @@ -431,20 +444,22 @@ const AccountTokenScreen = () => { showTicker={false} textVariant="h2Medium" justifyContent="flex-start" - ticker={routeTicker} + mint={mint} flex={1} /> - + {!!symbol && ( + + )} @@ -452,23 +467,25 @@ const AccountTokenScreen = () => { - + - - + + {!!symbol && ( + + )} diff --git a/src/features/account/AccountsScreen.tsx b/src/features/account/AccountsScreen.tsx index b0bf49bab..584622f68 100644 --- a/src/features/account/AccountsScreen.tsx +++ b/src/features/account/AccountsScreen.tsx @@ -1,3 +1,17 @@ +import { ReAnimatedBox } from '@components/AnimatedBox' +import Box from '@components/Box' +import { NavBarHeight } from '@components/NavBar' +import WarningBanner, { BannerType } from '@components/WarningBanner' +import BottomSheet from '@gorhom/bottom-sheet' +import { HNT_MINT } from '@helium/spl-utils' +import useAppear from '@hooks/useAppear' +import useDisappear from '@hooks/useDisappear' +import useHaptic from '@hooks/useHaptic' +import useLayoutHeight from '@hooks/useLayoutHeight' +import useSolanaHealth from '@hooks/useSolanaHealth' +import { useNavigation } from '@react-navigation/native' +import { CSAccount } from '@storage/cloudStorage' +import { useBackgroundStyle, useColors } from '@theme/themeHooks' import React, { memo, useCallback, @@ -6,48 +20,35 @@ import React, { useRef, useState, } from 'react' -import { Platform, View } from 'react-native' -import { useNavigation } from '@react-navigation/native' import { useAsync } from 'react-async-hook' -import SharedGroupPreferences from 'react-native-shared-group-preferences' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import BottomSheet from '@gorhom/bottom-sheet' +import { Platform, View } from 'react-native' import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated' -import Box from '@components/Box' -import useAppear from '@hooks/useAppear' -import useLayoutHeight from '@hooks/useLayoutHeight' -import useDisappear from '@hooks/useDisappear' -import { ReAnimatedBox } from '@components/AnimatedBox' -import { NavBarHeight } from '@components/NavBar' -import useHaptic from '@hooks/useHaptic' -import { useBackgroundStyle, useColors } from '@theme/themeHooks' -import WarningBanner, { BannerType } from '@components/WarningBanner' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import SharedGroupPreferences from 'react-native-shared-group-preferences' import { useSelector } from 'react-redux' -import useSolanaHealth from '@hooks/useSolanaHealth' -import { CSAccount } from '@storage/cloudStorage' +import { RootNavigationProp } from '../../navigation/rootTypes' +import { useSolana } from '../../solana/SolanaProvider' import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { useOnboarding } from '../onboarding/OnboardingProvider' -import { HomeNavigationProp } from '../home/homeTypes' -import { useNotificationStorage } from '../../storage/NotificationStorageProvider' import { useAppStorage } from '../../storage/AppStorageProvider' -import StatusBanner from '../StatusPage/StatusBanner' +import { useNotificationStorage } from '../../storage/NotificationStorageProvider' import { checkSecureAccount } from '../../storage/secureStorage' -import AccountsTopNav from './AccountsTopNav' -import AccountTokenList from './AccountTokenList' -import AccountView from './AccountView' +import { RootState } from '../../store/rootReducer' +import { appSlice } from '../../store/slices/appSlice' +import { useAppDispatch } from '../../store/store' +import { AccountBalance } from '../../types/balance' +import { useBalance } from '../../utils/Balance' +import StatusBanner from '../StatusPage/StatusBanner' +import { HomeNavigationProp } from '../home/homeTypes' +import { useOnboarding } from '../onboarding/OnboardingProvider' import { OnboardingOpt } from '../onboarding/onboardingTypes' +import AccountActionBar from './AccountActionBar' import AccountBalanceChart from './AccountBalanceChart' -import { RootNavigationProp } from '../../navigation/rootTypes' -import { ITEM_HEIGHT } from './TokenListItem' import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' -import AccountActionBar from './AccountActionBar' -import { useAppDispatch } from '../../store/store' -import { appSlice } from '../../store/slices/appSlice' -import { RootState } from '../../store/rootReducer' +import AccountTokenList from './AccountTokenList' +import AccountView from './AccountView' +import AccountsTopNav from './AccountsTopNav' +import { ITEM_HEIGHT } from './TokenListItem' import { withTransactionDetail } from './TransactionDetail' -import { useSolana } from '../../solana/SolanaProvider' -import { useBalance } from '../../utils/Balance' -import { AccountBalance } from '../../types/balance' const AccountsScreen = () => { const widgetGroup = 'group.com.helium.mobile.wallet.widget' @@ -274,7 +275,7 @@ const AccountsScreen = () => { - + ) }, [handleTopHeaderLayout, headerAnimatedStyle]) diff --git a/src/features/account/AirdropScreen.tsx b/src/features/account/AirdropScreen.tsx index 6a06bc9f8..5db0db711 100644 --- a/src/features/account/AirdropScreen.tsx +++ b/src/features/account/AirdropScreen.tsx @@ -1,16 +1,22 @@ +import DripLogo from '@assets/images/dripLogo.svg' +import { ReAnimatedBox } from '@components/AnimatedBox' import BackScreen from '@components/BackScreen' import Box from '@components/Box' import ButtonPressable from '@components/ButtonPressable' -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' -import * as solUtils from '@utils/solanaUtils' -import { useAccountStorage } from '@storage/AccountStorageProvider' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' +import CircleLoader from '@components/CircleLoader' import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' import TokenIcon from '@components/TokenIcon' -import { Edge } from 'react-native-safe-area-context' -import DripLogo from '@assets/images/dripLogo.svg' -import { ReAnimatedBox } from '@components/AnimatedBox' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { NATIVE_MINT } from '@solana/spl-token' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import * as logger from '@utils/logger' +import * as solUtils from '@utils/solanaUtils' +import axios from 'axios' +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { interpolate, runOnJS, @@ -20,12 +26,9 @@ import { withRepeat, withTiming, } from 'react-native-reanimated' -import Text from '@components/Text' -import axios from 'axios' -import * as logger from '@utils/logger' -import CircleLoader from '@components/CircleLoader' -import { HomeNavigationProp, HomeStackParamList } from '../home/homeTypes' +import { Edge } from 'react-native-safe-area-context' import { useSolana } from '../../solana/SolanaProvider' +import { HomeNavigationProp, HomeStackParamList } from '../home/homeTypes' const DROP_HEIGHT = 79 @@ -42,20 +45,22 @@ const AirdropScreen = () => { const [loading, setLoading] = useState(false) const route = useRoute() - const { ticker } = route.params + const { mint: mintStr } = route.params + const mint = usePublicKey(mintStr) + const { symbol, json } = useMetaplexMetadata(mint) const onAirdrop = useCallback(async () => { if (!currentAccount?.solanaAddress || !anchorProvider) return setLoading(true) - if (ticker === 'SOL') { + if (mint?.equals(NATIVE_MINT)) { solUtils.airdrop(anchorProvider, currentAccount?.solanaAddress) setLoading(false) navigation.goBack() } else { try { await axios.get( - `https://faucet.web.test-helium.com/${ticker.toLowerCase()}/${ + `https://faucet.web.test-helium.com/${symbol?.toLowerCase()}/${ currentAccount?.solanaAddress }?amount=2)`, ) @@ -68,7 +73,7 @@ const AirdropScreen = () => { setErrorMessage((error as Error).message) } } - }, [anchorProvider, currentAccount, navigation, ticker]) + }, [anchorProvider, currentAccount?.solanaAddress, mint, navigation, symbol]) const edges = useMemo(() => ['bottom'] as Edge[], []) @@ -163,7 +168,7 @@ const AirdropScreen = () => { marginBottom="l" > - + @@ -184,7 +189,11 @@ const AirdropScreen = () => { titleColorDisabled="black500" titleColor="primary" fontWeight="500" - title={!loading ? t('airdropScreen.airdropTicker', { ticker }) : ''} + title={ + !loading + ? t('airdropScreen.airdropTicker', { ticker: symbol || '' }) + : '' + } disabled={loading} marginVertical="l" marginHorizontal="l" diff --git a/src/features/account/BalancePill.tsx b/src/features/account/BalancePill.tsx deleted file mode 100644 index 3b71dafa3..000000000 --- a/src/features/account/BalancePill.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { NetTypes } from '@helium/address' -import Balance, { - AnyCurrencyType, - DataCredits, - SecurityTokens, - MobileTokens, -} from '@helium/currency' -import React, { memo, useCallback } from 'react' -import DC from '@assets/images/dc.svg' -import MobileIcon from '@assets/images/mobileIcon.svg' -import Helium from '@assets/images/helium.svg' -import Box from '@components/Box' -import Text from '@components/Text' -import { useColors } from '@theme/themeHooks' -import { balanceToString } from '../../utils/Balance' - -type Props = { - netType: NetTypes.NetType - balance?: Balance -} -const BalancePill = ({ netType, balance }: Props) => { - const colors = useColors() - - const getIcon = useCallback(() => { - switch (balance?.type.constructor) { - case DataCredits: - return - case MobileTokens: - return ( - - - - ) - case SecurityTokens: - return ( - - - - ) - default: - return ( - - - - ) - } - }, [balance, colors.blueBright500, colors.purple500]) - - if (!balance || balance.integerBalance <= 0) return null - - return ( - - {getIcon()} - - {balanceToString(balance, { - maxDecimalPlaces: 2, - showTicker: false, - })} - - - ) -} - -export default memo(BalancePill) diff --git a/src/features/account/TokenListItem.tsx b/src/features/account/TokenListItem.tsx index a944bda97..31cacd363 100644 --- a/src/features/account/TokenListItem.tsx +++ b/src/features/account/TokenListItem.tsx @@ -1,40 +1,49 @@ -import Balance, { AnyCurrencyType } from '@helium/currency' -import React, { useCallback, useMemo } from 'react' import Arrow from '@assets/images/listItemRight.svg' -import { useNavigation } from '@react-navigation/native' import Box from '@components/Box' import FadeInOut from '@components/FadeInOut' import Text from '@components/Text' -import TouchableContainer from '@components/TouchableContainer' import TokenIcon from '@components/TokenIcon' +import TouchableContainer from '@components/TouchableContainer' +import { useOwnedAmount } from '@helium/helium-react-hooks' +import { useCurrentWallet } from '@hooks/useCurrentWallet' import useHaptic from '@hooks/useHaptic' -import { balanceToString } from '@utils/Balance' -import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import { humanReadable } from '@utils/solanaUtils' +import BN from 'bn.js' +import React, { useCallback, useMemo } from 'react' import { HomeNavigationProp } from '../home/homeTypes' +import AccountTokenCurrencyBalance from './AccountTokenCurrencyBalance' export const ITEM_HEIGHT = 72 type Props = { - balance: Balance + mint: PublicKey } -const TokenListItem = ({ balance }: Props) => { +const TokenListItem = ({ mint }: Props) => { const navigation = useNavigation() + const wallet = useCurrentWallet() + const { + amount, + decimals, + loading: loadingOwned, + } = useOwnedAmount(wallet, mint) const { triggerImpact } = useHaptic() + const { json, symbol, loading } = useMetaplexMetadata(mint) + const mintStr = mint.toBase58() const handleNavigation = useCallback(() => { triggerImpact('light') navigation.navigate('AccountTokenScreen', { - tokenType: balance.type.ticker, + mint: mintStr, }) - }, [navigation, balance, triggerImpact]) + }, [navigation, mintStr, triggerImpact]) const balanceToDisplay = useMemo(() => { - return ( - balanceToString(balance, { - maxDecimalPlaces: 9, - showTicker: false, - }) || 0 - ) - }, [balance]) + return amount && typeof decimals !== 'undefined' + ? humanReadable(new BN(amount.toString()), decimals) + : '0' + }, [amount, decimals]) return ( @@ -48,29 +57,53 @@ const TokenListItem = ({ balance }: Props) => { borderBottomColor="primaryBackground" borderBottomWidth={1} > - + {loading ? ( + + ) : ( + + )} + - - - {`${balanceToDisplay} `} - - + + + + ) : ( + + + {`${balanceToDisplay} `} + + + {symbol} + + + )} + {symbol && ( + - {balance.type.ticker} - - - + ticker={symbol.toUpperCase()} + /> + )} @@ -78,6 +111,8 @@ const TokenListItem = ({ balance }: Props) => { ) } +export default TokenListItem + export const TokenSkeleton = () => { return ( @@ -104,5 +139,3 @@ export const TokenSkeleton = () => { ) } - -export default TokenListItem diff --git a/src/features/account/TransactionDetail.tsx b/src/features/account/TransactionDetail.tsx index 93b9aa2be..5e8d7c046 100644 --- a/src/features/account/TransactionDetail.tsx +++ b/src/features/account/TransactionDetail.tsx @@ -1,41 +1,38 @@ /* eslint-disable react/no-array-index-key */ +import BlurBox from '@components/BlurBox' +import HandleBasic from '@components/HandleBasic' +import SafeAreaBox from '@components/SafeAreaBox' +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetModalProvider, + BottomSheetScrollView, +} from '@gorhom/bottom-sheet' +import useBackHandler from '@hooks/useBackHandler' +import { PublicKey } from '@solana/web3.js' import React, { - createContext, FC, ReactNode, + createContext, useCallback, useContext, useMemo, useRef, useState, } from 'react' -import { - BottomSheetBackdrop, - BottomSheetModal, - BottomSheetModalProvider, - BottomSheetScrollView, -} from '@gorhom/bottom-sheet' -import { Edge } from 'react-native-safe-area-context' import { useTranslation } from 'react-i18next' -import { groupBy } from 'lodash' -import animalName from 'angry-purple-tiger' import { LayoutChangeEvent } from 'react-native' -import SafeAreaBox from '@components/SafeAreaBox' -import HandleBasic from '@components/HandleBasic' -import useBackHandler from '@hooks/useBackHandler' -import BlurBox from '@components/BlurBox' -import TransactionLineItem from './TransactionLineItem' -import { useTxnDetails } from './useTxn' -import { useBalance } from '../../utils/Balance' +import { Edge } from 'react-native-safe-area-context' import { useCreateExplorerUrl } from '../../constants/urls' -import { ellipsizeAddress } from '../../utils/accountUtils' import { Activity } from '../../types/activity' +import TransactionLineItem from './TransactionLineItem' +import { useTxnDetails } from './useTxn' const initialState = { show: () => undefined, } -type DetailData = { item: Activity; accountAddress: string } +type DetailData = { item: Activity; accountAddress: string; mint: PublicKey } type TransactionDetailSelectorActions = { show: (data: DetailData) => void } @@ -50,8 +47,7 @@ const TransactionDetailSelector = ({ children }: { children: ReactNode }) => { const [contentHeight, setContentHeight] = useState(0) const { handleDismiss, setIsShowing } = useBackHandler(bottomSheetModalRef) - const { intToBalance } = useBalance() - const { item: txn } = detailData || {} + const { item: txn, mint } = detailData || {} const { amount, @@ -59,14 +55,12 @@ const TransactionDetailSelector = ({ children }: { children: ReactNode }) => { color, fee, feePayer, - hotspotName, icon, paymentsReceived, paymentsSent, time, title, - validatorName, - } = useTxnDetails(txn) + } = useTxnDetails(mint, txn) const createExplorerUrl = useCreateExplorerUrl() const snapPoints = useMemo(() => { @@ -117,36 +111,6 @@ const TransactionDetailSelector = ({ children }: { children: ReactNode }) => { const handleComponent = useCallback(() => , []) - const rewards = useMemo(() => { - if (!txn?.rewards?.length || txn.type === 'subnetwork_rewards_v1') { - return null - } - - const grouped = groupBy(txn.rewards, (reward) => { - if (reward.type === 'securities') return reward.type - - return `${reward.gateway}.${reward.type}` - }) - - return Object.keys(grouped).map((key) => { - const group = grouped[key] - const totalAmount = group.reduce((sum, { amount: amt }) => sum + amt, 0) - const balance = intToBalance({ intValue: totalAmount }) - const typeName = t(`transactions.rewardTypes.${group[0].type}`) - let name = '' - if (group[0].gateway) { - name = animalName(group[0].gateway) - } else { - name = typeName - } - return { - name, - amount: balance, - type: typeName, - } - }) - }, [intToBalance, t, txn]) - const handleContentLayout = useCallback((e: LayoutChangeEvent) => { setContentHeight(e.nativeEvent.layout.height) }, []) @@ -175,62 +139,14 @@ const TransactionDetailSelector = ({ children }: { children: ReactNode }) => { icon={icon} /> - {!!hotspotName && ( - - )} - {!!validatorName && ( - - )} - - {!!txn?.buyer && ( - - )} - - {!!txn?.seller && ( - - )} - - {!!txn?.payee && ( - - )} - - {paymentsSent.map(({ amount: amt, payee }, index) => ( + {paymentsSent.map(({ amount: amt }, index) => ( - ))} @@ -240,33 +156,11 @@ const TransactionDetailSelector = ({ children }: { children: ReactNode }) => { - ))} - {rewards?.map((reward, index) => { - return ( - - ) - })} - {!!amountTitle && ( { /> )} - {!!txn?.owner && ( - - )} - - {!!txn?.oldOwner && ( - - )} - - {!!txn?.oldAddress && ( - - )} - - {!!txn?.newOwner && ( - - )} - - {!!txn?.newAddress && ( - - )} - { export const useTransactionDetail = () => useContext(TransactionDetailSelectorContext) -export const withTransactionDetail = (Component: FC) => () => - ( +export const withTransactionDetail = (Component: FC) => () => { + return ( ) +} diff --git a/src/features/account/TxnListItem.tsx b/src/features/account/TxnListItem.tsx index ef81dd23b..fab5acc52 100644 --- a/src/features/account/TxnListItem.tsx +++ b/src/features/account/TxnListItem.tsx @@ -1,19 +1,23 @@ -import React, { memo, useCallback, useMemo } from 'react' import Pending from '@assets/images/pending.svg' import Box from '@components/Box' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' -import useTxn from './useTxn' +import { PublicKey } from '@solana/web3.js' +import React, { memo, useCallback, useMemo } from 'react' import { Activity } from '../../types/activity' +import useTxn from './useTxn' type Props = { + mint: PublicKey item: Activity now: Date isLast: boolean onPress: (item: Activity) => void } -const TxnListItem = ({ item, now, isLast, onPress }: Props) => { - const { listIcon, title, color, time, getAmount } = useTxn(item, { now }) +const TxnListItem = ({ mint, item, now, isLast, onPress }: Props) => { + const { listIcon, title, color, time, getAmount } = useTxn(mint, item, { + now, + }) const amt = useMemo(() => getAmount(), [getAmount]) const handlePress = useCallback(() => { diff --git a/src/features/account/useSolanaActivityList.tsx b/src/features/account/useSolanaActivityList.tsx index 7c00eb5f5..5c74b53ba 100644 --- a/src/features/account/useSolanaActivityList.tsx +++ b/src/features/account/useSolanaActivityList.tsx @@ -1,24 +1,25 @@ -import { Ticker } from '@helium/currency' +import { DC_MINT } from '@helium/spl-utils' +import { PublicKey } from '@solana/web3.js' +import { onLogs, removeAccountChangeListener } from '@utils/solanaUtils' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSelector } from 'react-redux' -import { Mints } from '@utils/constants' -import { onLogs, removeAccountChangeListener } from '@utils/solanaUtils' +import { useSolana } from '../../solana/SolanaProvider' import { CSAccount } from '../../storage/cloudStorage' import { RootState } from '../../store/rootReducer' import { getTxns } from '../../store/slices/solanaSlice' import { useAppDispatch } from '../../store/store' import { FilterType } from './AccountActivityFilter' -import { useSolana } from '../../solana/SolanaProvider' export default ({ account, filter, - ticker, + mint, }: { account?: CSAccount | null filter: FilterType - ticker: Ticker + mint: PublicKey }) => { + const mintStr = mint.toBase58() const [now, setNow] = useState(new Date()) const dispatch = useAppDispatch() const { anchorProvider } = useSolana() @@ -40,8 +41,7 @@ export default ({ dispatch( getTxns({ account, - ticker, - mints: Mints, + mint: mintStr, requestType: 'start_fresh', anchorProvider, }), @@ -54,8 +54,7 @@ export default ({ dispatch( getTxns({ account, - ticker, - mints: Mints, + mint: mintStr, requestType: 'update_head', anchorProvider, }), @@ -67,7 +66,7 @@ export default ({ removeAccountChangeListener(anchorProvider, accountSubscriptionId.current) } accountSubscriptionId.current = subId - }, [account, dispatch, filter, ticker, anchorProvider]) + }, [account, dispatch, filter, mintStr, anchorProvider]) const requestMore = useCallback(() => { if (!account?.address || !anchorProvider) return @@ -75,38 +74,41 @@ export default ({ dispatch( getTxns({ account, - mints: Mints, - ticker, + mint: mintStr, requestType: 'fetch_more', anchorProvider, }), ) - }, [account, dispatch, ticker, anchorProvider]) + }, [account, dispatch, anchorProvider, mintStr]) const data = useMemo(() => { if (!account?.solanaAddress || !solanaActivity.data[account.solanaAddress]) return [] - if (ticker === 'DC' && (filter === 'delegate' || filter === 'mint')) { - return solanaActivity.data[account.solanaAddress][filter][ticker] + if ( + mintStr === DC_MINT.toBase58() && + (filter === 'delegate' || filter === 'mint') + ) { + return solanaActivity.data[account.solanaAddress][filter][mintStr] } if (filter !== 'in' && filter !== 'out' && filter !== 'all') return [] if (filter === 'in' || filter === 'out') { - const payments = solanaActivity.data[account.solanaAddress]?.payment[ - ticker - ]?.filter((txn) => txn.tokenType === ticker) + const payments = + solanaActivity.data[account.solanaAddress]?.payment[mintStr] return payments?.filter((txn) => - filter === 'out' - ? txn.payee === account.solanaAddress - : txn.payee !== account.solanaAddress, + txn.payments?.some((payment) => + payment.mint === mintStr && + payment.owner === account?.solanaAddress && + filter === 'out' + ? payment.amount < 0 + : payment.amount > 0, + ), ) } - return solanaActivity.data[account.solanaAddress][filter][ticker]?.filter( - (txn) => txn.tokenType === ticker, - ) - }, [account, filter, solanaActivity.data, ticker]) + return solanaActivity.data[account.solanaAddress][filter][mintStr] + }, [account?.solanaAddress, solanaActivity.data, mintStr, filter]) const loading = useMemo(() => { return solanaActivity.loading diff --git a/src/features/account/useTxn.tsx b/src/features/account/useTxn.tsx index a8245db1f..57e271949 100644 --- a/src/features/account/useTxn.tsx +++ b/src/features/account/useTxn.tsx @@ -1,4 +1,19 @@ -import React, { useCallback, useMemo, useState } from 'react' +import TxnReceive from '@assets/images/txnReceive.svg' +import TxnSend from '@assets/images/txnSend.svg' +import { useAccounts } from '@helium/account-fetch-cache-hooks' +import { truthy } from '@helium/spl-utils' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { + METADATA_PARSER, + getMetadataId, + useMetaplexMetadata, +} from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import { Mint, unpackMint } from '@solana/spl-token' +import { AccountInfo, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { Color } from '@theme/theme' +import { useColors } from '@theme/themeHooks' +import BN from 'bn.js' import { addMinutes, format, @@ -6,133 +21,82 @@ import { formatDistanceToNow, fromUnixTime, } from 'date-fns' -import { useTranslation } from 'react-i18next' -import Balance, { - AnyCurrencyType, - CurrencyType, - Ticker, -} from '@helium/currency' import { startCase } from 'lodash' -import TxnReceive from '@assets/images/txnReceive.svg' -import TxnSend from '@assets/images/txnSend.svg' +import React, { useCallback, useMemo, useState } from 'react' import { useAsync } from 'react-async-hook' -import animalName from 'angry-purple-tiger' -import { Color } from '@theme/theme' -import { useColors } from '@theme/themeHooks' -import shortLocale from '../../utils/formatDistance' -import { accountCurrencyType, ellipsizeAddress } from '../../utils/accountUtils' -import { balanceToString, useBalance } from '../../utils/Balance' -import { useOnboarding } from '../onboarding/OnboardingProvider' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { TXN_FEE_IN_LAMPORTS } from '../../utils/solanaUtils' +import { useTranslation } from 'react-i18next' import { Activity } from '../../types/activity' +import shortLocale from '../../utils/formatDistance' +import { TXN_FEE_IN_LAMPORTS, humanReadable } from '../../utils/solanaUtils' + +export const MintParser = (pubKey: PublicKey, info: AccountInfo) => { + const data = unpackMint(pubKey, info) -export const TxnTypeKeys = [ - 'rewards_v1', - 'rewards_v2', - 'payment_v1', - 'payment_v2', - 'add_gateway_v1', - 'assert_location_v1', - 'assert_location_v2', - 'transfer_hotspot_v1', - 'transfer_hotspot_v2', - 'token_burn_v1', - 'unstake_validator_v1', - 'stake_validator_v1', - 'transfer_validator_stake_v1', - 'subnetwork_rewards_v1', - 'dc_delegate', - 'dc_mint', -] as const + return data +} + +export const TxnTypeKeys = ['payment_v2'] as const type TxnType = typeof TxnTypeKeys[number] const useTxn = ( + mint?: PublicKey, item?: Activity, dateOpts?: { dateFormat?: string; now?: Date }, ) => { - const { currentNetworkAddress: address } = useAccountStorage() const colors = useColors() - const { bonesToBalance } = useBalance() const { t } = useTranslation() - const { makers } = useOnboarding() - - const ticker = useMemo(() => { - // Get the ticker from the item if it's available - if (item?.payments?.length) { - const firstPaymentTokenType = item.payments[0].tokenType - if (firstPaymentTokenType) { - return accountCurrencyType(address, firstPaymentTokenType).ticker - } - } - return accountCurrencyType(address, undefined).ticker - }, [address, item]) - - const dcBalance = (v: number | undefined | null) => - new Balance(v || 0, CurrencyType.dataCredit) - - const isSending = useMemo(() => { - return item?.payer === address - }, [address, item]) - - const isSelling = useMemo(() => { - if (item?.seller) return item?.seller === address // for transfer_v1 - if (item?.owner) return item?.owner === address // transfer_v2 - return false - }, [address, item]) - - const isHotspotTxn = useMemo( - () => - item?.type === 'assert_location_v1' || - item?.type === 'assert_location_v2' || - item?.type === 'add_gateway_v1' || - item?.type === 'transfer_hotspot_v1' || - item?.type === 'transfer_hotspot_v2', - [item], + const { symbol: ticker } = useMetaplexMetadata( + usePublicKey(item?.payments?.[0]?.mint || undefined), ) - - const isValidatorTxn = useMemo( + const wallet = useCurrentWallet() + const mintKeys = useMemo( () => - item?.type === 'stake_validator_v1' || - item?.type === 'transfer_validator_stake_v1' || - item?.type === 'unstake_validator_v1', - [item], + [...new Set(item?.payments?.map((p) => p.mint))] + .filter(truthy) + .map((k) => new PublicKey(k)), + [item?.payments], ) + const metadataKeys = useMemo( + () => mintKeys.map((m) => getMetadataId(m)), + [mintKeys], + ) + const { accounts: mintAccs } = useAccounts(mintKeys, MintParser) + const { accounts: metadataAccs } = useAccounts( + metadataKeys, + METADATA_PARSER, + true, + ) + const decimalsByMint = useMemo(() => { + return mintAccs?.reduce((acc, curr) => { + if (curr.info) { + acc[curr.publicKey.toBase58()] = (curr.info as Mint).decimals + } + return acc + }, {} as { [key: string]: number }) + }, [mintAccs]) + + const symbolsByMint = useMemo(() => { + return metadataAccs?.reduce((acc, curr, index) => { + if (curr.info && mintKeys[index]) { + acc[mintKeys[index].toBase58()] = curr.info.symbol + } + return acc + }, {} as { [key: string]: string }) + }, [metadataAccs, mintKeys]) - const getHotspotName = useCallback(() => { - if (!isHotspotTxn || !item?.gateway) return '' - return animalName(item.gateway) - }, [isHotspotTxn, item]) - - const getValidatorName = useCallback(() => { - if (!isValidatorTxn || !item?.address) return '' - return animalName(item.address) - }, [isValidatorTxn, item]) + const isSending = useMemo(() => { + return item?.payments?.some( + (p) => + p.owner === wallet?.toBase58() && + p.amount < 0 && + p.mint === mint?.toBase58(), + ) + }, [item?.payments, mint, wallet]) const color = useMemo((): Color => { switch (item?.type as TxnType) { - case 'transfer_hotspot_v1': - case 'transfer_hotspot_v2': - return 'orange500' - case 'payment_v1': case 'payment_v2': return isSending ? 'blueBright500' : 'greenBright500' - case 'add_gateway_v1': - case 'assert_location_v1': - case 'assert_location_v2': - return 'greenBright500' - case 'subnetwork_rewards_v1': - case 'rewards_v1': - case 'rewards_v2': - case 'stake_validator_v1': - case 'transfer_validator_stake_v1': - case 'dc_mint': - return 'greenBright500' - case 'token_burn_v1': - case 'dc_delegate': - return 'orange500' - case 'unstake_validator_v1': - return 'greenBright500' default: return 'primaryText' } @@ -145,21 +109,17 @@ const useTxn = ( if (item?.pending) { switch (item.type as TxnType) { - case 'payment_v1': case 'payment_v2': if (!isSending) return '' return t('transactions.pending.sending') } } switch (item?.type as TxnType) { - case 'add_gateway_v1': - return t('transactions.added') - case 'payment_v1': case 'payment_v2': { if (item?.payments?.length) { - const firstPaymentTokenType = item.payments[0].tokenType + const firstPaymentTokenType = item.payments[0].mint const hasMixedTokenTypes = item.payments.find( - (p) => p.tokenType !== firstPaymentTokenType, + (p) => p.mint !== firstPaymentTokenType, ) if (hasMixedTokenTypes) { return isSending @@ -171,66 +131,18 @@ const useTxn = ( ? t('transactions.sent', { ticker }) : t('transactions.received', { ticker }) } - case 'assert_location_v1': - return t('transactions.location') - case 'assert_location_v2': - return t('transactions.location_v2') - case 'transfer_hotspot_v1': - case 'transfer_hotspot_v2': - return isSelling - ? t('transactions.transferSell') - : t('transactions.transferBuy') - case 'rewards_v1': - case 'rewards_v2': - return t('transactions.mining') - case 'token_burn_v1': - return t('transactions.burnHNT', { ticker }) - case 'stake_validator_v1': - return t('transactions.stakeValidator', { ticker }) - case 'unstake_validator_v1': - return t('transactions.unstakeValidator', { ticker }) - case 'transfer_validator_stake_v1': - return t('transactions.transferValidator') - case 'subnetwork_rewards_v1': - return item?.tokenType === 'IOT' - ? t('transactions.iotRewards') - : t('transactions.mobileRewards') - case 'dc_delegate': - return t('transactions.delegated') - case 'dc_mint': - return t('transactions.received', { ticker: '' }) } - }, [item, t, isSending, ticker, isSelling]) + }, [item, t, isSending, ticker]) const listIcon = useMemo(() => { const iconColor = colors[color] switch (item?.type as TxnType) { - case 'stake_validator_v1': - return - case 'unstake_validator_v1': - return - case 'transfer_validator_stake_v1': - return - case 'payment_v1': case 'payment_v2': return isSending ? ( ) : ( ) - case 'assert_location_v1': - case 'assert_location_v2': - return - case 'rewards_v1': - case 'rewards_v2': - return - case 'token_burn_v1': - case 'dc_delegate': - return - case 'transfer_hotspot_v1': - case 'transfer_hotspot_v2': - case 'add_gateway_v1': - case 'dc_mint': default: return } @@ -239,191 +151,67 @@ const useTxn = ( const isFee = useMemo(() => { // // TODO: Determine if TransferStakeV1 is a fee const type = item?.type as TxnType - if (type === 'payment_v1' || type === 'payment_v2') { + if (type === 'payment_v2') { return isSending } - if ( - type === 'rewards_v1' || - type === 'rewards_v2' || - type === 'unstake_validator_v1' - ) { - return false - } - - if (type === 'transfer_hotspot_v1' || type === 'transfer_hotspot_v2') { - return isSelling - } - return true - }, [isSelling, isSending, item]) + }, [isSending, item]) const formatAmount = useCallback( - (prefix: '-' | '+' | '', amount?: Balance) => { - if (!amount) return '' - - if (amount?.floatBalance === 0) { - return balanceToString(amount) - } - - return `${prefix}${balanceToString(amount, { maxDecimalPlaces: 4 })}` + ( + prefix: '-' | '+' | '', + amount: number | undefined, + m: string | undefined | null, + ) => { + const decimals = m ? decimalsByMint?.[m] : undefined + if (!amount || typeof decimals === 'undefined') return '' + const symbolPart = m ? symbolsByMint?.[m] || '' : '' + + return `${prefix}${humanReadable( + new BN( + Math.abs(amount) + .toFixed(decimals || 0) + .replace('.', ''), + ), + decimals, + )} ${symbolPart}` }, - [], + [decimalsByMint, symbolsByMint], ) const getFee = useCallback(async () => { - return formatAmount( - '-', - new Balance(TXN_FEE_IN_LAMPORTS, CurrencyType.solTokens), - ) - }, [formatAmount]) - - const getFeePayer = useCallback(() => { - const type = item?.type - if ( - !item?.type || - !item.payer || - (type !== 'add_gateway_v1' && - type !== 'assert_location_v1' && - type !== 'assert_location_v2') - ) { - return '' - } - return ( - makers.find(({ address: makerAddress }) => makerAddress === item.payer) - ?.name || ellipsizeAddress(item.payer) - ) - }, [item, makers]) + return `-${TXN_FEE_IN_LAMPORTS / LAMPORTS_PER_SOL}` + }, []) const getAmountTitle = useCallback(async () => { - const feePayer = await getFeePayer() if (!item) return '' switch (item.type as TxnType) { - case 'transfer_hotspot_v1': - return t('transactions.amountToSeller') - case 'assert_location_v1': - case 'assert_location_v2': - case 'add_gateway_v1': - return t('transactions.feePaidBy', { feePayer }) - case 'stake_validator_v1': - return t('transactions.stake') - case 'transfer_validator_stake_v1': - case 'unstake_validator_v1': - return t('transactions.stakeAmount') - case 'token_burn_v1': - case 'subnetwork_rewards_v1': - return t('transactions.amount') - case 'payment_v1': - case 'payment_v2': - case 'rewards_v1': - case 'rewards_v2': { - return t('transactions.totalAmount') - } default: return '' } - }, [getFeePayer, item, t]) + }, [item]) const getAmount = useCallback(() => { if (!item) return '' switch (item.type as TxnType) { - case 'rewards_v1': - case 'rewards_v2': { - const rewardsAmount = - item.rewards?.reduce( - (sum, current) => sum.plus(bonesToBalance(current.amount, 'HNT')), - bonesToBalance(0, 'HNT'), - ) || bonesToBalance(0, 'HNT') - return formatAmount('+', rewardsAmount) - } - case 'subnetwork_rewards_v1': { - const { tokenType } = item - const tick = (tokenType?.toUpperCase() || 'MOBILE') as Ticker - const rewardsAmount = - item.rewards?.reduce((sum, current) => { - if (current.account !== address) return sum - return sum.plus(bonesToBalance(current.amount, tick)) - }, bonesToBalance(0, tick)) || bonesToBalance(0, tick) - return formatAmount('+', rewardsAmount) - } - case 'transfer_hotspot_v1': - return formatAmount( - isSelling ? '+' : '-', - bonesToBalance(item.amountToSeller, 'HNT'), - ) - case 'assert_location_v1': - case 'assert_location_v2': - case 'add_gateway_v1': - return formatAmount('-', dcBalance(item.stakingFee)) - case 'stake_validator_v1': - return formatAmount('-', bonesToBalance(item.stake, 'HNT')) - case 'unstake_validator_v1': - return formatAmount('-', bonesToBalance(item.stakeAmount, 'HNT')) - case 'transfer_validator_stake_v1': - return formatAmount( - item.payer === address ? '-' : '+', - bonesToBalance(item.stakeAmount, 'HNT'), - ) - case 'token_burn_v1': - return formatAmount('-', bonesToBalance(item.amount, 'HNT')) - case 'payment_v1': - return formatAmount('', bonesToBalance(item.amount, 'HNT')) - case 'dc_delegate': - return formatAmount( - '-', - new Balance(Number(item.amount), CurrencyType.dataCredit), - ) - case 'dc_mint': - return formatAmount( - '+', - new Balance(Number(item.amount), CurrencyType.dataCredit), - ) case 'payment_v2': { - if (item.payer === address) { - const paymentTotals = item.payments?.reduce( - (sums, current) => { - const tokenType = (current.tokenType?.toUpperCase() || - 'HNT') as Ticker - return { - ...sums, - [tokenType]: sums[tokenType].plus( - bonesToBalance(current.amount, tokenType), - ), - } - }, - { - HNT: bonesToBalance(0, 'HNT'), - IOT: bonesToBalance(0, 'IOT'), - MOBILE: bonesToBalance(0, 'MOBILE'), - } as Record>, + const payment = item.payments?.find( + (p) => p.mint === mint?.toBase58() && p.owner === wallet?.toBase58(), + ) + if (payment) { + return formatAmount( + payment.amount < 0 ? '-' : '+', + Math.abs(payment.amount), + payment.mint, ) - if (!paymentTotals) return '' - return Object.keys(paymentTotals) - .flatMap((p) => { - const tick = p.toUpperCase() as Ticker - const total = paymentTotals[tick] - if (total.integerBalance === 0) return [] - const amt = formatAmount('', paymentTotals[tick]) - return [amt] - }) - .join(', ') } - - return `+${item.payments - ?.filter((p) => p.payee === address) - .map((p) => - formatAmount( - '', - bonesToBalance(p.amount, p.tokenType?.toUpperCase() as Ticker), - ), - ) - .join(', ')}` } } return '' - }, [item, formatAmount, isSelling, bonesToBalance, address]) + }, [item, formatAmount, mint, wallet]) const time = useMemo(() => { if (!item) return '' @@ -454,34 +242,30 @@ const useTxn = ( }, [dateOpts, item, t]) const getPaymentsReceived = useCallback(async () => { - const payments = item?.payments?.filter(({ payee }) => payee === address) + const payments = item?.payments?.filter( + ({ owner, amount }) => owner === wallet?.toBase58() && amount > 0, + ) if (!payments) return [] const all = payments.map(async (p) => { - const balance = await formatAmount( - '+', - bonesToBalance(p.amount, p.tokenType?.toUpperCase() as Ticker), - ) - return { amount: balance, payee: p.payee, memo: p.memo || '' } + const balance = await formatAmount('+', p.amount, p.mint) + return { amount: balance, owner: p.owner, memo: p.memo || '' } }) return Promise.all(all) - }, [address, formatAmount, item, bonesToBalance]) + }, [formatAmount, item?.payments, wallet]) const getPaymentsSent = useCallback(async () => { - if (item?.payer !== address || !item?.payments) { + if (!item?.payments) { return [] } - const all = item.payments.map( - async ({ amount: amt, payee, memo: paymentMemo, tokenType }) => { - const balance = await formatAmount( - '', - bonesToBalance(amt, tokenType?.toUpperCase() as Ticker), - ) - return { amount: balance, payee, memo: paymentMemo || '' } - }, - ) + const all = item.payments + .filter((p) => p.amount < 0 && p.owner === wallet?.toBase58()) + .map(async ({ amount: amt, owner, memo: paymentMemo, mint: m }) => { + const balance = await formatAmount('', amt, m) + return { amount: balance, owner, memo: paymentMemo || '' } + }) return Promise.all(all) - }, [address, formatAmount, item, bonesToBalance]) + }, [formatAmount, item, wallet]) return { time, @@ -491,20 +275,15 @@ const useTxn = ( title, color, isFee, - getFeePayer, getPaymentsReceived, getPaymentsSent, - isHotspotTxn, - isValidatorTxn, - getHotspotName, - getValidatorName, getAmountTitle, } } type Payment = { amount: string - payee: string + owner: string memo: string } type TxnDetails = { @@ -518,28 +297,19 @@ type TxnDetails = { paymentsSent: Payment[] amount: string amountTitle: string - hotspotName: string - validatorName: string - isValidatorTxn: boolean - isHotspotTxn: boolean } -export const useTxnDetails = (item?: Activity) => { +export const useTxnDetails = (mint?: PublicKey, item?: Activity) => { const { listIcon, title, time, color, - getFeePayer, getFee, getPaymentsReceived, getPaymentsSent, getAmount, - getHotspotName, - getValidatorName, - isHotspotTxn, - isValidatorTxn, getAmountTitle, - } = useTxn(item, { + } = useTxn(mint, item, { dateFormat: 'dd MMMM yyyy HH:MM', }) @@ -553,24 +323,18 @@ export const useTxnDetails = (item?: Activity) => { paymentsSent: [], amount: '', amountTitle: '', - validatorName: '', - hotspotName: '', - isHotspotTxn: false, - isValidatorTxn: false, }) useAsync(async () => { - const feePayer = await getFeePayer() const fee = await getFee() const paymentsReceived = await getPaymentsReceived() const paymentsSent = await getPaymentsSent() const amount = await getAmount() const amountTitle = await getAmountTitle() - const validatorName = await getValidatorName() - const hotspotName = await getHotspotName() setDetails({ - feePayer, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain + feePayer: item?.feePayer!, icon: listIcon, title, time, @@ -580,16 +344,11 @@ export const useTxnDetails = (item?: Activity) => { paymentsSent, amount, amountTitle, - validatorName, - hotspotName, - isHotspotTxn, - isValidatorTxn, }) }, [ color, getAmount, getFee, - getFeePayer, getPaymentsReceived, getPaymentsSent, listIcon, diff --git a/src/features/burn/BurnScreen.tsx b/src/features/burn/BurnScreen.tsx index 71e31060b..543ff8fa9 100644 --- a/src/features/burn/BurnScreen.tsx +++ b/src/features/burn/BurnScreen.tsx @@ -1,66 +1,71 @@ -import React, { - useCallback, - memo as reactMemo, - useMemo, - useEffect, - useState, - useRef, -} from 'react' -import { useTranslation } from 'react-i18next' import Close from '@assets/images/close.svg' import QR from '@assets/images/qr.svg' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import { Platform } from 'react-native' -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import { Edge, useSafeAreaInsets } from 'react-native-safe-area-context' -import Balance, { CurrencyType, Ticker } from '@helium/currency' -import Address, { NetTypes } from '@helium/address' -import { TokenBurnV1 } from '@helium/transactions' -import { useSelector } from 'react-redux' -import Box from '@components/Box' -import Text from '@components/Text' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { useColors, useHitSlop } from '@theme/themeHooks' +import AccountButton from '@components/AccountButton' import AccountSelector, { AccountSelectorRef, } from '@components/AccountSelector' -import AccountButton from '@components/AccountButton' +import Box from '@components/Box' +import SafeAreaBox from '@components/SafeAreaBox' import SubmitButton from '@components/SubmitButton' -import LedgerBurnModal, { - LedgerBurnModalRef, -} from '@components/LedgerBurnModal' -import useAlert from '@hooks/useAlert' +import Text from '@components/Text' +import TokenButton from '@components/TokenButton' import TokenSelector, { TokenListItem, TokenSelectorRef, } from '@components/TokenSelector' -import TokenButton from '@components/TokenButton' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' -import { Mints } from '@utils/constants' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import Address, { NetTypes } from '@helium/address' +import { useOwnedAmount, useSolOwnedAmount } from '@helium/helium-react-hooks' +import { + DC_MINT, + IOT_MINT, + MOBILE_MINT, + humanReadable, +} from '@helium/spl-utils' +import useAlert from '@hooks/useAlert' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import { PublicKey } from '@solana/web3.js' -import SafeAreaBox from '@components/SafeAreaBox' -import { TXN_FEE_IN_SOL } from '../../utils/solanaUtils' +import { useColors, useHitSlop } from '@theme/themeHooks' +import { BN } from 'bn.js' +import React, { + memo as reactMemo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { Platform } from 'react-native' +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' +import { Edge, useSafeAreaInsets } from 'react-native-safe-area-context' +import { useSelector } from 'react-redux' +import AddressBookSelector, { + AddressBookRef, +} from '../../components/AddressBookSelector' +import HNTKeyboard, { HNTKeyboardRef } from '../../components/HNTKeyboard' +import IconPressedContainer from '../../components/IconPressedContainer' +import useSubmitTxn from '../../hooks/useSubmitTxn' +import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { CSAccount } from '../../storage/cloudStorage' import { RootState } from '../../store/rootReducer' -import { HomeNavigationProp, HomeStackParamList } from '../home/homeTypes' +import { useBalance } from '../../utils/Balance' import { accountNetType, ellipsizeAddress, formatAccountAlias, solAddressIsValid, } from '../../utils/accountUtils' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { balanceToString, useBalance } from '../../utils/Balance' -import PaymentSummary from '../payment/PaymentSummary' -import PaymentSubmit from '../payment/PaymentSubmit' -import HNTKeyboard, { HNTKeyboardRef } from '../../components/HNTKeyboard' -import IconPressedContainer from '../../components/IconPressedContainer' +import { TXN_FEE_IN_SOL } from '../../utils/solanaUtils' +import { HomeNavigationProp, HomeStackParamList } from '../home/homeTypes' import PaymentItem from '../payment/PaymentItem' -import AddressBookSelector, { - AddressBookRef, -} from '../../components/AddressBookSelector' -import { CSAccount } from '../../storage/cloudStorage' -import useSubmitTxn from '../../hooks/useSubmitTxn' +import PaymentSubmit from '../payment/PaymentSubmit' +import PaymentSummary from '../payment/PaymentSummary' + +const FEE = new BN(TXN_FEE_IN_SOL) type Route = RouteProp const BurnScreen = () => { @@ -75,35 +80,28 @@ const BurnScreen = () => { const { top } = useSafeAreaInsets() const navigation = useNavigation() const { t } = useTranslation() - const { primaryText, blueBright500 } = useColors() - const ledgerPaymentRef = useRef(null) + const { primaryText } = useColors() const hitSlop = useHitSlop('l') const accountSelectorRef = useRef(null) const { submitDelegateDataCredits } = useSubmitTxn() const addressBookRef = useRef(null) - const { - floatToBalance, - networkTokensToDc, - hntBalance, - solBalance, - dcBalance, - } = useBalance() + const { networkTokensToDc } = useBalance() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const dcBalance = useBN(useOwnedAmount(wallet, DC_MINT).amount) const { showOKAlert } = useAlert() const hntKeyboardRef = useRef(null) - const [dcAmount, setDcAmount] = useState( - new Balance(Number(route.params.amount), CurrencyType.dataCredit), - ) + const [dcAmount, setDcAmount] = useState(new BN(route.params.amount)) const [submitError, setSubmitError] = useState(undefined) const [delegateAddress, setDelegateAddress] = useState(route.params.address) const [hasError, setHasError] = useState(false) const delegatePayment = useSelector( (reduxState: RootState) => reduxState.solana.delegate, ) - const [ticker, setTicker] = useState('MOBILE') + const [mint, setMint] = useState(MOBILE_MINT) + const { symbol } = useMetaplexMetadata(mint) const tokenSelectorRef = useRef(null) - const mint = useMemo(() => new PublicKey(Mints[ticker]), [ticker]) - const { isDelegate } = useMemo(() => route.params, [route.params]) const containerStyle = useMemo( @@ -122,16 +120,12 @@ const BurnScreen = () => { }, [networkType]) const amountBalance = useMemo(() => { - const amount = parseFloat(route.params.amount) + const amount = new BN(route.params.amount) if (dcAmount) return dcAmount - return floatToBalance(amount, 'HNT') - }, [floatToBalance, dcAmount, route.params.amount]) - - const feeAsTokens = useMemo(() => { - return Balance.fromFloat(TXN_FEE_IN_SOL, CurrencyType.solTokens) - }, []) + return amount + }, [dcAmount, route.params.amount]) const amountInDc = useMemo(() => { if (!amountBalance) return @@ -184,7 +178,7 @@ const BurnScreen = () => { if (isDelegate && amountBalance) { await submitDelegateDataCredits( delegateAddress, - amountBalance.integerBalance, + amountBalance.toNumber(), mint, ) } @@ -204,52 +198,27 @@ const BurnScreen = () => { navigation.navigate('PaymentQrScanner') }, [navigation]) - const ledgerPaymentConfirmed = useCallback( - (_opts: { txn: TokenBurnV1; txnJson: string }) => { - console.error('Ledger payment not supported') - }, - [], - ) - - const handleLedgerError = useCallback( - async (error: Error) => { - await showOKAlert({ - title: t('generic.error'), - message: error.toString(), - }) - navigation.goBack() - }, - [navigation, showOKAlert, t], - ) - const insufficientFunds = useMemo(() => { - if (!amountBalance || !feeAsTokens || !dcBalance || !solBalance) - return false - - if (amountBalance.floatBalance > dcBalance.floatBalance) { - return true - } - - if (feeAsTokens.floatBalance > solBalance.floatBalance) { - return true - } + if (!amountBalance || !dcBalance || !solBalance) return false - return hntBalance && hntBalance.floatBalance < feeAsTokens.floatBalance - }, [amountBalance, dcBalance, feeAsTokens, hntBalance, solBalance]) + return amountBalance.gt(dcBalance) || FEE.gt(solBalance) + }, [amountBalance, dcBalance, solBalance]) const errors = useMemo(() => { const errStrings: string[] = [] if (insufficientFunds) { errStrings.push( - t('payment.insufficientFunds', { token: amountBalance?.type.ticker }), + t('payment.insufficientFunds', { + token: dcBalance && amountBalance.gt(dcBalance) ? 'DC' : 'SOL', + }), ) } return errStrings - }, [amountBalance, insufficientFunds, t]) + }, [amountBalance, dcBalance, insufficientFunds, t]) const onConfirmBalance = useCallback((opts) => { - setDcAmount(new Balance(opts.balance.floatBalance, CurrencyType.dataCredit)) + setDcAmount(new BN(opts.balance)) }, []) const handleAddressBookSelected = useCallback( @@ -299,8 +268,8 @@ const BurnScreen = () => { [networkType, isDelegate], ) - const onTickerSelected = useCallback((tick: Ticker) => { - setTicker(tick) + const onMintSelected = useCallback((tick: PublicKey) => { + setMint(tick) }, []) const handleTokenTypeSelected = useCallback(() => { @@ -310,19 +279,15 @@ const BurnScreen = () => { const data = useMemo( (): TokenListItem[] => [ { - label: 'MOBILE', - icon: , - value: 'MOBILE' as Ticker, - selected: ticker === 'MOBILE', + mint: MOBILE_MINT, + selected: mint.equals(MOBILE_MINT), }, { - label: 'IOT', - icon: , - value: 'IOT' as Ticker, - selected: ticker === 'IOT', + mint: IOT_MINT, + selected: mint.equals(IOT_MINT), }, ], - [blueBright500, ticker], + [mint], ) if (!amountBalance) return null @@ -331,13 +296,13 @@ const BurnScreen = () => { @@ -346,231 +311,211 @@ const BurnScreen = () => { onContactSelected={handleContactSelected} hideCurrentAccount > - - - - - - - - - + - {t(isDelegate ? 'delegate.title' : 'burn.title')} - - - - + + - - - 1 - } - address={currentAccount?.address} - onPress={handleShowAccounts} - showBubbleArrow - marginHorizontal="l" - marginBottom="xs" - /> - - + + + + + + + 1} + address={currentAccount?.address} + onPress={handleShowAccounts} + showBubbleArrow + marginHorizontal="l" + marginBottom="xs" + /> + + + + {isDelegate ? ( + { + setDelegateAddress(address) + handleAddressError({ + address, + }) + }} + handleAddressError={handleAddressError} + mint={DC_MINT} + address={delegateAddress} + amount={amountBalance} + hasError={hasError} + hideMemo /> - - {isDelegate ? ( - { - setDelegateAddress(address) - handleAddressError({ - address, - }) - }} - handleAddressError={handleAddressError} - ticker={amountBalance?.type.ticker} - address={delegateAddress} - amount={amountBalance} - hasError={hasError} - hideMemo + ) : ( + <> + - ) : ( - <> - + + + {t('burn.amount')} + + + {amountBalance.toString()} + + + {t('payment.fee', { + value: humanReadable(FEE, 9), + })} + + + + + {t('burn.equivalent')} + + - - {t('burn.amount')} - - - {amountBalance.toString()} - - - {t('payment.fee', { - value: balanceToString(feeAsTokens, { - maxDecimalPlaces: 4, - }), - })} - - - - - - {t('burn.equivalent')} - - - {amountInDc?.toString()} - - - - - - )} - - {submitError && ( - - - {submitError} - - + {amountInDc?.toString()} + + + + + )} - - + {submitError && ( + + + {submitError} + + + )} + + + + - - - - - - + + + diff --git a/src/features/collectables/AntennaSetupScreen.tsx b/src/features/collectables/AntennaSetupScreen.tsx new file mode 100644 index 000000000..cd9911835 --- /dev/null +++ b/src/features/collectables/AntennaSetupScreen.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState, useMemo, useCallback, memo } from 'react' +import BackScreen from '@components/BackScreen' +import { ReAnimatedBox } from '@components/AnimatedBox' +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TextInput from '@components/TextInput' +import useSubmitTxn from '@hooks/useSubmitTxn' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { useTranslation } from 'react-i18next' +import { + KeyboardAvoidingView, + Keyboard, + TouchableWithoutFeedback, +} from 'react-native' +import { useEntityKey } from '@hooks/useEntityKey' +import { useIotInfo } from '@hooks/useIotInfo' +import { Edge } from 'react-native-safe-area-context' +import { DelayedFadeIn } from '@components/FadeInOut' +import { + CollectableNavigationProp, + CollectableStackParamList, +} from './collectablesTypes' +import { parseH3BNLocation } from '../../utils/h3' +import * as Logger from '../../utils/logger' + +const BUTTON_HEIGHT = 65 +type Route = RouteProp +const AntennaSetupScreen = () => { + const { t } = useTranslation() + const nav = useNavigation() + const route = useRoute() + const { collectable } = route.params + const entityKey = useEntityKey(collectable) + const iotInfoAcc = useIotInfo(entityKey) + const safeEdges = useMemo(() => ['bottom'] as Edge[], []) + const backEdges = useMemo(() => ['top'] as Edge[], []) + const [hasSetDefaults, setHasSetDefaults] = useState(false) + const [gain, setGain] = useState() + const [elevation, setElevation] = useState() + const [updating, setUpdating] = useState(false) + const [transactionError, setTransactionError] = useState() + const { submitUpdateEntityInfo } = useSubmitTxn() + + const iotLocation = useMemo(() => { + if (!iotInfoAcc?.info?.location) { + return undefined + } + + return parseH3BNLocation(iotInfoAcc.info.location).reverse() + }, [iotInfoAcc]) + + useEffect(() => { + if (!hasSetDefaults && iotInfoAcc?.info) { + if (iotInfoAcc?.info?.gain) { + setGain(`${iotInfoAcc?.info?.gain / 10}`) + } + + if (iotInfoAcc?.info?.elevation) { + setElevation(`${iotInfoAcc?.info?.elevation}`) + } + + setHasSetDefaults(true) + } + }, [iotInfoAcc, setGain, setElevation, hasSetDefaults, setHasSetDefaults]) + + const handleUpdateElevGain = useCallback(async () => { + if (iotLocation && entityKey) { + setTransactionError(undefined) + setUpdating(true) + try { + await submitUpdateEntityInfo({ + type: 'iot', + entityKey, + lng: iotLocation[0], + lat: iotLocation[1], + elevation, + decimalGain: gain, + }) + nav.push('SettingUpAntennaScreen') + } catch (error) { + setUpdating(false) + Logger.error(error) + setTransactionError((error as Error).message) + } + } + }, [ + iotLocation, + entityKey, + elevation, + gain, + setUpdating, + setTransactionError, + submitUpdateEntityInfo, + nav, + ]) + + const showError = useMemo(() => { + if (transactionError) return transactionError + }, [transactionError]) + + return ( + + + Keyboard.dismiss()}> + + + + + {t('antennaSetupScreen.antennaSetup')} + + + {t('antennaSetupScreen.antennaSetupDescription')} + + + + + + + + + {showError && ( + + {showError} + + )} + + + + ) : undefined + } + /> + + + + + + + ) +} + +export default memo(AntennaSetupScreen) diff --git a/src/features/collectables/AssertLocationScreen.tsx b/src/features/collectables/AssertLocationScreen.tsx index 16b6aabc0..a0ab3ce7e 100644 --- a/src/features/collectables/AssertLocationScreen.tsx +++ b/src/features/collectables/AssertLocationScreen.tsx @@ -30,7 +30,12 @@ import React, { useRef, } from 'react' import { useTranslation } from 'react-i18next' -import { Alert, KeyboardAvoidingView } from 'react-native' +import { + Alert, + KeyboardAvoidingView, + Keyboard, + TouchableWithoutFeedback, +} from 'react-native' import { Config } from 'react-native-config' import { Edge } from 'react-native-safe-area-context' import 'text-encoding-polyfill' @@ -141,14 +146,16 @@ const AssertLocationScreen = () => { ]) useEffect(() => { - if (iotInfoAcc?.info?.gain) { - setGain(`${iotInfoAcc?.info?.gain / 10}`) - } + if (!elevGainVisible) { + if (iotInfoAcc?.info?.gain) { + setGain(`${iotInfoAcc?.info?.gain / 10}`) + } - if (iotInfoAcc?.info?.elevation) { - setElevation(`${iotInfoAcc?.info?.elevation}`) + if (iotInfoAcc?.info?.elevation) { + setElevation(`${iotInfoAcc?.info?.elevation}`) + } } - }, [iotInfoAcc, setGain, setElevation]) + }, [iotInfoAcc, elevGainVisible, setGain, setElevation]) const resetGain = useCallback( () => @@ -551,7 +558,7 @@ const AssertLocationScreen = () => { } TrailingComponent={ asserting ? ( - + ) : undefined } /> @@ -574,78 +581,86 @@ const AssertLocationScreen = () => { edges={backEdges} onClose={hideElevGain} > - - - - - {t('assertLocationScreen.antennaSetup')} - - - {t('assertLocationScreen.antennaSetupDescription')} - - - setGain(val), - multiline: true, - value: gain, - returnKeyType: 'next', - keyboardType: 'decimal-pad', - }} - /> - - Keyboard.dismiss()}> + + + + + {t('assertLocationScreen.antennaSetup')} + + + {t('assertLocationScreen.antennaSetupDescription')} + + + + + setElevation(val), - value: elevation, - keyboardType: 'decimal-pad', - }} + )}`} + textInputProps={{ + placeholder: t( + 'assertLocationScreen.elevationPlaceholder', + ), + onChangeText: setElevation, + value: elevation, + keyboardType: 'decimal-pad', + }} + /> + + + + - - - - - - + + + ) : undefined} diff --git a/src/features/collectables/ClaimAllRewardsScreen.tsx b/src/features/collectables/ClaimAllRewardsScreen.tsx index ba6992240..e398de7d6 100644 --- a/src/features/collectables/ClaimAllRewardsScreen.tsx +++ b/src/features/collectables/ClaimAllRewardsScreen.tsx @@ -6,6 +6,14 @@ 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' @@ -13,9 +21,9 @@ 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 { 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() @@ -23,6 +31,44 @@ const ClaimAllRewardsScreen = () => { const [redeeming, setRedeeming] = useState(false) const [claimError, setClaimError] = useState() 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, @@ -45,6 +91,9 @@ const ClaimAllRewardsScreen = () => { try { setClaimError(undefined) setRedeeming(true) + if (!hasEnoughSol) { + await showHNTConversionAlert() + } const balanceChanges: BalanceChange[] = [] @@ -78,11 +127,13 @@ const ClaimAllRewardsScreen = () => { setRedeeming(false) } }, [ - navigation, + hasEnoughSol, + pendingIotRewards, + pendingMobileRewards, submitClaimAllRewards, hotspotsWithMeta, - pendingMobileRewards, - pendingIotRewards, + navigation, + showHNTConversionAlert, ]) const addAllToAccountDisabled = useMemo(() => { @@ -120,14 +171,14 @@ const ClaimAllRewardsScreen = () => { > {pendingMobileRewards && pendingMobileRewards.gt(new BN(0)) && ( )} {pendingIotRewards && pendingIotRewards.gt(new BN(0)) && ( @@ -161,7 +212,9 @@ const ClaimAllRewardsScreen = () => { titleColor="black" marginHorizontal="l" onPress={onClaimRewards} - disabled={addAllToAccountDisabled || redeeming} + disabled={ + addAllToAccountDisabled || redeeming || hntEstimateLoading + } TrailingComponent={ redeeming ? ( diff --git a/src/features/collectables/ClaimRewardsScreen.tsx b/src/features/collectables/ClaimRewardsScreen.tsx index 5ccd9beb9..a96970061 100644 --- a/src/features/collectables/ClaimRewardsScreen.tsx +++ b/src/features/collectables/ClaimRewardsScreen.tsx @@ -1,20 +1,21 @@ -import React, { useMemo, memo, useState } from 'react' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' -import { Edge } from 'react-native-safe-area-context' -import { PublicKey, Transaction } from '@solana/web3.js' -import BN from 'bn.js' -import CircleLoader from '@components/CircleLoader' import { ReAnimatedBox } from '@components/AnimatedBox' import BackScreen from '@components/BackScreen' import Box from '@components/Box' -import Text from '@components/Text' import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' import { DelayedFadeIn } from '@components/FadeInOut' -import { useHotspot } from '@hooks/useHotspot' import RewardItem from '@components/RewardItem' -import { Mints } from '../../utils/constants' +import Text from '@components/Text' +import { IOT_MINT, MOBILE_MINT } from '@helium/spl-utils' +import { useHotspot } from '@hooks/useHotspot' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { PublicKey, Transaction } from '@solana/web3.js' +import BN from 'bn.js' +import React, { memo, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Edge } from 'react-native-safe-area-context' import useSubmitTxn from '../../hooks/useSubmitTxn' +import { Mints } from '../../utils/constants' import { CollectableNavigationProp, CollectableStackParamList, @@ -134,14 +135,14 @@ const ClaimRewardsScreen = () => { > {!!pendingMobileRewards && pendingMobileRewards.gt(new BN(0)) && ( )} {!!pendingIotRewards && pendingIotRewards.gt(new BN(0)) && ( diff --git a/src/features/collectables/ClaimingRewardsScreen.tsx b/src/features/collectables/ClaimingRewardsScreen.tsx index 969793d1e..af7222bc0 100644 --- a/src/features/collectables/ClaimingRewardsScreen.tsx +++ b/src/features/collectables/ClaimingRewardsScreen.tsx @@ -1,32 +1,36 @@ -import React, { memo, useCallback } from 'react' -import { useNavigation } from '@react-navigation/native' -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' -import 'text-encoding-polyfill' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import BackArrow from '@assets/images/backArrow.svg' -import IndeterminateProgressBar from '@components/IndeterminateProgressBar' -import { DelayedFadeIn } from '@components/FadeInOut' +import AccountIcon from '@components/AccountIcon' +import { ReAnimatedBox } from '@components/AnimatedBox' 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 { ReAnimatedBox } from '@components/AnimatedBox' -import AccountIcon from '@components/AccountIcon' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useNavigation } from '@react-navigation/native' +import { Transaction } from '@solana/web3.js' +import sendMail from '@utils/sendMail' import { parseTransactionError } from '@utils/solanaUtils' -import { useBalance } from '@utils/Balance' +import React, { memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import sendMail from '@utils/sendMail' import RNTestFlight from 'react-native-test-flight' -import { Transaction } from '@solana/web3.js' -import { RootState } from '../../store/rootReducer' -import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { useSelector } from 'react-redux' +import 'text-encoding-polyfill' import { TabBarNavigationProp } from '../../navigation/rootTypes' import { useSolana } from '../../solana/SolanaProvider' +import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { RootState } from '../../store/rootReducer' const ClaimingRewardsScreen = () => { const { currentAccount } = useAccountStorage() const navigation = useNavigation() - const { solBalance } = useBalance() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) const { bottom } = useSafeAreaInsets() const { cluster, anchorProvider } = useSolana() @@ -188,7 +192,27 @@ const ClaimingRewardsScreen = () => { {t('collectablesScreen.claimingRewardsBody')} - + {typeof solanaPayment.progress !== 'undefined' ? ( + + + + {solanaPayment.progress.text} + + + ) : ( + + )} )} diff --git a/src/features/collectables/CollectablesNavigator.tsx b/src/features/collectables/CollectablesNavigator.tsx index 5d403de6d..104becc5c 100644 --- a/src/features/collectables/CollectablesNavigator.tsx +++ b/src/features/collectables/CollectablesNavigator.tsx @@ -19,6 +19,8 @@ import ClaimAllRewardsScreen from './ClaimAllRewardsScreen' import ClaimingRewardsScreen from './ClaimingRewardsScreen' import CollectionScreen from './CollectionScreen' import NftDetailsScreen from './NftDetailsScreen' +import AntennaSetupScreen from './AntennaSetupScreen' +import SettingUpAntennaScreen from './SettingUpAntennaScreen' const CollectablesStack = createStackNavigator() @@ -41,6 +43,14 @@ const CollectablesStackScreen = () => { name="AssertLocationScreen" component={AssertLocationScreen} /> + + { const screenOpts = useCallback( ({ route }: { route: RouteProp }) => ({ + lazy: true, headerShown: false, tabBarLabelStyle: { fontFamily: Font.medium, diff --git a/src/features/collectables/HotspotCompressedListItem.tsx b/src/features/collectables/HotspotCompressedListItem.tsx index 7bdd3bc17..5c2f7eade 100644 --- a/src/features/collectables/HotspotCompressedListItem.tsx +++ b/src/features/collectables/HotspotCompressedListItem.tsx @@ -45,7 +45,7 @@ const HotspotListItem = ({ if (!hotspot.pendingRewards) return const num = toNumber( new BN(hotspot.pendingRewards[Mints.IOT]), - iotMint?.info.decimals || 6, + iotMint?.decimals || 6, ) return formatLargeNumber(new BigNumber(num)) }, [hotspot, iotMint]) @@ -60,7 +60,7 @@ const HotspotListItem = ({ if (!hotspot.pendingRewards) return const num = toNumber( new BN(hotspot.pendingRewards[Mints.MOBILE]), - mobileMint?.info.decimals || 6, + mobileMint?.decimals || 6, ) return formatLargeNumber(new BigNumber(num)) }, [hotspot, mobileMint]) diff --git a/src/features/collectables/HotspotDetailsScreen.tsx b/src/features/collectables/HotspotDetailsScreen.tsx index 341455a5d..135952772 100644 --- a/src/features/collectables/HotspotDetailsScreen.tsx +++ b/src/features/collectables/HotspotDetailsScreen.tsx @@ -21,6 +21,8 @@ import useHaptic from '@hooks/useHaptic' import useCopyText from '@hooks/useCopyText' import { ellipsizeAddress } from '@utils/accountUtils' import { toNumber } from '@helium/spl-utils' +import { useEntityKey } from '@hooks/useEntityKey' +import { useIotInfo } from '@hooks/useIotInfo' import { ww } from '../../utils/layout' import { CollectableNavigationProp, @@ -43,6 +45,9 @@ const HotspotDetailsScreen = () => { const copyText = useCopyText() const { collectable } = route.params + const entityKey = useEntityKey(collectable) + const iotInfoAcc = useIotInfo(entityKey) + const pendingIotRewards = collectable && collectable.pendingRewards && @@ -75,6 +80,13 @@ const HotspotDetailsScreen = () => { }) }, [collectable, navigation]) + const handleAntennaSetup = useCallback(() => { + setOptionsOpen(false) + navigation.navigate('AntennaSetupScreen', { + collectable, + }) + }, [collectable, navigation]) + const handleClaimRewards = useCallback(() => { navigation.navigate('ClaimRewardsScreen', { hotspot: collectable, @@ -123,6 +135,15 @@ const HotspotDetailsScreen = () => { selected={false} hasPressedState={false} /> + {iotInfoAcc?.info?.location && ( + + )} { /> ), - [handleSend, handleAssertLocation, handleCopyAddress, t], + [ + handleSend, + handleAssertLocation, + handleAntennaSetup, + handleCopyAddress, + iotInfoAcc, + t, + ], ) return ( diff --git a/src/features/collectables/HotspotList.tsx b/src/features/collectables/HotspotList.tsx index 0296e127b..a08c9c5ce 100644 --- a/src/features/collectables/HotspotList.tsx +++ b/src/features/collectables/HotspotList.tsx @@ -1,36 +1,91 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { times } from 'lodash' -import { FlatList } from 'react-native-gesture-handler' -import { RefreshControl } from 'react-native' -import { useIsFocused, useNavigation } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' -import BN from 'bn.js' -import listViewIcon from '@assets/images/listViewIcon.svg' import expandedViewIcon from '@assets/images/expandedViewIcon.svg' -import ListItem from '@components/ListItem' +import listViewIcon from '@assets/images/listViewIcon.svg' import BlurActionSheet from '@components/BlurActionSheet' -import { useColors } from '@theme/themeHooks' import Box from '@components/Box' import ButtonPressable from '@components/ButtonPressable' -import useHotspots from '@hooks/useHotspots' import CircleLoader from '@components/CircleLoader' -import useHaptic from '@hooks/useHaptic' +import ListItem from '@components/ListItem' +import TabBar from '@components/TabBar' import Text from '@components/Text' import TokenIcon from '@components/TokenIcon' -import TabBar from '@components/TabBar' import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { IOT_MINT, MOBILE_MINT, toNumber } from '@helium/spl-utils' import { useMint } from '@helium/helium-react-hooks' +import { IOT_MINT, MOBILE_MINT, toNumber } from '@helium/spl-utils' +import useHaptic from '@hooks/useHaptic' +import useHotspots from '@hooks/useHotspots' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { useIsFocused, useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import { useColors } from '@theme/themeHooks' import BigNumber from 'bignumber.js' +import BN from 'bn.js' +import { times } from 'lodash' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RefreshControl } from 'react-native' +import { FlatList } from 'react-native-gesture-handler' +import { CompressedNFT, HotspotWithPendingRewards } from '../../types/solana' import { formatLargeNumber } from '../../utils/accountUtils' import HotspotCompressedListItem from './HotspotCompressedListItem' import HotspotListItem from './HotspotListItem' -import { CollectableNavigationProp } from './collectablesTypes' -import { CompressedNFT, HotspotWithPendingRewards } from '../../types/solana' import { NFTSkeleton } from './NftListItem' +import { CollectableNavigationProp } from './collectablesTypes' export const DEFAULT_PAGE_AMOUNT = 20 +function RewardItem({ + mint, + amount, + marginStart, + marginEnd, +}: { + mint: PublicKey + amount: BN | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + marginStart?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + marginEnd?: any +}) { + const decimals = useMint(mint)?.info?.decimals + const { json, symbol } = useMetaplexMetadata(mint) + let realAmount = '' + if (amount) { + const num = toNumber(amount, decimals || 6) + realAmount = formatLargeNumber(new BigNumber(num)) + } + + return ( + + + + + + {realAmount} + + + {symbol} + + + + ) +} + const HotspotList = () => { const navigation = useNavigation() const { t } = useTranslation() @@ -42,9 +97,6 @@ const HotspotList = () => { DEFAULT_PAGE_AMOUNT, ) - const { info: iotMint } = useMint(IOT_MINT) - const { info: mobileMint } = useMint(MOBILE_MINT) - const tabBarOptions = useMemo( () => [ { @@ -131,12 +183,12 @@ const HotspotList = () => { hasPressedState={false} /> @@ -145,49 +197,6 @@ const HotspotList = () => { [handleSetPageAmount, pageAmount, t], ) - const RewardItem = useCallback( - ({ ticker, amount, ...rest }) => { - const decimals = - ticker === 'IOT' ? iotMint?.info.decimals : mobileMint?.info.decimals - let realAmount = '' - if (amount) { - const num = toNumber(amount, decimals || 6) - realAmount = formatLargeNumber(new BigNumber(num)) - } - - return ( - - - - - - {realAmount} - - - {ticker} - - - - ) - }, - [iotMint, mobileMint], - ) - const onTabSelected = useCallback( (value) => { setTabSelected(value) @@ -227,11 +236,15 @@ const HotspotList = () => { - + {pageAmount && hotspotsWithMeta?.length >= pageAmount && ( { handleNavigateToClaimRewards, pendingIotRewards, pendingMobileRewards, - RewardItem, t, onTabSelected, tabSelected, diff --git a/src/features/collectables/HotspotListItem.tsx b/src/features/collectables/HotspotListItem.tsx index 354dd1f68..959dc21a3 100644 --- a/src/features/collectables/HotspotListItem.tsx +++ b/src/features/collectables/HotspotListItem.tsx @@ -45,7 +45,7 @@ const HotspotListItem = ({ if (!hotspot.pendingRewards) return const num = toNumber( new BN(hotspot.pendingRewards[Mints.IOT]), - iotMint?.info.decimals || 6, + iotMint?.decimals || 6, ) return formatLargeNumber(new BigNumber(num)) }, [iotMint, hotspot]) @@ -60,7 +60,7 @@ const HotspotListItem = ({ if (!hotspot.pendingRewards) return const num = toNumber( new BN(hotspot.pendingRewards[Mints.MOBILE]), - mobileMint?.info.decimals || 6, + mobileMint?.decimals || 6, ) return formatLargeNumber(new BigNumber(num)) }, [hotspot, mobileMint]) diff --git a/src/features/collectables/SettingUpAntennaScreen.tsx b/src/features/collectables/SettingUpAntennaScreen.tsx new file mode 100644 index 000000000..b43693ee2 --- /dev/null +++ b/src/features/collectables/SettingUpAntennaScreen.tsx @@ -0,0 +1,196 @@ +import React, { memo, useCallback } from 'react' +import Box from '@components/Box' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { useNavigation } from '@react-navigation/native' +import { TabBarNavigationProp } from 'src/navigation/rootTypes' +import { ReAnimatedBox } from '@components/AnimatedBox' +import { DelayedFadeIn } from '@components/FadeInOut' +import BackArrow from '@assets/images/backArrow.svg' +import AccountIcon from '@components/AccountIcon' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import Text from '@components/Text' +import ButtonPressable from '@components/ButtonPressable' +import IndeterminateProgressBar from '@components/IndeterminateProgressBar' +import { parseTransactionError } from '@utils/solanaUtils' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { RootState } from '../../store/rootReducer' + +const SettingUpAntennaScreen = () => { + const { currentAccount } = useAccountStorage() + const navigation = useNavigation() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const { bottom } = useSafeAreaInsets() + + const { t } = useTranslation() + const solanaPayment = useSelector( + (reduxState: RootState) => reduxState.solana.payment, + ) + + const onReturn = useCallback(() => { + // Reset Collectables stack to first screen + navigation.reset({ + index: 0, + routes: [{ name: 'Collectables' }], + }) + }, [navigation]) + + if (!currentAccount) { + return null + } + + return ( + + + + + + + {solanaPayment && !solanaPayment.error && !solanaPayment.loading && ( + + + {t('antennaSetupScreen.settingUpComplete')} + + + {t('antennaSetupScreen.settingUpCompleteBody')} + + + )} + + {solanaPayment?.error && ( + + + {t('collectablesScreen.rewardsError')} + + + {parseTransactionError( + solBalance, + solanaPayment?.error?.message, + )} + + + )} + + {!solanaPayment && ( + + + {t('antennaSetupScreen.settingUpError')} + + + )} + + {solanaPayment && solanaPayment.loading && ( + + + {t('antennaSetupScreen.settingUp')} + + + {t('antennaSetupScreen.settingUpBody')} + + + + + + )} + + + + } + /> + + + + ) +} + +export default memo(SettingUpAntennaScreen) diff --git a/src/features/collectables/TransferCompleteScreen.tsx b/src/features/collectables/TransferCompleteScreen.tsx index 3411ea2d7..ca5afe8ac 100644 --- a/src/features/collectables/TransferCompleteScreen.tsx +++ b/src/features/collectables/TransferCompleteScreen.tsx @@ -1,28 +1,30 @@ -import React, { useCallback, useMemo, memo } from 'react' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import { LogBox } from 'react-native' -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' -import { Edge } from 'react-native-safe-area-context' -import 'text-encoding-polyfill' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import BackArrow from '@assets/images/backArrow.svg' -import IndeterminateProgressBar from '@components/IndeterminateProgressBar' -import { DelayedFadeIn } from '@components/FadeInOut' +import { ReAnimatedBox } from '@components/AnimatedBox' +import BackScreen from '@components/BackScreen' import Box from '@components/Box' -import ImageBox from '@components/ImageBox' import ButtonPressable from '@components/ButtonPressable' +import { DelayedFadeIn } from '@components/FadeInOut' +import ImageBox from '@components/ImageBox' +import IndeterminateProgressBar from '@components/IndeterminateProgressBar' import Text from '@components/Text' -import BackScreen from '@components/BackScreen' -import { ReAnimatedBox } from '@components/AnimatedBox' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import { useSpacing } from '@theme/themeHooks' import { parseTransactionError } from '@utils/solanaUtils' -import { useBalance } from '@utils/Balance' -import { ww } from '../../utils/layout' -import { RootState } from '../../store/rootReducer' -import { CollectableStackParamList } from './collectablesTypes' +import React, { memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { LogBox } from 'react-native' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { Edge } from 'react-native-safe-area-context' +import { useSelector } from 'react-redux' +import 'text-encoding-polyfill' import { TabBarNavigationProp } from '../../navigation/rootTypes' +import { RootState } from '../../store/rootReducer' import { Collectable, CompressedNFT } from '../../types/solana' +import { ww } from '../../utils/layout' +import { CollectableStackParamList } from './collectablesTypes' LogBox.ignoreLogs([ 'Non-serializable values were found in the navigation state', @@ -35,7 +37,7 @@ const TransferCollectableScreen = () => { const navigation = useNavigation() const COLLECTABLE_HEIGHT = ww const backEdges = useMemo(() => ['top'] as Edge[], []) - const { solBalance } = useBalance() + const solBalance = useBN(useSolOwnedAmount(useCurrentWallet()).amount) const { t } = useTranslation() const { collectable } = route.params diff --git a/src/features/collectables/collectablesTypes.ts b/src/features/collectables/collectablesTypes.ts index 19a499c71..0b4e65a8b 100644 --- a/src/features/collectables/collectablesTypes.ts +++ b/src/features/collectables/collectablesTypes.ts @@ -18,14 +18,16 @@ export type CollectableStackParamList = { AssertLocationScreen: { collectable: HotspotWithPendingRewards } + AntennaSetupScreen: { + collectable: HotspotWithPendingRewards + } + SettingUpAntennaScreen: undefined PaymentScreen: undefined | PaymentRouteParam - ClaimRewardsScreen: { hotspot: HotspotWithPendingRewards } ClaimAllRewardsScreen: undefined ClaimingRewardsScreen: undefined - CollectionScreen: { collection: Collectable[] } @@ -41,7 +43,6 @@ export type CollectableStackParamList = { TransferCompleteScreen: { collectable: CompressedNFT | Collectable } - AddNewContact: undefined PaymentQrScanner: undefined AddressBookNavigator: undefined diff --git a/src/features/dappLogin/DappLoginScreen.tsx b/src/features/dappLogin/DappLoginScreen.tsx index 97bbd47b2..1c6a273a4 100644 --- a/src/features/dappLogin/DappLoginScreen.tsx +++ b/src/features/dappLogin/DappLoginScreen.tsx @@ -1,40 +1,38 @@ +import Close from '@assets/images/close.svg' +import SafeAreaBox from '@components/SafeAreaBox' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import Address from '@helium/address' import { TokenBurnV1 } from '@helium/transactions' +import useAlert from '@hooks/useAlert' import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { useColors } from '@theme/themeHooks' import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import { useAsync } from 'react-async-hook' import { useTranslation } from 'react-i18next' import { ActivityIndicator, Linking } from 'react-native' import { useDebouncedCallback } from 'use-debounce/lib' -import Close from '@assets/images/close.svg' -import { useAsync } from 'react-async-hook' -import LedgerBurnModal, { - LedgerBurnModalRef, -} from '@components/LedgerBurnModal' -import SafeAreaBox from '@components/SafeAreaBox' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { useColors } from '@theme/themeHooks' -import useAlert from '@hooks/useAlert' -import Address from '@helium/address' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { HomeNavigationProp } from '../home/homeTypes' -import { useWalletConnect } from './WalletConnectProvider' -import DappConnect from './DappConnect' -import DappAccount from './DappAccount' import { RootNavigationProp, RootStackParamList, } from '../../navigation/rootTypes' +import { useAccountStorage } from '../../storage/AccountStorageProvider' import { getKeypair } from '../../storage/secureStorage' +import { HomeNavigationProp } from '../home/homeTypes' +import DappAccount from './DappAccount' +import DappConnect from './DappConnect' +import { useWalletConnect } from './WalletConnectProvider' -export const EMPTY_B58_ADDRESS = Address.fromB58( - '13PuqyWXzPYeXcF1B9ZRx7RLkEygeL374ZABiQdwRSNzASdA1sn', -) const makeBurnTxn = async (opts: { payerB58: string }) => { const { payerB58 } = opts const txn = new TokenBurnV1({ amount: 1, payer: Address.fromB58(payerB58), - payee: EMPTY_B58_ADDRESS, + // TODO: This must not be a global const or checksum fails for some reason?? + // This whole login process should go away anyway. + payee: Address.fromB58( + '13PuqyWXzPYeXcF1B9ZRx7RLkEygeL374ZABiQdwRSNzASdA1sn', + ), nonce: 0, memo: '', }) @@ -78,9 +76,7 @@ const DappLoginScreen = () => { const { currentAccount } = useAccountStorage() const { primaryText } = useColors() const { showOKAlert } = useAlert() - const ledgerRef = useRef(null) const hasRequestedPair = useRef(false) - const ledgerShown = useRef(false) useAsync(async () => { if (params.uri.includes('wc:') && !hasRequestedPair.current) { @@ -145,26 +141,10 @@ const DappLoginScreen = () => { useAsync(async () => { if (!currentAccount?.address || !loginRequest) return - const isLedger = !!currentAccount.ledgerDevice - - const { signedTxn, unsignedTxn, txnJson } = await makeBurnTxn({ + const { signedTxn } = await makeBurnTxn({ payerB58: currentAccount.address, }) - if (isLedger && currentAccount.ledgerDevice) { - if (ledgerShown.current) return - - ledgerShown.current = true - - ledgerRef.current?.show({ - unsignedTxn, - ledgerDevice: currentAccount.ledgerDevice, - accountIndex: currentAccount.accountIndex || 0, - txnJson, - }) - return - } - if (!signedTxn) return try { @@ -188,31 +168,6 @@ const DappLoginScreen = () => { } }, [currentAccount, loginRequest]) - const ledgerConfirmed = useCallback( - async ({ txn: signedTxn }: { txn: TokenBurnV1; txnJson: string }) => { - if (!currentAccount) return - - await login({ - txn: signedTxn.toString(), - address: currentAccount.address, - }) - - await goBack() - }, - [currentAccount, goBack, login], - ) - - const handleLedgerError = useCallback( - async (error: Error) => { - await showOKAlert({ - title: t('generic.error'), - message: error.toString(), - }) - await goBack() - }, - [goBack, showOKAlert, t], - ) - const checkTimeoutError = useCallback(async () => { if (connectionState !== 'undetermined') return await showOKAlert({ @@ -267,33 +222,22 @@ const DappLoginScreen = () => { ]) return ( - - - - - - {body} - - + + + {body} + ) } diff --git a/src/features/home/HomeNavigator.tsx b/src/features/home/HomeNavigator.tsx index aa40b6ce8..c2f096971 100644 --- a/src/features/home/HomeNavigator.tsx +++ b/src/features/home/HomeNavigator.tsx @@ -1,25 +1,26 @@ -import React, { memo } from 'react' +import ConfirmPinScreen from '@components/ConfirmPinScreen' import { createStackNavigator, StackNavigationOptions, } from '@react-navigation/stack' -import ConfirmPinScreen from '@components/ConfirmPinScreen' -import AccountAssignScreen from '../onboarding/AccountAssignScreen' +import React, { memo } from 'react' import AccountsScreen from '../account/AccountsScreen' -import PaymentScreen from '../payment/PaymentScreen' -import AddressBookNavigator from '../addressBook/AddressBookNavigator' -import SettingsNavigator from '../settings/SettingsNavigator' +import AccountTokenScreen from '../account/AccountTokenScreen' +import AirdropScreen from '../account/AirdropScreen' +import AccountManageTokenListScreen from '../account/AccountManageTokenListScreen' import AddNewContact from '../addressBook/AddNewContact' -import NotificationsNavigator from '../notifications/NotificationsNavigator' -import RequestScreen from '../request/RequestScreen' -import PaymentQrScanner from '../payment/PaymentQrScanner' +import AddressBookNavigator from '../addressBook/AddressBookNavigator' import AddressQrScanner from '../addressBook/AddressQrScanner' -import AccountTokenScreen from '../account/AccountTokenScreen' -import AddNewAccountNavigator from './addNewAccount/AddNewAccountNavigator' -import ImportAccountNavigator from '../onboarding/import/ImportAccountNavigator' import BurnScreen from '../burn/BurnScreen' +import NotificationsNavigator from '../notifications/NotificationsNavigator' +import AccountAssignScreen from '../onboarding/AccountAssignScreen' +import ImportAccountNavigator from '../onboarding/import/ImportAccountNavigator' +import PaymentQrScanner from '../payment/PaymentQrScanner' +import PaymentScreen from '../payment/PaymentScreen' +import RequestScreen from '../request/RequestScreen' +import SettingsNavigator from '../settings/SettingsNavigator' import SwapNavigator from '../swaps/SwapNavigator' -import AirdropScreen from '../account/AirdropScreen' +import AddNewAccountNavigator from './addNewAccount/AddNewAccountNavigator' const HomeStack = createStackNavigator() @@ -39,6 +40,10 @@ const HomeStackScreen = () => { name="AccountTokenScreen" component={AccountTokenScreen} /> + { - const { bonesToBalance } = useBalance() const colors = useColors() // TODO: Add other token types once nano app supports them - const balance = bonesToBalance(account.balance, 'HNT') + const balance = toBN(account.balance || 0, 8) const disabled = section.index === Section.ALREADY_LINKED const borderTopEndRadius = useMemo( @@ -96,9 +96,7 @@ const LedgerAccountListItem = ({ > {`${ellipsizeAddress(account.address, { numChars: 4, - })} | ${balanceToString(balance, { - maxDecimalPlaces: 2, - })}`} + })} | ${humanReadable(balance, 8)}`} diff --git a/src/features/payment/PaymentCard.tsx b/src/features/payment/PaymentCard.tsx index f2f5f0b69..a03396536 100644 --- a/src/features/payment/PaymentCard.tsx +++ b/src/features/payment/PaymentCard.tsx @@ -1,33 +1,29 @@ -import Balance, { - NetworkTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' -import { PaymentV2 } from '@helium/transactions' -import React, { memo, useCallback, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { LayoutChangeEvent } from 'react-native' import Box from '@components/Box' -import LedgerPayment, { LedgerPaymentRef } from '@components/LedgerPayment' import SubmitButton from '@components/SubmitButton' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' -import useAlert from '@hooks/useAlert' +import { PaymentV2 } from '@helium/transactions' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' +import React, { memo, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { LayoutChangeEvent } from 'react-native' import { useAccountStorage } from '../../storage/AccountStorageProvider' -import animateTransition from '../../utils/animateTransition' -import PaymentSummary from './PaymentSummary' import { checkSecureAccount } from '../../storage/secureStorage' +import animateTransition from '../../utils/animateTransition' import { SendDetails } from '../../utils/linking' +import PaymentSummary from './PaymentSummary' type Props = { handleCancel: () => void - totalBalance: Balance - feeTokenBalance?: Balance + totalBalance: BN + feeTokenBalance?: BN onSubmit: (opts?: { txn: PaymentV2; txnJson: string }) => void disabled?: boolean errors?: string[] payments?: SendDetails[] - ticker: Ticker + mint: PublicKey } const PaymentCard = ({ @@ -36,41 +32,25 @@ const PaymentCard = ({ feeTokenBalance, onSubmit, disabled, - ticker, + mint, payments, errors, }: Props) => { + const { symbol } = useMetaplexMetadata(mint) const { t } = useTranslation() const [payEnabled, setPayEnabled] = useState(false) const [height, setHeight] = useState(0) - const ledgerPaymentRef = useRef(null) - const { showOKAlert } = useAlert() const { currentAccount } = useAccountStorage() - const [options, setOptions] = useState<{ - txn: PaymentV2 - txnJson: string - }>() const handlePayPressed = useCallback(async () => { - if (!currentAccount?.ledgerDevice) { - const hasSecureAccount = await checkSecureAccount( - currentAccount?.address, - true, - ) - if (!hasSecureAccount) return - animateTransition('PaymentCard.payEnabled') - setPayEnabled(true) - } else { - // is ledger device - ledgerPaymentRef.current?.show({ - payments: payments || [], - ledgerDevice: currentAccount.ledgerDevice, - address: currentAccount.address, - accountIndex: currentAccount.accountIndex || 0, - speculativeNonce: 0, - }) - } - }, [currentAccount, payments]) + const hasSecureAccount = await checkSecureAccount( + currentAccount?.address, + true, + ) + if (!hasSecureAccount) return + animateTransition('PaymentCard.payEnabled') + setPayEnabled(true) + }, [currentAccount]) const handleLayout = useCallback( (e: LayoutChangeEvent) => { @@ -79,102 +59,80 @@ const PaymentCard = ({ }, [height], ) - const handleConfirm = useCallback( - (opts: { txn: PaymentV2; txnJson: string }) => { - setPayEnabled(true) - setOptions(opts) - }, - [], - ) - const handleSubmit = useCallback(() => { - onSubmit(options) - }, [onSubmit, options]) - - const handleLedgerError = useCallback( - (error: Error) => { - showOKAlert({ title: t('generic.error'), message: error.toString() }) - }, - [showOKAlert, t], - ) + const handleSubmit = onSubmit return ( - - - - - {!payEnabled ? ( - <> - - - - {t('generic.cancel')} - - - + + {!payEnabled ? ( + <> + + + + {t('generic.cancel')} + + + + - - {t('payment.pay')} - - - - - ) : ( - - )} - + {t('payment.pay')} + + + + + ) : ( + + )} - + ) } diff --git a/src/features/payment/PaymentError.tsx b/src/features/payment/PaymentError.tsx index 982585c03..cd48720f0 100644 --- a/src/features/payment/PaymentError.tsx +++ b/src/features/payment/PaymentError.tsx @@ -1,21 +1,25 @@ -import Balance, { NetworkTokens, TestNetworkTokens } from '@helium/currency' -import { useNavigation } from '@react-navigation/native' -import React, { memo, useMemo } from 'react' -import { useTranslation } from 'react-i18next' import FailureIcon from '@assets/images/paymentFailure.svg' -import { SerializedError } from '@reduxjs/toolkit' import BackgroundFill from '@components/BackgroundFill' import Box from '@components/Box' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { useOwnedAmount } from '@helium/helium-react-hooks' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useNavigation } from '@react-navigation/native' +import { SerializedError } from '@reduxjs/toolkit' +import { NATIVE_MINT } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' import { parseTransactionError } from '@utils/solanaUtils' -import { useBalance } from '@utils/Balance' +import BN from 'bn.js' +import React, { memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { Payment } from './PaymentItem' import PaymentSummary from './PaymentSummary' type Props = { - totalBalance: Balance - feeTokenBalance?: Balance + mint: PublicKey + totalBalance: BN + feeTokenBalance?: BN payments: Payment[] error?: Error | SerializedError onRetry: () => void @@ -27,15 +31,20 @@ const PaymentError = ({ payments, error, onRetry, + mint, }: Props) => { const navigation = useNavigation() const { t } = useTranslation() - const { solBalance } = useBalance() + const wallet = useCurrentWallet() + const { amount: solBalance } = useOwnedAmount(wallet, NATIVE_MINT) const errorMessage = useMemo(() => { if (!error) return '' - return parseTransactionError(solBalance, error.message) + return parseTransactionError( + new BN(solBalance?.toString() || '0'), + error.message, + ) }, [error, solBalance]) return ( @@ -65,6 +74,7 @@ const PaymentError = ({ > + amount?: BN hasError?: boolean max?: boolean - createTokenAccountFee?: Balance< - NetworkTokens | TestNetworkTokens | IotTokens | MobileTokens - > + createTokenAccountFee?: BN } & BoxProps type Props = { index: number hasError?: boolean - fee?: Balance + fee?: BN onAddressBookSelected: (opts: { address?: string; index: number }) => void onEditAmount: (opts: { address?: string; index: number }) => void onToggleMax?: (opts: { address?: string; index: number }) => void @@ -57,7 +52,7 @@ type Props = { onRemove?: (index: number) => void onUpdateError?: (index: number, hasError: boolean) => void hideMemo?: boolean - ticker?: string + mint?: PublicKey netType?: number showAmount?: boolean } & Payment @@ -80,14 +75,16 @@ const PaymentItem = ({ onRemove, onToggleMax, onUpdateError, - ticker, + mint, showAmount = true, ...boxProps }: Props) => { + const decimals = useMint(mint)?.info?.decimals const { colorStyle } = useOpacity('primaryText', 0.3) const { dcToNetworkTokens, oraclePrice } = useBalance() const { t } = useTranslation() const { secondaryText } = useColors() + const { symbol, loading: loadingMeta } = useMetaplexMetadata(mint) const addressIsWrongNetType = useMemo( () => @@ -235,24 +232,26 @@ const PaymentItem = ({ {showAmount && ( - {!amount || amount?.integerBalance === 0 ? ( + {!amount || amount?.isZero() ? ( <> - - {t('payment.enterAmount', { - ticker, - })} - + {!loadingMeta && ( + + {t('payment.enterAmount', { + ticker: symbol, + })} + + )} ) : ( @@ -266,16 +265,12 @@ const PaymentItem = ({ variant="subtitle2" color="primaryText" > - {balanceToString(amount, { - maxDecimalPlaces: amount.type.decimalPlaces.toNumber(), - })} + {humanReadable(amount, decimals)} {fee && ( {t('payment.fee', { - value: balanceToString(feeAsTokens, { - maxDecimalPlaces: 4, - }), + value: humanReadable(feeAsTokens, 8), })} )} diff --git a/src/features/payment/PaymentScreen.tsx b/src/features/payment/PaymentScreen.tsx index 1a557e556..9e1f6b677 100644 --- a/src/features/payment/PaymentScreen.tsx +++ b/src/features/payment/PaymentScreen.tsx @@ -1,96 +1,104 @@ -import React, { - useCallback, - useState, - memo as reactMemo, - useMemo, - useEffect, - useRef, -} from 'react' -import { useTranslation } from 'react-i18next' import Close from '@assets/images/close.svg' import QR from '@assets/images/qr.svg' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import Balance, { - CurrencyType, - NetworkTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' -import { Keyboard, Platform } from 'react-native' -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import Address, { NetTypes } from '@helium/address' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { PaymentV2 } from '@helium/transactions' -import { unionBy } from 'lodash' -import Toast from 'react-native-simple-toast' -import { useSelector } from 'react-redux' -import TokenButton from '@components/TokenButton' -import Box from '@components/Box' -import Text from '@components/Text' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { useColors, useHitSlop } from '@theme/themeHooks' +import AccountButton from '@components/AccountButton' import AccountSelector, { AccountSelectorRef, } from '@components/AccountSelector' -import TokenSelector, { - TokenListItem, - TokenSelectorRef, -} from '@components/TokenSelector' -import AccountButton from '@components/AccountButton' import AddressBookSelector, { AddressBookRef, } from '@components/AddressBookSelector' +import Box from '@components/Box' import HNTKeyboard, { HNTKeyboardRef } from '@components/HNTKeyboard' -import useDisappear from '@hooks/useDisappear' import IconPressedContainer from '@components/IconPressedContainer' -import TokenSOL from '@assets/images/tokenSOL.svg' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenHNT from '@assets/images/tokenHNT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' -import { calcCreateAssociatedTokenAccountAccountFee } from '@utils/solanaUtils' -import { Mints } from '@utils/constants' +import Text from '@components/Text' +import TokenButton from '@components/TokenButton' +import TokenSelector, { + TokenListItem, + TokenSelectorRef, +} from '@components/TokenSelector' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import Address, { NetTypes } from '@helium/address' +import { useMint, useOwnedAmount } from '@helium/helium-react-hooks' +import { DC_MINT, HNT_MINT } from '@helium/spl-utils' +import useDisappear from '@hooks/useDisappear' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { NATIVE_MINT } from '@solana/spl-token' import { PublicKey } from '@solana/web3.js' -import { useSolana } from '../../solana/SolanaProvider' +import { useVisibleTokens } from '@storage/TokensProvider' +import { useColors, useHitSlop } from '@theme/themeHooks' +import { Mints } from '@utils/constants' import { - HomeNavigationProp, - HomeStackParamList, - PaymentRouteParam, -} from '../home/homeTypes' + calcCreateAssociatedTokenAccountAccountFee, + humanReadable, +} from '@utils/solanaUtils' +import BN from 'bn.js' +import { unionBy } from 'lodash' +import React, { + memo as reactMemo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard, Platform } from 'react-native' +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import Toast from 'react-native-simple-toast' +import { useSelector } from 'react-redux' +import useSubmitTxn from '../../hooks/useSubmitTxn' +import { RootNavigationProp } from '../../navigation/rootTypes' +import { useSolana } from '../../solana/SolanaProvider' +import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { CSAccount } from '../../storage/cloudStorage' +import { RootState } from '../../store/rootReducer' +import { solanaSlice } from '../../store/slices/solanaSlice' +import { useAppDispatch } from '../../store/store' +import { useBalance } from '../../utils/Balance' import { accountNetType, formatAccountAlias, solAddressIsValid, } from '../../utils/accountUtils' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { balanceToString, useBalance } from '../../utils/Balance' -import PaymentItem from './PaymentItem' -import usePaymentsReducer, { MAX_PAYMENTS } from './usePaymentsReducer' +import { SendDetails } from '../../utils/linking' +import * as logger from '../../utils/logger' +import { + HomeNavigationProp, + HomeStackParamList, + PaymentRouteParam, +} from '../home/homeTypes' import PaymentCard from './PaymentCard' +import PaymentItem from './PaymentItem' import PaymentSubmit from './PaymentSubmit' -import { CSAccount } from '../../storage/cloudStorage' -import { RootState } from '../../store/rootReducer' -import { useAppDispatch } from '../../store/store' -import { solanaSlice } from '../../store/slices/solanaSlice' -import { RootNavigationProp } from '../../navigation/rootTypes' -import useSubmitTxn from '../../hooks/useSubmitTxn' -import { SendDetails } from '../../utils/linking' +import usePaymentsReducer, { MAX_PAYMENTS } from './usePaymentsReducer' type LinkedPayment = { amount?: string payee: string - defaultTokenType?: Ticker + mint?: string + defaultTokenType?: string } const parseLinkedPayments = (opts: PaymentRouteParam): LinkedPayment[] => { if (opts.payments) { - return JSON.parse(opts.payments) + return JSON.parse(opts.payments).map((p: LinkedPayment) => ({ + ...p, + mint: + p.mint || + (p.defaultTokenType && Mints[p.defaultTokenType.toUpperCase()]), + })) } if (opts.payee) { return [ { payee: opts.payee, amount: opts.amount, - defaultTokenType: opts.defaultTokenType?.toUpperCase() as Ticker, + mint: + opts.mint || + (opts.defaultTokenType && Mints[opts.defaultTokenType.toUpperCase()]), }, ] } @@ -104,16 +112,9 @@ const PaymentScreen = () => { const accountSelectorRef = useRef(null) const tokenSelectorRef = useRef(null) const hntKeyboardRef = useRef(null) - const { oraclePrice, hntBalance, solBalance, iotBalance, mobileBalance } = - useBalance() - const { anchorProvider } = useSolana() - - const appDispatch = useAppDispatch() - const navigation = useNavigation() - const rootNav = useNavigation() - const { t } = useTranslation() - const { primaryText, blueBright500, white } = useColors() - const hitSlop = useHitSlop('l') + const { oraclePrice } = useBalance() + const { visibleTokens } = useVisibleTokens() + const [mint, setMint] = useState(HNT_MINT) const { currentAccount, currentNetworkAddress, @@ -122,14 +123,22 @@ const PaymentScreen = () => { setCurrentAccount, sortedAccountsForNetType, } = useAccountStorage() - const [ticker, setTicker] = useState( - (route.params?.defaultTokenType?.toUpperCase() as Ticker) || 'HNT', - ) - const [mint, setMint] = useState( - (route.params?.defaultTokenType?.toUpperCase() as Ticker) - ? Mints[route.params?.defaultTokenType?.toUpperCase() as Ticker] - : Mints.HNT, - ) + const wallet = usePublicKey(currentAccount?.solanaAddress) + const { amount: solBalance } = useOwnedAmount(wallet, NATIVE_MINT) + const { amount: balanceBigint } = useOwnedAmount(wallet, mint) + const balance = useMemo(() => { + if (typeof balanceBigint !== 'undefined') { + return new BN(balanceBigint.toString()) + } + }, [balanceBigint]) + const { anchorProvider } = useSolana() + + const appDispatch = useAppDispatch() + const navigation = useNavigation() + const rootNav = useNavigation() + const { t } = useTranslation() + const { primaryText } = useColors() + const hitSlop = useHitSlop('l') useDisappear(() => { appDispatch(solanaSlice.actions.resetPayment()) @@ -161,22 +170,26 @@ const PaymentScreen = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]) - const currencyType = useMemo(() => CurrencyType.fromTicker(ticker), [ticker]) - const [paymentState, dispatch] = usePaymentsReducer({ - currencyType, + mint, oraclePrice, - accountMobileBalance: mobileBalance, - accountIotBalance: iotBalance, - accountNetworkBalance: hntBalance, + balance, netType: networkType, }) - const { submitPayment, submitLedger } = useSubmitTxn() + useEffect(() => { + dispatch({ + type: 'updateTokenBalance', + balance, + }) + }, [dispatch, balance]) + + const { submitPayment } = useSubmitTxn() const solanaPayment = useSelector( (reduxState: RootState) => reduxState.solana.payment, ) + const { symbol } = useMetaplexMetadata(mint) const { top } = useSafeAreaInsets() @@ -210,8 +223,8 @@ const PaymentScreen = () => { if (!paymentsArr?.length) return - if (paymentsArr[0].defaultTokenType) { - onTickerSelected(paymentsArr[0].defaultTokenType) + if (paymentsArr[0].mint) { + onTokenSelected(new PublicKey(paymentsArr[0].mint)) } if ( @@ -241,11 +254,7 @@ const PaymentScreen = () => { }, [route]) const handleBalance = useCallback( - (opts: { - balance: Balance - payee?: string - index?: number - }) => { + (opts: { balance: BN; payee?: string; index?: number }) => { if (opts.index === undefined || !currentAccount) return dispatch({ @@ -274,7 +283,7 @@ const PaymentScreen = () => { paymentState.payments.length < MAX_PAYMENTS && !!lastPayee.address && !!lastPayee.amount && - lastPayee.amount.integerBalance > 0 + lastPayee.amount?.gt(new BN(0)) ) }, [currentAccount, paymentState.payments]) @@ -293,71 +302,62 @@ const PaymentScreen = () => { [paymentState.payments], ) - const handleSubmit = useCallback( - async (opts?: { txn: PaymentV2; txnJson: string }) => { - try { - if (!opts) { - await submitPayment(payments) - } else { - // This is a ledger device - submitLedger() - } - } catch (e) { - console.error(e) - } - }, - [payments, submitPayment, submitLedger], - ) + const handleSubmit = useCallback(async () => { + try { + await submitPayment( + paymentState.payments + .filter((p) => p.address && p.amount) + .map((payment) => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + payee: payment.address!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + balanceAmount: payment.amount!, + })), + paymentState.mint, + ) + } catch (e) { + logger.error(e) + } + }, [submitPayment, paymentState.mint, paymentState.payments]) const insufficientFunds = useMemo((): [ value: boolean, - errorTicker: string, + errorMint: PublicKey | undefined, ] => { - if (!hntBalance || !paymentState.totalAmount) { - return [true, ''] + if (paymentState.balance.isZero()) { + return [true, undefined] } - if (paymentState.networkFee?.integerBalance === undefined) - return [false, ''] + if (typeof paymentState.networkFee === 'undefined') + return [false, undefined] try { let hasEnoughSol = false if (solBalance) { - hasEnoughSol = - solBalance.minus(paymentState.networkFee).integerBalance >= 0 - } - let hasEnoughToken = false - if (ticker === 'MOBILE' && mobileBalance) { - hasEnoughToken = - mobileBalance.minus(paymentState.totalAmount).integerBalance >= 0 - } else if (ticker === 'IOT' && iotBalance) { - hasEnoughToken = - iotBalance.minus(paymentState.totalAmount).integerBalance >= 0 - } else if (ticker === 'HNT' && hntBalance) { - hasEnoughToken = - hntBalance.minus(paymentState.totalAmount).integerBalance >= 0 - } else if (ticker === 'SOL' && solBalance) { - hasEnoughToken = - solBalance.minus(paymentState.totalAmount).integerBalance >= 0 + hasEnoughSol = new BN(solBalance.toString()) + .sub(paymentState.networkFee) + .gte(new BN(0)) } + const hasEnoughToken = balance + ?.sub(paymentState.totalAmount) + .gte(new BN(0)) - if (!hasEnoughSol) return [true, 'SOL' as Ticker] - if (!hasEnoughToken) return [true, paymentState.totalAmount.type.ticker] - return [false, ''] + if (!hasEnoughSol) return [true, NATIVE_MINT] + if (!hasEnoughToken) return [true, mint] + return [false, undefined] } catch (e) { // if the screen was already open, then a deep link of a different net type // is selected there will be a brief arithmetic error that can be ignored. if (__DEV__) { console.warn(e) } - return [false, ''] + return [false, undefined] } }, [ - hntBalance, - paymentState.totalAmount, + paymentState.balance, paymentState.networkFee, + paymentState.totalAmount, solBalance, - ticker, - mobileBalance, - iotBalance, + balance, + mint, ]) const selfPay = useMemo( @@ -384,7 +384,9 @@ const PaymentScreen = () => { } if (insufficientFunds[0]) { errStrings.push( - t('payment.insufficientFunds', { token: insufficientFunds[1] }), + t('payment.insufficientFunds', { + token: insufficientFunds[1]?.equals(NATIVE_MINT) ? 'SOL' : symbol, + }), ) } @@ -397,20 +399,17 @@ const PaymentScreen = () => { } return errStrings }, [ - currentAccount, + currentAccount?.ledgerDevice, + paymentState.payments.length, insufficientFunds, selfPay, - paymentState.payments.length, - t, wrongNetTypePay, + t, + symbol, ]) const isFormValid = useMemo(() => { - if ( - selfPay || - !paymentState.networkFee?.integerBalance || - (!!currentAccount?.ledgerDevice && paymentState.payments.length > 1) // ledger payments are limited to one payee - ) { + if (selfPay || !paymentState.networkFee) { return false } @@ -419,25 +418,24 @@ const PaymentScreen = () => { paymentState.payments.every((p) => { const addressValid = !!(p.address && solAddressIsValid(p.address)) - const paymentValid = p.amount && p.amount.integerBalance > 0 + const paymentValid = p.amount && p.amount?.gt(new BN(0)) return addressValid && paymentValid && !p.hasError }) return paymentsValid && !insufficientFunds[0] - }, [selfPay, paymentState, currentAccount, insufficientFunds]) + }, [selfPay, paymentState, insufficientFunds]) const handleTokenTypeSelected = useCallback(() => { tokenSelectorRef?.current?.showTokens() }, []) - const onTickerSelected = useCallback( - (tick: Ticker) => { - setTicker(tick) - setMint(Mints[tick]) + const onTokenSelected = useCallback( + (m: PublicKey) => { + setMint(m) dispatch({ type: 'changeToken', - currencyType: CurrencyType.fromTicker(tick), + mint: m, }) }, [dispatch], @@ -615,56 +613,27 @@ const PaymentScreen = () => { accountSelectorRef?.current.showAccountTypes(netType)() }, [sortedAccountsForNetType]) + const decimals = useMint(mint)?.info?.decimals const tokenButtonBalance = useMemo(() => { - switch (ticker) { - case 'HNT': - return balanceToString(hntBalance) - case 'SOL': - return balanceToString(solBalance) - case 'MOBILE': - return balanceToString(mobileBalance) - case 'IOT': - return balanceToString(iotBalance) - } - }, [ticker, hntBalance, solBalance, mobileBalance, iotBalance]) + return humanReadable(balance, decimals) + }, [balance, decimals]) const data = useMemo((): TokenListItem[] => { - const tokens = [ - { - label: 'HNT', - icon: , - value: 'HNT' as Ticker, - selected: ticker === 'HNT', - }, - { - label: 'MOBILE', - icon: , - value: 'MOBILE' as Ticker, - selected: ticker === 'MOBILE', - }, - { - label: 'IOT', - icon: , - value: 'IOT' as Ticker, - selected: ticker === 'IOT', - }, - { - label: 'SOL', - icon: , - value: 'SOL' as Ticker, - selected: ticker === 'SOL', - }, - ] - + const tokens = [...visibleTokens] + .filter((vt: string) => vt !== DC_MINT.toBase58()) + .map((token) => ({ + mint: new PublicKey(token), + selected: mint.toBase58() === token, + })) return tokens - }, [blueBright500, white, ticker]) + }, [mint, visibleTokens]) return ( <> @@ -675,7 +644,7 @@ const PaymentScreen = () => { > { {paymentState.payments.map((p, index) => ( @@ -770,7 +739,7 @@ const PaymentScreen = () => { onEditAddress={handleEditAddress} handleAddressError={handleAddressError} onUpdateError={handleSetPaymentError} - ticker={currencyType.ticker} + mint={mint} onRemove={ paymentState.payments.length > 1 ? handleRemove @@ -801,7 +770,7 @@ const PaymentScreen = () => { { - feeTokenBalance?: Balance + totalBalance: BN + feeTokenBalance?: BN payments?: Payment[] onRetry: () => void onSuccess: () => void @@ -23,6 +25,7 @@ type Props = { } const PaymentSubmit = ({ + mint, submitLoading, submitError, submitSucceeded, @@ -87,6 +90,7 @@ const PaymentSubmit = ({ return ( - feeTokenBalance?: Balance + mint: PublicKey + totalBalance: BN + feeTokenBalance?: BN payments: Payment[] onSuccess: () => void actionTitle: string @@ -23,6 +25,7 @@ const PaymentSuccess = ({ payments, onSuccess, actionTitle, + mint, }: Props) => { const { t } = useTranslation() return ( @@ -41,6 +44,7 @@ const PaymentSuccess = ({ > - feeTokenBalance?: Balance + mint: PublicKey + totalBalance: BN + feeTokenBalance?: BN disabled?: boolean errors?: string[] payments?: Payment[] @@ -25,17 +28,19 @@ const PaymentSummary = ({ payments = [], errors, alwaysShowRecipients, + mint, }: Props) => { const { t } = useTranslation() + const decimals = useMint(mint)?.info?.decimals - const total = useMemo(() => balanceToString(totalBalance), [totalBalance]) + const total = useMemo(() => { + return humanReadable(totalBalance, decimals) || '' + }, [totalBalance, decimals]) const fee = useMemo( () => feeTokenBalance ? t('payment.fee', { - value: balanceToString(feeTokenBalance, { - maxDecimalPlaces: 4, - }), + value: humanReadable(feeTokenBalance, 9), }) : '', [feeTokenBalance, t], diff --git a/src/features/payment/PaymentTypeSelector.tsx b/src/features/payment/PaymentTypeSelector.tsx deleted file mode 100644 index a4065c530..000000000 --- a/src/features/payment/PaymentTypeSelector.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import React, { memo, useCallback, useMemo } from 'react' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' -import TokenHNT from '@assets/images/tokenHNT.svg' -import { BoxProps } from '@shopify/restyle' -import { Ticker } from '@helium/currency' -import Box from '@components/Box' -import { Theme } from '@theme/theme' -import Text from '@components/Text' -import { useColors } from '@theme/themeHooks' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { accountCurrencyType } from '../../utils/accountUtils' - -type Props = { - onChangeTokenType: (ticker: Ticker) => void - ticker: Ticker -} & BoxProps - -const TokenTypeItem = ({ - ticker, - selected, - onPress, -}: { - ticker: Ticker - selected: boolean - onPress: (ticker: Ticker) => void -}) => { - const { currentAccount } = useAccountStorage() - const colors = useColors() - const color = useCallback( - (isIcon = true) => { - const selectedColor = - ticker === 'MOBILE' && isIcon ? 'blueBright500' : 'primaryText' - return selected ? selectedColor : 'secondaryIcon' - }, - [selected, ticker], - ) - const handlePress = useCallback(() => onPress(ticker), [onPress, ticker]) - - const title = useMemo( - () => accountCurrencyType(currentAccount?.address, ticker).ticker, - [currentAccount, ticker], - ) - - return ( - - {ticker === 'HNT' ? ( - - ) : ( - - )} - - {title} - - {selected && ( - - )} - - ) -} - -const PaymentTypeSelector = ({ - onChangeTokenType, - ticker: tokenType, - ...boxProps -}: Props) => { - return ( - - - - - - - - ) -} - -export default memo(PaymentTypeSelector) diff --git a/src/features/payment/usePaymentsReducer.ts b/src/features/payment/usePaymentsReducer.ts index fc6978440..b0215e187 100644 --- a/src/features/payment/usePaymentsReducer.ts +++ b/src/features/payment/usePaymentsReducer.ts @@ -1,41 +1,33 @@ import { NetTypes } from '@helium/address' -import Balance, { - CurrencyType, - IotTokens, - MobileTokens, - NetworkTokens, - SolTokens, - TestNetworkTokens, - USDollars, -} from '@helium/currency' +import { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' import { useReducer } from 'react' import { CSAccount } from '../../storage/cloudStorage' import { TXN_FEE_IN_LAMPORTS } from '../../utils/solanaUtils' import { Payment } from './PaymentItem' -type PaymentCurrencyType = - | NetworkTokens - | TestNetworkTokens - | IotTokens - | MobileTokens - type UpdatePayeeAction = { type: 'updatePayee' contact?: CSAccount address: string index: number payer: string - createTokenAccountFee: Balance + createTokenAccountFee: BN } type UpdateBalanceAction = { type: 'updateBalance' address?: string index: number - value?: Balance + value?: BN payer: string } +type UpdateTokenBalanceAction = { + type: 'updateTokenBalance' + balance?: BN +} + type RemovePayment = { type: 'removePayment' index: number @@ -58,7 +50,7 @@ type AddPayee = { type ChangeToken = { type: 'changeToken' - currencyType: PaymentCurrencyType + mint: PublicKey } type AddLinkedPayments = { @@ -74,40 +66,37 @@ export const MAX_PAYMENTS = 10 type PaymentState = { payments: Payment[] - totalAmount: Balance + totalAmount: BN error?: string - currencyType: PaymentCurrencyType - oraclePrice?: Balance + mint: PublicKey + oraclePrice?: BN netType: NetTypes.NetType - networkFee?: Balance - accountMobileBalance?: Balance - accountIotBalance?: Balance - accountNetworkBalance?: Balance + networkFee?: BN + balance: BN } const initialState = (opts: { - currencyType: PaymentCurrencyType + mint: PublicKey payments?: Payment[] netType: NetTypes.NetType - oraclePrice?: Balance - accountMobileBalance?: Balance - accountIotBalance?: Balance - accountNetworkBalance?: Balance + oraclePrice?: BN + balance?: BN }): PaymentState => ({ error: undefined, payments: [{}] as Array, - totalAmount: new Balance(0, opts.currencyType), + totalAmount: new BN(0), ...calculateFee([{}]), ...opts, + balance: opts.balance || new BN(0), }) -const paymentsSum = (payments: Payment[], type: PaymentCurrencyType) => { +const paymentsSum = (payments: Payment[]) => { return payments.reduce((prev, current) => { if (!current.amount) { return prev } - return prev.plus(current.amount) - }, new Balance(0, type)) + return prev.add(current.amount) + }, new BN(0)) } const calculateFee = (payments: Payment[]) => { @@ -115,14 +104,11 @@ const calculateFee = (payments: Payment[]) => { if (!current.createTokenAccountFee) { return prev } - return prev.plus(current.createTokenAccountFee) - }, new Balance(0, CurrencyType.solTokens)) + return prev.add(current.createTokenAccountFee) + }, new BN(0)) - const txnFeeInLammportsFee = new Balance( - TXN_FEE_IN_LAMPORTS, - CurrencyType.solTokens, - ) - const networkFee = totalFee.plus(txnFeeInLammportsFee) + const txnFeeInLammportsFee = new BN(TXN_FEE_IN_LAMPORTS) + const networkFee = totalFee.add(txnFeeInLammportsFee) return { networkFee, @@ -130,22 +116,21 @@ const calculateFee = (payments: Payment[]) => { } const recalculate = (payments: Payment[], state: PaymentState) => { - const accountBalance = getAccountBalance(state) + const accountBalance = state.balance const { networkFee } = calculateFee(payments) const maxPayment = payments.find((p) => p.max) - const totalAmount = paymentsSum(payments, state.currencyType) + const totalAmount = paymentsSum(payments) if (!maxPayment) { return { networkFee, payments, totalAmount } } - const prevPaymentAmount = - maxPayment?.amount ?? new Balance(0, state.currencyType) + const prevPaymentAmount = maxPayment?.amount ?? new BN(0) - const totalMinusPrevPayment = totalAmount.minus(prevPaymentAmount) - let maxBalance = accountBalance?.minus(totalMinusPrevPayment) + const totalMinusPrevPayment = totalAmount.sub(prevPaymentAmount) + let maxBalance = accountBalance?.sub(totalMinusPrevPayment) - if ((maxBalance?.integerBalance ?? 0) < 0) { - maxBalance = new Balance(0, state.currencyType) + if (maxBalance.lt(new BN(0))) { + maxBalance = new BN(0) } maxPayment.amount = maxBalance @@ -153,25 +138,8 @@ const recalculate = (payments: Payment[], state: PaymentState) => { return { networkFee, payments, - totalAmount: paymentsSum(payments, state.currencyType), - } -} - -const getAccountBalance = ({ - accountIotBalance, - accountMobileBalance, - accountNetworkBalance, - currencyType, -}: PaymentState) => { - if (currencyType.ticker === CurrencyType.iot.ticker) { - return accountIotBalance - } - - if (currencyType.ticker === CurrencyType.mobile.ticker) { - return accountMobileBalance + totalAmount: paymentsSum(payments), } - - return accountNetworkBalance } function reducer( @@ -179,6 +147,7 @@ function reducer( action: | UpdatePayeeAction | UpdateBalanceAction + | UpdateTokenBalanceAction | UpdateErrorAction | AddPayee | AddLinkedPayments @@ -246,6 +215,13 @@ function reducer( }) return { ...state, ...recalculate(nextPayments, state) } } + + case 'updateTokenBalance': { + return { + ...state, + balance: action.balance || new BN(0), + } + } case 'addPayee': { if (state.payments.length >= MAX_PAYMENTS) return state @@ -266,12 +242,10 @@ function reducer( }) return initialState({ - currencyType: action.currencyType, + mint: action.mint, payments: newPayments, oraclePrice: state.oraclePrice, - accountMobileBalance: state.accountMobileBalance, - accountIotBalance: state.accountIotBalance, - accountNetworkBalance: state.accountNetworkBalance, + balance: state.balance, netType: state.netType, }) } @@ -279,11 +253,9 @@ function reducer( case 'addLinkedPayments': { if (!action.payments.length) { return initialState({ - currencyType: state.currencyType, + mint: state.mint, oraclePrice: state.oraclePrice, - accountMobileBalance: state.accountMobileBalance, - accountIotBalance: state.accountIotBalance, - accountNetworkBalance: state.accountNetworkBalance, + balance: state.balance, netType: state.netType, }) } @@ -291,12 +263,10 @@ function reducer( const nextPayments: Payment[] = action.payments.map((p) => ({ address: p.address, account: p.account, - amount: p.amount - ? new Balance(parseInt(p.amount, 10), state.currencyType) - : undefined, - createTokenAccountFee: new Balance(0, CurrencyType.solTokens), + amount: p.amount ? new BN(parseInt(p.amount, 10)) : undefined, + createTokenAccountFee: new BN(0), })) - const totalAmount = paymentsSum(nextPayments, state.currencyType) + const totalAmount = paymentsSum(nextPayments) const fees = calculateFee(nextPayments) @@ -337,9 +307,7 @@ function reducer( export default (opts: { netType: NetTypes.NetType - currencyType: PaymentCurrencyType - oraclePrice?: Balance - accountMobileBalance?: Balance - accountIotBalance?: Balance - accountNetworkBalance?: Balance + mint: PublicKey + oraclePrice?: BN + balance?: BN }) => useReducer(reducer, initialState(opts)) diff --git a/src/features/request/RequestScreen.tsx b/src/features/request/RequestScreen.tsx index 2b2e368b2..85ec79e17 100644 --- a/src/features/request/RequestScreen.tsx +++ b/src/features/request/RequestScreen.tsx @@ -1,71 +1,67 @@ +import ShareIcon from '@assets/images/share.svg' +import AccountButton from '@components/AccountButton' +import AccountSelector, { + AccountSelectorRef, +} from '@components/AccountSelector' +import BackgroundFill from '@components/BackgroundFill' +import Box from '@components/Box' +import FadeInOut from '@components/FadeInOut' +import HNTKeyboard, { HNTKeyboardRef } from '@components/HNTKeyboard' +import TabBar, { TabBarOption } from '@components/TabBar' +import Text from '@components/Text' +import TokenButton from '@components/TokenButton' +import TokenSelector, { + TokenListItem, + TokenSelectorRef, +} from '@components/TokenSelector' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { NetTypes as NetType } from '@helium/address' +import { useMint } from '@helium/helium-react-hooks' +import useHaptic from '@hooks/useHaptic' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import Clipboard from '@react-native-community/clipboard' +import { useKeyboard } from '@react-native-community/hooks' +import { useNavigation } from '@react-navigation/native' +import { PublicKey } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { useVisibleTokens } from '@storage/TokensProvider' +import { + useBorderRadii, + useColors, + useOpacity, + useSpacing, +} from '@theme/themeHooks' +import animateTransition from '@utils/animateTransition' +import { makePayRequestLink } from '@utils/linking' +import { humanReadable } from '@utils/solanaUtils' +import BN from 'bn.js' import React, { memo, useCallback, - useMemo, - useState, useEffect, + useMemo, useRef, + useState, } from 'react' import { useTranslation } from 'react-i18next' -import { useNavigation } from '@react-navigation/native' import { + ActivityIndicator, Keyboard, LayoutChangeEvent, - ActivityIndicator, Platform, } from 'react-native' -import Share, { ShareOptions } from 'react-native-share' -import Clipboard from '@react-native-community/clipboard' -import Toast from 'react-native-simple-toast' import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import ShareIcon from '@assets/images/share.svg' -import { useDebounce } from 'use-debounce' -import { useKeyboard } from '@react-native-community/hooks' -import Balance, { - CurrencyType, - MobileTokens, - NetworkTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' -import { NetTypes as NetType } from '@helium/address' import QRCode from 'react-native-qrcode-svg' import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' -import TabBar, { TabBarOption } from '@components/TabBar' -import Text from '@components/Text' -import { useAccountStorage } from '@storage/AccountStorageProvider' -import Box from '@components/Box' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { - useBorderRadii, - useColors, - useOpacity, - useSpacing, -} from '@theme/themeHooks' -import { balanceToString } from '@utils/Balance' -import AccountButton from '@components/AccountButton' -import AccountSelector, { - AccountSelectorRef, -} from '@components/AccountSelector' -import { makePayRequestLink } from '@utils/linking' -import useHaptic from '@hooks/useHaptic' -import BackgroundFill from '@components/BackgroundFill' -import animateTransition from '@utils/animateTransition' -import HNTKeyboard, { HNTKeyboardRef } from '@components/HNTKeyboard' -import TokenButton from '@components/TokenButton' -import TokenSelector, { - TokenListItem, - TokenSelectorRef, -} from '@components/TokenSelector' -import FadeInOut from '@components/FadeInOut' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenHNT from '@assets/images/tokenHNT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' +import Share, { ShareOptions } from 'react-native-share' +import Toast from 'react-native-simple-toast' +import { useDebounce } from 'use-debounce' const QR_CONTAINER_SIZE = 220 type RequestType = 'qr' | 'link' const RequestScreen = () => { + const { visibleTokens } = useVisibleTokens() const { currentAccount, currentNetworkAddress: networkAddress } = useAccountStorage() const { t } = useTranslation() @@ -77,25 +73,22 @@ const RequestScreen = () => { const navigation = useNavigation() const { l } = useSpacing() const { l: borderRadius } = useBorderRadii() - const { secondaryText, primaryText, white, blueBright500 } = useColors() + const { secondaryText, primaryText } = useColors() const [isEditing, setIsEditing] = useState(false) const { keyboardShown } = useKeyboard() const hntKeyboardRef = useRef(null) const [hntKeyboardVisible, setHNTKeyboardVisible] = useState(false) - const [paymentAmount, setPaymentAmount] = - useState>() - const [ticker, setTicker] = useState('HNT') + const [paymentAmount, setPaymentAmount] = useState() + const [mint, setMint] = useState() const tokenSelectorRef = useRef(null) const qrRef = useRef<{ toDataURL: (callback: (url: string) => void) => void }>(null) + const decimals = useMint(mint)?.info?.decimals + const { symbol } = useMetaplexMetadata(mint) const handleBalance = useCallback( - (opts: { - balance: Balance - payee?: string - index?: number - }) => { + (opts: { balance: BN; payee?: string; index?: number }) => { setPaymentAmount(opts.balance) }, [setPaymentAmount], @@ -127,9 +120,9 @@ const RequestScreen = () => { return makePayRequestLink({ payee: networkAddress, balanceAmount: paymentAmount, - defaultTokenType: ticker, + mint: mint?.toBase58(), }) - }, [networkAddress, paymentAmount, ticker]) + }, [networkAddress, paymentAmount, mint]) const [qrLink] = useDebounce(link, 500) @@ -204,8 +197,6 @@ const RequestScreen = () => { triggerNavHaptic() }, [link, showToast, triggerNavHaptic]) - const currencyType = useMemo(() => CurrencyType.fromTicker(ticker), [ticker]) - const requestTypeOptions = useMemo( (): Array => [ { title: t('request.qr'), value: 'qr' }, @@ -218,43 +209,23 @@ const RequestScreen = () => { tokenSelectorRef?.current?.showTokens() }, []) - const onTickerSelected = useCallback((tick: Ticker) => { - setTicker(tick) - }, []) - const handleAccountButtonPress = useCallback(() => { if (!accountSelectorRef?.current) return accountSelectorRef?.current?.show() }, []) const data = useMemo((): TokenListItem[] => { - const tokens = [ - { - label: 'HNT', - icon: , - value: 'HNT' as Ticker, - selected: ticker === 'HNT', - }, - { - label: 'MOBILE', - icon: , - value: 'MOBILE' as Ticker, - selected: ticker === 'MOBILE', - }, - { - label: 'IOT', - icon: , - value: 'IOT' as Ticker, - selected: ticker === 'IOT', - }, - ] - - return tokens - }, [blueBright500, white, ticker]) + return [...visibleTokens].map((m) => { + return { + selected: mint?.toBase58() === m, + mint: new PublicKey(m), + } + }) + }, [visibleTokens, mint]) return ( { { /> { {t('request.amount')} - {!paymentAmount || paymentAmount.integerBalance === 0 ? ( + {!paymentAmount || paymentAmount.isZero() ? ( {t('request.enterAmount', { - ticker: paymentAmount?.type.ticker, + ticker: symbol, })} ) : ( - {balanceToString(paymentAmount)} + {humanReadable(paymentAmount, decimals)} )} diff --git a/src/features/swaps/SwapItem.tsx b/src/features/swaps/SwapItem.tsx index cf570072e..7f7410834 100644 --- a/src/features/swaps/SwapItem.tsx +++ b/src/features/swaps/SwapItem.tsx @@ -1,19 +1,20 @@ -import React, { memo, useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Ticker } from '@helium/currency' -import { GestureResponderEvent, Pressable, StyleSheet } from 'react-native' -import { BoxProps } from '@shopify/restyle' -import { useCreateOpacity } from '@theme/themeHooks' -import Text from '@components/Text' import Box from '@components/Box' +import Text from '@components/Text' import TokenIcon from '@components/TokenIcon' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { BoxProps } from '@shopify/restyle' +import { PublicKey } from '@solana/web3.js' import { Theme } from '@theme/theme' +import { useCreateOpacity } from '@theme/themeHooks' +import React, { memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { GestureResponderEvent, Pressable, StyleSheet } from 'react-native' import CarotDown from '../../assets/images/carotDownFull.svg' export type SwapItemProps = { isPaying: boolean onCurrencySelect: () => void - currencySelected: Ticker + mintSelected: PublicKey amount: number loading?: boolean onPress?: ((event: GestureResponderEvent) => void) | null | undefined @@ -23,7 +24,7 @@ export type SwapItemProps = { const SwapItem = ({ isPaying, onCurrencySelect, - currencySelected, + mintSelected, amount, loading = false, onPress, @@ -31,6 +32,7 @@ const SwapItem = ({ ...rest }: SwapItemProps) => { const { t } = useTranslation() + const { symbol, json } = useMetaplexMetadata(mintSelected) const { backgroundStyle: generateBackgroundStyle } = useCreateOpacity() @@ -80,7 +82,7 @@ const SwapItem = ({ alignItems="center" borderRadius="round" > - + - {currencySelected} + {symbol} @@ -98,7 +100,7 @@ const SwapItem = ({ ) - }, [currencySelected, getBackgroundColorStylePill, onCurrencySelect]) + }, [symbol, json, getBackgroundColorStylePill, onCurrencySelect]) return ( @@ -119,10 +121,7 @@ const SwapItem = ({ {/** If last decimals are zeroes do not show */} {!loading ? amount.toString() : t('generic.loading')} - {`${currencySelected}`} + {`${symbol}`} {isPaying && ( diff --git a/src/features/swaps/SwapScreen.tsx b/src/features/swaps/SwapScreen.tsx index 7bc5ad35a..48424d6cc 100644 --- a/src/features/swaps/SwapScreen.tsx +++ b/src/features/swaps/SwapScreen.tsx @@ -1,16 +1,13 @@ import Menu from '@assets/images/menu.svg' -import Refresh from '@assets/images/refresh.svg' -import TokenDC from '@assets/images/tokenDC.svg' -import TokenHNT from '@assets/images/tokenHNT.svg' -import TokenIOT from '@assets/images/tokenIOT.svg' -import TokenMOBILE from '@assets/images/tokenMOBILE.svg' import Plus from '@assets/images/plus.svg' +import Refresh from '@assets/images/refresh.svg' import AddressBookSelector, { AddressBookRef, } from '@components/AddressBookSelector' import { ReAnimatedBox } from '@components/AnimatedBox' import Box from '@components/Box' import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' import CloseButton from '@components/CloseButton' import HNTKeyboard, { HNTKeyboardRef } from '@components/HNTKeyboard' import SafeAreaBox from '@components/SafeAreaBox' @@ -20,27 +17,47 @@ import TextTransform from '@components/TextTransform' import TokenSelector, { TokenSelectorRef } from '@components/TokenSelector' import TouchableOpacityBox from '@components/TouchableOpacityBox' import TreasuryWarningScreen from '@components/TreasuryWarningScreen' -import Balance, { CurrencyType, SolTokens, Ticker } from '@helium/currency' +import { + useMint, + useOwnedAmount, + useSolOwnedAmount, +} from '@helium/helium-react-hooks' +import { + DC_MINT, + HNT_MINT, + IOT_MINT, + MOBILE_MINT, + toBN, + toNumber, +} from '@helium/spl-utils' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' import { useTreasuryPrice } from '@hooks/useTreasuryPrice' import { useNavigation } from '@react-navigation/native' import { PublicKey } from '@solana/web3.js' import { useAccountStorage } from '@storage/AccountStorageProvider' import { CSAccount } from '@storage/cloudStorage' -import { Mints } from '@utils/constants' -import { getAtaAccountCreationFee, TXN_FEE_IN_SOL } from '@utils/solanaUtils' +import { useColors, useHitSlop } from '@theme/themeHooks' +import { + TXN_FEE_IN_LAMPORTS, + TXN_FEE_IN_SOL, + getAtaAccountCreationFee, + humanReadable, +} from '@utils/solanaUtils' +import BN from 'bn.js' import React, { memo, useCallback, useMemo, useRef, useState } from 'react' import { useAsync } from 'react-async-hook' import { useTranslation } from 'react-i18next' import { LayoutAnimation } from 'react-native' import { Edge } from 'react-native-safe-area-context' -import { useColors, useHitSlop } from '@theme/themeHooks' -import CircleLoader from '@components/CircleLoader' import useSubmitTxn from '../../hooks/useSubmitTxn' -import { solAddressIsValid } from '../../utils/accountUtils' +import { useSolana } from '../../solana/SolanaProvider' import { useBalance } from '../../utils/Balance' +import { solAddressIsValid } from '../../utils/accountUtils' import SwapItem from './SwapItem' import { SwapNavigationProp } from './swapTypes' -import { useSolana } from '../../solana/SolanaProvider' + +const SOL_TXN_FEE = new BN(TXN_FEE_IN_SOL) // Selector Mode enum enum SelectorMode { @@ -48,14 +65,6 @@ enum SelectorMode { youReceive = 'youReceive', } -enum Tokens { - HNT = 'HNT', - MOBILE = 'MOBILE', - IOT = 'IOT', - SOL = 'SOL', - DC = 'DC', -} - const SwapScreen = () => { const { t } = useTranslation() const { currentAccount } = useAccountStorage() @@ -64,25 +73,26 @@ const SwapScreen = () => { const { submitTreasurySwap, submitMintDataCredits } = useSubmitTxn() const edges = useMemo(() => ['bottom'] as Edge[], []) const [selectorMode, setSelectorMode] = useState(SelectorMode.youPay) - const [youPayTokenType, setYouPayTokenType] = useState(Tokens.MOBILE) + const [youPayMint, setYouPayMint] = useState(MOBILE_MINT) const colors = useColors() const [youPayTokenAmount, setYouPayTokenAmount] = useState(0) - const [youReceiveTokenType, setYouReceiveTokenType] = useState( - Tokens.HNT, - ) - const [solFee, setSolFee] = useState(undefined) + const [youReceiveMint, setYouReceiveMint] = useState(HNT_MINT) + const [solFee, setSolFee] = useState(undefined) const [hasInsufficientBalance, setHasInsufficientBalance] = useState< undefined | boolean >() const [networkError, setNetworkError] = useState() const hntKeyboardRef = useRef(null) - const { networkTokensToDc, hntBalance, solBalance } = useBalance() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const hntBalance = useBN(useOwnedAmount(wallet, HNT_MINT).amount) + const { networkTokensToDc } = useBalance() const tokenSelectorRef = useRef(null) const { price, loading: loadingPrice, freezeDate, - } = useTreasuryPrice(new PublicKey(Mints[youPayTokenType]), youPayTokenAmount) + } = useTreasuryPrice(youPayMint, youPayTokenAmount) const [swapping, setSwapping] = useState(false) const [transactionError, setTransactionError] = useState() const [hasRecipientError, setHasRecipientError] = useState(false) @@ -114,18 +124,18 @@ const SwapScreen = () => { // If user does not have enough tokens to swap for greater than 0.00000001 tokens const insufficientTokensToSwap = useMemo(() => { if ( - youPayTokenType === Tokens.HNT && - (hntBalance?.floatBalance || 0) < 0.00000001 + youPayMint.equals(HNT_MINT) && + (hntBalance || new BN(0)).lt(new BN(1)) ) { return true } return ( - youPayTokenType !== Tokens.HNT && + !youPayMint.equals(HNT_MINT) && !(price && price > 0) && youPayTokenAmount > 0 ) - }, [hntBalance, price, youPayTokenAmount, youPayTokenType]) + }, [hntBalance, price, youPayTokenAmount, youPayMint]) const showError = useMemo(() => { if (hasRecipientError) return t('generic.notValidSolanaAddress') @@ -150,8 +160,8 @@ const SwapScreen = () => { const refresh = useCallback(async () => { setYouPayTokenAmount(0) - setYouReceiveTokenType(Tokens.HNT) - setYouPayTokenType(Tokens.MOBILE) + setYouReceiveMint(HNT_MINT) + setYouPayMint(MOBILE_MINT) setSelectorMode(SelectorMode.youPay) setSolFee(undefined) setNetworkError(undefined) @@ -166,27 +176,26 @@ const SwapScreen = () => { ) return - const toMint = new PublicKey(Mints[youReceiveTokenType]) - let fee = TXN_FEE_IN_SOL + let fee = new BN(TXN_FEE_IN_LAMPORTS) const ataFee = await getAtaAccountCreationFee({ solanaAddress: currentAccount.solanaAddress, connection, - mint: toMint, + mint: youReceiveMint, }) - fee += ataFee.floatBalance + fee = fee.add(ataFee) setSolFee(fee) setHasInsufficientBalance( - fee > solBalance.integerBalance || solBalance.floatBalance < 0.000005, + fee.gt(solBalance || new BN(0)) || solBalance?.lt(new BN(5000)), ) }, [ anchorProvider, currentAccount?.solanaAddress, solBalance, - youReceiveTokenType, - youPayTokenType, + youReceiveMint, + youPayMint, ]) const handleClose = useCallback(() => { @@ -217,80 +226,56 @@ const SwapScreen = () => { }, [refresh, t, handleClose]) const setTokenTypeHandler = useCallback( - (ticker: Ticker) => { + (mint: PublicKey) => { if (selectorMode === SelectorMode.youPay) { refresh() - setYouPayTokenType(ticker) + setYouPayMint(mint) } if (selectorMode === SelectorMode.youReceive) { - setYouReceiveTokenType(ticker) + setYouReceiveMint(mint) } if ( selectorMode === SelectorMode.youPay && - ticker !== Tokens.HNT && - youReceiveTokenType === Tokens.DC + !mint.equals(HNT_MINT) && + !youReceiveMint.equals(DC_MINT) ) { - setYouReceiveTokenType(Tokens.HNT) + setYouReceiveMint(HNT_MINT) setYouPayTokenAmount(0) } - if (selectorMode === SelectorMode.youPay && ticker === Tokens.HNT) { - setYouReceiveTokenType(Tokens.DC) + if (selectorMode === SelectorMode.youPay && mint.equals(HNT_MINT)) { + setYouReceiveMint(DC_MINT) } - if (selectorMode === SelectorMode.youReceive && ticker === Tokens.HNT) { - setYouPayTokenType(Tokens.MOBILE) + if (selectorMode === SelectorMode.youReceive && mint.equals(HNT_MINT)) { + setYouPayMint(MOBILE_MINT) } - if (selectorMode === SelectorMode.youReceive && ticker === Tokens.DC) { - setYouPayTokenType(Tokens.HNT) + if (selectorMode === SelectorMode.youReceive && mint.equals(DC_MINT)) { + setYouPayMint(HNT_MINT) } }, - [refresh, selectorMode, youReceiveTokenType], + [refresh, selectorMode, youReceiveMint], ) const tokenData = useMemo(() => { const tokens = { - [SelectorMode.youPay]: [ - { - label: Tokens.MOBILE, - icon: , - value: Tokens.MOBILE, - selected: youPayTokenType === Tokens.MOBILE, - }, - { - label: Tokens.HNT, - icon: , - value: Tokens.HNT, - selected: youPayTokenType === Tokens.HNT, - }, - { - label: Tokens.IOT, - icon: , - value: Tokens.IOT, - selected: youPayTokenType === Tokens.IOT, - }, - ], - [SelectorMode.youReceive]: [ - { - label: Tokens.HNT, - icon: , - value: Tokens.HNT, - selected: youReceiveTokenType === Tokens.HNT, - }, - { - label: Tokens.DC, - icon: , - value: Tokens.DC, - selected: youReceiveTokenType === Tokens.DC, - }, - ], + [SelectorMode.youPay]: [MOBILE_MINT, HNT_MINT, IOT_MINT].map((mint) => ({ + mint, + selected: youPayMint.equals(mint), + })), + [SelectorMode.youReceive]: [MOBILE_MINT, HNT_MINT, IOT_MINT].map( + (mint) => ({ + mint, + selected: youReceiveMint.equals(mint), + }), + ), } return tokens[selectorMode] - }, [selectorMode, youPayTokenType, youReceiveTokenType]) + }, [selectorMode, youPayMint, youReceiveMint]) const onCurrencySelect = useCallback( (youPay: boolean) => () => { @@ -300,35 +285,42 @@ const SwapScreen = () => { [], ) + const decimals = useMint(youPayMint)?.info?.decimals + const onTokenItemPressed = useCallback(() => { - hntKeyboardRef.current?.show({ - payer: currentAccount, - }) - }, [currentAccount]) + if (typeof decimals !== undefined) { + hntKeyboardRef.current?.show({ + payer: currentAccount, + }) + } + }, [currentAccount, decimals]) const onConfirmBalance = useCallback( - ({ balance }: { balance: Balance }) => { - const amount = balance.floatBalance.valueOf() + ({ balance }: { balance: BN }) => { + if (typeof decimals === 'undefined') return + + const amount = toNumber(balance, decimals) setYouPayTokenAmount(amount) }, - [], + [decimals], ) const hitSlop = useHitSlop('l') const youReceiveTokenAmount = useMemo(() => { - if (price && youPayTokenType !== Tokens.HNT) { + if (price && !youPayMint.equals(HNT_MINT)) { return price } - if (youPayTokenType === Tokens.HNT && currentAccount) { - const networkTokens = Balance.fromFloat( - Number(youPayTokenAmount), - CurrencyType.networkToken, + if ( + youPayMint.equals(HNT_MINT) && + currentAccount && + typeof decimals !== 'undefined' && + typeof youPayTokenAmount !== 'undefined' + ) { + return toNumber( + networkTokensToDc(toBN(youPayTokenAmount, decimals)) || new BN(0), + decimals, ) - const rawBalance = networkTokensToDc(networkTokens)?.floatBalance - if (typeof rawBalance !== 'undefined') { - return Math.floor(rawBalance) - } } return 0 @@ -337,7 +329,8 @@ const SwapScreen = () => { networkTokensToDc, price, youPayTokenAmount, - youPayTokenType, + youPayMint, + decimals, ]) const handleSwapTokens = useCallback(async () => { @@ -358,26 +351,22 @@ const SwapScreen = () => { ? new PublicKey(recipient) : new PublicKey(currentAccount.solanaAddress) - if (youPayTokenType === Tokens.HNT) { + if (youPayMint.equals(HNT_MINT) && youReceiveTokenAmount) { await submitMintDataCredits({ - dcAmount: youReceiveTokenAmount, + dcAmount: new BN(youReceiveTokenAmount), recipient: recipientAddr, }) } - if (youPayTokenType !== Tokens.HNT) { - await submitTreasurySwap( - new PublicKey(Mints[youPayTokenType]), - youPayTokenAmount, - recipientAddr, - ) + if (!youPayMint.equals(HNT_MINT)) { + await submitTreasurySwap(youPayMint, youPayTokenAmount, recipientAddr) } setSwapping(false) navigation.push('SwappingScreen', { - tokenA: youPayTokenType, - tokenB: youReceiveTokenType, + tokenA: youPayMint.toBase58(), + tokenB: youReceiveMint.toBase58(), }) } catch (error) { setSwapping(false) @@ -388,9 +377,9 @@ const SwapScreen = () => { connection, currentAccount, recipient, - youPayTokenType, + youPayMint, navigation, - youReceiveTokenType, + youReceiveMint, submitMintDataCredits, youReceiveTokenAmount, submitTreasurySwap, @@ -408,11 +397,8 @@ const SwapScreen = () => { { marginHorizontal="m" isPaying onCurrencySelect={onCurrencySelect(true)} - currencySelected={youPayTokenType} + mintSelected={youPayMint} amount={youPayTokenAmount} /> @@ -439,7 +425,7 @@ const SwapScreen = () => { marginHorizontal="m" isPaying={false} onCurrencySelect={onCurrencySelect(false)} - currencySelected={youReceiveTokenType} + mintSelected={youReceiveMint} amount={youReceiveTokenAmount} loading={loadingPrice} /> @@ -551,7 +537,7 @@ const SwapScreen = () => { variant="body3Medium" color="white" i18nKey="collectablesScreen.transferFee" - values={{ amount: solFee }} + values={{ amount: humanReadable(solFee, 9) }} /> ) : ( diff --git a/src/features/swaps/SwappingScreen.tsx b/src/features/swaps/SwappingScreen.tsx index ede5117b8..714f01582 100644 --- a/src/features/swaps/SwappingScreen.tsx +++ b/src/features/swaps/SwappingScreen.tsx @@ -1,25 +1,29 @@ -import React, { memo, useCallback, useMemo } from 'react' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' -import { Edge } from 'react-native-safe-area-context' -import 'text-encoding-polyfill' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import IndeterminateProgressBar from '@components/IndeterminateProgressBar' -import { DelayedFadeIn } from '@components/FadeInOut' +import { ReAnimatedBox } from '@components/AnimatedBox' +import BackScreen from '@components/BackScreen' import Box from '@components/Box' import ButtonPressable from '@components/ButtonPressable' +import { DelayedFadeIn } from '@components/FadeInOut' +import IndeterminateProgressBar from '@components/IndeterminateProgressBar' import Text from '@components/Text' -import BackScreen from '@components/BackScreen' -import { ReAnimatedBox } from '@components/AnimatedBox' import TokenIcon from '@components/TokenIcon' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { usePublicKey } from '@hooks/usePublicKey' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import { parseTransactionError } from '@utils/solanaUtils' -import { useBalance } from '@utils/Balance' -import { RootState } from '../../store/rootReducer' -import BackArrow from '../../assets/images/backArrow.svg' -import { SwapStackParamList } from './swapTypes' +import React, { memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { Edge } from 'react-native-safe-area-context' +import { useSelector } from 'react-redux' +import 'text-encoding-polyfill' import ArrowRight from '../../assets/images/arrowRight.svg' +import BackArrow from '../../assets/images/backArrow.svg' import { TabBarNavigationProp } from '../../navigation/rootTypes' +import { RootState } from '../../store/rootReducer' +import { SwapStackParamList } from './swapTypes' type Route = RouteProp @@ -27,10 +31,12 @@ const SwappingScreen = () => { const route = useRoute() const navigation = useNavigation() const backEdges = useMemo(() => ['bottom'] as Edge[], []) - const { solBalance } = useBalance() + const solBalance = useBN(useSolOwnedAmount(useCurrentWallet()).amount) const { t } = useTranslation() const { tokenA, tokenB } = route.params + const { json: jsonA } = useMetaplexMetadata(usePublicKey(tokenA)) + const { json: jsonB } = useMetaplexMetadata(usePublicKey(tokenB)) const solanaPayment = useSelector( (reduxState: RootState) => reduxState.solana.payment, @@ -53,7 +59,7 @@ const SwappingScreen = () => { padding="s" marginEnd="m" > - + { borderRadius="round" padding="s" > - + ) - }, [tokenA, tokenB]) + }, [jsonA?.image, jsonB?.image]) return ( diff --git a/src/features/txnDelegation/SignHotspot.tsx b/src/features/txnDelegation/SignHotspot.tsx index 1fa673fb6..4304deca3 100644 --- a/src/features/txnDelegation/SignHotspot.tsx +++ b/src/features/txnDelegation/SignHotspot.tsx @@ -1,34 +1,35 @@ -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { ActivityIndicator, Linking } from 'react-native' -import animalHash from 'angry-purple-tiger' -import { useAsync } from 'react-async-hook' -import { - verifyWalletLinkToken, - parseWalletLinkToken, - SignHotspotResponse, - createSignHotspotCallbackUrl, -} from '@helium/wallet-link' -import Toast from 'react-native-simple-toast' +import AccountIcon from '@components/AccountIcon' import Box from '@components/Box' import SafeAreaBox from '@components/SafeAreaBox' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' -import AccountIcon from '@components/AccountIcon' +import OnboardingClient, { OnboardingRecord } from '@helium/onboarding' +import { + SignHotspotResponse, + createSignHotspotCallbackUrl, + parseWalletLinkToken, + verifyWalletLinkToken, +} from '@helium/wallet-link' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import { useColors } from '@theme/themeHooks' +import animalHash from 'angry-purple-tiger' +import BN from 'bn.js' +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { useAsync } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator, Linking } from 'react-native' import Config from 'react-native-config' -import OnboardingClient, { OnboardingRecord } from '@helium/onboarding' -import { HomeNavigationProp } from '../home/homeTypes' -import { useAccountStorage } from '../../storage/AccountStorageProvider' -import { formatAccountAlias } from '../../utils/accountUtils' -import { getKeypair } from '../../storage/secureStorage' -import * as Logger from '../../utils/logger' -import useSolTxns from './useSolTxns' +import Toast from 'react-native-simple-toast' import { RootNavigationProp, RootStackParamList, } from '../../navigation/rootTypes' +import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { getKeypair } from '../../storage/secureStorage' +import { formatAccountAlias } from '../../utils/accountUtils' +import * as Logger from '../../utils/logger' +import { HomeNavigationProp } from '../home/homeTypes' +import useSolTxns from './useSolTxns' type Route = RouteProp @@ -312,14 +313,14 @@ const SignHotspot = () => { )} - {((solana.burnAmounts?.hntFee?.integerBalance || 0) > 0 || - (solana.burnAmounts?.dcFee?.integerBalance || 0) > 0) && ( + {(solana.burnAmounts?.hntFee?.gt(new BN(0)) || + solana.burnAmounts?.dcFee?.gt(new BN(0))) && ( <> {t('signHotspot.burnAmounts')} - {(solana.burnAmounts?.dcFee?.integerBalance || 0) > 0 && ( + {solana.burnAmounts?.dcFee?.gt(new BN(0)) && ( { )} - {(solana.burnAmounts?.hntFee?.integerBalance || 0) > 0 && ( + {solana.burnAmounts?.hntFee?.gt(new BN(0)) && ( | null - hntFee?: Balance | null + dcFee?: BN | null + hntFee?: BN | null } const useSolTxns = (heliumAddress: string, solanaTransactions?: string) => { @@ -268,17 +262,17 @@ const useSolTxns = (heliumAddress: string, solanaTransactions?: string) => { args: { dcAmount: string | null; hntAmount: string | null } } - let dcFee = 0 - let hntFee = 0 + let dcFee = new BN(0) + let hntFee = new BN(0) if (data.args.dcAmount) { - dcFee = new BN(data.args.dcAmount).toNumber() + dcFee = new BN(data.args.dcAmount) } if (data.args.hntAmount) { - hntFee = new BN(data.args.hntAmount).toNumber() + hntFee = new BN(data.args.hntAmount) } return { - hntFee: new Balance(hntFee, CurrencyType.networkToken), - dcFee: new Balance(dcFee, CurrencyType.dataCredit), + hntFee, + dcFee, name: decodedInstruction.name, } }, diff --git a/src/hooks/useBN.ts b/src/hooks/useBN.ts new file mode 100644 index 000000000..5833092a3 --- /dev/null +++ b/src/hooks/useBN.ts @@ -0,0 +1,9 @@ +import BN from 'bn.js' +import { useMemo } from 'react' + +export function useBN(num: bigint | undefined): BN | undefined { + return useMemo(() => { + if (typeof num === 'undefined') return undefined + return new BN(num.toString()) + }, [num]) +} diff --git a/src/hooks/useCurrentWallet.ts b/src/hooks/useCurrentWallet.ts new file mode 100644 index 000000000..40ec0bd97 --- /dev/null +++ b/src/hooks/useCurrentWallet.ts @@ -0,0 +1,9 @@ +import { PublicKey } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { usePublicKey } from './usePublicKey' + +export function useCurrentWallet(): PublicKey | undefined { + const { currentAccount } = useAccountStorage() + + return usePublicKey(currentAccount?.solanaAddress) +} diff --git a/src/hooks/useEntityKey.ts b/src/hooks/useEntityKey.ts index 2d3eb26c6..d6e134d46 100644 --- a/src/hooks/useEntityKey.ts +++ b/src/hooks/useEntityKey.ts @@ -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() + 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 } diff --git a/src/hooks/useHntSolConvert.ts b/src/hooks/useHntSolConvert.ts index 6f7765b7f..4494e5565 100644 --- a/src/hooks/useHntSolConvert.ts +++ b/src/hooks/useHntSolConvert.ts @@ -1,17 +1,21 @@ -import { Config } from 'react-native-config' -import { useAsync } from 'react-async-hook' +import { useOwnedAmount, useSolOwnedAmount } from '@helium/helium-react-hooks' +import { HNT_MINT } from '@helium/spl-utils' +import { LAMPORTS_PER_SOL, Transaction } from '@solana/web3.js' import axios from 'axios' -import { Transaction } from '@solana/web3.js' -import { useMemo } from 'react' -import { useBalance } from '@utils/Balance' -import { toNumber } from '@helium/spl-utils' import BN from 'bn.js' +import { useMemo } from 'react' +import { useAsync } from 'react-async-hook' +import { Config } from 'react-native-config' import { useSolana } from '../solana/SolanaProvider' import * as logger from '../utils/logger' +import { useBN } from './useBN' +import { useCurrentWallet } from './useCurrentWallet' export function useHntSolConvert() { const { cluster, anchorProvider } = useSolana() - const { hntBalance, solBalance } = useBalance() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const hntBalance = useBN(useOwnedAmount(wallet, HNT_MINT).amount) const baseUrl = useMemo(() => { let url = Config.HNT_TO_RENT_SERVICE_DEVNET_URL @@ -29,7 +33,7 @@ export function useHntSolConvert() { } = useAsync(async () => { try { const { estimate } = (await axios.get(`${baseUrl}/estimate`)).data - return toNumber(new BN(estimate), 8) + return new BN(estimate) } catch (e) { logger.error(e) return 0 @@ -37,11 +41,11 @@ export function useHntSolConvert() { }, [baseUrl]) const hasEnoughSol = useMemo(() => { - if (!hntBalance || !solBalance || !hntEstimate) return true + if (!hntBalance || !hntEstimate) return true - if (hntBalance.floatBalance < hntEstimate) return true + if (hntBalance.lt(hntEstimate)) return true - return solBalance.floatBalance > 0.02 + return (solBalance || new BN(0)).gt(new BN(0.02 * LAMPORTS_PER_SOL)) }, [hntBalance, solBalance, hntEstimate]) const { diff --git a/src/hooks/useIotInfo.ts b/src/hooks/useIotInfo.ts index 16ca0dd58..4a605a942 100644 --- a/src/hooks/useIotInfo.ts +++ b/src/hooks/useIotInfo.ts @@ -1,12 +1,11 @@ -import { IDL } from '@helium/idls/lib/esm/helium_entity_manager' -import { HeliumEntityManager } from '@helium/idls/lib/types/helium_entity_manager' import { IdlAccounts } from '@coral-xyz/anchor' -import { PublicKey } from '@solana/web3.js' -import { UseAccountState, useIdlAccount } from '@helium/helium-react-hooks' import { iotInfoKey, 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 { PublicKey } from '@solana/web3.js' import { IOT_SUB_DAO_KEY } from '@utils/constants' const type = 'iotHotspotInfoV0' @@ -15,16 +14,11 @@ export type IotHotspotInfoV0 = pubKey: PublicKey } -export const useIotInfo = ( - entityKey: string | undefined, -): UseAccountState | undefined => { +export const useIotInfo = (entityKey: string | undefined) => { const [iotConfigKey] = rewardableEntityConfigKey(IOT_SUB_DAO_KEY, 'IOT') const [iotInfo] = iotInfoKey(iotConfigKey, entityKey || '') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return useIdlAccount( - iotInfo, - IDL as HeliumEntityManager, - type, - ) + return useAnchorAccount(iotInfo, type) } diff --git a/src/hooks/useKeyToAsset.ts b/src/hooks/useKeyToAsset.ts new file mode 100644 index 000000000..354fff239 --- /dev/null +++ b/src/hooks/useKeyToAsset.ts @@ -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(mobileInfo, type) +} diff --git a/src/hooks/useMetaplexMetadata.ts b/src/hooks/useMetaplexMetadata.ts new file mode 100644 index 000000000..bdd965ca2 --- /dev/null +++ b/src/hooks/useMetaplexMetadata.ts @@ -0,0 +1,96 @@ +import { TypedAccountParser } from '@helium/account-fetch-cache' +import { useAccount } from '@helium/account-fetch-cache-hooks' +import { + Metadata, + parseMetadataAccount, + sol, + toMetadata, +} from '@metaplex-foundation/js' +import { NATIVE_MINT } from '@solana/spl-token' +import { AccountInfo, PublicKey } from '@solana/web3.js' +import { useMemo } from 'react' +import { useAsync } from 'react-async-hook' + +const MPL_PID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const cache: Record> = {} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getMetadata(uri: string | undefined): Promise { + if (uri) { + if (!cache[uri]) { + cache[uri] = fetch(uri).then((res) => res.json()) + } + return cache[uri] + } + return Promise.resolve(undefined) +} + +export const METADATA_PARSER: TypedAccountParser = ( + publicKey: PublicKey, + account: AccountInfo, +) => { + return toMetadata( + parseMetadataAccount({ + ...account, + lamports: sol(account.lamports), + data: account.data, + publicKey, + }), + ) +} + +export function getMetadataId(mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('metadata', 'utf-8'), MPL_PID.toBuffer(), mint.toBuffer()], + MPL_PID, + )[0] +} + +export function useMetaplexMetadata(mint: PublicKey | undefined): { + loading: boolean + metadata: Metadata | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json: any | undefined + symbol: string | undefined + name: string | undefined +} { + const metadataAddr = useMemo(() => { + if (mint) { + return getMetadataId(mint) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mint?.toBase58()]) + + const { info: metadataAcc, loading } = useAccount( + metadataAddr, + METADATA_PARSER, + true, + ) + const { result: json, loading: jsonLoading } = useAsync(getMetadata, [ + metadataAcc?.uri, + ]) + + if (mint?.equals(NATIVE_MINT)) { + return { + metadata: undefined, + loading: false, + json: { + name: 'SOL', + symbol: 'SOL', + image: + 'https://github.com/solana-labs/token-list/blob/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png?raw=true', + }, + symbol: 'SOL', + name: 'SOL', + } + } + + return { + loading: jsonLoading || loading, + json, + metadata: metadataAcc, + symbol: json?.symbol || metadataAcc?.symbol, + name: json?.name || metadataAcc?.name, + } +} diff --git a/src/hooks/useMobileInfo.ts b/src/hooks/useMobileInfo.ts index c5eb5a031..40272d8d5 100644 --- a/src/hooks/useMobileInfo.ts +++ b/src/hooks/useMobileInfo.ts @@ -1,12 +1,11 @@ -import { IDL } from '@helium/idls/lib/esm/helium_entity_manager' -import { HeliumEntityManager } from '@helium/idls/lib/types/helium_entity_manager' import { IdlAccounts } from '@coral-xyz/anchor' -import { PublicKey } from '@solana/web3.js' -import { UseAccountState, useIdlAccount } from '@helium/helium-react-hooks' 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 { PublicKey } from '@solana/web3.js' import { MOBILE_SUB_DAO_KEY } from '@utils/constants' const type = 'mobileHotspotInfoV0' @@ -15,19 +14,14 @@ export type MobileHotspotInfoV0 = pubKey: PublicKey } -export const useMobileInfo = ( - entityKey: string | undefined, -): UseAccountState | undefined => { +export const useMobileInfo = (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 useIdlAccount( - mobileInfo, - IDL as HeliumEntityManager, - type, - ) + return useAnchorAccount(mobileInfo, type) } diff --git a/src/hooks/usePublicKey.ts b/src/hooks/usePublicKey.ts new file mode 100644 index 000000000..fcdd71c7b --- /dev/null +++ b/src/hooks/usePublicKey.ts @@ -0,0 +1,10 @@ +import { PublicKey } from '@solana/web3.js' +import { useMemo } from 'react' + +export const usePublicKey = (publicKey: string | undefined) => { + return useMemo(() => { + if (publicKey) { + return new PublicKey(publicKey) + } + }, [publicKey]) +} diff --git a/src/hooks/useRecipient.tsx b/src/hooks/useRecipient.tsx deleted file mode 100644 index 790e7d06e..000000000 --- a/src/hooks/useRecipient.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { IDL } from '@helium/idls/lib/esm/lazy_distributor' -import { LazyDistributor } from '@helium/idls/lib/types/lazy_distributor' -import { PublicKey } from '@solana/web3.js' -import { IdlAccounts } from '@coral-xyz/anchor' -import { UseAccountState, useIdlAccount } from '@helium/helium-react-hooks' -import { useAsync } from 'react-async-hook' -import { useCallback, useState } from 'react' - -export type Recipient = IdlAccounts['recipientV0'] & { - pubkey: PublicKey -} -const t = 'recipientV0' -export function useRecipient(key: PublicKey): UseAccountState { - const [, updateState] = useState() - - const forceUpdate = useCallback(() => updateState({}), []) - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - const { loading, info } = useIdlAccount( - key, - IDL as LazyDistributor, - t, - ) - - useAsync(async () => { - if (!info && !loading) { - forceUpdate() - } - }, [info, loading]) - - return { - loading, - info: info as Recipient, - } -} diff --git a/src/hooks/useSimulatedTransaction.ts b/src/hooks/useSimulatedTransaction.ts index 88d121a45..7f953d0b2 100644 --- a/src/hooks/useSimulatedTransaction.ts +++ b/src/hooks/useSimulatedTransaction.ts @@ -1,6 +1,5 @@ import { toNumber, truthy, sendAndConfirmWithRetry } from '@helium/spl-utils' import useAlert from '@hooks/useAlert' -import { Metaplex } from '@metaplex-foundation/js' import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { AddressLookupTableAccount, @@ -25,7 +24,7 @@ type BalanceChange = { nativeChange?: number mint?: PublicKey symbol?: string - type?: 'send' | 'recieve' + type?: 'send' | 'receive' } type BalanceChanges = BalanceChange[] | null @@ -43,7 +42,7 @@ export function useSimulatedTransaction( ): SimulatedTransactionResult { const { showOKCancelAlert } = useAlert() const { tokenAccounts } = useBalance() - const { connection, anchorProvider, cluster } = useSolana() + const { connection, anchorProvider } = useSolana() const { t: tr } = useTranslation() const { hntSolConvertTransaction, hntEstimate, hasEnoughSol } = useHntSolConvert() @@ -51,13 +50,6 @@ export function useSimulatedTransaction( const [simulationError, setSimulationError] = useState(false) const [insufficientFunds, setInsufficientFunds] = useState(false) - const metaplex = useMemo(() => { - if (!connection || !cluster) return - return new Metaplex(connection, { - cluster, - }) - }, [connection, cluster]) - const transaction = useMemo(() => { if (!serializedTx) return undefined try { @@ -168,7 +160,6 @@ export function useSimulatedTransaction( !connection || !transaction || !anchorProvider || - !metaplex || !wallet || !simulationAccounts || !tokenAccounts @@ -191,11 +182,8 @@ export function useSimulatedTransaction( if (result?.value.err) { console.warn('failed to simulate', result?.value.err) - if ( - JSON.stringify(result?.value.err).includes( - 'InstructionError":[0,{"Custom":1}]', - ) - ) { + console.warn(result?.value.logs?.join('\n')) + if (JSON.stringify(result?.value.err).includes('{"Custom":1}')) { if (!hasEnoughSol) { await showHNTConversionAlert() } @@ -296,12 +284,12 @@ export function useSimulatedTransaction( const tokenMetadata = await getCollectableByMint( tokenMint, - metaplex, + connection, ) const type = accountNativeBalance.lt(existingNativeBalance) ? 'send' - : 'recieve' + : 'receive' // Filter out zero change if (!accountNativeBalance.eq(existingNativeBalance)) { @@ -345,7 +333,6 @@ export function useSimulatedTransaction( transaction, tokenAccounts, hasEnoughSol, - metaplex, anchorProvider, wallet, ]) diff --git a/src/hooks/useSolanaHealth.ts b/src/hooks/useSolanaHealth.ts index 6121541dd..49f9490ba 100644 --- a/src/hooks/useSolanaHealth.ts +++ b/src/hooks/useSolanaHealth.ts @@ -14,18 +14,17 @@ function doTimeout() { lastUpdated = new Date().valueOf() } } -let cachedHealthByEndpoint: Record = {} +let cachedHealthByEndpoint: Record> = {} async function getCachedHealth(connection: WrappedConnection): Promise { doTimeout() if (!cachedHealthByEndpoint[connection.rpcEndpoint]) { - const { result: health } = await connection.getHealth() - cachedHealthByEndpoint[connection.rpcEndpoint] = health + cachedHealthByEndpoint[connection.rpcEndpoint] = connection.getHealth() } - return cachedHealthByEndpoint[connection.rpcEndpoint] + return (await cachedHealthByEndpoint[connection.rpcEndpoint]).result } -let cachedTps: Record = {} +let cachedTps: Record> = {} async function getCachedTPS(connection: WrappedConnection): Promise { doTimeout() if (!cachedTps[connection.rpcEndpoint]) { diff --git a/src/hooks/useSubmitTxn.ts b/src/hooks/useSubmitTxn.ts index 2450691bc..bdbeef351 100644 --- a/src/hooks/useSubmitTxn.ts +++ b/src/hooks/useSubmitTxn.ts @@ -1,21 +1,23 @@ -import { useCallback } from 'react' -import Balance, { AnyCurrencyType } from '@helium/currency' +import { HotspotType } from '@helium/onboarding' +import { chunks } from '@helium/spl-utils' import { PublicKey, Transaction } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' import i18n from '@utils/i18n' -import { Mints } from '@utils/constants' import * as solUtils from '@utils/solanaUtils' -import { useAccountStorage } from '@storage/AccountStorageProvider' -import { HotspotType } from '@helium/onboarding' +import BN from 'bn.js' +import { useCallback } from 'react' +import { useSolana } from '../solana/SolanaProvider' +import { useWalletSign } from '../solana/WalletSignProvider' import { WalletStandardMessageTypes } from '../solana/walletSignBottomSheetTypes' import { + claimAllRewards, + claimRewards, makeCollectablePayment, makePayment, - claimRewards, - claimAllRewards, sendAnchorTxn, - sendTreasurySwap, - sendMintDataCredits, sendDelegateDataCredits, + sendMintDataCredits, + sendTreasurySwap, sendUpdateIotInfo, sendUpdateMobileInfo, } from '../store/slices/solanaSlice' @@ -24,10 +26,7 @@ import { Collectable, CompressedNFT, HotspotWithPendingRewards, - toMintAddress, } from '../types/solana' -import { useSolana } from '../solana/SolanaProvider' -import { useWalletSign } from '../solana/WalletSignProvider' export default () => { const { currentAccount } = useAccountStorage() @@ -41,9 +40,10 @@ export default () => { async ( payments: { payee: string - balanceAmount: Balance + balanceAmount: BN max?: boolean }[], + mint: PublicKey, ) => { if ( !currentAccount?.solanaAddress || @@ -53,28 +53,28 @@ export default () => { throw new Error(t('errors.account')) } - const [firstPayment] = payments - const mintAddress = - firstPayment.balanceAmount.type.ticker !== 'SOL' - ? toMintAddress(firstPayment.balanceAmount.type.ticker, Mints) - : undefined - const paymentTxn = await solUtils.transferToken( - anchorProvider, - currentAccount.solanaAddress, - currentAccount.address, - payments, - mintAddress, + const txns = await Promise.all( + chunks(payments, 5).map((p) => { + return solUtils.transferToken( + anchorProvider, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + currentAccount.solanaAddress!, + currentAccount.address, + p, + mint.toBase58(), + ) + }), ) - const serializedTx = paymentTxn.serialize({ - requireAllSignatures: false, - }) - const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', additionalMessage: t('transactions.signPaymentTxn'), - serializedTxs: [Buffer.from(serializedTx)], + serializedTxs: txns.map((tx) => + tx.serialize({ + requireAllSignatures: false, + }), + ), }) if (!decision) { @@ -83,7 +83,7 @@ export default () => { dispatch( makePayment({ - paymentTxn, + paymentTxns: txns, account: currentAccount, cluster, anchorProvider, @@ -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, }), @@ -357,13 +335,7 @@ export default () => { }, []) const submitMintDataCredits = useCallback( - async ({ - dcAmount, - recipient, - }: { - dcAmount: number - recipient: PublicKey - }) => { + async ({ dcAmount, recipient }: { dcAmount: BN; recipient: PublicKey }) => { if (!currentAccount || !anchorProvider || !walletSignBottomSheetRef) { throw new Error(t('errors.account')) } diff --git a/src/hooks/useTreasuryManagement.tsx b/src/hooks/useTreasuryManagement.tsx index 93e9c0d56..973bcaf1d 100644 --- a/src/hooks/useTreasuryManagement.tsx +++ b/src/hooks/useTreasuryManagement.tsx @@ -1,8 +1,8 @@ -import { IDL } from '@helium/idls/lib/esm/treasury_management' -import { TreasuryManagement as TreasuryManagementType } from '@helium/idls/lib/types/treasury_management' import { IdlAccounts } from '@coral-xyz/anchor' +import { UseAccountState } from '@helium/account-fetch-cache-hooks' +import { useAnchorAccount } from '@helium/helium-react-hooks' +import { TreasuryManagement as TreasuryManagementType } from '@helium/idls/lib/types/treasury_management' import { PublicKey } from '@solana/web3.js' -import { UseAccountState, useIdlAccount } from '@helium/helium-react-hooks' export type TreasuryManagement = IdlAccounts['treasuryManagementV0'] & { @@ -14,9 +14,8 @@ export function useTreasuryManagement( ): UseAccountState { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return useIdlAccount( + return useAnchorAccount( key, - IDL as TreasuryManagementType, type, ) } diff --git a/src/hooks/useTreasuryPrice.tsx b/src/hooks/useTreasuryPrice.tsx index 4bf3cce85..91983472a 100644 --- a/src/hooks/useTreasuryPrice.tsx +++ b/src/hooks/useTreasuryPrice.tsx @@ -44,8 +44,7 @@ export function useTreasuryPrice( // only works for basic exponential curves // dR = (R / S^(1 + k)) ((S + dS)^(1 + k) - S^(1 + k)) const S = Number( - (fromMintAcc as any).info.supply / - BigInt(Math.pow(10, (fromMintAcc as any).info.decimals)), + fromMintAcc.supply / BigInt(Math.pow(10, fromMintAcc.decimals)), ) const R = amountAsNum(r, rDecimals) diff --git a/src/locales/en.ts b/src/locales/en.ts index 3db9cc0e7..db630f4d0 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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?', @@ -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', @@ -324,6 +324,21 @@ export default { locationNotFound: 'Location not found, Please try again.', mobileTitle: 'MOBILE', }, + antennaSetupScreen: { + title: 'Antenna Setup', + antennaSetup: 'Antenna Setup', + antennaSetupDescription: + 'Submit gain and elevation details for your Hotspot', + gainPlaceholder: 'TX / RX Gain (dBi)', + elevationPlaceholder: 'Elevation (meters)', + submit: 'Update Antenna', + settingUp: 'Setting up your antenna...', + settingUpBody: 'Please wait while we update your Antenna!', + settingUpError: 'Antenna Setup failed. Please try again later.', + settingUpComplete: 'Antenna Setup!', + settingUpCompleteBody: + 'We’ve updated the gain and elevation of your antenna.', + }, swapsScreen: { title: 'Swap my Tokens', swapTokens: 'Swap Tokens', @@ -332,7 +347,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.', @@ -382,7 +397,7 @@ export default { connectToWebsitesYouTrust: 'Only connect to websites you trust', estimatedChanges: 'Estimated Changes', sendToken: 'Send {{amount}} {{ticker}}', - recieveToken: 'Receive {{amount}} {{ticker}}', + receiveToken: 'Receive {{amount}} {{ticker}}', insufficientFunds: 'Insufficient funds', insufficientRentExempt: 'Solana wallets must have a minimum of ~{{amount}} SOL to cover rent. The result of this transaction would leave your wallet with less than the rent-exempt minimum.', @@ -399,6 +414,7 @@ export default { }, accountTokenList: { tokens: 'Tokens', + manage: 'Manage Visible Tokens', }, accountView: { balance: 'Balance', @@ -803,14 +819,14 @@ export default { alertTitle: 'Are you sure?', done: 'Done', subtitle: - 'Do not share your private key!\n\nIf someone has your private key they will have full control of your wallet!', + 'Do not share your private key!\n\nIf someone has your private key they will have full control of your wallet! Do not enter this into any websites. Any individual asking for this key is likely a scammer.', tap: 'Tap to reveal your private key', title: 'Your Private Key', }, revealWords: { next: 'I have written these down', subtitle: - 'It is crucial you write all of these\n{{numWords}} words down, in order.\n\nHelium cannot recover these words.', + 'Never give these words to anyone, or enter them into any website. Any person or website asking for these words is likely a scammer. It is crucial you write all of these\n{{numWords}} words down, in order, and keep them safe.\n\nHelium cannot recover these words.', title: 'Your {{numWords}} Word Password', warning: 'Helium cannot recover these words', }, @@ -951,8 +967,7 @@ export default { newOwner: 'New Owner', oldAddress: 'Old Address', oldOwner: 'Old Owner', - owner: 'Owner', - payee: 'Payee {{index}}', + owner: 'Owner {{index}}', pending: { inProcess: 'In Process', pending: 'Pending', diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx index b6bdc8e58..38eeb4c94 100644 --- a/src/navigation/RootNavigator.tsx +++ b/src/navigation/RootNavigator.tsx @@ -1,31 +1,31 @@ -import React, { memo, useCallback, useEffect, useRef } from 'react' -import changeNavigationBarColor from 'react-native-navigation-bar-color' +import { useNavigation } from '@react-navigation/native' import { - createStackNavigator, StackNavigationOptions, + createStackNavigator, } from '@react-navigation/stack' -import { useNavigation } from '@react-navigation/native' -import { useSelector } from 'react-redux' import { useColors } from '@theme/themeHooks' -import { - RootStackParamList, - RootNavigationProp, - TabBarNavigationProp, -} from './rootTypes' -import OnboardingNavigator from '../features/onboarding/OnboardingNavigator' -import TabBarNavigator from './TabBarNavigator' -import { HomeNavigationProp } from '../features/home/homeTypes' +import React, { memo, useCallback, useEffect, useRef } from 'react' +import changeNavigationBarColor from 'react-native-navigation-bar-color' +import { useSelector } from 'react-redux' +import DappLoginScreen from '../features/dappLogin/DappLoginScreen' import ConnectedWallets, { ConnectedWalletsRef, } from '../features/account/ConnectedWallets' +import { HomeNavigationProp } from '../features/home/homeTypes' +import OnboardingNavigator from '../features/onboarding/OnboardingNavigator' +import ImportPrivateKey from '../features/onboarding/import/ImportPrivateKey' +import PaymentScreen from '../features/payment/PaymentScreen' +import LinkWallet from '../features/txnDelegation/LinkWallet' +import SignHotspot from '../features/txnDelegation/SignHotspot' import { RootState } from '../store/rootReducer' import { appSlice } from '../store/slices/appSlice' import { useAppDispatch } from '../store/store' -import LinkWallet from '../features/txnDelegation/LinkWallet' -import PaymentScreen from '../features/payment/PaymentScreen' -import SignHotspot from '../features/txnDelegation/SignHotspot' -import DappLoginScreen from '../features/dappLogin/DappLoginScreen' -import ImportPrivateKey from '../features/onboarding/import/ImportPrivateKey' +import TabBarNavigator from './TabBarNavigator' +import { + RootNavigationProp, + RootStackParamList, + TabBarNavigationProp, +} from './rootTypes' const screenOptions = { headerShown: false } as StackNavigationOptions diff --git a/src/navigation/TabBarNavigator.tsx b/src/navigation/TabBarNavigator.tsx index 94716f85f..2134934bc 100644 --- a/src/navigation/TabBarNavigator.tsx +++ b/src/navigation/TabBarNavigator.tsx @@ -201,6 +201,7 @@ const TabBarNavigator = () => { tabBar={(props: BottomTabBarProps) => } screenOptions={{ headerShown: false, + lazy: true, }} sceneContainerStyle={{ paddingBottom: NavBarHeight + bottom, diff --git a/src/solana/SolanaProvider.tsx b/src/solana/SolanaProvider.tsx index 304904d35..238fd564b 100644 --- a/src/solana/SolanaProvider.tsx +++ b/src/solana/SolanaProvider.tsx @@ -1,30 +1,37 @@ +import { AnchorProvider, Wallet } from '@coral-xyz/anchor' +import { AccountFetchCache } from '@helium/account-fetch-cache' +import { init as initDc } from '@helium/data-credits-sdk' +import { init as initHem } from '@helium/helium-entity-manager-sdk' +import { init as initHsd } from '@helium/helium-sub-daos-sdk' +import { init as initLazy } from '@helium/lazy-distributor-sdk' +import { DC_MINT, HNT_MINT } from '@helium/spl-utils' +import { + AccountInfo, + Cluster, + Commitment, + PublicKey, + RpcResponseAndContext, + Transaction, +} from '@solana/web3.js' import React, { - createContext, ReactNode, - useContext, + createContext, useCallback, - useState, - useRef, + useContext, useEffect, + useMemo, + useState, } from 'react' -import { init as initHsd } from '@helium/helium-sub-daos-sdk' -import { init as initDc } from '@helium/data-credits-sdk' -import { init as initHem } from '@helium/helium-entity-manager-sdk' -import { init as initLazy } from '@helium/lazy-distributor-sdk' -import { AnchorProvider, Wallet } from '@coral-xyz/anchor' +import { useAsync } from 'react-async-hook' import Config from 'react-native-config' import { useSelector } from 'react-redux' -import { Cluster, Transaction } from '@solana/web3.js' -import { AccountFetchCache } from '@helium/account-fetch-cache' import { useAccountStorage } from '../storage/AccountStorageProvider' import { getSessionKey, getSolanaKeypair } from '../storage/secureStorage' -import { getConnection } from '../utils/solanaUtils' import { RootState } from '../store/rootReducer' import { appSlice } from '../store/slices/appSlice' import { useAppDispatch } from '../store/store' -import usePrevious from '../hooks/usePrevious' -import { WrappedConnection } from '../utils/WrappedConnection' import { DcProgram, HemProgram, HsdProgram, LazyProgram } from '../types/solana' +import { getConnection } from '../utils/solanaUtils' const useSolanaHook = () => { const { currentAccount } = useAccountStorage() @@ -32,50 +39,34 @@ const useSolanaHook = () => { const cluster = useSelector( (state: RootState) => state.app.cluster || 'mainnet-beta', ) - const [connection, setConnection] = useState() const [dcProgram, setDcProgram] = useState() const [hemProgram, setHemProgram] = useState() const [hsdProgram, setHsdProgram] = useState() const [lazyProgram, setLazyProgram] = useState() - const [anchorProvider, setAnchorProvider] = useState() - const [cache, setCache] = useState() - - const initialized = useRef(false) - const prevAddress = usePrevious(currentAccount?.address) - const prevCluster = usePrevious(cluster) - - const handleConnectionChanged = useCallback(async () => { - if (!cluster) return + const { loading, result: sessionKey } = useAsync(getSessionKey, []) + const connection = useMemo(() => { + const sessionKeyActual = + !loading && !sessionKey ? Config.RPC_SESSION_KEY_FALLBACK : sessionKey - const sessionKey = - (await getSessionKey()) || Config.RPC_SESSION_KEY_FALLBACK - const nextConn = getConnection(cluster, sessionKey) - - if (!currentAccount?.address) { - setConnection(nextConn) - return - } - - if ( - initialized.current && - prevAddress === currentAccount.address && - prevCluster === cluster - ) { - return + if (sessionKeyActual) { + return getConnection(cluster, sessionKeyActual) } + }, [cluster, sessionKey, loading]) + const address = useMemo( + () => currentAccount?.address, + [currentAccount?.address], + ) + const { result: secureAcct } = useAsync( + async (addr: string | undefined) => { + if (addr) { + return getSolanaKeypair(addr) + } + }, + [address], + ) - initialized.current = true - - if ( - nextConn.baseURL === connection?.baseURL && - prevAddress === currentAccount.address - ) - return - - setConnection(nextConn) - - const secureAcct = await getSolanaKeypair(currentAccount.address) - if (!secureAcct) return + const anchorProvider = useMemo(() => { + if (!secureAcct || !connection) return const anchorWallet = { signTransaction: async (transaction: Transaction) => { @@ -92,40 +83,70 @@ const useSolanaHook = () => { return secureAcct?.publicKey }, } as Wallet - - const nextProvider = new AnchorProvider(nextConn, anchorWallet, { + return new AnchorProvider(connection, anchorWallet, { preflightCommitment: 'confirmed', commitment: 'confirmed', }) + }, [connection, secureAcct]) - setAnchorProvider(nextProvider) - initHem(nextProvider).then(setHemProgram) - initHsd(nextProvider).then(setHsdProgram) - initDc(nextProvider).then(setDcProgram) - initLazy(nextProvider).then(setLazyProgram) - - cache?.close() - setCache( - new AccountFetchCache({ - connection: nextConn, - delay: 100, - commitment: 'confirmed', - missingRefetchDelay: 60 * 1000, - extendConnection: true, - }), - ) - }, [ - cache, - cluster, - connection?.baseURL, - currentAccount, - prevAddress, - prevCluster, - ]) + const cache = useMemo(() => { + if (!connection) return + + const c = new AccountFetchCache({ + connection, + delay: 100, + commitment: 'confirmed', + missingRefetchDelay: 60 * 1000, + extendConnection: true, + }) + const oldGetAccountinfoAndContext = + connection.getAccountInfoAndContext.bind(connection) + + // Anchor uses this call on .fetch and .fetchNullable even though it doesn't actually need the context. Add caching. + connection.getAccountInfoAndContext = async ( + publicKey: PublicKey, + com?: Commitment, + ): Promise | null>> => { + if ( + (com || connection.commitment) === 'confirmed' || + typeof (com || connection.commitment) === 'undefined' + ) { + const [result, dispose] = await c.searchAndWatch(publicKey) + setTimeout(dispose, 30 * 1000) // cache for 30s + return { + value: result?.account || null, + context: { + slot: 0, + }, + } + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return oldGetAccountinfoAndContext!(publicKey, com) + } + + return c + }, [connection]) + useEffect(() => { + // Don't sub to hnt or dc they change a bunch + cache?.statics.add(HNT_MINT.toBase58()) + cache?.statics.add(DC_MINT.toBase58()) + + return () => cache?.close() + }, [cache]) + + const handleConnectionChanged = useCallback(async () => { + if (!anchorProvider) return + + initHem(anchorProvider).then(setHemProgram) + initHsd(anchorProvider).then(setHsdProgram) + initDc(anchorProvider).then(setDcProgram) + initLazy(anchorProvider).then(setLazyProgram) + }, [anchorProvider]) useEffect(() => { handleConnectionChanged() - }, [cluster, currentAccount, handleConnectionChanged]) + }, [cluster, address, handleConnectionChanged]) const updateCluster = useCallback( (nextCluster: Cluster) => { @@ -143,6 +164,7 @@ const useSolanaHook = () => { hsdProgram, lazyProgram, updateCluster, + cache, } } @@ -154,6 +176,7 @@ const initialState = { hemProgram: undefined, hsdProgram: undefined, lazyProgram: undefined, + cache: undefined, updateCluster: (_nextCluster: Cluster) => {}, } const SolanaContext = diff --git a/src/solana/WalletSignBottomSheet.tsx b/src/solana/WalletSignBottomSheet.tsx index 3913002e5..fec09339b 100644 --- a/src/solana/WalletSignBottomSheet.tsx +++ b/src/solana/WalletSignBottomSheet.tsx @@ -1,7 +1,25 @@ +import Checkmark from '@assets/images/checkmark.svg' +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetModalProvider, + useBottomSheetDynamicSnapPoints, +} from '@gorhom/bottom-sheet' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { usePublicKey } from '@hooks/usePublicKey' +import { useRentExempt } from '@hooks/useRentExempt' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { useColors, useOpacity } from '@theme/themeHooks' +import BN from 'bn.js' import React, { + Ref, forwardRef, memo, - Ref, useCallback, useEffect, useImperativeHandle, @@ -9,31 +27,16 @@ import React, { useRef, useState, } from 'react' -import Checkmark from '@assets/images/checkmark.svg' import { useTranslation } from 'react-i18next' -import { - BottomSheetBackdrop, - useBottomSheetDynamicSnapPoints, - BottomSheetModal, - BottomSheetModalProvider, -} from '@gorhom/bottom-sheet' -import { Edge } from 'react-native-safe-area-context' -import SafeAreaBox from '@components/SafeAreaBox' -import Box from '@components/Box' -import Text from '@components/Text' -import { useColors, useOpacity } from '@theme/themeHooks' -import ButtonPressable from '@components/ButtonPressable' -import { LAMPORTS_PER_SOL } from '@solana/web3.js' -import { useBalance } from '@utils/Balance' import { ScrollView } from 'react-native-gesture-handler' -import { useRentExempt } from '@hooks/useRentExempt' +import { Edge } from 'react-native-safe-area-context' +import WalletSignBottomSheetTransaction from './WalletSignBottomSheetTransaction' import { - WalletSignBottomSheetRef, WalletSignBottomSheetProps, + WalletSignBottomSheetRef, WalletSignOpts, WalletStandardMessageTypes, } from './walletSignBottomSheetTypes' -import WalletSignBottomSheetTransaction from './WalletSignBottomSheetTransaction' let promiseResolve: (value: boolean | PromiseLike) => void @@ -47,7 +50,9 @@ const WalletSignBottomSheet = forwardRef( const { backgroundStyle } = useOpacity('surfaceSecondary', 1) const { secondaryText } = useColors() const { t } = useTranslation() - const { solBalance } = useBalance() + const { currentAccount } = useAccountStorage() + const solanaAddress = usePublicKey(currentAccount?.solanaAddress) + const { amount: solBalance } = useSolOwnedAmount(solanaAddress) const bottomSheetModalRef = useRef(null) const [totalSolFee, setTotalSolFee] = useState(0) const [isVisible, setIsVisible] = useState(false) @@ -96,22 +101,21 @@ const WalletSignBottomSheet = forwardRef( return 5000 / LAMPORTS_PER_SOL }, [walletSignOpts, totalSolFee, currentTxs]) - const insufficientRentExempt = useMemo( - () => - (solBalance?.floatBalance || 0) - estimatedTotalSolByLamports < - (rentExempt || 0), - [solBalance?.floatBalance, estimatedTotalSolByLamports, rentExempt], - ) + const insufficientRentExempt = useMemo(() => { + if (solBalance) { + return new BN(solBalance.toString()) + .sub(new BN(estimatedTotalSolByLamports)) + .lt(new BN(rentExempt || 0)) + } + }, [solBalance, estimatedTotalSolByLamports, rentExempt]) const insufficientFunds = useMemo( () => nestedInsufficentFunds || - estimatedTotalSolByLamports > (solBalance?.floatBalance || 0), - [ - solBalance?.floatBalance, - estimatedTotalSolByLamports, - nestedInsufficentFunds, - ], + new BN(estimatedTotalSolByLamports).gt( + new BN(solBalance?.toString() || '0'), + ), + [solBalance, estimatedTotalSolByLamports, nestedInsufficentFunds], ) const safeEdges = useMemo(() => ['bottom'] as Edge[], []) diff --git a/src/solana/WalletSignBottomSheetTransaction.tsx b/src/solana/WalletSignBottomSheetTransaction.tsx index ca54e9a31..fde0b60e8 100644 --- a/src/solana/WalletSignBottomSheetTransaction.tsx +++ b/src/solana/WalletSignBottomSheetTransaction.tsx @@ -100,7 +100,7 @@ const WalletSignBottomSheetTransaction = ({ amount: change.nativeChange, }) } else { - balanceChange = t('browserScreen.recieveToken', { + balanceChange = t('browserScreen.receiveToken', { ticker: change.symbol, amount: change.nativeChange, }) diff --git a/src/storage/TokensProvider.tsx b/src/storage/TokensProvider.tsx new file mode 100644 index 000000000..da28a47f5 --- /dev/null +++ b/src/storage/TokensProvider.tsx @@ -0,0 +1,108 @@ +import { DC_MINT, HNT_MINT, IOT_MINT, MOBILE_MINT } from '@helium/spl-utils' +import { NATIVE_MINT } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' +import React, { + ReactNode, + createContext, + useCallback, + useContext, + useState, +} from 'react' +import { useAsync } from 'react-async-hook' +import * as Logger from '../utils/logger' +import { useAccountStorage } from './AccountStorageProvider' +import { + CSToken, + restoreVisibleTokens, + updateVisibleTokens, +} from './cloudStorage' + +export const DEFAULT_TOKENS = new Set([ + HNT_MINT.toBase58(), + MOBILE_MINT.toBase58(), + IOT_MINT.toBase58(), + DC_MINT.toBase58(), + NATIVE_MINT.toBase58(), +]) + +const useVisibleTokensHook = () => { + const { currentAccount } = useAccountStorage() + const [visibleTokens, setVisibleTokens] = useState< + Record> + >({ + [currentAccount?.address || '']: DEFAULT_TOKENS, + }) + + useAsync(async () => { + try { + const response = await restoreVisibleTokens() + + if (response) { + setVisibleTokens( + Object.entries(response).reduce((acc, [key, s]) => { + acc[key] = new Set([...s, ...DEFAULT_TOKENS]) + return acc + }, {} as Record>), + ) + } + } catch { + Logger.error('Restore visible tokens failed') + } + }, []) + + const handleUpdateTokens = useCallback( + (token: PublicKey, value: boolean) => { + if (!currentAccount?.address) return + + const tokens = new Set(visibleTokens[currentAccount.address] || new Set()) + if (value) { + tokens.add(token.toBase58()) + } else { + tokens.delete(token.toBase58()) + } + const newVisibleTokens = { + ...visibleTokens, + [currentAccount.address]: tokens, + } + + updateVisibleTokens( + Object.entries(newVisibleTokens).reduce((acc, [key, s]) => { + acc[key] = Array.from(s) + return acc + }, {} as CSToken), + ) + setVisibleTokens(newVisibleTokens) + }, + [currentAccount?.address, visibleTokens], + ) + + return { + visibleTokens, + setVisibleTokens: handleUpdateTokens, + } +} + +const initialState = { + visibleTokens: {} as Record>, + setVisibleTokens: (_token: PublicKey, _value: boolean) => {}, +} + +const TokensContext = + createContext>(initialState) +const { Provider } = TokensContext + +const TokensProvider = ({ children }: { children: ReactNode }) => { + return {children} +} + +export const useVisibleTokens = () => { + const { currentAccount } = useAccountStorage() + const { visibleTokens, setVisibleTokens } = useContext(TokensContext) + + return { + visibleTokens: visibleTokens[currentAccount?.address || ''] || new Set(), + setVisibleTokens, + } +} + +export default TokensProvider diff --git a/src/storage/cloudStorage.ts b/src/storage/cloudStorage.ts index ce26a3bc9..253d1496c 100644 --- a/src/storage/cloudStorage.ts +++ b/src/storage/cloudStorage.ts @@ -21,6 +21,8 @@ export type CSAccount = { } export type CSAccounts = Record +export type CSToken = Record + // for android we use AsyncStorage and auto backup to Google Drive using // https://developer.android.com/guide/topics/data/autobackup const CloudStorage = Platform.OS === 'ios' ? iCloudStorage : AsyncStorage @@ -29,9 +31,21 @@ enum CloudStorageKeys { ACCOUNTS = 'accounts', CONTACTS = 'contacts', LAST_VIEWED_NOTIFICATIONS = 'lastViewedNotifications', + VISIBLE_TOKENS = 'visibleTokens', DEFAULT_ACCOUNT_ADDRESS = 'defaultAccountAddress', } +export const restoreVisibleTokens = async () => { + const tokens = await getFromCloudStorage( + CloudStorageKeys.VISIBLE_TOKENS, + ) + + return tokens +} + +export const updateVisibleTokens = (tokens: CSToken) => + CloudStorage.setItem(CloudStorageKeys.VISIBLE_TOKENS, JSON.stringify(tokens)) + export const sortAccounts = ( accts: CSAccounts, defaultAddress: string | undefined | null, diff --git a/src/store/slices/balancesSlice.ts b/src/store/slices/balancesSlice.ts index 91f6c55d3..2cc7208b5 100644 --- a/src/store/slices/balancesSlice.ts +++ b/src/store/slices/balancesSlice.ts @@ -1,12 +1,10 @@ import { AnchorProvider } from '@coral-xyz/anchor' import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit' +import { AccountLayout, TOKEN_PROGRAM_ID, getMint } from '@solana/spl-token' import { Cluster, PublicKey } from '@solana/web3.js' -import { AccountLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token' -import BN from 'bn.js' import { CSAccount } from '../../storage/cloudStorage' -import { getBalanceHistory, getTokenPrices } from '../../utils/walletApiV2' import { AccountBalance, Prices, TokenAccount } from '../../types/balance' -import { getEscrowTokenAccount } from '../../utils/solanaUtils' +import { getBalanceHistory, getTokenPrices } from '../../utils/walletApiV2' type BalanceHistoryByCurrency = Record type BalanceHistoryByWallet = Record @@ -15,7 +13,6 @@ type BalanceHistoryByCluster = Record export type Tokens = { atas: TokenAccount[] sol: { tokenAccount: string; balance: number } - dcEscrow: { tokenAccount: string; balance: number } } type AtaBalances = Record> @@ -59,38 +56,24 @@ export const syncTokenAccounts = createAsyncThunk( const tokenAccounts = await connection.getTokenAccountsByOwner(pubKey, { programId: TOKEN_PROGRAM_ID, }) - - const atas = tokenAccounts.value.map((tokenAccount) => { - const accountData = AccountLayout.decode(tokenAccount.account.data) - const { mint } = accountData - - return { - tokenAccount: tokenAccount.pubkey.toBase58(), - mint: mint.toBase58(), - balance: Number(accountData.amount || 0), - } - }) - - const escrowAccount = getEscrowTokenAccount(acct.solanaAddress) - let escrowBalance = 0 - const [dcEscrowAcc, solAcc] = await Promise.all([ - connection.getAccountInfo(escrowAccount), - connection.getAccountInfo(pubKey), - ]) - try { - const dcEscrowBalance = - dcEscrowAcc && AccountLayout.decode(dcEscrowAcc.data).amount - escrowBalance = dcEscrowBalance - ? new BN(dcEscrowBalance.toString()).toNumber() - : 0 - } catch {} - - const dcEscrow = { - tokenAccount: escrowAccount.toBase58(), - balance: escrowBalance, - } - - const solBalance = solAcc?.lamports || 0 + const solAcct = await connection.getAccountInfo(pubKey) + + const atas = await Promise.all( + tokenAccounts.value.map(async (tokenAccount) => { + const accountData = AccountLayout.decode(tokenAccount.account.data) + const { mint } = accountData + const mintAcc = await getMint(connection, mint) + + return { + tokenAccount: tokenAccount.pubkey.toBase58(), + mint: mint.toBase58(), + balance: Number(accountData.amount || 0), + decimals: mintAcc.decimals, + } + }), + ) + + const solBalance = solAcct?.lamports || 0 const sol = { tokenAccount: acct.solanaAddress, balance: solBalance, @@ -98,7 +81,6 @@ export const syncTokenAccounts = createAsyncThunk( return { atas, - dcEscrow, sol, } }, @@ -140,24 +122,16 @@ const balancesSlice = createSlice({ cluster: Cluster solanaAddress: string balance: number - type: 'dcEscrow' | 'sol' tokenAccount: string }>, ) => { const { payload } = action - const { cluster, solanaAddress, balance, type, tokenAccount } = payload + const { cluster, solanaAddress, balance, tokenAccount } = payload const next = { tokenAccount, balance } const prevTokens = state.balances?.[cluster]?.[solanaAddress] if (!prevTokens) return - switch (type) { - case 'dcEscrow': - prevTokens.dcEscrow = next - break - case 'sol': - prevTokens.sol = next - break - } + prevTokens.sol = next }, updateAtaBalance: ( state, @@ -230,5 +204,5 @@ const balancesSlice = createSlice({ }) const { reducer, name } = balancesSlice -export { name, balancesSlice } +export { balancesSlice, name } export default reducer diff --git a/src/store/slices/solanaSlice.ts b/src/store/slices/solanaSlice.ts index f1af96337..0b0d6dcae 100644 --- a/src/store/slices/solanaSlice.ts +++ b/src/store/slices/solanaSlice.ts @@ -1,30 +1,47 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-await-in-loop */ import { AnchorProvider } from '@coral-xyz/anchor' -import { Ticker } from '@helium/currency' +import { + formBulkTransactions, + getBulkRewards, +} from '@helium/distributor-oracle' +import { + decodeEntityKey, + init, + keyToAssetForAsset, +} from '@helium/helium-entity-manager-sdk' +import * as lz from '@helium/lazy-distributor-sdk' import { bulkSendRawTransactions, + bulkSendTransactions, + chunks, sendAndConfirmWithRetry, } from '@helium/spl-utils' import { + PayloadAction, SerializedError, createAsyncThunk, createSlice, } from '@reduxjs/toolkit' import { Cluster, + PublicKey, SignaturesForAddressOptions, Transaction, } from '@solana/web3.js' +import BN from 'bn.js' +import bs58 from 'bs58' import { first, last } from 'lodash' import { CSAccount } from '../../storage/cloudStorage' import { Activity } from '../../types/activity' -import { toMintAddress } from '../../types/solana' +import { HotspotWithPendingRewards } from '../../types/solana' import * as Logger from '../../utils/logger' import * as solUtils from '../../utils/solanaUtils' import { postPayment } from '../../utils/walletApiV2' import { fetchCollectables } from './collectablesSlice' import { fetchHotspots } from './hotspotsSlice' -type TokenActivity = Record +type TokenActivity = Record type SolActivity = { all: TokenActivity @@ -39,6 +56,7 @@ export type SolanaState = { error?: SerializedError success?: boolean signature?: string + progress?: { percent: number; text: string } // 0-100 } activity: { loading?: boolean @@ -56,7 +74,7 @@ type PaymentInput = { account: CSAccount cluster: Cluster anchorProvider: AnchorProvider - paymentTxn: Transaction + paymentTxns: Transaction[] } type CollectablePaymentInput = { @@ -81,7 +99,8 @@ type ClaimRewardInput = { type ClaimAllRewardsInput = { account: CSAccount - txns: Transaction[] + lazyDistributors: PublicKey[] + hotspots: HotspotWithPendingRewards[] anchorProvider: AnchorProvider cluster: Cluster } @@ -120,21 +139,19 @@ type UpdateMobileInfoInput = { export const makePayment = createAsyncThunk( 'solana/makePayment', - async ({ account, cluster, anchorProvider, paymentTxn }: PaymentInput) => { + async ({ account, cluster, anchorProvider, paymentTxns }: PaymentInput) => { if (!account?.solanaAddress) throw new Error('No solana account found') - const signed = await anchorProvider.wallet.signTransaction(paymentTxn) + const signatures = await bulkSendTransactions(anchorProvider, paymentTxns) - const signature = await anchorProvider.sendAndConfirm(signed) - - postPayment({ signature, cluster }) + postPayment({ signatures, cluster }) postPayment({ - signature, + signatures, cluster, }) - return signature + return signatures }, ) @@ -151,7 +168,7 @@ export const makeCollectablePayment = createAsyncThunk( const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) dispatch( fetchCollectables({ @@ -177,7 +194,7 @@ export const sendTreasurySwap = createAsyncThunk( const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) } catch (error) { Logger.error(error) throw error @@ -194,7 +211,7 @@ export const sendMintDataCredits = createAsyncThunk( const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) } catch (error) { Logger.error(error) throw error @@ -215,7 +232,7 @@ export const sendDelegateDataCredits = createAsyncThunk( const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) } catch (error) { Logger.error(error) throw error @@ -241,7 +258,7 @@ export const sendAnchorTxn = createAsyncThunk( 'confirmed', ) - postPayment({ signature: txid, cluster }) + postPayment({ signatures: [txid], cluster }) } catch (error) { Logger.error(error) throw error @@ -257,20 +274,15 @@ export const claimRewards = createAsyncThunk( { dispatch }, ) => { try { - const signed = await anchorProvider.wallet.signAllTransactions(txns) + const signatures = await bulkSendTransactions(anchorProvider, txns) - const sigs = await bulkSendRawTransactions( - anchorProvider.connection, - signed.map((s) => s.serialize()), - ) - - postPayment({ signature: sigs[0], cluster }) + postPayment({ signatures, cluster }) // If the transfer is successful, we need to update the hotspots so pending rewards are updated. dispatch(fetchHotspots({ account, anchorProvider, cluster })) return { - signature: sigs[0], + signatures, } } catch (error) { Logger.error(error) @@ -279,20 +291,170 @@ export const claimRewards = createAsyncThunk( }, ) +const CHUNK_SIZE = 25 export const claimAllRewards = createAsyncThunk( 'solana/claimAllRewards', async ( - { account, anchorProvider, cluster, txns }: ClaimAllRewardsInput, + { + account, + anchorProvider, + cluster, + lazyDistributors, + hotspots, + }: ClaimAllRewardsInput, { dispatch }, ) => { try { - const signed = await anchorProvider.wallet.signAllTransactions(txns) - - // eslint-disable-next-line no-await-in-loop - await bulkSendRawTransactions( - anchorProvider.connection, - signed.map((s) => s.serialize()), + const ret: string[] = [] + let triesRemaining = 10 + const program = await lz.init(anchorProvider) + const hemProgram = await init(anchorProvider) + + const mints = await Promise.all( + lazyDistributors.map(async (d) => { + return (await program.account.lazyDistributorV0.fetch(d)).rewardsMint + }), + ) + const ldToMint = lazyDistributors.reduce((acc, ld, index) => { + acc[ld.toBase58()] = mints[index] + return acc + }, {} as Record) + // One tx per hotspot per mint/lazy dist + const totalTxns = hotspots.reduce((acc, hotspot) => { + mints.forEach((mint) => { + if ( + hotspot.pendingRewards && + hotspot.pendingRewards[mint.toString()] && + new BN(hotspot.pendingRewards[mint.toString()]).gt(new BN(0)) + ) + acc += 1 + }) + return acc + }, 0) + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: 0, + text: 'Preparing transactions...', + }), ) + for (const lazyDistributor of lazyDistributors) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mint = ldToMint[lazyDistributor.toBase58()]! + const hotspotsWithRewards = hotspots.filter( + (hotspot) => + hotspot.pendingRewards && + new BN(hotspot.pendingRewards[mint.toBase58()]).gt(new BN(0)), + ) + for (let chunk of chunks(hotspotsWithRewards, CHUNK_SIZE)) { + const thisRet: string[] = [] + // Continually send in bulk while resetting blockhash until we send them all + // eslint-disable-next-line no-constant-condition + while (true) { + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: ((ret.length + thisRet.length) * 100) / totalTxns, + text: `Preparing batch of ${chunk.length} transactions.\n${ + totalTxns - ret.length + } total transactions remaining.`, + }), + ) + const recentBlockhash = + // eslint-disable-next-line no-await-in-loop + await anchorProvider.connection.getLatestBlockhash('confirmed') + + const keyToAssets = chunk.map((h) => + keyToAssetForAsset(solUtils.toAsset(h)), + ) + const ktaAccs = await Promise.all( + keyToAssets.map((kta) => + hemProgram.account.keyToAssetV0.fetch(kta), + ), + ) + const entityKeys = ktaAccs.map( + (kta) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + decodeEntityKey(kta.entityKey, kta.keySerialization)!, + ) + + const rewards = await getBulkRewards( + program, + lazyDistributor, + entityKeys, + ) + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: ((ret.length + thisRet.length) * 100) / totalTxns, + text: `Sending batch of ${chunk.length} transactions.\n${ + totalTxns - ret.length + } total transactions remaining.`, + }), + ) + + const txns = await formBulkTransactions({ + program, + rewards, + assets: chunk.map((h) => new PublicKey(h.id)), + compressionAssetAccs: chunk.map(solUtils.toAsset), + lazyDistributor, + assetEndpoint: anchorProvider.connection.rpcEndpoint, + wallet: anchorProvider.wallet.publicKey, + }) + const signedTxs = await anchorProvider.wallet.signAllTransactions( + txns, + ) + // eslint-disable-next-line @typescript-eslint/no-loop-func + const txsWithSigs = signedTxs.map((tx, index) => { + return { + transaction: chunk[index], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sig: bs58.encode(tx.signatures[0]!.signature!), + } + }) + // eslint-disable-next-line no-await-in-loop + const confirmedTxs = await bulkSendRawTransactions( + anchorProvider.connection, + signedTxs.map((s) => s.serialize()), + ({ totalProgress }) => + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: + ((totalProgress + ret.length + thisRet.length) * 100) / + totalTxns, + text: `Confiming ${txns.length - totalProgress}/${ + txns.length + } transactions.\n${ + totalTxns - ret.length - thisRet.length + } total transactions remaining`, + }), + ), + recentBlockhash.lastValidBlockHeight, + // Hail mary, try with preflight enabled. Sometimes this causes + // errors that wouldn't otherwise happen + triesRemaining !== 1, + ) + thisRet.push(...confirmedTxs) + if (confirmedTxs.length === signedTxs.length) { + break + } + + const retSet = new Set(thisRet) + + chunk = txsWithSigs + .filter(({ sig }) => !retSet.has(sig)) + .map(({ transaction }) => transaction) + + triesRemaining -= 1 + if (triesRemaining <= 0) { + throw new Error( + `Failed to submit all txs after blockhashes expired, ${ + signedTxs.length - confirmedTxs.length + } remain`, + ) + } + } + ret.push(...thisRet) + } + } // If the claim is successful, we need to update the hotspots so pending rewards are updated. dispatch(fetchHotspots({ account, anchorProvider, cluster })) @@ -309,14 +471,12 @@ export const getTxns = createAsyncThunk( { account, anchorProvider, - ticker, + mint, requestType, - mints, }: { account: CSAccount anchorProvider: AnchorProvider - ticker: Ticker - mints: Record + mint: string requestType: 'update_head' | 'start_fresh' | 'fetch_more' }, { getState }, @@ -331,7 +491,7 @@ export const getTxns = createAsyncThunk( solana: SolanaState } - const existing = solana.activity.data[account.solanaAddress]?.all?.[ticker] + const existing = solana.activity.data[account.solanaAddress]?.all?.[mint] if (requestType === 'fetch_more') { const lastActivity = last(existing) @@ -351,8 +511,7 @@ export const getTxns = createAsyncThunk( return solUtils.getTransactions( anchorProvider, account.solanaAddress, - toMintAddress(ticker, mints), - mints, + mint, options, ) }, @@ -365,7 +524,7 @@ export const sendUpdateIotInfo = createAsyncThunk( const signed = await anchorProvider.wallet.signTransaction(updateTxn) const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) } catch (error) { Logger.error(error) throw error @@ -381,7 +540,7 @@ export const sendUpdateMobileInfo = createAsyncThunk( const signed = await anchorProvider.wallet.signTransaction(updateTxn) const sig = await anchorProvider.sendAndConfirm(signed) - postPayment({ signature: sig, cluster }) + postPayment({ signatures: [sig], cluster }) } catch (error) { Logger.error(error) throw error @@ -394,12 +553,21 @@ const solanaSlice = createSlice({ name: 'solana', initialState, reducers: { + setPaymentProgress: ( + state, + action: PayloadAction<{ percent: number; text: string }>, + ) => { + if (state.payment) { + state.payment.progress = action.payload + } + }, resetPayment: (state) => { state.payment = { success: false, loading: false, error: undefined, signature: undefined, + progress: undefined, } }, }, @@ -435,6 +603,7 @@ const solanaSlice = createSlice({ success: true, loading: false, error: undefined, + progress: undefined, } }) builder.addCase(claimRewards.rejected, (state, action) => { @@ -443,6 +612,7 @@ const solanaSlice = createSlice({ loading: false, error: action.error, signature: undefined, + progress: undefined, } }) builder.addCase(claimRewards.pending, (state, _action) => { @@ -451,15 +621,17 @@ const solanaSlice = createSlice({ loading: true, error: undefined, signature: undefined, + progress: undefined, } }) builder.addCase(claimRewards.fulfilled, (state, _action) => { - const { signature } = _action.payload + const { signatures } = _action.payload state.payment = { success: true, loading: false, error: undefined, - signature, + signature: signatures[0], + progress: undefined, } }) builder.addCase(sendAnchorTxn.rejected, (state, action) => { @@ -591,7 +763,7 @@ const solanaSlice = createSlice({ if (!meta.arg.account.solanaAddress) return const { - ticker, + mint, account: { solanaAddress: address }, requestType, } = meta.arg @@ -615,47 +787,46 @@ const solanaSlice = createSlice({ if (!state.activity.data[address].mint) { state.activity.data[address].mint = state.activity.data[address].all } - - const prevAll = state.activity.data[address].all[ticker] - const prevPayment = state.activity.data[address].payment[ticker] - const prevDelegate = state.activity.data[address].delegate[ticker] - const prevMint = state.activity.data[address].mint[ticker] + const prevAll = state.activity.data[address].all[mint] + const prevPayment = state.activity.data[address].payment[mint] + const prevDelegate = state.activity.data[address].delegate[mint] + const prevMint = state.activity.data[address].mint[mint] switch (requestType) { case 'start_fresh': { - state.activity.data[address].all[ticker] = payload - state.activity.data[address].payment[ticker] = payload - state.activity.data[address].delegate[ticker] = payload - state.activity.data[address].mint[ticker] = payload + state.activity.data[address].all[mint] = payload + state.activity.data[address].payment[mint] = payload + state.activity.data[address].delegate[mint] = payload + state.activity.data[address].mint[mint] = payload break } case 'fetch_more': { - state.activity.data[address].all[ticker] = [...prevAll, ...payload] - state.activity.data[address].payment[ticker] = [ + state.activity.data[address].all[mint] = [...prevAll, ...payload] + state.activity.data[address].payment[mint] = [ ...prevPayment, ...payload, ] - state.activity.data[address].delegate[ticker] = [ + state.activity.data[address].delegate[mint] = [ ...prevDelegate, ...payload, ] - state.activity.data[address].mint[ticker] = [ + state.activity.data[address].mint[mint] = [ ...prevDelegate, ...payload, ] break } case 'update_head': { - state.activity.data[address].all[ticker] = [...payload, ...prevAll] - state.activity.data[address].payment[ticker] = [ + state.activity.data[address].all[mint] = [...payload, ...prevAll] + state.activity.data[address].payment[mint] = [ ...payload, ...prevPayment, ] - state.activity.data[address].delegate[ticker] = [ + state.activity.data[address].delegate[mint] = [ ...payload, ...prevDelegate, ] - state.activity.data[address].mint[ticker] = [...payload, ...prevMint] + state.activity.data[address].mint[mint] = [...payload, ...prevMint] break } } diff --git a/src/store/store.ts b/src/store/store.ts index 163dcfbea..97ad74d3a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -33,6 +33,7 @@ const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false, + immutableCheck: { warnAfter: 500 }, }).concat([solanaStatusApi.middleware]), enhancers, }) diff --git a/src/types/activity.ts b/src/types/activity.ts index 146846559..01cbaf275 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -1,53 +1,21 @@ -import { Ticker } from '@helium/currency' - export type Activity = { - account?: null | string address?: null | string amount?: null | number - amountToSeller?: null | number buyer?: null | string - elevation?: null | number - endEpoch?: null | number + feePayer?: string fee?: null | number - gain?: null | number gateway?: null | string hash: string height?: null | number - lat?: null | number - lng?: null | number - location?: null | string - memo?: null | string - newAddress?: null | string - newOwner?: null | string - nonce?: null | number - oldAddress?: null | string - oldOwner?: null | string - owner?: null | string - payee?: null | string - payer?: null | string payments?: null | Array pending?: null | boolean - rewards?: null | Array - seller?: null | string - stake?: null | number - stakeAmount?: null | number - stakingFee?: null | number - startEpoch?: null | number time?: null | number - tokenType?: null | Ticker type: string } export type Payment = { amount: number memo?: null | string - payee: string - tokenType?: null | Ticker -} - -export type Reward = { - account?: null | string - amount: number - gateway?: null | string - type: string + owner: string + mint?: null | string } diff --git a/src/types/balance.ts b/src/types/balance.ts index 1033b1c83..36dec160c 100644 --- a/src/types/balance.ts +++ b/src/types/balance.ts @@ -1,15 +1,8 @@ -import Balance, { - DataCredits, - IotTokens, - MobileTokens, - NetworkTokens, - SolTokens, -} from '@helium/currency' - export type TokenAccount = { tokenAccount?: string mint: string balance: number + decimals: number } export type AccountBalance = { @@ -32,9 +25,6 @@ export type Prices = Record> export type BalanceInfo = { atas: Required[] - dcBalance: Balance - dcEscrowBalance: Balance - dcEscrowToken: Omit formattedDcValue: string formattedEscrowDcValue: string formattedHntValue: string @@ -42,13 +32,4 @@ export type BalanceInfo = { formattedMobileValue: string formattedSolValue: string formattedTotal: string - hntBalance: Balance - hntValue: number - iotBalance: Balance - iotValue: number - mobileBalance: Balance - mobileValue: number - solBalance: Balance - solToken: Omit - solValue: number } diff --git a/src/types/solana.ts b/src/types/solana.ts index a74035b12..0d3efdd89 100644 --- a/src/types/solana.ts +++ b/src/types/solana.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Ticker } from '@helium/currency' import { JsonMetadata, Nft, @@ -12,12 +10,15 @@ import { init as initHsd } from '@helium/helium-sub-daos-sdk' import { init as initDc } from '@helium/data-credits-sdk' import { init as initHem } from '@helium/helium-entity-manager-sdk' import { init as initLazy } from '@helium/lazy-distributor-sdk' +import { BulkRewards } from '@helium/distributor-oracle' import { TokenAmount } from '@solana/web3.js' import { Creator } from '@metaplex-foundation/mpl-bubblegum' export type HotspotWithPendingRewards = CompressedNFT & { // mint id to pending rewards pendingRewards: Record | undefined + // mint id to rewards + rewards: Record | undefined } export type HemProgram = Awaited> @@ -42,20 +43,6 @@ export type SolPaymentInfo = { wallet: string } -export const toMintAddress = ( - symbol: string, - mints: Record, -) => { - const ticker = symbol.toUpperCase() as Ticker - return mints[ticker] -} - -export const mintToTicker = (mint: string, mints: Record) => { - const found = Object.keys(mints).find((key) => mints[key as Ticker] === mint) - - return found as Ticker | undefined -} - export type CompressedNFT = { interface: string id: string diff --git a/src/utils/Balance.tsx b/src/utils/Balance.tsx index 37660a300..283ccf159 100644 --- a/src/utils/Balance.tsx +++ b/src/utils/Balance.tsx @@ -1,18 +1,13 @@ -import { NetTypes } from '@helium/address' -import Balance, { - CurrencyType, - DataCredits, - IotTokens, - MobileTokens, - NetworkTokens, - SolTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' import { getOraclePrice } from '@helium/currency-utils' -import { DC_MINT, HNT_MINT, IOT_MINT, MOBILE_MINT } from '@helium/spl-utils' -import { PublicKey } from '@solana/web3.js' -import { round } from 'lodash' +import { + DC_MINT, + HNT_MINT, + IOT_MINT, + MOBILE_MINT, + toNumber, +} from '@helium/spl-utils' +import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import BN from 'bn.js' import React, { ReactNode, createContext, @@ -34,10 +29,7 @@ import { syncTokenAccounts } from '../store/slices/balancesSlice' import { useAppDispatch } from '../store/store' import { AccountBalance, BalanceInfo, TokenAccount } from '../types/balance' import StoreAtaBalance from './StoreAtaBalance' -import StoreSolBalance from './StoreSolBalance' -import StoreTokenBalance from './StoreTokenBalance' -import { accountCurrencyType } from './accountUtils' -import { decimalSeparator, groupSeparator } from './i18n' +import { humanReadable } from './solanaUtils' import { useBalanceHistory } from './useBalanceHistory' import { usePollTokenPrices } from './usePollTokenPrices' @@ -57,7 +49,7 @@ const useBalanceHook = () => { ) const allBalances = useSelector((state: RootState) => state.balances.balances) - const { convertToCurrency, currency: currencyRaw } = useAppStorage() + const { currency: currencyRaw } = useAppStorage() const currency = useMemo(() => currencyRaw?.toLowerCase(), [currencyRaw]) @@ -82,7 +74,7 @@ const useBalanceHook = () => { ) }, [cluster, anchorProvider, allBalances]) - const { result: oraclePrice } = useAsync(async () => { + const { result: hntToDcPrice } = useAsync(async () => { if (!anchorProvider) { return } @@ -91,18 +83,16 @@ const useBalanceHook = () => { cluster, connection: anchorProvider.connection, }) - return Balance.fromFloat( - oraclePriceRaw.emaPrice.value - oraclePriceRaw.emaConfidence.value * 2, - CurrencyType.usd, + return new BN( + (oraclePriceRaw.emaPrice.value - oraclePriceRaw.emaConfidence.value * 2) * + 100000, ) }, [cluster, anchorProvider?.connection]) const solanaPrice = useMemo(() => { if (!tokenPrices?.solana) return - const price = tokenPrices.solana[currency] - - return new Balance(price, CurrencyType.usd) + return tokenPrices.solana[currency] }, [currency, tokenPrices]) useEffect(() => { @@ -112,94 +102,26 @@ const useBalanceHook = () => { }, [tokenPrices]) const dcToNetworkTokens = useCallback( - ( - dcBalance: Balance, - ): Balance | undefined => { - if (!oraclePrice) return - - if (currentAccount?.netType === NetTypes.TESTNET) { - return dcBalance.toTestNetworkTokens(oraclePrice) - } - return dcBalance.toNetworkTokens(oraclePrice) - }, - [currentAccount, oraclePrice], - ) - - const networkTokensToDc = useCallback( - ( - balance: Balance, - ): Balance | undefined => { - if (!oraclePrice) return + (dcBalance: BN): BN | undefined => { + if (!hntToDcPrice) return - return balance.toDataCredits(oraclePrice) + return dcBalance.div(hntToDcPrice) }, - [oraclePrice], + [hntToDcPrice], ) - - const floatToBalance = useCallback( - (value: number, ticker: Ticker) => { - if (!currentAccount) { - console.warn('Cannot convert float to balance for nil account') - return - } - return Balance.fromFloatAndTicker(value, ticker) - }, - [currentAccount], - ) - - const bonesToBalance = useCallback( - (v: number | undefined | null, ticker: Ticker | null | undefined) => { - return Balance.fromIntAndTicker(v || 0, ticker || 'HNT') - }, - [], - ) - - const intToBalance = useCallback( - (opts: { intValue?: number }) => { - if (!opts.intValue === undefined || !currentAccount) { - console.warn('Cannot convert int to balance') - return - } - return new Balance( - opts.intValue, - accountCurrencyType(currentAccount.address, undefined), - ) - }, - [currentAccount], - ) - - const toPreferredCurrencyString = useCallback( - ( - balance?: Balance, - opts?: { maxDecimalPlaces?: number; showTicker?: boolean }, - ): Promise => { - if (!balance) { - return new Promise((resolve) => resolve('')) - } - const multiplier = tokenPrices?.helium[currency] || 0 - - const showAsHnt = - !convertToCurrency || - !multiplier || - balance?.type.ticker === CurrencyType.dataCredit.ticker || - balance?.type.ticker === CurrencyType.testNetworkToken.ticker - - if (!showAsHnt) { - const convertedValue = multiplier * (balance?.floatBalance || 0) - return CurrencyFormatter.format(convertedValue, currency) - } - return new Promise((resolve) => - resolve(balanceToString(balance, opts)), - ) + const networkTokensToDc = useCallback( + (balance: BN): BN | undefined => { + if (!hntToDcPrice) return + return balance.mul(hntToDcPrice) }, - [convertToCurrency, currency, tokenPrices], + [hntToDcPrice], ) const getBalance = useCallback( (mint: PublicKey, atas: Required[]) => { const mintStr = mint.toBase58() const ata = atas.find((a) => a.mint === mintStr) - return ata?.balance || 0 + return new BN(ata?.balance || 0) }, [], ) @@ -214,61 +136,31 @@ const useBalanceHook = () => { const solToken = accountBalancesForCluster?.sol - const dcEscrowToken = accountBalancesForCluster?.dcEscrow - - const dcEscrowBalance = new Balance( - dcEscrowToken?.balance || 0, - CurrencyType.dataCredit, - ) - const formattedEscrowDcValue = await CurrencyFormatter.format( - dcEscrowBalance.toUsd(oraclePrice).floatBalance, - 'usd', - ) - - const solBalance = Balance.fromIntAndTicker(solToken?.balance || 0, 'SOL') + const solBalance = solToken?.balance const solPrice = tokenPrices?.solana?.[currency] || 0 - const solAmount = solBalance?.floatBalance - const solValue = solPrice * solAmount + const solValue = solPrice * (solBalance / LAMPORTS_PER_SOL) const formattedSolValue = await CurrencyFormatter.format(solValue, currency) - const hntBalance = Balance.fromIntAndTicker( - getBalance(HNT_MINT, atas), - 'HNT', - ) + const hntBalance = getBalance(HNT_MINT, atas) const hntPrice = tokenPrices?.helium?.[currency] || 0 - const hntAmount = hntBalance?.floatBalance - const hntValue = hntPrice * hntAmount + const hntValue = hntPrice * toNumber(hntBalance, 8) const formattedHntValue = await CurrencyFormatter.format(hntValue, currency) - const iotBalance = Balance.fromIntAndTicker( - getBalance(IOT_MINT, atas), - 'IOT', - ) + const iotBalance = getBalance(IOT_MINT, atas) const iotPrice = tokenPrices?.['helium-iot']?.[currency] || 0 - const iotAmount = iotBalance?.floatBalance - const iotValue = iotPrice * iotAmount + const iotValue = iotPrice * toNumber(iotBalance, 6) const formattedIotValue = await CurrencyFormatter.format(iotValue, currency) - const mobileBalance = Balance.fromIntAndTicker( - getBalance(MOBILE_MINT, atas), - 'MOBILE', - ) + const mobileBalance = getBalance(MOBILE_MINT, atas) const mobilePrice = tokenPrices?.['helium-mobile']?.[currency] || 0 - const mobileAmount = mobileBalance?.floatBalance - const mobileValue = mobilePrice * mobileAmount + const mobileValue = mobilePrice * toNumber(mobileBalance, 6) const formattedMobileValue = await CurrencyFormatter.format( mobileValue, currency, ) - const dcBalance = new Balance( - getBalance(DC_MINT, atas), - CurrencyType.dataCredit, - ) - const formattedDcValue = await CurrencyFormatter.format( - dcBalance.toUsd(oraclePrice).floatBalance, - 'usd', - ) + const dcBalance = new BN(getBalance(DC_MINT, atas)) + const formattedDcValue = humanReadable(dcBalance, 5) const formattedTotal = await CurrencyFormatter.format( solValue + hntValue + mobileValue + iotValue, @@ -277,32 +169,19 @@ const useBalanceHook = () => { return { atas, - dcBalance, - dcEscrowBalance, - dcEscrowToken, formattedDcValue, - formattedEscrowDcValue, formattedHntValue, formattedIotValue, formattedMobileValue, formattedSolValue, formattedTotal, - hntBalance, - hntValue, - iotBalance, - iotValue, - mobileBalance, - mobileValue, - solBalance, - solToken, - solValue, } }, [ allBalances, cluster, currency, getBalance, - oraclePrice, + hntToDcPrice, solanaAddress, tokenPrices, ]) @@ -325,103 +204,39 @@ const useBalanceHook = () => { return } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore setBalanceInfo(tokenInfo) }, [cluster, prevCluster, prevSolAddress, solanaAddress, tokenInfo]) - const toCurrencyString = useCallback( - ( - balance: Balance, - ticker: Ticker = 'HNT', - ): Promise => { - const defaultResponse = new Promise((resolve) => resolve('')) - - const bal = Balance.fromIntAndTicker(balance.integerBalance, ticker) - - let value = 0 - switch (ticker) { - case 'HNT': - value = tokenPrices?.helium[currency] || 0 - break - case 'SOL': - value = tokenPrices?.solana[currency] || 0 - break - case 'IOT': - value = tokenPrices?.['helium-iot'][currency] || 0 - break - case 'MOBILE': - value = tokenPrices?.['helium-mobile'][currency] || 0 - break - } - - if (!value) return defaultResponse - - return CurrencyFormatter.format(value * bal.floatBalance, currency) - }, - [currency, tokenPrices], - ) - - const toUsd = useCallback( - (balance?: Balance): number => { - if (!balance) { - return 0 - } - const multiplier = tokenPrices?.helium?.usd || 0 - - return round(multiplier * (balance?.floatBalance || 0), 2) - }, - [tokenPrices], - ) - return { balanceHistory, - bonesToBalance, dcToNetworkTokens, - floatToBalance, ...balanceInfo, - intToBalance, networkTokensToDc, oracleDateTime, - oraclePrice, + oraclePrice: hntToDcPrice, solanaPrice, - toCurrencyString, - toPreferredCurrencyString, - toUsd, tokenAccounts, } } const initialState = { balanceHistory: [] as AccountBalance[], - bonesToBalance: () => new Balance(0, CurrencyType.networkToken), - dcBalance: new Balance(0, CurrencyType.dataCredit), - dcDelegatedBalance: new Balance(0, CurrencyType.dataCredit), - dcEscrowBalance: new Balance(0, CurrencyType.dataCredit), dcToNetworkTokens: () => undefined, - floatToBalance: () => undefined, formattedDcValue: '', formattedHntValue: '', formattedIotValue: '', formattedMobileValue: '', formattedSolValue: '', formattedTotal: undefined, - hntBalance: new Balance(0, CurrencyType.networkToken), - intToBalance: () => undefined, - iotBalance: new Balance(0, CurrencyType.iot), - mobileBalance: new Balance(0, CurrencyType.mobile), networkTokensToDc: () => undefined, oracleDateTime: undefined, oraclePrice: undefined, solanaPrice: undefined, - solBalance: new Balance(0, CurrencyType.solTokens), solBalancesLoading: false, - toCurrencyString: () => new Promise((resolve) => resolve('')), - toPreferredCurrencyString: () => - new Promise((resolve) => resolve('')), - toUsd: () => 0, atas: [], updating: false, - solToken: undefined, - dcEscrowToken: undefined, tokenAccounts: undefined, } const BalanceContext = @@ -431,17 +246,12 @@ const { Provider } = BalanceContext export const BalanceProvider = ({ children }: { children: ReactNode }) => { const balanceHook = useBalanceHook() - const { atas, dcEscrowToken, solToken } = balanceHook + const { atas } = balanceHook const { cluster } = useSolana() - const prevSolAddress = usePrevious(solToken?.tokenAccount) const prevCluster = usePrevious(cluster) const clusterChanged = prevCluster && cluster && prevCluster !== cluster - const addressChanged = - solToken?.tokenAccount && - prevSolAddress && - solToken.tokenAccount !== prevSolAddress - if (clusterChanged || addressChanged) { + if (clusterChanged) { return <>{children} } @@ -450,15 +260,6 @@ export const BalanceProvider = ({ children }: { children: ReactNode }) => { {atas?.map((ta) => ( ))} - {dcEscrowToken?.tokenAccount && ( - - )} - {solToken?.tokenAccount && ( - - )} {children} @@ -466,22 +267,3 @@ export const BalanceProvider = ({ children }: { children: ReactNode }) => { } export const useBalance = () => useContext(BalanceContext) - -export const balanceToString = ( - balance?: Balance< - | DataCredits - | NetworkTokens - | TestNetworkTokens - | MobileTokens - | IotTokens - | SolTokens - >, - opts?: { maxDecimalPlaces?: number; showTicker?: boolean }, -) => { - if (!balance) return '' - return balance.toString(opts?.maxDecimalPlaces, { - groupSeparator, - decimalSeparator, - showTicker: opts?.showTicker, - }) -} diff --git a/src/utils/StoreSolBalance.ts b/src/utils/StoreSolBalance.ts index cb5737aaf..92cf74464 100644 --- a/src/utils/StoreSolBalance.ts +++ b/src/utils/StoreSolBalance.ts @@ -1,34 +1,32 @@ -import { useAccount } from '@helium/helium-react-hooks' -import { PublicKey } from '@solana/web3.js' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { usePublicKey } from '@hooks/usePublicKey' import { useEffect } from 'react' +import { useSolana } from '../solana/SolanaProvider' import { balancesSlice } from '../store/slices/balancesSlice' import { useAppDispatch } from '../store/store' -import { useSolana } from '../solana/SolanaProvider' type Props = { solanaAddress: string } const StoreSolBalance = ({ solanaAddress }: Props) => { - const account = useAccount(new PublicKey(solanaAddress)) + const key = usePublicKey(solanaAddress) + const { amount } = useSolOwnedAmount(key) const dispatch = useAppDispatch() const { cluster } = useSolana() useEffect(() => { - if (account.account?.lamports === undefined) return - - const amount = account.account?.lamports + if (typeof amount === 'undefined') return dispatch( balancesSlice.actions.updateBalance({ cluster, solanaAddress, - balance: amount, - type: 'sol', + balance: Number(amount), tokenAccount: solanaAddress, }), ) - }, [account.account?.lamports, cluster, dispatch, solanaAddress]) + }, [amount, cluster, dispatch, solanaAddress]) return null } diff --git a/src/utils/StoreTokenBalance.ts b/src/utils/StoreTokenBalance.ts index 9a345eaac..25d29f74d 100644 --- a/src/utils/StoreTokenBalance.ts +++ b/src/utils/StoreTokenBalance.ts @@ -1,18 +1,17 @@ import { useTokenAccount } from '@helium/helium-react-hooks' +import { AccountLayout } from '@solana/spl-token' import { PublicKey } from '@solana/web3.js' import { useEffect } from 'react' -import { AccountLayout } from '@solana/spl-token' +import { useSolana } from '../solana/SolanaProvider' import { useAccountStorage } from '../storage/AccountStorageProvider' import { balancesSlice } from '../store/slices/balancesSlice' import { useAppDispatch } from '../store/store' -import { useSolana } from '../solana/SolanaProvider' type TokenInput = { tokenAccount: string - type: 'dcEscrow' | 'sol' } -const StoreTokenBalance = ({ tokenAccount, type }: TokenInput) => { +const StoreTokenBalance = ({ tokenAccount }: TokenInput) => { const tokenAccountResponse = useTokenAccount(new PublicKey(tokenAccount)) const { currentAccount } = useAccountStorage() const dispatch = useAppDispatch() @@ -39,7 +38,6 @@ const StoreTokenBalance = ({ tokenAccount, type }: TokenInput) => { cluster, solanaAddress: currentAccount?.solanaAddress, balance: amount, - type, tokenAccount, }), ) @@ -49,7 +47,6 @@ const StoreTokenBalance = ({ tokenAccount, type }: TokenInput) => { dispatch, tokenAccount, tokenAccountResponse, - type, ]) return null diff --git a/src/utils/accountUtils.ts b/src/utils/accountUtils.ts index 3e7c53447..4d0303d16 100644 --- a/src/utils/accountUtils.ts +++ b/src/utils/accountUtils.ts @@ -1,10 +1,9 @@ import Address, { NetTypes as NetType, utils } from '@helium/address' -import { CurrencyType, Ticker } from '@helium/currency' -import Bcrypt from 'bcrypt-react-native' import { PublicKey } from '@solana/web3.js' +import Bcrypt from 'bcrypt-react-native' +import BigNumber from 'bignumber.js' import bs58 from 'bs58' import { round } from 'lodash' -import BigNumber from 'bignumber.js' export type AccountNetTypeOpt = 'all' | NetType.NetType @@ -47,37 +46,6 @@ export const heliumAddressIsValid = (address: string) => { } } -export const accountCurrencyType = (address?: string, tokenType?: Ticker) => { - if (!address) return CurrencyType.default - if (!tokenType) { - return accountNetType(address) === NetType.MAINNET - ? CurrencyType.default - : CurrencyType.testNetworkToken - } - // If token type is passed in, we need to check if to return testnet token or default token - switch (tokenType) { - default: - case 'HNT': - return accountNetType(address) === NetType.MAINNET - ? CurrencyType.default - : CurrencyType.testNetworkToken - case 'HST': - return CurrencyType.security - case 'IOT': - return CurrencyType.iot - case 'MOBILE': - return CurrencyType.mobile - case 'DC': - return CurrencyType.dataCredit - } -} - -export const networkCurrencyType = (netType?: NetType.NetType) => { - return netType === NetType.TESTNET - ? CurrencyType.testNetworkToken - : CurrencyType.default -} - export const accountNetType = (address?: string) => { if (!address || !Address.isValid(address)) return NetType.MAINNET return Address.fromB58(address)?.netType diff --git a/src/utils/linking.ts b/src/utils/linking.ts index 8e352ccd7..5065df2df 100644 --- a/src/utils/linking.ts +++ b/src/utils/linking.ts @@ -1,15 +1,10 @@ import Address from '@helium/address' -import Balance, { - CurrencyType, - NetworkTokens, - TestNetworkTokens, - Ticker, -} from '@helium/currency' +import { LinkingOptions } from '@react-navigation/native' +import BigNumber from 'bignumber.js' +import BN from 'bn.js' import * as Linking from 'expo-linking' import qs from 'qs' import queryString from 'query-string' -import BigNumber from 'bignumber.js' -import { LinkingOptions } from '@react-navigation/native' import { BurnRouteParam, PaymentRouteParam } from '../features/home/homeTypes' import { RootStackParamList } from '../navigation/rootTypes' import { useAccountStorage } from '../storage/AccountStorageProvider' @@ -20,7 +15,7 @@ export const HELIUM_WALLET_LINK_SCHEME = 'https://wallet.helium.com/' export type SendDetails = { payee: string - balanceAmount: Balance | Balance + balanceAmount: BN max?: boolean } @@ -67,46 +62,22 @@ export const useDeepLinking = () => { export const makePayRequestLink = ({ payee, balanceAmount, - defaultTokenType, -}: Partial & { defaultTokenType?: Ticker }) => { + mint, +}: Partial & { mint?: string }) => { return [ HELIUM_WALLET_LINK_SCHEME + PAYMENT_PATH, qs.stringify( { payee, - amount: balanceAmount?.integerBalance || null, + amount: balanceAmount?.toString(), memo: '', - defaultTokenType, + mint, }, { skipNulls: true }, ), ].join('?') } -export const makeMultiPayRequestLink = ({ - payments, - payer, -}: { - payer?: string - payments: Array & { defaultTokenType?: Ticker }> -}) => { - const ironed = payments.map( - ({ payee: address, balanceAmount, defaultTokenType }) => ({ - payee: address || null, - amount: balanceAmount?.integerBalance || null, - memo: '', - defaultTokenType, - }), - ) - return [ - HELIUM_WALLET_LINK_SCHEME + PAYMENT_PATH, - qs.stringify( - { payer, payments: JSON.stringify(ironed) }, - { skipNulls: true }, - ), - ].join('?') -} - export const parseBurn = (qrContent: string) => { try { const parsedJson = JSON.parse(qrContent) @@ -161,8 +132,6 @@ export const parsePaymentLink = ( return } - const { coefficient } = new Balance(0, CurrencyType.networkToken).type - if (parsedJson.amount !== undefined) { const amount = typeof parsedJson.amount === 'string' @@ -171,7 +140,7 @@ export const parsePaymentLink = ( return { payee: parsedJson.address || parsedJson.payee, payer: parsedJson.payer, - amount: new BigNumber(amount).dividedBy(coefficient).toString(), + amount: new BigNumber(amount).dividedBy(10 ** 8).toString(), memo: '', } } @@ -183,7 +152,7 @@ export const parsePaymentLink = ( const amountFloat = typeof amount === 'string' ? parseFloat(amount) : amount return { - amount: new BigNumber(amountFloat).dividedBy(coefficient).toString(), + amount: new BigNumber(amountFloat).dividedBy(10 ** 8).toString(), payee: address, memo: '', } diff --git a/src/utils/solanaUtils.ts b/src/utils/solanaUtils.ts index d0463792a..63c78af73 100644 --- a/src/utils/solanaUtils.ts +++ b/src/utils/solanaUtils.ts @@ -1,125 +1,152 @@ /* eslint-disable no-underscore-dangle */ -import { - Cluster, - clusterApiUrl, - ConfirmedSignatureInfo, - Connection, - Keypair, - LAMPORTS_PER_SOL, - Logs, - PublicKey, - SignaturesForAddressOptions, - Signer, - SystemProgram, - TransactionInstruction, - TransactionMessage, - VersionedMessage, - VersionedTransaction, - VersionedTransactionResponse, - ComputeBudgetProgram, - AccountMeta, - SignatureResult, - ParsedTransactionWithMeta, - Transaction, -} from '@solana/web3.js' +import { AnchorProvider, BN } from '@coral-xyz/anchor' import * as dc from '@helium/data-credits-sdk' -import { subDaoKey } from '@helium/helium-sub-daos-sdk' import { - TOKEN_PROGRAM_ID, - AccountLayout, - createTransferCheckedInstruction, - getOrCreateAssociatedTokenAccount, - getAssociatedTokenAddress, - getMint, - getAssociatedTokenAddressSync, - getAccount, - ASSOCIATED_TOKEN_PROGRAM_ID, - createAssociatedTokenAccountInstruction, - createAssociatedTokenAccountIdempotentInstruction, -} from '@solana/spl-token' + delegatedDataCreditsKey, + escrowAccountKey, +} from '@helium/data-credits-sdk' +import { getPendingRewards } from '@helium/distributor-oracle' import { - init as initHem, + PROGRAM_ID as FanoutProgramId, + fanoutKey, + membershipCollectionKey, +} from '@helium/fanout-sdk' +import { + decodeEntityKey, entityCreatorKey, - rewardableEntityConfigKey, - updateMobileMetadata, - updateIotMetadata, - keyToAssetKey, + init, + init as initHem, iotInfoKey, + keyToAssetForAsset, + keyToAssetKey, mobileInfoKey, + rewardableEntityConfigKey, + updateIotMetadata, + updateMobileMetadata, } from '@helium/helium-entity-manager-sdk' -import Balance, { AnyCurrencyType, CurrencyType } from '@helium/currency' -import { JsonMetadata, Metadata, Metaplex } from '@metaplex-foundation/js' -import axios from 'axios' -import Config from 'react-native-config' -import { - TreeConfig, - createTransferInstruction, - PROGRAM_ID as BUBBLEGUM_PROGRAM_ID, -} from '@metaplex-foundation/mpl-bubblegum' -import { - ConcurrentMerkleTreeAccount, - SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, - SPL_NOOP_PROGRAM_ID, -} from '@solana/spl-account-compression' -import bs58 from 'bs58' +import { subDaoKey } from '@helium/helium-sub-daos-sdk' +import * as lz from '@helium/lazy-distributor-sdk' +import { HotspotType } from '@helium/onboarding' import { Asset, + DC_MINT, HNT_MINT, + IOT_MINT, + MOBILE_MINT, getAsset, searchAssets, - toBN, sendAndConfirmWithRetry, - IOT_MINT, - DC_MINT, - MOBILE_MINT, - truthy, + toBN, } from '@helium/spl-utils' -import { AnchorProvider, BN } from '@coral-xyz/anchor' import * as tm from '@helium/treasury-management-sdk' -import { - delegatedDataCreditsKey, - escrowAccountKey, -} from '@helium/data-credits-sdk' -import { - getPendingRewards, - getBulkRewards, - formBulkTransactions, -} from '@helium/distributor-oracle' -import * as lz from '@helium/lazy-distributor-sdk' -import { - PROGRAM_ID as FanoutProgramId, - fanoutKey, - membershipCollectionKey, -} from '@helium/fanout-sdk' import { PROGRAM_ID as VoterStakeRegistryProgramId, - registrarKey, registrarCollectionKey, + registrarKey, } from '@helium/voter-stake-registry-sdk' -import { BaseCurrencyType } from '@helium/currency/build/currency_types' -import { HotspotType } from '@helium/onboarding' +import { + METADATA_PARSER, + getMetadata, + getMetadataId, +} from '@hooks/useMetaplexMetadata' +import { JsonMetadata, Metadata, Metaplex } from '@metaplex-foundation/js' +import { + PROGRAM_ID as BUBBLEGUM_PROGRAM_ID, + TreeConfig, + createTransferInstruction, +} from '@metaplex-foundation/mpl-bubblegum' +import { + ConcurrentMerkleTreeAccount, + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + SPL_NOOP_PROGRAM_ID, +} from '@solana/spl-account-compression' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + AccountLayout, + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + createAssociatedTokenAccountInstruction, + createTransferCheckedInstruction, + getAccount, + getAssociatedTokenAddress, + getAssociatedTokenAddressSync, + getMint, + getOrCreateAssociatedTokenAccount, +} from '@solana/spl-token' +import { + AccountMeta, + Cluster, + ComputeBudgetProgram, + ConfirmedSignatureInfo, + Connection, + Keypair, + LAMPORTS_PER_SOL, + Logs, + ParsedTransactionWithMeta, + PublicKey, + SignatureResult, + SignaturesForAddressOptions, + Signer, + SystemProgram, + Transaction, + TransactionInstruction, + TransactionMessage, + VersionedMessage, + VersionedTransaction, + VersionedTransactionResponse, + clusterApiUrl, +} from '@solana/web3.js' +import axios from 'axios' +import bs58 from 'bs58' +import Config from 'react-native-config' import { getKeypair, getSessionKey } from '../storage/secureStorage' import { Activity, Payment } from '../types/activity' -import sleep from './sleep' import { Collectable, CompressedNFT, EnrichedTransaction, HotspotWithPendingRewards, - mintToTicker, } from '../types/solana' -import * as Logger from './logger' import { WrappedConnection } from './WrappedConnection' +import { solAddressIsValid } from './accountUtils' import { DAO_KEY, IOT_LAZY_KEY, IOT_SUB_DAO_KEY, - Mints, MOBILE_LAZY_KEY, MOBILE_SUB_DAO_KEY, + Mints, } from './constants' -import { solAddressIsValid } from './accountUtils' import { getH3Location } from './h3' +import { decimalSeparator, groupSeparator } from './i18n' +import * as Logger from './logger' +import sleep from './sleep' + +export function humanReadable( + amount?: BN, + decimals?: number, +): string | undefined { + if (typeof decimals === 'undefined' || typeof amount === 'undefined') return + + const input = amount.toString() + const integerPart = + input.length > decimals ? input.slice(0, input.length - decimals) : '' + const formattedIntegerPart = integerPart.replace( + /\B(?=(\d{3})+(?!\d))/g, + groupSeparator, + ) + const decimalPart = + decimals !== 0 + ? input + .slice(-decimals) + .padStart(decimals, '0') // Add prefix zeros + .replace(/0+$/, '') // Remove trailing zeros + : '' + + return `${formattedIntegerPart.length > 0 ? formattedIntegerPart : '0'}${ + Number(decimalPart) !== 0 ? `${decimalSeparator}${decimalPart}` : '' + }` +} const govProgramId = new PublicKey( 'hgovkRU6Ghe1Qoyb54HdSLdqN7VtxaifBzRmh9jtd3S', @@ -194,7 +221,7 @@ export const createTransferSolTxn = async ( signer: Signer, payments: { payee: string - balanceAmount: Balance + balanceAmount: BN max?: boolean }[], ) => { @@ -204,12 +231,12 @@ export const createTransferSolTxn = async ( let instructions: TransactionInstruction[] = [] payments.forEach((p) => { - const amount = p.balanceAmount.integerBalance + const amount = p.balanceAmount const instruction = SystemProgram.transfer({ fromPubkey: payer, toPubkey: new PublicKey(p.payee), - lamports: amount, + lamports: BigInt(amount.toString()), }) instructions = [...instructions, instruction] @@ -234,7 +261,7 @@ export const createTransferTxn = async ( signer: Signer, payments: { payee: string - balanceAmount: Balance + balanceAmount: BN max?: boolean }[], mintAddress: string, @@ -243,11 +270,10 @@ export const createTransferTxn = async ( const conn = anchorProvider.connection - const [firstPayment] = payments - const payer = signer.publicKey const mint = new PublicKey(mintAddress) + const mintAcc = await getMint(conn, mint) const payerATA = await getOrCreateAssociatedTokenAccount( conn, @@ -258,7 +284,7 @@ export const createTransferTxn = async ( let instructions: TransactionInstruction[] = [] payments.forEach((p) => { - const amount = p.balanceAmount.integerBalance + const amount = p.balanceAmount const ata = getAssociatedTokenAddressSync(mint, new PublicKey(p.payee)) instructions = [ @@ -274,8 +300,8 @@ export const createTransferTxn = async ( mint, ata, payer, - amount, - firstPayment.balanceAmount.type.decimalPlaces.toNumber(), + BigInt(amount.toString()), + mintAcc.decimals, [signer], ), ] @@ -301,7 +327,7 @@ export const transferToken = async ( heliumAddress: string, payments: { payee: string - balanceAmount: Balance + balanceAmount: BN max?: boolean }[], mintAddress?: string, @@ -361,7 +387,6 @@ export const getTransactions = async ( anchorProvider: AnchorProvider, walletAddress: string, mintAddress: string, - mints: typeof Mints, options?: SignaturesForAddressOptions, ) => { try { @@ -375,7 +400,7 @@ export const getTransactions = async ( }) return transactionDetails - .map((td, idx) => solInstructionsToActivity(td, sigs[idx], mints)) + .map((td, idx) => solInstructionsToActivity(td, sigs[idx])) .filter((a) => !!a) as Activity[] } catch (e) { Logger.error(e) @@ -580,9 +605,9 @@ export const getAtaAccountCreationFee = async ({ try { await getAccount(connection, ataAddress) - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } catch { - return Balance.fromFloat(0.00203928, CurrencyType.solTokens) + return new BN(0.00203928 * LAMPORTS_PER_SOL) } } @@ -592,7 +617,7 @@ export const mintDataCredits = async ({ recipient, }: { anchorProvider: AnchorProvider - dcAmount: number + dcAmount: BN recipient: PublicKey }) => { try { @@ -604,7 +629,7 @@ export const mintDataCredits = async ({ const tx = await program.methods .mintDataCreditsV0({ hntAmount: null, - dcAmount: toBN(dcAmount, 0), + dcAmount, }) .accounts({ dcMint: DC_MINT, @@ -659,16 +684,20 @@ export const delegateDataCredits = async ( } } -export const getEscrowTokenAccount = (address: string) => { - try { - const subDao = IOT_SUB_DAO_KEY - const delegatedDataCredits = delegatedDataCreditsKey(subDao, address)[0] - const escrowTokenAccount = escrowAccountKey(delegatedDataCredits)[0] - - return escrowTokenAccount - } catch (e) { - Logger.error(e) - throw e as Error +export const getEscrowTokenAccount = ( + address: string | undefined, + subDao: PublicKey, +) => { + if (address) { + try { + const delegatedDataCredits = delegatedDataCreditsKey(subDao, address)[0] + const escrowTokenAccount = escrowAccountKey(delegatedDataCredits)[0] + + return escrowTokenAccount + } catch (e) { + Logger.error(e) + throw e as Error + } } } @@ -1032,9 +1061,7 @@ export const getCompressedNFTMetadata = async ( const collectablesWithMetadata = await Promise.all( collectables.map(async (col) => { try { - const { data } = await axios.get(col.content.json_uri, { - timeout: 3000, - }) + const { data } = await getMetadata(col.content.json_uri) return { ...col, content: { @@ -1064,10 +1091,19 @@ export async function annotateWithPendingRewards( hotspots: CompressedNFT[], ): Promise { const program = await lz.init(provider) + const hemProgram = await init(provider) const dao = DAO_KEY - const entityKeys = hotspots.map((h) => { - return h.content.json_uri.split('/').slice(-1)[0] - }) + const keyToAssets = hotspots.map((h) => + keyToAssetForAsset(toAsset(h as CompressedNFT)), + ) + const ktaAccs = await Promise.all( + keyToAssets.map((kta) => hemProgram.account.keyToAssetV0.fetch(kta)), + ) + const entityKeys = ktaAccs.map( + (kta) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + decodeEntityKey(kta.entityKey, kta.keySerialization)!, + ) const mobileRewards = await getPendingRewards( program, @@ -1153,15 +1189,22 @@ export const groupCollectablesWithMetaData = ( */ export const getCollectableByMint = async ( mint: PublicKey, - metaplex: Metaplex, + connection: Connection, ): Promise => { try { - const collectable = await metaplex.nfts().findByMint({ mintAddress: mint }) - if (!collectable.json && collectable.uri) { - const json = await (await fetch(collectable.uri)).json() - return { ...collectable, json } + const metadata = getMetadataId(mint) + const metadataAccount = await connection.getAccountInfo(metadata) + if (metadataAccount) { + const collectable = METADATA_PARSER(metadata, metadataAccount) + if (!collectable.json && collectable.uri) { + const json = await (await fetch(collectable.uri)).json() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return { ...collectable, json } + } } - return collectable + + return null } catch (e) { return null } @@ -1182,7 +1225,6 @@ export const getAllTransactions = async ( ): Promise<(EnrichedTransaction | ConfirmedSignatureInfo)[]> => { const pubKey = new PublicKey(address) const conn = anchorProvider.connection - const metaplex = new Metaplex(conn, { cluster }) const sessionKey = await getSessionKey() const parseTransactionsUrl = `${ @@ -1213,7 +1255,7 @@ export const getAllTransactions = async ( if (firstTokenTransfer && firstTokenTransfer.mint) { const tokenMetadata = await getCollectableByMint( new PublicKey(firstTokenTransfer.mint), - metaplex, + conn, ) return { @@ -1414,7 +1456,6 @@ export async function createTreasurySwapMessage( export const solInstructionsToActivity = ( parsedTxn: ParsedTransactionWithMeta | null, signature: string, - mints: typeof Mints, ) => { if (!parsedTxn) return @@ -1424,33 +1465,33 @@ export const solInstructionsToActivity = ( activity.fee = meta?.fee activity.height = slot + activity.feePayer = + parsedTxn.transaction.message.accountKeys[0].pubkey.toBase58() if (blockTime) { activity.time = blockTime } - if (meta?.preTokenBalances && meta.postTokenBalances) { const { preTokenBalances, postTokenBalances } = meta + let payments = [] as Payment[] postTokenBalances.forEach((post) => { const preBalance = preTokenBalances.find( ({ accountIndex }) => accountIndex === post.accountIndex, ) - const pre = preBalance || { uiTokenAmount: { amount: '0' } } - const preAmount = parseInt(pre.uiTokenAmount.amount, 10) - const postAmount = parseInt(post.uiTokenAmount.amount, 10) + const pre = preBalance || { + uiTokenAmount: { uiAmount: 0 }, + owner: post.owner, + } + const preAmount = pre.uiTokenAmount.uiAmount || 0 + const postAmount = post.uiTokenAmount.uiAmount || 0 const amount = postAmount - preAmount - if (amount < 0) { - // is payer - activity.payer = post.owner - activity.tokenType = mintToTicker(post.mint, mints) - activity.amount = -1 * amount - } else { - // is payee + if (amount !== 0 && !Number.isNaN(amount)) { const p: Payment = { amount, - payee: post.owner || '', - tokenType: mintToTicker(post.mint, mints), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + owner: (post.owner || pre.owner)!, + mint: post.mint, } payments = [...payments, p] } @@ -1458,22 +1499,13 @@ export const solInstructionsToActivity = ( activity.payments = payments } - const transfer = (activity.payments?.length || 0) > 0 - - if (transfer) { + if ((activity.payments?.length || 0) > 0) { // We have a payment activity.type = 'payment_v2' } if (activity.type === 'unknown') return - const payment = activity.payments?.[0] - if (payment && payment.tokenType === 'DC') { - activity.type = payment.payee !== activity.payer ? 'dc_delegate' : 'dc_mint' - activity.amount = payment.amount - activity.tokenType = 'DC' - } - return activity } @@ -1495,11 +1527,8 @@ export const submitSolana = async ({ return txid } -export const parseTransactionError = ( - balance?: Balance, - message?: string, -) => { - if ((balance?.floatBalance || 0) < 0.02) { +export const parseTransactionError = (balance?: BN, message?: string) => { + if (balance?.lt(new BN(0.02))) { return 'The SOL balance on this account is too low to complete this transaction' } @@ -1527,19 +1556,19 @@ export const calcCreateAssociatedTokenAccountAccountFee = async ( provider: AnchorProvider, payee: string, mint: PublicKey, -): Promise> => { +): Promise => { if (!payee) { - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } if (!solAddressIsValid(payee)) { - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } const payeePubKey = new PublicKey(payee) const ata = await getAssociatedTokenAddress(mint, payeePubKey) if (ata) { - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } const transaction = new Transaction().add( @@ -1562,11 +1591,11 @@ export const calcCreateAssociatedTokenAccountAccountFee = async ( const fee = await transaction.getEstimatedFee(provider.connection) if (!fee) { - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } - return new Balance(fee, CurrencyType.solTokens) + return new BN(fee) } catch (e) { - return new Balance(0, CurrencyType.solTokens) + return new BN(0) } } @@ -1638,7 +1667,9 @@ export const updateEntityInfoTxn = async ({ ) const mobileInfo = await program.account.mobileHotspotInfoV0.fetchNullable( - mobileInfoKey(mobileConfigKey, entityKey)[0], + ( + await mobileInfoKey(mobileConfigKey, entityKey) + )[0], ) if (!mobileInfo) { @@ -1673,12 +1704,7 @@ export const updateEntityInfoTxn = async ({ } } -const chunks = (array: T[], size: number): T[][] => - Array.apply(0, new Array(Math.ceil(array.length / size))).map((_, index) => - array.slice(index * size, (index + 1) * size), - ) - -function toAsset(hotspot: HotspotWithPendingRewards): Asset { +export function toAsset(hotspot: CompressedNFT): Asset { return { ...hotspot, id: new PublicKey(hotspot.id), @@ -1704,61 +1730,3 @@ function toAsset(hotspot: HotspotWithPendingRewards): Asset { }, } } - -export async function claimAllRewardsTxns( - anchorProvider: AnchorProvider, - lazyDistributors: PublicKey[], - hotspots: HotspotWithPendingRewards[], -) { - try { - const { connection } = anchorProvider - const { publicKey: payer } = anchorProvider.wallet - const lazyProgram = await lz.init(anchorProvider) - let txns: Transaction[] | undefined - - // Use for loops to linearly order promises - // eslint-disable-next-line no-restricted-syntax - for (const lazyDistributor of lazyDistributors) { - const lazyDistributorAcc = - // eslint-disable-next-line no-await-in-loop - await lazyProgram.account.lazyDistributorV0.fetch(lazyDistributor) - // eslint-disable-next-line no-restricted-syntax - for (const chunk of chunks(hotspots, 25)) { - const entityKeys = chunk.map( - (h) => h.content.json_uri.split('/').slice(-1)[0], - ) - - // eslint-disable-next-line no-await-in-loop - const rewards = await getBulkRewards( - lazyProgram, - lazyDistributor, - entityKeys, - ) - - // eslint-disable-next-line no-await-in-loop - const txs = await formBulkTransactions({ - program: lazyProgram, - rewards, - assets: chunk.map((h) => new PublicKey(h.id)), - compressionAssetAccs: chunk.map(toAsset), - lazyDistributor, - lazyDistributorAcc, - assetEndpoint: connection.rpcEndpoint, - wallet: payer, - }) - - const validTxns = txs.filter(truthy) - txns = [...(txns || []), ...validTxns] - } - } - - if (!txns) { - throw new Error('Unable to form transactions') - } - - return txns - } catch (e) { - Logger.error(e) - throw e as Error - } -} diff --git a/src/utils/walletApiV2.ts b/src/utils/walletApiV2.ts index ff6185b88..91a6ad675 100644 --- a/src/utils/walletApiV2.ts +++ b/src/utils/walletApiV2.ts @@ -114,14 +114,14 @@ export const postNotificationRead = async ({ } export const postPayment = async ({ - signature, + signatures, cluster, }: { - signature: string + signatures: string[] cluster: Cluster }) => { const url = `/payments?cluster=${cluster}` - return axiosInstance.post(url, { signature }) + return axiosInstance.post(url, { signature: signatures[0], signatures }) } export const getRecommendedDapps = async () => { diff --git a/yarn.lock b/yarn.lock index d9d8983c5..2f750a3af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2259,17 +2259,26 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@helium/account-fetch-cache@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/account-fetch-cache/-/account-fetch-cache-0.2.5.tgz#518e945abd51bad1811ff0b4b5c80d62ebafdb6f" - integrity sha512-Cv8ST15awWpaHJGkY3SGeb+BuZ0wzbS3AMtQ8ZAeUr+XuJ5mbaGZUE9QPI9dNPKV1txzgn4JkEs+BQywt8LwRg== +"@helium/account-fetch-cache-hooks@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/account-fetch-cache-hooks/-/account-fetch-cache-hooks-0.2.17.tgz#78076508bc2d825022db1b6befd43de378f32bb6" + integrity sha512-A0DHHFLlF1B5EyWLHzLs6hZtax7V5LD48XM0xEYDIIJHkPkUs+gFQCGZyki3NjJpQ/t3RfEpLpmXchP0Yr/OLQ== + dependencies: + "@helium/account-fetch-cache" "^0.2.17" + "@solana/web3.js" "^1.66.2" + react-async-hook "^4.0.0" + +"@helium/account-fetch-cache@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/account-fetch-cache/-/account-fetch-cache-0.2.17.tgz#e016ab0447666bab68fa6669101a34bf89b6feb2" + integrity sha512-N7iyyTMounGLsMaLkxxgc7UWEnwy7Ja9CiAhebF4QDB3DWzO+0YVSM5hCYaPexzgFfewqRiJvmczt3YMXxWERA== dependencies: "@solana/web3.js" "^1.43.4" -"@helium/address@4.6.2": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@helium/address/-/address-4.6.2.tgz#0356bd9693ff7184f93f3c4e12c04f4f9c1db7b6" - integrity sha512-MXlT9SH4HPrBj+r71w51grZj4MUNi4o4SnDJds/yallROBK0qlO8m/bDCsKfmj0FVm861tIfQBrJc6y5vjsymg== +"@helium/address@4.10.2", "@helium/address@^4.10.2": + version "4.10.2" + resolved "https://registry.yarnpkg.com/@helium/address/-/address-4.10.2.tgz#56960b118fceb6b6ddabe3e4ecec467d9ae50e26" + integrity sha512-qCswC7Z3GXuJyHv36RcOSnffeghjqJQx0fdu2Lxpf9fgOnIi1JZO2tjjk1mBaqOwCyp+0YzrTPUoEukL/WCtsA== dependencies: bs58 "^5.0.0" js-sha256 "^0.9.0" @@ -2284,10 +2293,10 @@ js-sha256 "^0.9.0" multiformats "^9.6.4" -"@helium/anchor-resolvers@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/anchor-resolvers/-/anchor-resolvers-0.2.5.tgz#d23fcd319d1f2444d9ebfa2273f9339beb8204bd" - integrity sha512-AOVa93cbFEUSDHR6OWHk5SlwwrnyStAZbjM1AGGUJdZEVzR1YnkYCl8aFZtVBCkCPmFz5iZDBjEx32chhRsEJA== +"@helium/anchor-resolvers@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/anchor-resolvers/-/anchor-resolvers-0.2.17.tgz#662fe995d2c9112f214698bfff2bdd0cf3510b75" + integrity sha512-Fd9WzdEbpRBiYc9uBUNlLu/BS2rNCn2qCdEC1bpZTq8tNAzkOM6KU3mBt4GvY3t1A5NVJjpW2H4sLHz9Uu0JiA== dependencies: "@solana/spl-token" "^0.3.6" "@solana/web3.js" "^1.43.4" @@ -2304,14 +2313,14 @@ bn.js "^5.2.0" bs58 "^4.0.1" -"@helium/circuit-breaker-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/circuit-breaker-sdk/-/circuit-breaker-sdk-0.2.5.tgz#5124fa950f706b324eb53ffc3b718a6ecf5cdd76" - integrity sha512-IG/f0ffY+lWPVOAYTH/yyloXRfCShUydwsyAjdYz4tI1tGVlysf88rR9kYWY/cJpep3ftemxZdvOwOSdhVt72A== +"@helium/circuit-breaker-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/circuit-breaker-sdk/-/circuit-breaker-sdk-0.2.17.tgz#79baa8778bec3bde091d6189eb03004e553e7da0" + integrity sha512-QZ2k6QeI5hNApcdXO835vcphdEnxZADkEGFGnjNCSkffRQOx5iPdR8EDSAfmYeUHFH917I57qxirRyCE5RPqBw== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/idls" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/idls" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" @@ -2334,13 +2343,6 @@ "@solana/spl-token" "^0.3.7" "@solana/web3.js" "^1.73.0" -"@helium/currency@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@helium/currency/-/currency-4.11.1.tgz#4658037197638406f0e60b246db70acd3323e46b" - integrity sha512-WxsMm3TvIXV8itR36yYxjXjtWAgJlId/OOrHivNTXUIdnxi6/j3JO/hY7EUHYygcw57O90Zqg4EBnypXzI4EKw== - dependencies: - bignumber.js "^9.0.0" - "@helium/currency@^4.7.3": version "4.10.0" resolved "https://registry.yarnpkg.com/@helium/currency/-/currency-4.10.0.tgz#c2cdd385c1c810d8664c1c3f88e888f802e299d3" @@ -2348,36 +2350,37 @@ dependencies: bignumber.js "^9.0.0" -"@helium/data-credits-sdk@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@helium/data-credits-sdk/-/data-credits-sdk-0.1.2.tgz#eefb1437bba789f5a4490d9567491a1091f148bc" - integrity sha512-S5zgjbZ/yMQwiHZmixFj+o1gzVsCu07WcrwW0brbBTrPXcMjwyDOfSxLfEmBPRLku/V6DXZjeNYiTmrFJGet2A== +"@helium/data-credits-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/data-credits-sdk/-/data-credits-sdk-0.2.17.tgz#778bf5b0c19237ff02b8973f4ab2d6a0fc4b64db" + integrity sha512-6n9quQRnbuCmv2OHkpbALWt2oMUtUnFJhj8m3mLbodeaed+/WDQnStufiK2RhiRv35grKQRxEmz08rbnoM04fA== dependencies: - "@coral-xyz/anchor" "0.26.0" - "@helium/circuit-breaker-sdk" "^0.1.2" - "@helium/helium-sub-daos-sdk" "^0.1.2" - "@helium/idls" "^0.1.1" - "@helium/spl-utils" "^0.1.2" - "@solana/spl-token" "^0.3.6" + "@coral-xyz/anchor" "^0.26.0" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/circuit-breaker-sdk" "^0.2.17" + "@helium/helium-sub-daos-sdk" "^0.2.17" + "@helium/idls" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" crypto-js "^4.1.1" -"@helium/distributor-oracle@^0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@helium/distributor-oracle/-/distributor-oracle-0.2.6.tgz#db9bbbe485fec8192a8d85f6e5681a14d4c985c3" - integrity sha512-s8aeRBOR4W8KG6ORg0I+VsJveKTCnLTscP/puY20Rv41Gtb1vsUH/QwSYGVFlWueuWaS42vAIE5/STkg95c2xw== +"@helium/distributor-oracle@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/distributor-oracle/-/distributor-oracle-0.2.17.tgz#1c0449ff5f6c7e3d401ceab18f08f9c35fc85ce4" + integrity sha512-okZvhpWmogcUZwstwRLDu16qLyf8DLFYbOfnCniv6bj/SVnVqz2P2sYVE5ktU3xtuCZb8gXX9gebv/hlRuU8Rw== dependencies: "@coral-xyz/anchor" "^0.26.0" "@fastify/cors" "^8.1.1" - "@helium/account-fetch-cache" "^0.2.5" - "@helium/address" "^4.6.2" - "@helium/helium-entity-manager-sdk" "^0.2.6" - "@helium/helium-sub-daos-sdk" "^0.2.5" - "@helium/idls" "^0.2.5" - "@helium/lazy-distributor-sdk" "^0.2.5" - "@helium/rewards-oracle-sdk" "^0.2.5" - "@helium/spl-utils" "^0.2.6" + "@helium/account-fetch-cache" "^0.2.17" + "@helium/address" "^4.10.2" + "@helium/helium-entity-manager-sdk" "^0.2.17" + "@helium/helium-sub-daos-sdk" "^0.2.17" + "@helium/idls" "^0.2.17" + "@helium/lazy-distributor-sdk" "^0.2.17" + "@helium/rewards-oracle-sdk" "^0.2.17" + "@helium/spl-utils" "^0.2.17" + "@metaplex-foundation/mpl-bubblegum" "^0.7.0" + "@solana/spl-token" "^0.3.8" "@types/sequelize" "^4.28.14" aws-sdk "^2.1313.0" axios "^0.27.2" @@ -2391,68 +2394,57 @@ sequelize "^6.28.0" typescript-collections "^1.3.3" -"@helium/fanout-sdk@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@helium/fanout-sdk/-/fanout-sdk-0.1.2.tgz#05ed4ec1eb8323e42bb5de69f0e1e9c2ee34f672" - integrity sha512-islOocQkPIpQpW/avWAE6v7N+uJ2AxPqMmmTXIeGUhALPDvrGU7m9a76lo1eDK5Ghakkt9N1LOH+Nd2+EfsXqA== +"@helium/fanout-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/fanout-sdk/-/fanout-sdk-0.2.17.tgz#62f252bcf3a733650b06853174dd9aa21305c922" + integrity sha512-ZS/0tOT3Xd6g5ZsV1/3NYaIWfP1+HTD4sqgYwohFOsAd/1ufBPMeaqv1DD04AMK+wW6cZXriFNOx8orydMbeWA== dependencies: - "@coral-xyz/anchor" "0.26.0" - "@helium/idls" "^0.1.1" - "@helium/spl-utils" "^0.1.2" + "@coral-xyz/anchor" "^0.26.0" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/idls" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" -"@helium/helium-entity-manager-sdk@^0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@helium/helium-entity-manager-sdk/-/helium-entity-manager-sdk-0.2.6.tgz#6175ff12c2ba58923ab3a69f18158120de13c1af" - integrity sha512-Z5LvS/MtELlVH/fGf0n6k5UEdjUAQn6MEMU1oO91lj3ft3mqi7BVVMttESohTnsWi3tVgceTs/Rye8e07TfD9A== +"@helium/helium-entity-manager-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/helium-entity-manager-sdk/-/helium-entity-manager-sdk-0.2.17.tgz#819ea9eaecf8c6c69218cb0e7be4daf5cfd98cb0" + integrity sha512-xDxP4twLo9LaNO85m1jvm8KZx29RNFPMqK7/RtWS0PAVURgIH5t5/85aqge8/dmanzpMMuEySX5Gs66BlUVqAQ== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/address" "^4.6.2" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/helium-sub-daos-sdk" "^0.2.5" - "@helium/idls" "^0.2.5" - "@helium/spl-utils" "^0.2.6" + "@helium/address" "^4.10.2" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/helium-sub-daos-sdk" "^0.2.17" + "@helium/idls" "^0.2.17" + "@helium/spl-utils" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" crypto-js "^4.1.1" js-sha256 "^0.9.0" -"@helium/helium-react-hooks@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@helium/helium-react-hooks/-/helium-react-hooks-0.1.2.tgz#ad609c3eee61de5d73507bcd68ff09eb8760dea2" - integrity sha512-36uINzAPlduQ+p1kF21JPgQMrpB4ECj9eBYnvdnppkXw3/dOH5KKNq4vkE1U7r7g8/Ymnyx6soVlGhUl8HHZ0w== +"@helium/helium-react-hooks@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/helium-react-hooks/-/helium-react-hooks-0.2.17.tgz#19b9c13daa57f5c9486fdd1b34e6e9cce26e8ce2" + integrity sha512-x7af6grDch8khPuZc+kLfGWKF9rhYjaRazyRoueFK2qbdezF70Y6SdW0EtF5acS5DubKZjCz/51DBD8CSUXKBQ== dependencies: - "@coral-xyz/anchor" "0.26.0" - "@helium/spl-utils" "^0.1.2" + "@coral-xyz/anchor" "^0.26.0" + "@helium/account-fetch-cache" "^0.2.17" + "@helium/account-fetch-cache-hooks" "^0.2.17" "@solana/spl-token" "^0.3.6" "@solana/web3.js" "^1.66.2" bs58 "^5.0.0" + pako "^2.0.3" react-async-hook "^4.0.0" -"@helium/helium-sub-daos-sdk@0.1.2", "@helium/helium-sub-daos-sdk@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@helium/helium-sub-daos-sdk/-/helium-sub-daos-sdk-0.1.2.tgz#c0ea730f7b26d2f42c0d62c13caace67f8f8c023" - integrity sha512-n/ZwnDDSxniYgxvAom0qfzWk2xZMlN7mUEzrzt+3MwiNX3Qm0/khHjLv6MdgfXnCapunekEUl2xU6I7YpYxyOw== - dependencies: - "@coral-xyz/anchor" "0.26.0" - "@helium/circuit-breaker-sdk" "^0.1.2" - "@helium/spl-utils" "^0.1.2" - "@helium/treasury-management-sdk" "^0.1.2" - "@helium/voter-stake-registry-sdk" "^0.1.2" - bn.js "^5.2.0" - bs58 "^4.0.1" - -"@helium/helium-sub-daos-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/helium-sub-daos-sdk/-/helium-sub-daos-sdk-0.2.5.tgz#1f9af001a3784250bce899a9ce4a35020da11dde" - integrity sha512-U+AT8t3wJhZTambCtSHnvXBl+5fVw2NhQ49AgbzKm3Yo3JBnyXJ31mdTGyeT67QrviOW2nOmU8adozp7AXsiOw== +"@helium/helium-sub-daos-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/helium-sub-daos-sdk/-/helium-sub-daos-sdk-0.2.17.tgz#dbd279eceaff152791cea316495cc51c1d275391" + integrity sha512-P7q3wLVdpsvq5eJpWy1BdCy6amQjkz1ZEa/aIPTKNvuGPIjJjRzARzP0Aht4119JjvlMYd8tFsZYCqrDUnoiXA== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/circuit-breaker-sdk" "^0.2.5" - "@helium/treasury-management-sdk" "^0.2.5" - "@helium/voter-stake-registry-sdk" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/circuit-breaker-sdk" "^0.2.17" + "@helium/treasury-management-sdk" "^0.2.17" + "@helium/voter-stake-registry-sdk" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" @@ -2491,10 +2483,10 @@ borsh "^0.7.0" bs58 "^4.0.1" -"@helium/idls@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/idls/-/idls-0.2.5.tgz#96f00926027a8f6beb3a51e877cc5a74210dcd2c" - integrity sha512-KzoKeWnfvQJiRDi6eiDADbUTvVyOLre8ht46LeSq02Nx/UQO2SmgymI/YjwlO0o8Zm7L/r8dxym0+MYG++lLHw== +"@helium/idls@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/idls/-/idls-0.2.17.tgz#d9908804df406a2e2953fc1f2038b3fbd62d4d41" + integrity sha512-/RUreFrmAEeis2+b29sgMPfmfGU5VL1z8N9B4NzB+iRkDCxMa3nXn0HRKdNGj0nmawpZlQhG90uOz/NKlluwjQ== dependencies: "@coral-xyz/anchor" "^0.26.0" "@solana/web3.js" "^1.43.4" @@ -2502,27 +2494,14 @@ borsh "^0.7.0" bs58 "^4.0.1" -"@helium/lazy-distributor-sdk@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@helium/lazy-distributor-sdk/-/lazy-distributor-sdk-0.1.2.tgz#fba4313826eee6ce199143104bbd502c29d25711" - integrity sha512-kAFexiDHjGbEpm2H+C/8omOlu+0pClX61js8eslZxJgtWoLjMa+iwWNp2CPILDplsP/pKJyMPEHFHW67+ezCMQ== - dependencies: - "@coral-xyz/anchor" "0.26.0" - "@helium/circuit-breaker-sdk" "^0.1.2" - "@helium/spl-utils" "^0.1.2" - "@metaplex-foundation/mpl-token-metadata" "^2.2.3" - "@solana/spl-token" "^0.3.6" - bn.js "^5.2.0" - bs58 "^4.0.1" - -"@helium/lazy-distributor-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/lazy-distributor-sdk/-/lazy-distributor-sdk-0.2.5.tgz#b01bbc57b87f5f24168ecdfaaf2af6e45d17cebe" - integrity sha512-pkG1jUbkGehiurO15ew4AEN0j0WaSQKk0AQNVEZoJy4TG3TFVuCJPr8gyN430nRuwdl5KZgs4gItD1cigw7/dw== +"@helium/lazy-distributor-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/lazy-distributor-sdk/-/lazy-distributor-sdk-0.2.17.tgz#4c2b0ecb89b92d9c2084ff9c04e365a275c50d61" + integrity sha512-FgdeV9msUyHGuNfofohc9nj9ioryRVQfMDJAEzFx4qxoWTKkcbQJKGy578JZR3ci0QlARayEBJUf+I5JljK07Q== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/circuit-breaker-sdk" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/circuit-breaker-sdk" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" @@ -2554,13 +2533,13 @@ resolved "https://registry.yarnpkg.com/@helium/react-native-sdk/-/react-native-sdk-1.0.0.tgz#41024fa99859490bd8a0b717f52acc11ae72f114" integrity sha512-Qi1Nnp/q2hsz2D7aeuM6LxXhNX8NrHz1U+PoQslwK2XfqPFZEYb4uAzjXDKlc+JBWPiF96GMJywv/ofxlZ9XLg== -"@helium/rewards-oracle-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/rewards-oracle-sdk/-/rewards-oracle-sdk-0.2.5.tgz#24f0fa7cb5971264c4b5b99e89033ffeab3be000" - integrity sha512-eiOAMhNcQg6bAm/bbIKW3UM/TqVO38HUWJur8HdzCCjfxYtxXjtAozDggfePJAZ1WGe2oaReEBXId+KRkBPE5w== +"@helium/rewards-oracle-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/rewards-oracle-sdk/-/rewards-oracle-sdk-0.2.17.tgz#a356d1407c06862a382ecfa0889bf37667ee33e8" + integrity sha512-Uq1DcmdcfkrpQ839KQeBv38hMmaeHm33Iq8Nz8ajah/YbqYMgIsvgvr0999+t37T/yn8xvjJGpVm3rHAjPgEwg== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" "@helium/idls" "^0.0.43" bn.js "^5.2.0" bs58 "^4.0.1" @@ -2579,15 +2558,15 @@ borsh "^0.7.0" bs58 "^5.0.0" -"@helium/spl-utils@^0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.2.6.tgz#20b136f13d9e1d58e7032b4c55b5cc0c1ad9f6dc" - integrity sha512-Ut1bdWRMyru4YOYFB4NS31fbSGYTtW3gMxEg3/c+SKMwAfoZcRIOE8FgOV1nugSrFJ570zsx8R7lHIQYfAOmog== +"@helium/spl-utils@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.2.17.tgz#150a7942039a844a3c11368fc5f93002db7dd708" + integrity sha512-m+IGp4Un9p6XDu/4tgox7guoVbiiyj0LJ9qSPsDaAF1LAVq8N+uzgkacElotHF1d0ePjN8Ku+Q9IEM9+xyCjeg== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/account-fetch-cache" "^0.2.5" - "@helium/address" "^4.8.1" - "@helium/anchor-resolvers" "^0.2.5" + "@helium/account-fetch-cache" "^0.2.17" + "@helium/address" "^4.10.2" + "@helium/anchor-resolvers" "^0.2.17" "@metaplex-foundation/mpl-token-metadata" "^2.5.2" "@solana/spl-account-compression" "^0.1.7" "@solana/spl-token" "^0.3.6" @@ -2607,7 +2586,7 @@ long "^4.0.0" path "^0.12.7" -"@helium/treasury-management-sdk@0.1.2", "@helium/treasury-management-sdk@^0.1.2": +"@helium/treasury-management-sdk@0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@helium/treasury-management-sdk/-/treasury-management-sdk-0.1.2.tgz#abdb08548e05535eeee32a492238a821bd95e5fe" integrity sha512-rMazqYxFSxjn+tjQOLKbFORKZDqhfKX9OeHueZN1N+t2a4c839Nl495ai6crjQ0546MSHUx5M29XTVXRY5ceOA== @@ -2620,19 +2599,19 @@ bn.js "^5.2.0" bs58 "^4.0.1" -"@helium/treasury-management-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/treasury-management-sdk/-/treasury-management-sdk-0.2.5.tgz#fc3babc07a26da6bd75c9cd7fa9774e7803e4305" - integrity sha512-j1faBZ52KLAaCz5GHmK5Xugg7PvOi5kpP4PmB0DmTRdhF8f7mfTIPWvD/SVI1vIQ4p1ng8ucURKTpCWRGRzIqA== +"@helium/treasury-management-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/treasury-management-sdk/-/treasury-management-sdk-0.2.17.tgz#b5495352e948aa576ea2a24d68406d921c37079f" + integrity sha512-me5Ni6HrCRDRho8IkdUE29zD83wi8UwzLdZpVBIjdrmXVJ9gKLSTkOxVtIq1/nYFJc7IYy3fkXUbOdMs9rt7lA== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/circuit-breaker-sdk" "^0.2.5" - "@helium/idls" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/circuit-breaker-sdk" "^0.2.17" + "@helium/idls" "^0.2.17" bn.js "^5.2.0" bs58 "^4.0.1" -"@helium/voter-stake-registry-sdk@0.1.2", "@helium/voter-stake-registry-sdk@^0.1.2": +"@helium/voter-stake-registry-sdk@0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.1.2.tgz#413d3e56b1746de9a4986dd503d19bc8dc958c13" integrity sha512-Xs2VOZaYnHgtA803HgJEiVJv6BeDyZxeJCapZBInVATO7Ry7cdnmaso8h5KW+3wI/wk3/LsyX8BvKIxUJk3nFQ== @@ -2645,14 +2624,14 @@ bn.js "^5.2.0" bs58 "^4.0.1" -"@helium/voter-stake-registry-sdk@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.2.5.tgz#eefc2de7a4d77a016fefb63446816ccac8b9ff8c" - integrity sha512-+u87bpAHDZRosMEjqa3xTGSGEvFjrC6Mq6b0AqzMOAc0z3rYxNCRaSXRZ9fpOoGwm+hJfVTdP9JEHt82dqs6wQ== +"@helium/voter-stake-registry-sdk@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.2.17.tgz#3411a60c25d687502ed2dc6fe8f5b0f4aa3c4046" + integrity sha512-WyDHS/JE6RzA4mKC1EXBG4HZv9BqiMZjGnRkNTwlhEAvfaxB44XUlhZpTEIIffiFbzACxHYFaBm10HokwgVFPw== dependencies: "@coral-xyz/anchor" "^0.26.0" - "@helium/anchor-resolvers" "^0.2.5" - "@helium/idls" "^0.2.5" + "@helium/anchor-resolvers" "^0.2.17" + "@helium/idls" "^0.2.17" "@metaplex-foundation/mpl-token-metadata" "^2.2.3" "@solana/spl-token" "^0.3.6" bn.js "^5.2.0" @@ -3214,6 +3193,20 @@ bn.js "^5.2.0" js-sha3 "^0.8.0" +"@metaplex-foundation/mpl-bubblegum@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-0.7.0.tgz#b34067ad4fe846ceb60e47e49f221ecf4730add7" + integrity sha512-HCo6q+nh8M3KRv9/aUaZcJo5/vPJEeZwPGRDWkqN7lUXoMIvhd83fZi7MB1rIg1gwpVHfHqim0A02LCYKisWFg== + dependencies: + "@metaplex-foundation/beet" "0.7.1" + "@metaplex-foundation/beet-solana" "0.4.0" + "@metaplex-foundation/cusper" "^0.0.2" + "@metaplex-foundation/mpl-token-metadata" "^2.5.2" + "@solana/spl-account-compression" "^0.1.4" + "@solana/spl-token" "^0.1.8" + "@solana/web3.js" "^1.50.1" + js-sha3 "^0.8.0" + "@metaplex-foundation/mpl-candy-guard@^0.3.0": version "0.3.2" resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-candy-guard/-/mpl-candy-guard-0.3.2.tgz#426e89793676b42e9bbb5e523303fba36ccd5281" @@ -3408,6 +3401,13 @@ dependencies: merge-options "^3.0.4" +"@react-native-async-storage/async-storage@^1.17.7": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.19.1.tgz#09d35caaa31823b40fdfeebf95decf8f992a6274" + integrity sha512-5QXuGCtB+HL3VtKL2JN3+6t4qh8VXizK+aGDAv6Dqiq3MLrzgZHb4tjVgtEWMd8CcDtD/JqaAI1b6/EaYGtFIA== + dependencies: + merge-options "^3.0.4" + "@react-native-community/blur@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.0.tgz#e5018b3b0bd6de9632ac6cf34e9f8e0f1a9a28ec" @@ -3750,6 +3750,30 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@solana-mobile/mobile-wallet-adapter-protocol-web3js@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@solana-mobile/mobile-wallet-adapter-protocol-web3js/-/mobile-wallet-adapter-protocol-web3js-2.0.1.tgz#096599cd2073d35f77be9121222e654321b16e85" + integrity sha512-zpMEU40PJ2rmRgqXbn7JqyHhrygQLX9MVszAczVDzqg39aMD7yo6VyTI8NEH422JzCj3Cjl9DndQZJRNdqdOHw== + dependencies: + "@solana-mobile/mobile-wallet-adapter-protocol" "^1.0.0" + bs58 "^5.0.0" + js-base64 "^3.7.2" + +"@solana-mobile/mobile-wallet-adapter-protocol@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@solana-mobile/mobile-wallet-adapter-protocol/-/mobile-wallet-adapter-protocol-1.0.1.tgz#31bf53610c6502a40a1545b9d73009bf3f32d0d6" + integrity sha512-T+xroGLYaYeI8TXy85oNul2m1k/oF9dAW7eRy/MF9up1EQ5SPL5KWFICQfV2gy87jBZd1y0k0M2GayVN7QdQpA== + +"@solana-mobile/wallet-adapter-mobile@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@solana-mobile/wallet-adapter-mobile/-/wallet-adapter-mobile-2.0.1.tgz#1f36b56013c5bd016a4c846b1c00b31452083139" + integrity sha512-QIM8nqVRmv8yEsTEfMbzWH7NoVVC6F417Z/tf6FpOVn1N6MqiZc5kvajsaRJKXrHzi5r5023SKErjg9LjZshXw== + dependencies: + "@react-native-async-storage/async-storage" "^1.17.7" + "@solana-mobile/mobile-wallet-adapter-protocol-web3js" "^2.0.1" + "@solana/wallet-adapter-base" "^0.9.17" + js-base64 "^3.7.2" + "@solana/buffer-layout-utils@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" @@ -3819,7 +3843,16 @@ "@solana/buffer-layout-utils" "^0.2.0" buffer "^6.0.3" -"@solana/wallet-adapter-base@^0.9.2": +"@solana/spl-token@^0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.8.tgz#8e9515ea876e40a4cc1040af865f61fc51d27edf" + integrity sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + buffer "^6.0.3" + +"@solana/wallet-adapter-base@^0.9.17", "@solana/wallet-adapter-base@^0.9.2", "@solana/wallet-adapter-base@^0.9.21", "@solana/wallet-adapter-base@^0.9.22": version "0.9.22" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.22.tgz#97812eaf6aebe01e5fe714326b3c9a0614ae6112" integrity sha512-xbLEZPGSJFvgTeldG9D55evhl7QK/3e/F7vhvcA97mEt1eieTgeKMnGlmmjs3yivI3/gtZNZeSk1XZLnhKcQvw== @@ -3829,6 +3862,22 @@ "@wallet-standard/features" "^1.0.3" eventemitter3 "^4.0.7" +"@solana/wallet-adapter-react@^0.15.33": + version "0.15.33" + resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-react/-/wallet-adapter-react-0.15.33.tgz#85c7e6528cb572338cc5fa331e2371a3c2818392" + integrity sha512-0bkP6wbWuLtIiswwFke4XjF++Tbyk+cBa1XY7r4msMCg+oQo+BA3TvoiuuL3dPOgsZ2f21A+cr6ekXZ5nfZNIA== + dependencies: + "@solana-mobile/wallet-adapter-mobile" "^2.0.0" + "@solana/wallet-adapter-base" "^0.9.22" + "@solana/wallet-standard-wallet-adapter-react" "^1.0.2" + +"@solana/wallet-standard-chains@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@solana/wallet-standard-chains/-/wallet-standard-chains-1.0.0.tgz#4c291a2f79f0e105ce0a47b30d98dbd7f8ba69be" + integrity sha512-kr3+JAo7mEBhVCH9cYzjn/vXeUiZeYfB4BF6E8u3U2jq3KlZA/KB+YM976+zGumTfN0NmMXUm066pTTG9kJsNQ== + dependencies: + "@wallet-standard/base" "^1.0.0" + "@solana/wallet-standard-features@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@solana/wallet-standard-features/-/wallet-standard-features-1.0.0.tgz#fee71c47fa8c4bacbdc5c8750487e60a2e5e6746" @@ -3837,7 +3886,7 @@ "@wallet-standard/base" "^1.0.0" "@wallet-standard/features" "^1.0.0" -"@solana/wallet-standard-features@^1.0.1": +"@solana/wallet-standard-features@^1.0.0", "@solana/wallet-standard-features@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@solana/wallet-standard-features/-/wallet-standard-features-1.0.1.tgz#36270a646f74a80e51b9e21fb360edb64f840c68" integrity sha512-SUfx7KtBJ55XIj0qAhhVcC1I6MklAXqWFEz9hDHW+6YcJIyvfix/EilBhaBik1FJ2JT0zukpOfFv8zpuAbFRbw== @@ -3845,6 +3894,37 @@ "@wallet-standard/base" "^1.0.1" "@wallet-standard/features" "^1.0.3" +"@solana/wallet-standard-util@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@solana/wallet-standard-util/-/wallet-standard-util-1.0.0.tgz#597b3a240f1855d25d93c2cd7efe8631a1a241ac" + integrity sha512-qRAOBXnN7dwvtgzTtxIHsSeJAMbGNZdSWs57TT8pCyBrKL5dVxaK2u95Dm17SRSzwfKl/EByV1lTjOxXyKWS+g== + dependencies: + "@solana/wallet-standard-chains" "^1.0.0" + "@solana/wallet-standard-features" "^1.0.0" + +"@solana/wallet-standard-wallet-adapter-base@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@solana/wallet-standard-wallet-adapter-base/-/wallet-standard-wallet-adapter-base-1.0.2.tgz#d6028c27381fe384a34a7426b3eb828a1da6bff0" + integrity sha512-QqkupdWvWuihX87W6ijt174u6ZdP5OSFlNhZhuhoMlIdyI/sj7MhGsdppuRlMh65oVO2WNWTL9y2bO5Pbx+dfg== + dependencies: + "@solana/wallet-adapter-base" "^0.9.21" + "@solana/wallet-standard-chains" "^1.0.0" + "@solana/wallet-standard-features" "^1.0.1" + "@solana/wallet-standard-util" "^1.0.0" + "@wallet-standard/app" "^1.0.1" + "@wallet-standard/base" "^1.0.1" + "@wallet-standard/features" "^1.0.3" + "@wallet-standard/wallet" "^1.0.1" + +"@solana/wallet-standard-wallet-adapter-react@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@solana/wallet-standard-wallet-adapter-react/-/wallet-standard-wallet-adapter-react-1.0.2.tgz#8359900de37b05f921eb2318162a4dc179ce9864" + integrity sha512-0YTPUnjiSG5ajDP2hK8EipxkeHhO3+nCtXeF1eS/ZP2QcFAgS/4luywrn/6CdfzQ2cQYPCFdnG/QculpUp6bBg== + dependencies: + "@solana/wallet-standard-wallet-adapter-base" "^1.0.2" + "@wallet-standard/app" "^1.0.1" + "@wallet-standard/base" "^1.0.1" + "@solana/web3.js@1.64.0": version "1.64.0" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.64.0.tgz#b7f5a976976039a0161242e94d6e1224ab5d30f9" @@ -4801,6 +4881,13 @@ "@urql/core" ">=2.3.1" wonka "^4.0.14" +"@wallet-standard/app@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wallet-standard/app/-/app-1.0.1.tgz#f83c3ae887f7fb52497a7b259bba734ae10a2994" + integrity sha512-LnLYq2Vy2guTZ8GQKKSXQK3+FRGPil75XEdkZqE6fiLixJhZJoJa5hT7lXxwe0ykVTt9LEThdTbOpT7KadS26Q== + dependencies: + "@wallet-standard/base" "^1.0.1" + "@wallet-standard/base@^1.0.0", "@wallet-standard/base@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@wallet-standard/base/-/base-1.0.1.tgz#860dd94d47c9e3c5c43b79d91c6afdbd7a36264e" @@ -4813,6 +4900,13 @@ dependencies: "@wallet-standard/base" "^1.0.1" +"@wallet-standard/wallet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wallet-standard/wallet/-/wallet-1.0.1.tgz#95438941a2a1ee12a794444357b59d53e19b374c" + integrity sha512-qkhJeuQU2afQTZ02yMZE5SFc91Fo3hyFjFkpQglHudENNyiSG0oUKcIjky8X32xVSaumgTZSQUAzpXnCTWHzKQ== + dependencies: + "@wallet-standard/base" "^1.0.1" + "@walletconnect/core@2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.2.1.tgz#2dc6b8b2c438919a800ae4b76832661b922a3511" @@ -10467,6 +10561,11 @@ join-component@^1.1.0: resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ== +js-base64@^3.7.2: + version "3.7.5" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" + integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA== + js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"