diff --git a/index.js b/index.js index 1c6897859..d36d631a1 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +import './shim' import { ThemeProvider } from '@shopify/restyle' import React from 'react' import { ErrorBoundary } from 'react-error-boundary' diff --git a/ios/HeliumWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/HeliumWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7ebcc34da..8c8230ba7 100644 --- a/ios/HeliumWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/HeliumWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "e70d3525c8e2819a8b34f22909815dab5c700c25a06c32388f3930f7b3627768", "pins" : [ { "identity" : "maplibre-gl-native-distribution", @@ -8,25 +9,7 @@ "revision" : "ffda61e298c1490d4860d5184e80d618aaadc089", "version" : "5.13.0" } - }, - { - "identity" : "swiftui-charts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/spacenation/swiftui-charts", - "state" : { - "revision" : "b044e7eb04d0026490eecb115f4fc07197dad942", - "version" : "1.1.0" - } - }, - { - "identity" : "swiftui-shapes", - "kind" : "remoteSourceControl", - "location" : "https://github.com/spacenation/swiftui-shapes.git", - "state" : { - "revision" : "c58b15c37eae9bd20525c6daa93a06a689ca75cb", - "version" : "1.1.0" - } } ], - "version" : 2 + "version" : 3 } diff --git a/package.json b/package.json index 18ab4993b..eff00ef23 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@helium/voter-stake-registry-sdk": "0.9.7", "@helium/wallet-link": "4.11.0", "@jup-ag/api": "^6.0.6", + "@keystonehq/keystone-sdk": "^0.8.0", "@ledgerhq/hw-app-solana": "7.0.13", "@ledgerhq/react-native-hid": "6.30.0", "@ledgerhq/react-native-hw-transport-ble": "6.29.5", @@ -68,6 +69,7 @@ "@maplibre/maplibre-react-native": "^9.1.0", "@metaplex-foundation/mpl-bubblegum": "0.6.0", "@metaplex-foundation/mpl-token-metadata": "2.10.0", + "@ngraveio/bc-ur": "^1.1.13", "@onsol/tldparser": "^0.5.3", "@react-native-async-storage/async-storage": "1.18.1", "@react-native-community/blur": "4.3.0", diff --git a/src/App.tsx b/src/App.tsx index c026f40ea..0eaba2306 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,7 @@ import { GovernanceProvider } from './storage/GovernanceProvider' import { useNotificationStorage } from './storage/NotificationStorageProvider' import { BalanceProvider } from './utils/Balance' import { useDeepLinking } from './utils/linking' +import KeystoneOnboardingProvider from './features/keystone/KeystoneOnboardingProvider' SplashLib.preventAutoHideAsync().catch(() => { /* reloading the app might trigger some race conditions, ignore them */ @@ -122,46 +123,48 @@ const App = () => { - - - - {accountsRestored && ( - <> - - - - - - - - - + + + + + {accountsRestored && ( + <> + + + + + + + + + - {/* place app specific modals here */} - - - - - - - - - - )} - - - + {/* place app specific modals here */} + + + + + + + + + + )} + + + + diff --git a/src/assets/images/connectKeystoneLogo.png b/src/assets/images/connectKeystoneLogo.png new file mode 100644 index 000000000..3e4239ea3 Binary files /dev/null and b/src/assets/images/connectKeystoneLogo.png differ diff --git a/src/assets/images/connectKeystoneLogo@2x.png b/src/assets/images/connectKeystoneLogo@2x.png new file mode 100644 index 000000000..31b904dd0 Binary files /dev/null and b/src/assets/images/connectKeystoneLogo@2x.png differ diff --git a/src/assets/images/connectKeystoneLogo@3x.png b/src/assets/images/connectKeystoneLogo@3x.png new file mode 100644 index 000000000..de915495c Binary files /dev/null and b/src/assets/images/connectKeystoneLogo@3x.png differ diff --git a/src/assets/images/keystoneLogo.svg b/src/assets/images/keystoneLogo.svg new file mode 100644 index 000000000..65a85eb06 --- /dev/null +++ b/src/assets/images/keystoneLogo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/scannerLine.png b/src/assets/images/scannerLine.png new file mode 100644 index 000000000..c390a8821 Binary files /dev/null and b/src/assets/images/scannerLine.png differ diff --git a/src/assets/images/scannerLine@2x.png b/src/assets/images/scannerLine@2x.png new file mode 100644 index 000000000..763ddaa0a Binary files /dev/null and b/src/assets/images/scannerLine@2x.png differ diff --git a/src/assets/images/scannerLine@3x.png b/src/assets/images/scannerLine@3x.png new file mode 100644 index 000000000..ff3e01aa3 Binary files /dev/null and b/src/assets/images/scannerLine@3x.png differ diff --git a/src/assets/images/warningKeystone.svg b/src/assets/images/warningKeystone.svg new file mode 100644 index 000000000..9b1341fba --- /dev/null +++ b/src/assets/images/warningKeystone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/BackScreen.tsx b/src/components/BackScreen.tsx index eee02f9cf..7dd38f126 100644 --- a/src/components/BackScreen.tsx +++ b/src/components/BackScreen.tsx @@ -107,7 +107,8 @@ const BackScreen = ({ )} - + {/* if padding is not set, set it to 'lx' , if padding set to 'none' , set it to 0 */} + {children} diff --git a/src/components/CameraScannerLayout.tsx b/src/components/CameraScannerLayout.tsx new file mode 100644 index 000000000..187d82d66 --- /dev/null +++ b/src/components/CameraScannerLayout.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react' +import { Image } from 'react-native' +import Animated, { + useSharedValue, + useAnimatedStyle, + withRepeat, + withTiming, +} from 'react-native-reanimated' +import Box from './Box' + +const SCANNER_SIZE = 300 +const SCANNER_LINE_HEIGHT = 43 +const SCAN_DURATION = 2000 +const BORDER_SEGMENT_SIZE = 40 +export const CameraScannerLayout = () => { + const linePosition = useSharedValue(-SCANNER_LINE_HEIGHT) + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: linePosition.value }], + })) + + useEffect(() => { + linePosition.value = withRepeat( + withTiming(SCANNER_SIZE, { + duration: SCAN_DURATION, + }), + -1, + ) + }, [linePosition]) + + return ( + + + {/* top left */} + + {/* top right */} + + {/* bottom left */} + + {/* bottom right */} + + {/* animated scanner line */} + + + + + + ) +} diff --git a/src/components/DynamicQrScanner.tsx b/src/components/DynamicQrScanner.tsx new file mode 100644 index 000000000..1402a3870 --- /dev/null +++ b/src/components/DynamicQrScanner.tsx @@ -0,0 +1,98 @@ +/* eslint-disable no-console */ +import React, { useEffect, useState } from 'react' +import { BarcodeScanningResult, Camera, CameraView } from 'expo-camera' +import { Linking, Platform, StyleSheet } from 'react-native' +import { useNavigation } from '@react-navigation/native' +import { useAsync } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import useAlert from '@hooks/useAlert' +import { CameraScannerLayout } from './CameraScannerLayout' +import Box from './Box' +import BackScreen from './BackScreen' +import ProgressBar from './ProgressBar' +import Text from './Text' + +type Props = { + progress: number + onBarCodeScanned: (data: string) => void +} +const DynamicQrScanner = ({ onBarCodeScanned, progress }: Props) => { + const [hasPermission, setHasPermission] = useState() + const navigation = useNavigation() + const { showOKCancelAlert } = useAlert() + const { t } = useTranslation() + + useEffect(() => { + Camera.requestCameraPermissionsAsync().then( + ({ status }: { status: string }) => { + setHasPermission(status === 'granted') + }, + ) + }, []) + + useAsync(async () => { + if (hasPermission !== false) return + + // if permission is not granted, show alert to open settings + const decision = await showOKCancelAlert({ + title: t('qrScanner.deniedAlert.title'), + message: t('qrScanner.deniedAlert.message'), + ok: t('qrScanner.deniedAlert.ok'), + }) + + // if user clicks ok, open settings + if (decision) { + if (Platform.OS === 'ios') { + Linking.openURL('app-settings:') + } else { + Linking.openSettings() + } + } + + // if user clicks cancel, go back to the previous screen + if (decision === false) { + navigation.goBack() + } + }, [hasPermission, navigation, showOKCancelAlert]) + + const handleBarCodeScanned = (result: BarcodeScanningResult) => { + onBarCodeScanned(result.data) + } + + return ( + + {/* if permission is not granted, show a black screen and notice alert modal */} + {hasPermission !== true && } + + {hasPermission === true && ( + + + + + + + + + + + {t('keystone.payment.scanTxQrcodeScreenSubtitle3')} + + + + )} + + ) +} +export default DynamicQrScanner diff --git a/src/components/StaticQrCode.tsx b/src/components/StaticQrCode.tsx new file mode 100644 index 000000000..4e785618d --- /dev/null +++ b/src/components/StaticQrCode.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useMemo, useState } from 'react' +import QRCode from 'react-native-qrcode-svg' +import { UR, UREncoder } from '@ngraveio/bc-ur' +import Box from './Box' + +const QR_CONTAINER_SIZE = 300 +const MAX_FRAGMENT_CAPACITY = 200 +export type StaticQrCodeProps = { + size?: number + ecl?: 'L' | 'M' | 'Q' | 'H' + quietZone?: number + data: string +} + +const StaticQrCode = ({ size, data, ecl, quietZone }: StaticQrCodeProps) => { + const qrCodeSize = size ?? QR_CONTAINER_SIZE + const qrCodeQuietZone = quietZone ?? 20 + const qrCodeEcl = ecl ?? 'L' + return ( + + + + ) +} + +export type AnimatedQrCodeProps = { + size?: number + ecl?: 'L' | 'M' | 'Q' | 'H' + quietZone?: number + refreshSpeed?: number + qrCodeType: string + cborData: string +} +const AnimatedQrCode = ({ + size, + cborData, + ecl, + quietZone, + qrCodeType, + refreshSpeed, +}: AnimatedQrCodeProps) => { + const qrCodeSize = size ?? QR_CONTAINER_SIZE + const qrCodeQuietZone = quietZone ?? 20 + const qrCodeEcl = ecl ?? 'L' + const qrRefreshSpeed = refreshSpeed ?? 200 + const urEncoder = useMemo(() => { + const ur = new UR(Buffer.from(cborData, 'hex'), qrCodeType) + return new UREncoder(ur, MAX_FRAGMENT_CAPACITY) + }, [cborData, qrCodeType]) + const firstUR = useMemo(() => urEncoder.nextPart(), [urEncoder]) + const [ur, setUR] = useState(firstUR) + + useEffect(() => { + const interval = setInterval(() => { + setUR(urEncoder.nextPart()) + }, qrRefreshSpeed) + return () => clearInterval(interval) + }, [urEncoder, qrRefreshSpeed]) + + return ( + + ) +} + +export { StaticQrCode, AnimatedQrCode } diff --git a/src/features/account/AccountsScreen.tsx b/src/features/account/AccountsScreen.tsx index 720d3c085..62e566c10 100644 --- a/src/features/account/AccountsScreen.tsx +++ b/src/features/account/AccountsScreen.tsx @@ -141,9 +141,15 @@ const AccountsScreen = () => { useEffect(() => { if (currentAccount?.ledgerDevice) return + // if current account is keystone account , check pass + if (currentAccount?.keystoneDevice) return const address = currentAccount?.address if (address) checkSecureAccount(address) - }, [currentAccount?.address, currentAccount?.ledgerDevice]) + }, [ + currentAccount?.address, + currentAccount?.ledgerDevice, + currentAccount?.keystoneDevice, + ]) useEffect(() => { if (openedNotification && !locked) { diff --git a/src/features/browser/BrowserWebViewScreen.tsx b/src/features/browser/BrowserWebViewScreen.tsx index 829c9549b..46aad86e8 100644 --- a/src/features/browser/BrowserWebViewScreen.tsx +++ b/src/features/browser/BrowserWebViewScreen.tsx @@ -271,39 +271,21 @@ const BrowserWebViewScreen = () => { return } - const signedTransactions = await Promise.all( - transactions.map( - async ({ - transaction, - }: SolanaSignAndSendTransactionInput & { - transaction: Transaction | VersionedTransaction - }) => { - let signedTransaction: - | Transaction - | VersionedTransaction - | undefined - if (!isVersionedTransaction) { - // TODO: Verify when lookup table is needed - // transaction.add(lookupTableAddress) - signedTransaction = - await anchorProvider?.wallet.signTransaction( - transaction as Transaction, - ) - } else { - signedTransaction = - await anchorProvider?.wallet.signTransaction( - transaction as VersionedTransaction, - ) - } - - if (!signedTransaction) { - throw new Error('Failed to sign transaction') - } - - return signedTransaction - }, - ), - ) + const signedTransactions: (Transaction | VersionedTransaction)[] = [] + // eslint-disable-next-line no-restricted-syntax + for (const txInput of transactions) { + try { + const { transaction } = txInput + const convertTx = isVersionedTransaction + ? (transaction as VersionedTransaction) + : (transaction as Transaction) + const signedTransaction = + await anchorProvider?.wallet.signTransaction(convertTx) + signedTransactions.push(signedTransaction) + } catch (e) { + throw new Error('Failed to sign transaction') + } + } outputs.push( ...signedTransactions.map((signedTransaction) => { diff --git a/src/features/home/HomeNavigator.tsx b/src/features/home/HomeNavigator.tsx index d2c3286c8..736a7ad13 100644 --- a/src/features/home/HomeNavigator.tsx +++ b/src/features/home/HomeNavigator.tsx @@ -22,6 +22,7 @@ import SettingsNavigator from '../settings/SettingsNavigator' import SwapNavigator from '../swaps/SwapNavigator' import AddNewAccountNavigator from './addNewAccount/AddNewAccountNavigator' import ImportSubAccountsScreen from '../onboarding/import/ImportSubAccountsScreen' +import KeystoneAccountAssignScreen from '../keystone/KeystoneAccountAssignScreen' const HomeStack = createStackNavigator() @@ -49,6 +50,10 @@ const HomeStackScreen = () => { name="AccountAssignScreen" component={AccountAssignScreen} /> + { name="CLIAccountNavigator" component={CLIAccountNavigator} /> + ) } diff --git a/src/features/home/addNewAccount/AddNewAccountScreen.tsx b/src/features/home/addNewAccount/AddNewAccountScreen.tsx index c650b9954..0395e205d 100644 --- a/src/features/home/addNewAccount/AddNewAccountScreen.tsx +++ b/src/features/home/addNewAccount/AddNewAccountScreen.tsx @@ -8,6 +8,7 @@ import FadeInOut from '@components/FadeInOut' import TabBar from '@components/TabBar' import Text from '@components/Text' import globalStyles from '@theme/globalStyles' +import ConnectKeystoneStart from '../../keystone/ConnectKeystoneStartScreen' import PairStart from '../../ledger/PairStart' import AccountCreateStart from '../../onboarding/create/AccountCreateStart' import AccountImportStartScreen from '../../onboarding/import/AccountImportStartScreen' @@ -27,6 +28,7 @@ const AddNewAccountScreen = () => { { value: 'create', title: t('onboarding.create') }, { value: 'import', title: t('onboarding.import') }, { value: 'ledger', title: t('onboarding.ledger') }, + { value: 'keystone', title: t('onboarding.keystone') }, ] }, [t]) @@ -77,6 +79,11 @@ const AddNewAccountScreen = () => { )} + {selectedOption === 'keystone' && ( + + + + )} diff --git a/src/features/home/addNewAccount/addNewAccountTypes.ts b/src/features/home/addNewAccount/addNewAccountTypes.ts index f1b872202..c81c6760a 100644 --- a/src/features/home/addNewAccount/addNewAccountTypes.ts +++ b/src/features/home/addNewAccount/addNewAccountTypes.ts @@ -13,6 +13,7 @@ export type AddNewAccountParamList = { } } LedgerNavigator: undefined + KeystoneNavigator: undefined CLIAccountNavigator: undefined VoteNavigator: undefined } diff --git a/src/features/home/homeTypes.ts b/src/features/home/homeTypes.ts index 6d6a4a9c0..599746ab3 100644 --- a/src/features/home/homeTypes.ts +++ b/src/features/home/homeTypes.ts @@ -25,6 +25,7 @@ export type HomeStackParamList = { AccountManageTokenListScreen: undefined AccountTokenScreen: { mint: string } AccountAssignScreen: undefined | RouteAccount + KeystoneAccountAssignScreen: undefined ConfirmPin: { action: 'payment' } diff --git a/src/features/keystone/ConnectKeystoneStartScreen.tsx b/src/features/keystone/ConnectKeystoneStartScreen.tsx new file mode 100644 index 000000000..b0b620a32 --- /dev/null +++ b/src/features/keystone/ConnectKeystoneStartScreen.tsx @@ -0,0 +1,180 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import { useNavigation } from '@react-navigation/native' +import WarningKeystone from '@assets/images/warningKeystone.svg' +import React, { + forwardRef, + ReactNode, + Ref, + useCallback, + useImperativeHandle, + useMemo, + useRef, +} from 'react' +import useCamera from '@hooks/useCamera' +import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' +import { useOpacity, useSpacing } from '@theme/themeHooks' +import useBackHandler from '@hooks/useBackHandler' +import { useTheme } from '@shopify/restyle' +import { t } from 'i18next' +import { RootNavigationProp } from 'src/navigation/rootTypes' +import { Image, Linking, Platform } from 'react-native' + +type CameraPermissionBottomSheetAlertRef = { + show: () => void + dismiss: () => void +} + +const CameraPermissionBottomSheetAlert = forwardRef( + ( + { children }: { children: ReactNode }, + ref: Ref, + ) => { + useImperativeHandle(ref, () => ({ + show: () => { + bottomSheetModalRef.current?.present() + }, + dismiss: () => { + bottomSheetModalRef.current?.dismiss() + }, + })) + + const bottomSheetModalRef = useRef(null) + const { backgroundStyle } = useOpacity('surfaceSecondary', 1) + const { m } = useSpacing() + const { colors } = useTheme() + const snapPoints = useMemo(() => ['40%'], []) + const sheetHandleStyle = useMemo(() => ({ padding: m }), [m]) + const { handleDismiss } = useBackHandler(bottomSheetModalRef) + const handleIndicatorStyle = useMemo(() => { + return { + backgroundColor: colors.secondaryText, + } + }, [colors.secondaryText]) + const renderBackdrop = useCallback( + (props) => ( + + ), + [], + ) + + return ( + + {children} + + ) + }, +) + +const WarningContent = () => { + const handleOpenSettings = () => { + if (Platform.OS === 'ios') { + Linking.openURL('app-settings:') + } else { + Linking.openSettings() + } + } + return ( + + + + + + {t('keystone.connectKeystoneStart.warning') as string} + + + + + + ) +} + +const ConnectKeystoneStart = () => { + const { hasPermission } = useCamera() + const cameraPermissionBottomSheetAlertRef = + useRef(null) + const rootNav = useNavigation() + const handleStart = useCallback(() => { + if (!hasPermission) { + cameraPermissionBottomSheetAlertRef.current?.show() + } else { + rootNav.navigate('ScanQrCode') + } + }, [rootNav, hasPermission]) + return ( + + + + + + {t('keystone.connectKeystoneStart.title') as string} + + + {t('keystone.connectKeystoneStart.subtitle') as string} + + + + + + + + + ) +} + +export default React.memo(ConnectKeystoneStart) diff --git a/src/features/keystone/KeystoneAccountAssignScreen.tsx b/src/features/keystone/KeystoneAccountAssignScreen.tsx new file mode 100644 index 000000000..bca13beab --- /dev/null +++ b/src/features/keystone/KeystoneAccountAssignScreen.tsx @@ -0,0 +1,222 @@ +import AccountIcon from '@components/AccountIcon' +import Box from '@components/Box' +import CircleLoader from '@components/CircleLoader' +import FabButton from '@components/FabButton' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TextInput from '@components/TextInput' +import CheckBox from '@react-native-community/checkbox' +import { useNavigation } from '@react-navigation/native' +import { useColors, useSpacing } from '@theme/themeHooks' +import React, { memo, useCallback, useMemo, useState } from 'react' +import { useAsyncCallback } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import base58 from 'bs58' +import { CSAccountVersion } from '@storage/cloudStorage' +import { hex } from '@coral-xyz/anchor/dist/cjs/utils/bytes' +import { PublicKey } from '@solana/web3.js' +import Address from '@helium/address' +import { ED25519_KEY_TYPE } from '@helium/address/build/KeyTypes' +import { RootNavigationProp } from '../../navigation/rootTypes' +import { useAccountStorage } from '../../storage/AccountStorageProvider' +import { ImportAccountNavigationProp } from '../onboarding/import/importAccountNavTypes' +import { CreateAccountNavigationProp } from '../onboarding/create/createAccountNavTypes' +import { useKeystoneOnboarding } from './KeystoneOnboardingProvider' + +const KeystoneAccountAssignScreen = () => { + const onboardingNav = useNavigation< + ImportAccountNavigationProp & CreateAccountNavigationProp + >() + const rootNav = useNavigation() + const { t } = useTranslation() + const [alias, setAlias] = useState('') + const { keystoneOnboardingData } = useKeystoneOnboarding() + const insets = useSafeAreaInsets() + const spacing = useSpacing() + const colors = useColors() + const { hasAccounts, accounts } = useAccountStorage() + const [setAsDefault, toggleSetAsDefault] = useState(false) + + const existingNames = useMemo( + () => accounts && new Set(Object.values(accounts).map((a) => a.alias)), + [accounts], + ) + const accountStorage = useAccountStorage() + const { execute: handlePress, loading } = useAsyncCallback(async () => { + const getName = (index: number): string => { + const name = `${alias} ${index + 1}` + if (!existingNames?.has(name)) { + return name + } + return getName(index + 1) + } + + // convert solana public key to helium address + const solanaPublicKeyToHeliumAddress = (publicKey: string): string => { + const pkey = new PublicKey(hex.decode(publicKey)) + const heliumAddr = new Address(0, 0, ED25519_KEY_TYPE, pkey.toBytes()) + const heliumAddress = heliumAddr.b58 + return heliumAddress + } + const accountBulk = keystoneOnboardingData.accounts.map( + (account, index) => ({ + alias: getName(index), + address: solanaPublicKeyToHeliumAddress( + keystoneOnboardingData.accounts[index].publicKey, + ), + solanaAddress: base58.encode( + hex.decode(keystoneOnboardingData.accounts[index].publicKey), + ), + derivationPath: account.path, + keystoneDevice: { + masterFingerprint: account.masterFingerprint, + device: account.device, + }, + version: 'v1' as CSAccountVersion, + }), + ) + accountStorage.upsertAccounts(accountBulk) + + if (hasAccounts) { + rootNav.reset({ + index: 0, + routes: [{ name: 'TabBarNavigator' }], + }) + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + onboardingNav.replace('CreateAccount', { + screen: 'AccountCreatePinScreen', + params: { + pinReset: true, + }, + }) + } + }) + + const onCheckboxToggled = useCallback( + (newValue) => toggleSetAsDefault(newValue), + [], + ) + + return ( + + + + + {t('accountAssign.title')} + + + + + + + + + + + + {t('accountAssign.setDefault')} + + + + + {!loading && existingNames?.has(alias) ? ( + + {t('accountAssign.nameExists')} + + ) : null} + {loading ? ( + + ) : ( + + )} + + + + ) +} + +const styles = StyleSheet.create({ + container: { width: '100%', flex: 1 }, +}) + +export default memo(KeystoneAccountAssignScreen) diff --git a/src/features/keystone/KeystoneModal.tsx b/src/features/keystone/KeystoneModal.tsx new file mode 100644 index 000000000..a1beed44f --- /dev/null +++ b/src/features/keystone/KeystoneModal.tsx @@ -0,0 +1,152 @@ +import Box from '@components/Box' +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetModalProvider, +} from '@gorhom/bottom-sheet' +import useBackHandler from '@hooks/useBackHandler' +import { useTheme } from '@shopify/restyle' +import { useOpacity, useSpacing } from '@theme/themeHooks' +import React, { + ReactNode, + Ref, + forwardRef, + memo, + useCallback, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' +import EventEmitter from 'events' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { KeystoneSolanaSDK } from '@keystonehq/keystone-sdk' +import { uuid } from '@keystonehq/keystone-sdk/dist/utils' +import SignTxModal from './SignTx/SignTxModal' +import { KeystoneSolSignRequest } from './types/keystoneSolanaTxType' + +export type KeystoneModalRef = { + showKeystoneModal: ({ + transaction, + }: { + transaction?: Buffer + message?: Buffer + }) => Promise +} + +let promiseResolve: (value: Buffer | PromiseLike) => void +const KeystoneModal = forwardRef( + ( + { children }: { children: ReactNode }, + ref: Ref, + ) => { + useImperativeHandle(ref, () => ({ showKeystoneModal })) + const eventEmitter = useMemo(() => new EventEmitter(), []) + const { currentAccount } = useAccountStorage() + const { backgroundStyle } = useOpacity('surfaceSecondary', 1) + const bottomSheetModalRef = useRef(null) + const [solSignRequest, setSolSignRequest] = + useState() + const showKeystoneModal = useCallback( + async ({ + transaction, + message, + }: { + transaction?: Buffer + message?: Buffer + }): Promise => { + const requestId = uuid.v4() + // why need setTimeout? modal mounted --> sleep 1s --> modal present + setTimeout(() => { + bottomSheetModalRef.current?.present() + }, 1000) + if (transaction) { + setSolSignRequest({ + requestId, + signData: transaction.toString('hex'), + dataType: KeystoneSolanaSDK.DataType.Message, + path: currentAccount?.derivationPath || '', + xfp: currentAccount?.keystoneDevice?.masterFingerprint || '', + chainId: 1, + origin: 'Helium', + }) + } + if (message) { + setSolSignRequest({ + requestId, + signData: message.toString('hex'), + dataType: KeystoneSolanaSDK.DataType.Message, + path: currentAccount?.derivationPath || '', + xfp: currentAccount?.keystoneDevice?.masterFingerprint as string, + chainId: 1, + origin: 'Helium', + }) + } + const keystonePromise = new Promise((resolve) => { + promiseResolve = resolve + }) + // listen the keystone signature event + eventEmitter.on(`keystoneSignature_${requestId}`, (signature) => { + bottomSheetModalRef.current?.dismiss() + promiseResolve(Buffer.from(signature, 'hex')) + }) + + eventEmitter.on('closeKeystoneSignatureModal', () => { + bottomSheetModalRef.current?.dismiss() + promiseResolve(Buffer.from([])) + }) + return keystonePromise + }, + [ + eventEmitter, + currentAccount?.derivationPath, + currentAccount?.keystoneDevice?.masterFingerprint, + ], + ) + const renderBackdrop = useCallback( + (props) => ( + + ), + [], + ) + const snapPoints = useMemo(() => ['100%'], []) + const { m } = useSpacing() + const { colors } = useTheme() + const sheetHandleStyle = useMemo(() => ({ padding: m }), [m]) + const { handleDismiss } = useBackHandler(bottomSheetModalRef) + + const handleIndicatorStyle = useMemo(() => { + return { + backgroundColor: colors.secondaryText, + } + }, [colors.secondaryText]) + return ( + + + + + + {children} + + + ) + }, +) + +export default memo(KeystoneModal) diff --git a/src/features/keystone/KeystoneNavigator.tsx b/src/features/keystone/KeystoneNavigator.tsx new file mode 100644 index 000000000..641bfaa74 --- /dev/null +++ b/src/features/keystone/KeystoneNavigator.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { createStackNavigator } from '@react-navigation/stack' +import ScanQrCode from './ScanQrCodeScreen' +import SelectKeystoneAccountsScreen from './SelectKeystoneAccountsScreen' +import KeystoneAccountAssignScreen from './KeystoneAccountAssignScreen' + +const KeystoneStack = createStackNavigator() + +const KeystoneNavigator = () => { + return ( + + + + + + ) +} + +export default KeystoneNavigator diff --git a/src/features/keystone/KeystoneOnboardingProvider.tsx b/src/features/keystone/KeystoneOnboardingProvider.tsx new file mode 100644 index 000000000..7404e58da --- /dev/null +++ b/src/features/keystone/KeystoneOnboardingProvider.tsx @@ -0,0 +1,58 @@ +import { NetTypes as NetType } from '@helium/address' +import React, { + createContext, + ReactNode, + useContext, + useMemo, + useState, +} from 'react' + +type KeystoneResolvedPath = { + path: string + publicKey: string + masterFingerprint: string + device: string +} + +type KeystoneOnboardingData = { + accounts: KeystoneResolvedPath[] +} + +const useKeystoneOnboardingHook = () => { + const initialState = useMemo(() => { + return { + accounts: [], + } as KeystoneOnboardingData + }, []) + const [keystoneOnboardingData, setKeystoneOnboardingData] = + useState(initialState) + + return { + keystoneOnboardingData, + setKeystoneOnboardingData, + } +} + +const initialState = { + keystoneOnboardingData: { + netType: NetType.MAINNET, + accounts: [] as KeystoneResolvedPath[], + }, + setNetType: () => undefined, + setKeystoneOnboardingData: () => undefined, +} + +const KeystoneOnboardingContext = + createContext>(initialState) + +const KeystoneOnboardingProvider = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ) +} + +export const useKeystoneOnboarding = () => useContext(KeystoneOnboardingContext) + +export default KeystoneOnboardingProvider diff --git a/src/features/keystone/ScanQrCodeScreen.tsx b/src/features/keystone/ScanQrCodeScreen.tsx new file mode 100644 index 000000000..04926b961 --- /dev/null +++ b/src/features/keystone/ScanQrCodeScreen.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useMemo, useState } from 'react' +import DynamicQrScanner from '@components/DynamicQrScanner' +import SafeAreaBox from '@components/SafeAreaBox' +import { URDecoder } from '@ngraveio/bc-ur' +import KeystoneSDK, { MultiAccounts, UR } from '@keystonehq/keystone-sdk' +import { RootNavigationProp } from 'src/navigation/rootTypes' +import { useNavigation } from '@react-navigation/native' +import { Alert } from 'react-native' +import { useTranslation } from 'react-i18next' +import { KeystoneAccountType } from './SelectKeystoneAccountsScreen' + +const ScanQrCodeScreen = () => { + const { t } = useTranslation() + const navigation = useNavigation() + const [multiAccounts, setMultiAccounts] = useState() + const decoder = useMemo(() => new URDecoder(), []) + const [isScanQrCodeComplete, setIsScanQrCodeComplete] = useState(false) + const [progress, setProgress] = useState(0) + const [isUnexpectedQrCode, setIsUnexpectedQrCode] = useState(false) + const handleBarCodeScanned = (qrString: string) => { + // fix unexpected qrcode string + try { + decoder.receivePart(qrString.toLowerCase()) + setProgress(Number((decoder.getProgress() * 100).toFixed(0))) + if (decoder.isComplete()) { + const ur = decoder.resultUR() + const qrCodeDataRes: MultiAccounts = + new KeystoneSDK().parseMultiAccounts( + new UR(Buffer.from(ur.cbor.toString('hex'), 'hex'), ur.type), + ) + setProgress(100) + setIsScanQrCodeComplete(true) + setMultiAccounts(qrCodeDataRes) + } + } catch (error) { + setIsUnexpectedQrCode(true) + } + } + + useEffect(() => { + if (isUnexpectedQrCode) { + Alert.alert( + t('keystone.connectKeystoneStart.unexpectedQrCodeTitle'), + t('keystone.connectKeystoneStart.unexpectedQrCodeContent'), + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUnexpectedQrCode]) + + useEffect(() => { + if (isScanQrCodeComplete) { + const derivationAccounts: KeystoneAccountType[] = [] + multiAccounts?.keys.forEach((key) => { + derivationAccounts.push({ + path: key.path, + publicKey: key.publicKey, + masterFingerprint: multiAccounts.masterFingerprint, + device: multiAccounts.device || 'Keystone Device', + }) + }) + setProgress(0) + setIsScanQrCodeComplete(false) + navigation.navigate('SelectKeystoneAccounts', { + derivationAccounts, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isScanQrCodeComplete]) + return ( + + + + ) +} + +export default ScanQrCodeScreen diff --git a/src/features/keystone/SelectKeystoneAccountsScreen.tsx b/src/features/keystone/SelectKeystoneAccountsScreen.tsx new file mode 100644 index 000000000..6d125d03c --- /dev/null +++ b/src/features/keystone/SelectKeystoneAccountsScreen.tsx @@ -0,0 +1,257 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable no-restricted-syntax */ +import React, { useCallback, useEffect, useState } from 'react' +import Box from '@components/Box' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import ButtonPressable from '@components/ButtonPressable' +import { FlatList, RefreshControl } from 'react-native' +import { useColors } from '@theme/themeHooks' +import CheckBox from '@react-native-community/checkbox' +import TouchableContainer from '@components/TouchableContainer' +import { humanReadable } from '@helium/spl-utils' +import BN from 'bn.js' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { + RootNavigationProp, + RootStackParamList, +} from 'src/navigation/rootTypes' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { ellipsizeAddress } from '@utils/accountUtils' +import base58 from 'bs58' +import { retryWithBackoff } from '@utils/retryWithBackoff' +import { PublicKey } from '@solana/web3.js' +import { useTranslation } from 'react-i18next' +import { useSolana } from '../../solana/SolanaProvider' +import { useKeystoneOnboarding } from './KeystoneOnboardingProvider' + +export type KeystoneAccountType = { + path: string + publicKey: string + masterFingerprint: string + device: string + balanceSol?: string +} + +type SelectKeystoneAccountsScreenRouteProp = RouteProp< + RootStackParamList, + 'SelectKeystoneAccounts' +> + +const SelectKeystoneAccountsScreen = () => { + const colors = useColors() + const route = useRoute() + const { setKeystoneOnboardingData } = useKeystoneOnboarding() + const { hasAccounts } = useAccountStorage() + const { derivationAccounts } = route.params + const [selected, setSelected] = React.useState>( + new Set(derivationAccounts.map((item) => item.path)), + ) + const { t } = useTranslation() + const [loading, setLoading] = useState(true) + const { connection } = useSolana() + // storage the selected accounts + const storageSelectedAccounts = () => { + const selectedAccounts: KeystoneAccountType[] = Array.from(selected).map( + (path) => { + const account = derivationAccounts.find((item) => item.path === path) + return { + path, + publicKey: account?.publicKey || '', + masterFingerprint: account?.masterFingerprint || '', + device: account?.device || '', + balanceSol: account?.balanceSol || '0', + } + }, + ) + setKeystoneOnboardingData({ + accounts: selectedAccounts, + }) + } + // next page + const navigation = useNavigation() + + const onNext = useCallback(() => { + storageSelectedAccounts() + if (hasAccounts) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + navigation.replace('TabBarNavigator', { + screen: 'Home', + params: { + screen: 'KeystoneAccountAssignScreen', + }, + }) + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + navigation.replace('OnboardingNavigator', { + screen: 'KeystoneNavigator', + params: { + screen: 'KeystoneAccountAssignScreen', + }, + }) + } + }, [hasAccounts, navigation, selected]) + + const fetchBalance = async (publicKey: string) => { + if (connection) { + const balance = await connection.getBalance( + new PublicKey(base58.encode(Buffer.from(publicKey, 'hex'))), + ) + return balance.toString() + } + return '0' + } + useEffect(() => { + setLoading(true) + async function fetchBalanceForAccounts() { + for (const account of derivationAccounts) { + const balance = await retryWithBackoff(() => + fetchBalance(account.publicKey), + ) + account.balanceSol = balance + } + } + fetchBalanceForAccounts().then(() => { + setLoading(false) + }) + }, [derivationAccounts]) + + const renderItem = useCallback( + // eslint-disable-next-line react/no-unused-prop-types + ({ item, index }: { item: KeystoneAccountType; index: number }) => { + const onSelect = () => { + if (selected.has(item.path as string)) { + selected.delete(item.path as string) + setSelected(selected) + } else { + selected.add(item.path as string) + setSelected(selected) + } + } + + return ( + + + + + {item.path} + + + {ellipsizeAddress( + base58.encode(Buffer.from(item.publicKey, 'hex')), + )} + + + {item.balanceSol + ? humanReadable(new BN(item.balanceSol), 9) + : '0'}{' '} + SOL + + + + + {}} + /> + + + ) + }, + [ + colors.primaryText, + colors.secondary, + colors.transparent10, + derivationAccounts, + selected, + ], + ) + return ( + + + + {t('keystone.selectKeystoneAccounts.title')} + + + {t('keystone.selectKeystoneAccounts.subtitle')} + + {}} + title="" + tintColor={colors.primaryText} + /> + } + data={derivationAccounts} + renderItem={renderItem} + keyExtractor={(item) => item.path as string} + refreshing={loading} + onEndReached={() => {}} + /> + + + + + ) +} + +export default React.memo(SelectKeystoneAccountsScreen) diff --git a/src/features/keystone/SignTx/SignTxModal.tsx b/src/features/keystone/SignTx/SignTxModal.tsx new file mode 100644 index 000000000..6abd97d9d --- /dev/null +++ b/src/features/keystone/SignTx/SignTxModal.tsx @@ -0,0 +1,219 @@ +import SafeAreaBox from '@components/SafeAreaBox' +import React, { useEffect, useMemo, useState } from 'react' +import Keystone from '@assets/images/keystoneLogo.svg' +import Box from '@components/Box' +import Text from '@components/Text' +import ButtonPressable from '@components/ButtonPressable' +import { useTranslation } from 'react-i18next' +import KeystoneSDK, { + SolSignature, + UR, + URDecoder, +} from '@keystonehq/keystone-sdk' +import { AnimatedQrCode } from '@components/StaticQrCode' +import useAlert from '@hooks/useAlert' +import { BarcodeScanningResult, Camera, CameraView } from 'expo-camera' +import { Linking, Platform, StyleSheet } from 'react-native' +import ProgressBar from '@components/ProgressBar' +import { useAsync } from 'react-async-hook' +import EventEmitter from 'events' + +import CloseButton from '@components/CloseButton' +import { useHitSlop } from '@theme/themeHooks' +import { CameraScannerLayout } from '../../../components/CameraScannerLayout' +import { KeystoneSolSignRequest } from '../types/keystoneSolanaTxType' + +type Props = { + progress: number + onBarCodeScanned: (data: string) => void +} +const DaynamicQrScanner = ({ onBarCodeScanned, progress }: Props) => { + const [hasPermission, setHasPermission] = useState() + const { showOKCancelAlert } = useAlert() + const { t } = useTranslation() + useEffect(() => { + Camera.requestCameraPermissionsAsync().then(({ status }) => { + setHasPermission(status === 'granted') + }) + }, []) + + useAsync(async () => { + if (hasPermission !== false) return + + const decision = await showOKCancelAlert({ + title: t('qrScanner.deniedAlert.title'), + message: t('qrScanner.deniedAlert.message'), + ok: t('qrScanner.deniedAlert.ok'), + }) + + if (decision) { + if (Platform.OS === 'ios') { + Linking.openURL('app-settings:') + } else { + Linking.openSettings() + } + } + }, [hasPermission, showOKCancelAlert]) + + const handleBarCodeScanned = (result: BarcodeScanningResult) => { + onBarCodeScanned(result.data) + } + + return ( + + + + {progress > 0 && ( + + + + )} + + + + {t('keystone.payment.scanTxQrcodeScreenSubtitle3')} + + + + ) +} +const ScanTxQrcodeScreen = ({ + eventEmitter, + solSignRequest, +}: { + eventEmitter: EventEmitter + solSignRequest: KeystoneSolSignRequest +}) => { + const { t } = useTranslation() + const keystoneSDK = useMemo(() => new KeystoneSDK(), []) + const decoder = useMemo(() => new URDecoder(), []) + const solSignRequestUr = useMemo(() => { + return keystoneSDK.sol.generateSignRequest(solSignRequest) + }, [keystoneSDK, solSignRequest]) + const [openQrCodeScanner, setOpenQrCodeScanner] = useState(false) + const [progress, setProgress] = useState(0) + const [signature, setSignature] = useState(null) + const handleGetSignature = () => { + setOpenQrCodeScanner(true) + } + + const handleBarCodeScanned = (qrString: string) => { + decoder.receivePart(qrString.toLowerCase()) + setProgress(Number((decoder.getProgress() * 100).toFixed(0))) + if (decoder.isComplete()) { + const ur = decoder.resultUR() + const buffer = Buffer.from(ur.cbor.toString('hex'), 'hex') + setProgress(100) + setSignature(keystoneSDK.sol.parseSignature(new UR(buffer, ur.type))) + } + } + useEffect(() => { + if (progress === 100 && signature) { + setOpenQrCodeScanner(false) + eventEmitter.emit( + `keystoneSignature_${solSignRequest.requestId}`, + signature?.signature, + ) + setProgress(0) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [signature, progress]) + + const hitSlop = useHitSlop('l') + return ( + + {openQrCodeScanner && ( + + )} + + + {openQrCodeScanner && ( + + {t('keystone.scanQrCode')} + + )} + { + eventEmitter.emit('closeKeystoneSignatureModal', '') + }} + /> + + {!openQrCodeScanner && ( + + + + + + {t('keystone.payment.scanTxQrcodeScreenTitle')} + + + {t('keystone.payment.scanTxQrcodeScreenSubtitle1')} + + {/* show the sol sign request qrcode */} + {solSignRequestUr && ( + + )} + + {t('keystone.payment.scanTxQrcodeScreenSubtitle2')} + + + + + + )} + + ) +} + +export default React.memo(ScanTxQrcodeScreen) diff --git a/src/features/keystone/types/keystoneSolanaTxType.ts b/src/features/keystone/types/keystoneSolanaTxType.ts new file mode 100644 index 000000000..9dc465ae2 --- /dev/null +++ b/src/features/keystone/types/keystoneSolanaTxType.ts @@ -0,0 +1,9 @@ +export type KeystoneSolSignRequest = { + requestId: string + signData: string + dataType: number + path: string + xfp: string + chainId: number + origin: string +} diff --git a/src/features/migration/SolanaMigration.tsx b/src/features/migration/SolanaMigration.tsx index 5b39ef84b..df7c95944 100644 --- a/src/features/migration/SolanaMigration.tsx +++ b/src/features/migration/SolanaMigration.tsx @@ -82,15 +82,17 @@ const SolanaMigration = ({ ) const { loading, error } = useAsync(async () => { + // if current account is keystone account, then we don't need to migrate if ( !currentAccount?.solanaAddress || + currentAccount?.keystoneDevice || !anchorProvider || !cluster || (doneSolanaMigration[cluster]?.includes(currentAccount?.solanaAddress) && !manual) ) return - + // eslint-disable-next-line no-console try { await migrateWallet( anchorProvider, diff --git a/src/features/onboarding/CreateImportAccountScreen.tsx b/src/features/onboarding/CreateImportAccountScreen.tsx index bad69c894..26d3d5b96 100644 --- a/src/features/onboarding/CreateImportAccountScreen.tsx +++ b/src/features/onboarding/CreateImportAccountScreen.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import Plus from '@assets/images/plus.svg' import DownArrow from '@assets/images/importIcon.svg' import Ledger from '@assets/images/ledger.svg' +import Keystone from '@assets/images/keystoneLogo.svg' import { useSafeAreaInsets } from 'react-native-safe-area-context' import Box from '@components/Box' import Text from '@components/Text' @@ -33,6 +34,10 @@ const CreateImportAccountScreen = () => { navigation.navigate('LedgerNavigator') }, [navigation]) + const connectKeystone = useCallback(() => { + navigation.navigate('KeystoneNavigator') + }, [navigation]) + return ( @@ -57,6 +62,15 @@ const CreateImportAccountScreen = () => { + + + + {t('accountSetup.createImport.keystone')} + + + + + diff --git a/src/features/onboarding/OnboardingNavigator.tsx b/src/features/onboarding/OnboardingNavigator.tsx index 213252605..8859f24d4 100644 --- a/src/features/onboarding/OnboardingNavigator.tsx +++ b/src/features/onboarding/OnboardingNavigator.tsx @@ -10,6 +10,7 @@ import CreateAccountNavigator from './create/CreateAccountNavigator' import ImportAccountNavigator from './import/ImportAccountNavigator' import ImportPrivateKey from './import/ImportPrivateKey' import { OnboardingStackParamList } from './onboardingTypes' +import KeystoneNavigator from '../keystone/KeystoneNavigator' const OnboardingStack = createStackNavigator() @@ -50,6 +51,11 @@ const OnboardingNavigator = () => { component={LedgerNavigator} options={subScreenOptions} /> + -export type OnboardingOpt = 'import' | 'create' | 'ledger' +export type OnboardingOpt = 'import' | 'create' | 'ledger' | 'keystone' diff --git a/src/features/payment/PaymentCard.tsx b/src/features/payment/PaymentCard.tsx index bcf7b0539..5d3258eb9 100644 --- a/src/features/payment/PaymentCard.tsx +++ b/src/features/payment/PaymentCard.tsx @@ -43,7 +43,7 @@ const PaymentCard = ({ const { currentAccount } = useAccountStorage() const handlePayPressed = useCallback(async () => { - if (!currentAccount?.ledgerDevice) { + if (!currentAccount?.ledgerDevice && !currentAccount?.keystoneDevice) { const hasSecureAccount = await checkSecureAccount( currentAccount?.address, true, @@ -52,7 +52,11 @@ const PaymentCard = ({ } animateTransition('PaymentCard.payEnabled') setPayEnabled(true) - }, [currentAccount?.ledgerDevice, currentAccount?.address]) + }, [ + currentAccount?.ledgerDevice, + currentAccount?.address, + currentAccount?.keystoneDevice, + ]) const handleLayout = useCallback( (e: LayoutChangeEvent) => { diff --git a/src/hooks/useCamera.ts b/src/hooks/useCamera.ts new file mode 100644 index 000000000..ac3cae80d --- /dev/null +++ b/src/hooks/useCamera.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react' +import { Camera } from 'expo-camera' + +const useCamera = () => { + const [hasPermission, setHasPermission] = useState(false) + + useEffect(() => { + ;(async () => { + const { status } = await Camera.requestCameraPermissionsAsync() + setHasPermission(status === 'granted') + })() + }, []) + + return { hasPermission } +} + +export default useCamera diff --git a/src/locales/en.ts b/src/locales/en.ts index 93619cf94..d84d72352 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -167,6 +167,7 @@ export default { importPrivateKey: 'Import a Private Key', ledger: 'Pair with Ledger', title: 'What would\nyou like to do?', + keystone: 'Connect Keystone to Wallet', }, createPin: { subtitle: 'Let’s secure your wallet with a PIN Code.', @@ -762,6 +763,34 @@ export default { tap: 'Get Started', title: 'Welcome to\nHelium Wallet', }, + keystone: { + connectKeystoneStart: { + subtitle: + 'Click on the "Connect with Keystone" button below to scan the QR code displayed on the Keystone device.', + title: 'Connect Keystone to Wallet', + scanQrCode: 'Scan QR Code', + warning: 'Please enable your camera permission via [Settings]', + ok: 'OK', + unexpectedQrCodeContent: + 'The QR code you scanned is not valid. Please try again.', + unexpectedQrCodeTitle: 'Unexpected QR Code', + }, + selectKeystoneAccounts: { + subtitle: + 'A secret phrase can be used to generate multiple wallets by using derivation paths. The following derivation paths have been automatically detected. Select the wallets you would like to import.', + title: 'Select Keystone Accounts', + }, + scanQrCode: 'Scan the QR Code', + payment: { + scanTxQrcodeScreenTitle: 'Scan the QR Code', + scanTxQrcodeScreenSubtitle1: 'Scan the QR code via your Keystone device', + scanTxQrcodeScreenSubtitle2: + "Click on the 'Get Signature' button after signing the transaction with your Keystone device.", + scanTxQrcodeScreenSubtitle3: + 'Place the QR code from your Keystone device in front of the camera.', + getSignature: 'Get Signature', + }, + }, ledger: { openTheSolanaApp: 'Open the Solana app on your {{ device }}', pleaseConfirmTransaction: 'Please confirm transaction on your {{ device }}', @@ -868,6 +897,7 @@ export default { create: 'New', import: 'Import', ledger: 'Ledger', + keystone: 'Keystone', }, ordinals: [ '1st', diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx index 2f76ba21a..9ad2f2f55 100644 --- a/src/navigation/RootNavigator.tsx +++ b/src/navigation/RootNavigator.tsx @@ -18,6 +18,8 @@ import { useAsync, useAsyncCallback } from 'react-async-hook' import changeNavigationBarColor from 'react-native-navigation-bar-color' import Toast from 'react-native-simple-toast' import { useSelector } from 'react-redux' +import ScanQrCodeScreen from '../features/keystone/ScanQrCodeScreen' +import SelectKeystoneAccountsScreen from '../features/keystone/SelectKeystoneAccountsScreen' import ConnectedWallets, { ConnectedWalletsRef, } from '../features/account/ConnectedWallets' @@ -180,6 +182,18 @@ const RootNavigator = () => { component={ImportPrivateKey} options={screenOptions} /> + + ) diff --git a/src/navigation/TabBarNavigator.tsx b/src/navigation/TabBarNavigator.tsx index 43a4550cb..9ffdc6a13 100644 --- a/src/navigation/TabBarNavigator.tsx +++ b/src/navigation/TabBarNavigator.tsx @@ -169,9 +169,11 @@ const TabBarNavigator = () => { updateCluster, ]) + // keystone account do not need to migrate return ( <> {currentAccount?.solanaAddress && + !currentAccount?.keystoneDevice && anchorProvider && !doneSolanaMigration[cluster]?.includes(currentAccount.solanaAddress) && !manualMigration[cluster]?.includes(currentAccount.solanaAddress) && ( diff --git a/src/navigation/rootTypes.ts b/src/navigation/rootTypes.ts index 652d28783..79ddc408e 100644 --- a/src/navigation/rootTypes.ts +++ b/src/navigation/rootTypes.ts @@ -1,5 +1,6 @@ import { LinkWalletRequest, SignHotspotRequest } from '@helium/wallet-link' import { StackNavigationProp } from '@react-navigation/stack' +import { KeystoneAccountType } from 'src/features/keystone/SelectKeystoneAccountsScreen' import { PaymentRouteParam } from '../features/home/homeTypes' export type RootStackParamList = { @@ -11,6 +12,8 @@ export type RootStackParamList = { RequestScreen: undefined DappLoginScreen: { uri: string; callback: string } ImportPrivateKey: { key?: string } + SelectKeystoneAccounts: { derivationAccounts: KeystoneAccountType[] } + ScanQrCode: undefined } export type TabBarStackParamList = { diff --git a/src/solana/SolanaProvider.tsx b/src/solana/SolanaProvider.tsx index 5502f9c20..e296cd168 100644 --- a/src/solana/SolanaProvider.tsx +++ b/src/solana/SolanaProvider.tsx @@ -28,6 +28,9 @@ import { useAsync } from 'react-async-hook' import Config from 'react-native-config' import { useSelector } from 'react-redux' import nacl from 'tweetnacl' +import KeystoneModal, { + KeystoneModalRef, +} from '../features/keystone/KeystoneModal' import LedgerModal, { LedgerModalRef } from '../features/ledger/LedgerModal' import { useAccountStorage } from '../storage/AccountStorageProvider' import { getSessionKey, getSolanaKeypair } from '../storage/secureStorage' @@ -45,6 +48,7 @@ const useSolanaHook = () => { ) const { loading, result: sessionKey } = useAsync(getSessionKey, []) const ledgerModalRef = useRef() + const keystoneModalRef = useRef() const connection = useMemo(() => { const sessionKeyActual = !loading && !sessionKey ? Config.RPC_SESSION_KEY_FALLBACK : sessionKey @@ -69,10 +73,12 @@ const useSolanaHook = () => { const signTxn = useCallback( async (transaction: Transaction | VersionedTransaction) => { + // ledger device and keystone device will use cold wallet sign tx if ( - !currentAccount?.ledgerDevice?.id || - !currentAccount?.ledgerDevice?.type || - currentAccount?.accountIndex === undefined + (!currentAccount?.ledgerDevice?.id || + !currentAccount?.ledgerDevice?.type || + currentAccount?.accountIndex === undefined) && + !currentAccount?.keystoneDevice ) { if (!isVersionedTransaction(transaction)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -92,6 +98,21 @@ const useSolanaHook = () => { return transaction } + if (currentAccount?.keystoneDevice) { + const signature = await keystoneModalRef.current?.showKeystoneModal({ + transaction: isVersionedTransaction(transaction) + ? Buffer.from(transaction.message.serialize()) + : transaction.serializeMessage(), + }) + if (!signature || signature.length === 0) { + throw new Error('Transaction is not signed') + } + transaction.addSignature( + new PublicKey(currentAccount.solanaAddress as string), + signature, + ) + return transaction + } const signature = await ledgerModalRef?.current?.showLedgerModal({ transaction: isVersionedTransaction(transaction) @@ -144,7 +165,8 @@ const useSolanaHook = () => { if ( (!secureAcct && !currentAccount?.ledgerDevice && - !currentAccount?.solanaAddress) || + !currentAccount?.solanaAddress && + !currentAccount?.keystoneDevice) || !connection ) return @@ -184,6 +206,7 @@ const useSolanaHook = () => { connection, currentAccount?.solanaAddress, currentAccount?.ledgerDevice, + currentAccount?.keystoneDevice, secureAcct, signTxn, ]) @@ -293,6 +316,7 @@ const useSolanaHook = () => { cache, signMsg, ledgerModalRef, + keystoneModalRef, } } @@ -305,6 +329,7 @@ const initialState: { updateCluster: (nextCluster: Cluster) => void signMsg: (msg: Buffer) => Promise ledgerModalRef: React.MutableRefObject + keystoneModalRef: React.MutableRefObject } = { anchorProvider: undefined, cluster: 'mainnet-beta' as Cluster, @@ -314,6 +339,7 @@ const initialState: { updateCluster: (_nextCluster: Cluster) => {}, signMsg: (_msg: Buffer) => Promise.resolve(_msg), ledgerModalRef: { current: undefined }, + keystoneModalRef: { current: undefined }, } const SolanaContext = createContext>(initialState) @@ -329,7 +355,11 @@ const SolanaProvider = ({ children }: { children: ReactNode }) => { connection={values.connection} cluster={values.cluster} > - {children} + + + {children} + + diff --git a/src/storage/AccountStorageProvider.tsx b/src/storage/AccountStorageProvider.tsx index 84b6c2130..7c0630fb4 100644 --- a/src/storage/AccountStorageProvider.tsx +++ b/src/storage/AccountStorageProvider.tsx @@ -271,6 +271,7 @@ const useAccountStorageHook = () => { address, netType: accountNetType(address), accountIndex: ledgerIndex ?? 0, + keystoneDevice: curr.keystoneDevice, } as CSAccount return acc }, {}) diff --git a/src/storage/cloudStorage.ts b/src/storage/cloudStorage.ts index ac770d2c3..012c58aaa 100644 --- a/src/storage/cloudStorage.ts +++ b/src/storage/cloudStorage.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-debugger */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-console */ import { NetTypes as NetType } from '@helium/address' import { heliumAddressToSolAddress } from '@helium/spl-utils' import { HELIUM_DERIVATION } from '@hooks/useDerivationAccounts' @@ -14,6 +17,11 @@ export type LedgerDevice = { type: 'usb' | 'bluetooth' } +export type KeystoneDevice = { + masterFingerprint: string + device: string +} + export type CSAccount = { alias: string address: string @@ -21,6 +29,7 @@ export type CSAccount = { netType: NetType.NetType derivationPath?: string ledgerDevice?: LedgerDevice + keystoneDevice?: KeystoneDevice accountIndex?: number // Hash of the mnemonic so we can group accts with the same mnemonic mnemonicHash?: string @@ -33,7 +42,7 @@ export type CSAccountVersion = 'v1' export type CSAccounts = Record export type CSToken = Record - +// android use sqlite as local storage and the total default sqlite size is 6 MB // 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 @@ -63,18 +72,23 @@ export const sortAccounts = ( ) => { const acctList = values(accts) const sortedByAlias = sortBy(acctList, 'alias') || [] + const sortedByAliasWithSolanaAddress = sortedByAlias.map((acct) => { + return acct + }) if (defaultAddress) { - const defaultAccount = sortedByAlias.find( + const defaultAccount = sortedByAliasWithSolanaAddress.find( (a) => a.address === defaultAddress, ) if (defaultAccount) { // put default at beginning - const filtered = sortedByAlias.filter((a) => a.address !== defaultAddress) + const filtered = sortedByAliasWithSolanaAddress.filter( + (a) => a.address !== defaultAddress, + ) filtered.unshift(defaultAccount) return filtered } } - return sortedByAlias + return sortedByAliasWithSolanaAddress } const getAccounts = async (): Promise => { const csAccounts = await CloudStorage.getItem(CloudStorageKeys.ACCOUNTS) diff --git a/yarn.lock b/yarn.lock index a1aded871..031585792 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1762,6 +1762,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^1.2.0": + version: 1.10.0 + resolution: "@bufbuild/protobuf@npm:1.10.0" + checksum: 84ba0bed65ebfc75dcb31d231af4226c87148cc4e333b59979674b187dd3e52b2a8f76431b236195d8cde5ea555bdc1ad38a76f418956f874041c15448f53402 + languageName: node + linkType: hard + "@coral-xyz/anchor@npm:^0.28.0": version: 0.28.0 resolution: "@coral-xyz/anchor@npm:0.28.0" @@ -1864,6 +1871,25 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/rlp@npm:^5.0.2": + version: 5.0.2 + resolution: "@ethereumjs/rlp@npm:5.0.2" + bin: + rlp: bin/rlp.cjs + checksum: b569061ddb1f4cf56a82f7a677c735ba37f9e94e2bbaf567404beb9e2da7aa1f595e72fc12a17c61f7aec67fd5448443efe542967c685a2fe0ffc435793dcbab + languageName: node + linkType: hard + +"@ethereumjs/util@npm:^9.0.3": + version: 9.1.0 + resolution: "@ethereumjs/util@npm:9.1.0" + dependencies: + "@ethereumjs/rlp": ^5.0.2 + ethereum-cryptography: ^2.2.1 + checksum: 594e009c3001ca1ca658b4ded01b38e72f5dd5dd76389efd90cb020de099176a3327685557df268161ac3144333cfe8abaae68cda8ae035d9cc82409d386d79a + languageName: node + linkType: hard + "@ethersproject/bytes@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/bytes@npm:5.7.0" @@ -3785,6 +3811,200 @@ __metadata: languageName: node linkType: hard +"@keystonehq/alias-sampling@npm:^0.1.1": + version: 0.1.2 + resolution: "@keystonehq/alias-sampling@npm:0.1.2" + checksum: 4dfdfb91e070b1d9f28058c92b5b8fad81696ac63bd432cd6bd359f2ab92eb50df75e8c5da1f75a351756387e9902f043b3ecc2cbf662c9c9456ecacc848abfd + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-aptos@npm:^0.6.3": + version: 0.6.3 + resolution: "@keystonehq/bc-ur-registry-aptos@npm:0.6.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: 5be87f8aaefd038121c049fd725b3fce7867122642042299b690bce7c0b40ea98cc2d5f17c187d511e03d24d3e617ef0df70fcb1d40a5b6877710c03e3630801 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-arweave@npm:^0.5.3": + version: 0.5.3 + resolution: "@keystonehq/bc-ur-registry-arweave@npm:0.5.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^8.3.2 + checksum: 0a967f318343022dc1201561bb3cd7f5a889135bf65bb48e959153802207b0693dd9a1f6cc653eb40e0dc8e3675471df4584dc287ff7a2b7e6d6d128e38a52d4 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-btc@npm:^0.1.1": + version: 0.1.1 + resolution: "@keystonehq/bc-ur-registry-btc@npm:0.1.1" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^8.3.2 + checksum: d0d7ec983db55374715c04226a7fd70c82b5758c64eae8e70fb3285f8fa3e6d3f124cadb409c3371f7ae18862494ed0fa60cea4c55099eb6ba9cebda9bf2c89d + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-cardano@npm:^0.3.9": + version: 0.3.9 + resolution: "@keystonehq/bc-ur-registry-cardano@npm:0.3.9" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^8.3.2 + checksum: 185e9d92af543124e6ea30965ef45d51b9564e8bf88eb358a1af271d8f7b6e13fc0a81445320fbdb53f0bd8676312032d62c34a5a1bcf8fc763f3a4768b03f69 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-cosmos@npm:^0.5.3": + version: 0.5.3 + resolution: "@keystonehq/bc-ur-registry-cosmos@npm:0.5.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: 6ca85a739cd2c15f2534735c996fbd888a42b81691cbf34c5421bde4a1bad93cd99d1ea09b6a691305d4656aa2904f592e248498e631470a17d00ed800d6a3e0 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-eth@npm:^0.20.1": + version: 0.20.1 + resolution: "@keystonehq/bc-ur-registry-eth@npm:0.20.1" + dependencies: + "@ethereumjs/util": ^9.0.3 + "@keystonehq/bc-ur-registry": ^0.6.4 + hdkey: ^2.0.1 + uuid: ^8.3.2 + checksum: 79b6312d27d1c30f4aa32f28c82ddad9acd2a47c3f3e45c816834ca8a1491284d801752fddb9883299f8f6e4b13d08e1a8e34d1fc19d028406dfc91b77ea460c + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-evm@npm:^0.5.3": + version: 0.5.3 + resolution: "@keystonehq/bc-ur-registry-evm@npm:0.5.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^9.0.0 + checksum: a98251b7164397edc7dcda154ebfe2adf239954bf7acb42af42162ffefec648df2384908766780ab729b8264bffc99ed4dd8de4bded74e229a281620671a326c + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-keystone@npm:^0.4.3": + version: 0.4.3 + resolution: "@keystonehq/bc-ur-registry-keystone@npm:0.4.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + checksum: 04848bad6fe149bebbb18113e13249acad15b1eea96cee667966f6f8ba568595d40927e46ee1202c5ea02fa475d1aff25b1c797f1ce88152804795ccb3487718 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-near@npm:^0.9.3": + version: 0.9.3 + resolution: "@keystonehq/bc-ur-registry-near@npm:0.9.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: 494bc0842b63c701797b6cf8d06e7e980584b8efe42f9b1f3ef2d064157c4cc1b01a3c27ab74089bbfd4111c1879ede39c697ee7a11a5556aed858220878ad3d + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-sol@npm:^0.9.3": + version: 0.9.3 + resolution: "@keystonehq/bc-ur-registry-sol@npm:0.9.3" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: 026409bcb1d321ccc071617dfaeeb6a6d815d97c3755273ba9581958df6536580cd6e1448614dece0f80b21d9dd9f3f8331b2195e15911856be1981fb159b413 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-stellar@npm:^0.0.4": + version: 0.0.4 + resolution: "@keystonehq/bc-ur-registry-stellar@npm:0.0.4" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: 25366676d1987f05398cc7094d59020596db76c621facc1e6dc40119a3e35f231befea9911194e7c869d9177ff9704aa74033963cf90cef824c113dbffc335e5 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-sui@npm:^0.3.1": + version: 0.3.1 + resolution: "@keystonehq/bc-ur-registry-sui@npm:0.3.1" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^9.0.0 + checksum: 809f1c7ccb6e2017d9892e9b537b854a3e6b3d7147bf9ee8d177be7c8f4c754e48056258ab9012490544ac1994f56db2ad20b1fb83e9d5f392176eaf572da8f7 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry-ton@npm:^0.1.2": + version: 0.1.2 + resolution: "@keystonehq/bc-ur-registry-ton@npm:0.1.2" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^9.0.0 + checksum: 28458a641d02366187e9ec8f2498fc0a7ba31125b072e50ec7bfc1328dcbcbd0fc005fa29411400b5ea27ab5ba1baf53e3259516aa7346c5a2556e7deb3dc431 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry@npm:^0.6.4": + version: 0.6.4 + resolution: "@keystonehq/bc-ur-registry@npm:0.6.4" + dependencies: + "@ngraveio/bc-ur": ^1.1.5 + bs58check: ^2.1.2 + tslib: ^2.3.0 + checksum: 8b73edd304fc2c6a7faa3fae320348e9fc58493c2d75276b792ef37560534e18117c114bfb9edddd90639e81710dd660fb1a405d7c5de05e17d44613c691fdb3 + languageName: node + linkType: hard + +"@keystonehq/bc-ur-registry@npm:^0.7.0": + version: 0.7.0 + resolution: "@keystonehq/bc-ur-registry@npm:0.7.0" + dependencies: + "@ngraveio/bc-ur": ^1.1.5 + bs58check: ^2.1.2 + tslib: ^2.3.0 + checksum: d6017e8fda67fc01e28aa1c047b20cce8f07b026f110a5771920879fbd658b845f529b054d1dce2fbabadcfd8da47a2160ab50c73f0bd56678aab4d83899ffcc + languageName: node + linkType: hard + +"@keystonehq/keystone-sdk@npm:^0.8.0": + version: 0.8.0 + resolution: "@keystonehq/keystone-sdk@npm:0.8.0" + dependencies: + "@bufbuild/protobuf": ^1.2.0 + "@keystonehq/bc-ur-registry": ^0.7.0 + "@keystonehq/bc-ur-registry-aptos": ^0.6.3 + "@keystonehq/bc-ur-registry-arweave": ^0.5.3 + "@keystonehq/bc-ur-registry-btc": ^0.1.1 + "@keystonehq/bc-ur-registry-cardano": ^0.3.9 + "@keystonehq/bc-ur-registry-cosmos": ^0.5.3 + "@keystonehq/bc-ur-registry-eth": ^0.20.1 + "@keystonehq/bc-ur-registry-evm": ^0.5.3 + "@keystonehq/bc-ur-registry-keystone": ^0.4.3 + "@keystonehq/bc-ur-registry-near": ^0.9.3 + "@keystonehq/bc-ur-registry-sol": ^0.9.3 + "@keystonehq/bc-ur-registry-stellar": ^0.0.4 + "@keystonehq/bc-ur-registry-sui": ^0.3.1 + "@keystonehq/bc-ur-registry-ton": ^0.1.2 + "@ngraveio/bc-ur": ^1.1.6 + bs58check: ^3.0.1 + pako: ^2.1.0 + ripple-binary-codec: ^1.4.3 + uuid: ^9.0.0 + checksum: 3a931b6f8b5fc3e162dd1475deab89fa3414a2e382acdb4373e9e81b1455192d919bb5fa94fb4ddb620f7394dea3f61bb9620e5857ca6ff5700e6f609d85365d + languageName: node + linkType: hard + "@ledgerhq/devices@npm:^8.0.7": version: 8.0.7 resolution: "@ledgerhq/devices@npm:8.0.7" @@ -4019,6 +4239,21 @@ __metadata: languageName: node linkType: hard +"@ngraveio/bc-ur@npm:^1.1.13, @ngraveio/bc-ur@npm:^1.1.5, @ngraveio/bc-ur@npm:^1.1.6": + version: 1.1.13 + resolution: "@ngraveio/bc-ur@npm:1.1.13" + dependencies: + "@keystonehq/alias-sampling": ^0.1.1 + assert: ^2.0.0 + bignumber.js: ^9.0.1 + cbor-sync: ^1.0.4 + crc: ^3.8.0 + jsbi: ^3.1.5 + sha.js: ^2.4.11 + checksum: 3f8e565c6a6dd7af7489a884f7d4d85d274ce7ce41f9fdb7e362b8a75ccbb2c934b369fd4ea58b2214d6039462ee0e933de61f372c04c551a47a75e1cad14cfd + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -4028,6 +4263,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": + version: 1.4.2 + resolution: "@noble/curves@npm:1.4.2" + dependencies: + "@noble/hashes": 1.4.0 + checksum: c475a83c4263e2c970eaba728895b9b5d67e0ca880651e9c6e3efdc5f6a4f07ceb5b043bf71c399fc80fada0b8706e69d0772bffdd7b9de2483b988973a34cba + languageName: node + linkType: hard + "@noble/curves@npm:^1.1.0, @noble/curves@npm:^1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" @@ -4044,7 +4288,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" checksum: 8ba816ae26c90764b8c42493eea383716396096c5f7ba6bea559993194f49d80a73c081f315f4c367e51bd2d5891700bcdfa816b421d24ab45b41cb03e4f3342 @@ -5014,6 +5258,34 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.6": + version: 1.1.8 + resolution: "@scure/base@npm:1.1.8" + checksum: 1fc8a355ba68663c0eb430cf6a2c5ff5af790c347c1ba1953f344e8681ab37e37e2545e495f7f971b0245727d710fea8c1e57d232d0c6c543cbed4965c7596a1 + languageName: node + linkType: hard + +"@scure/bip32@npm:1.4.0": + version: 1.4.0 + resolution: "@scure/bip32@npm:1.4.0" + dependencies: + "@noble/curves": ~1.4.0 + "@noble/hashes": ~1.4.0 + "@scure/base": ~1.1.6 + checksum: eff491651cbf2bea8784936de75af5fc020fc1bbb9bcb26b2cfeefbd1fb2440ebfaf30c0733ca11c0ae1e272a2ef4c3c34ba5c9fb3e1091c3285a4272045b0c6 + languageName: node + linkType: hard + +"@scure/bip39@npm:1.3.0": + version: 1.3.0 + resolution: "@scure/bip39@npm:1.3.0" + dependencies: + "@noble/hashes": ~1.4.0 + "@scure/base": ~1.1.6 + checksum: dbb0b27df753eb6c6380010b25cc9a9ea31f9cb08864fc51e69e5880ff7e2b8f85b72caea1f1f28af165e83b72c48dd38617e43fc632779d025b50ba32ea759e + languageName: node + linkType: hard + "@segment/loosely-validate-event@npm:^2.0.0": version: 2.0.0 resolution: "@segment/loosely-validate-event@npm:2.0.0" @@ -8089,6 +8361,15 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^3.0.9": + version: 3.0.10 + resolution: "base-x@npm:3.0.10" + dependencies: + safe-buffer: ^5.0.1 + checksum: 52307739559e81d9980889de2359cb4f816cc0eb9a463028fa3ab239ab913d9044a1b47b4520f98e68453df32a457b8ba58b8d0ee7e757fc3fb971f3fa7a1482 + languageName: node + linkType: hard + "base-x@npm:^4.0.0": version: 4.0.0 resolution: "base-x@npm:4.0.0" @@ -8158,6 +8439,13 @@ __metadata: languageName: node linkType: hard +"big-integer@npm:^1.6.48": + version: 1.6.52 + resolution: "big-integer@npm:1.6.52" + checksum: 6e86885787a20fed96521958ae9086960e4e4b5e74d04f3ef7513d4d0ad631a9f3bde2730fc8aaa4b00419fc865f6ec573e5320234531ef37505da7da192c40b + languageName: node + linkType: hard + "bigint-buffer@npm:^1.1.5": version: 1.1.5 resolution: "bigint-buffer@npm:1.1.5" @@ -8460,6 +8748,27 @@ __metadata: languageName: node linkType: hard +"bs58check@npm:^2.1.2": + version: 2.1.2 + resolution: "bs58check@npm:2.1.2" + dependencies: + bs58: ^4.0.0 + create-hash: ^1.1.0 + safe-buffer: ^5.1.2 + checksum: 43bdf08a5dd04581b78f040bc4169480e17008da482ffe2a6507327bbc4fc5c28de0501f7faf22901cfe57fbca79cbb202ca529003fedb4cb8dccd265b38e54d + languageName: node + linkType: hard + +"bs58check@npm:^3.0.1": + version: 3.0.1 + resolution: "bs58check@npm:3.0.1" + dependencies: + "@noble/hashes": ^1.2.0 + bs58: ^5.0.0 + checksum: dbbecc7a09f3836e821149266c864c4bbd545539cea43c35f23f4c3c46b54c86c52b65d224b9ea2e916fa6d93bd2ce9fac5b6c6bfcf19621a9c209a5602f71c8 + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -8542,7 +8851,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.0.0, buffer@npm:^5.4.3, buffer@npm:^5.5.0": +"buffer@npm:^5.0.0, buffer@npm:^5.1.0, buffer@npm:^5.4.3, buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -8716,6 +9025,13 @@ __metadata: languageName: node linkType: hard +"cbor-sync@npm:^1.0.4": + version: 1.0.4 + resolution: "cbor-sync@npm:1.0.4" + checksum: 147834c64b43511b2ea601f02bc2cc4190ec8d41a7b8dc3e9037c636b484ca2124bc7d49da7a0f775ea5153ff799d57e45992816851dbb1d61335f308a0d0120 + languageName: node + linkType: hard + "chalk@npm:^2.0.1, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -9267,6 +9583,15 @@ __metadata: languageName: node linkType: hard +"crc@npm:^3.8.0": + version: 3.8.0 + resolution: "crc@npm:3.8.0" + dependencies: + buffer: ^5.1.0 + checksum: dabbc4eba223b206068b92ca82bb471d583eb6be2384a87f5c3712730cfd6ba4b13a45e8ba3ef62174d5a781a2c5ac5c20bf36cf37bba73926899bd0aa19186f + languageName: node + linkType: hard + "create-ecdh@npm:^4.0.0": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" @@ -9610,6 +9935,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.2.0": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0, decode-uri-component@npm:^0.2.2": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -10093,6 +10425,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.5.4": + version: 6.5.7 + resolution: "elliptic@npm:6.5.7" + dependencies: + bn.js: ^4.11.9 + brorand: ^1.1.0 + hash.js: ^1.0.0 + hmac-drbg: ^1.0.1 + inherits: ^2.0.4 + minimalistic-assert: ^1.0.1 + minimalistic-crypto-utils: ^1.0.1 + checksum: af0ffddffdbc2fea4eeec74388cd73e62ed5a0eac6711568fb28071566319785df529c968b0bf1250ba4bc628e074b2d64c54a633e034aa6f0c6b152ceb49ab8 + languageName: node + linkType: hard + "eme-encryption-scheme-polyfill@npm:^2.0.1": version: 2.1.1 resolution: "eme-encryption-scheme-polyfill@npm:2.1.1" @@ -10927,6 +11274,18 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^2.2.1": + version: 2.2.1 + resolution: "ethereum-cryptography@npm:2.2.1" + dependencies: + "@noble/curves": 1.4.2 + "@noble/hashes": 1.4.0 + "@scure/bip32": 1.4.0 + "@scure/bip39": 1.3.0 + checksum: 1466e4c417b315a6ac67f95088b769fafac8902b495aada3c6375d827e5a7882f9e0eea5f5451600d2250283d9198b8a3d4d996e374e07a80a324e29136f25c6 + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0, event-target-shim@npm:^5.0.1": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -12324,6 +12683,18 @@ __metadata: languageName: node linkType: hard +"hdkey@npm:^2.0.1": + version: 2.1.0 + resolution: "hdkey@npm:2.1.0" + dependencies: + bs58check: ^2.1.2 + ripemd160: ^2.0.2 + safe-buffer: ^5.1.1 + secp256k1: ^4.0.0 + checksum: 042f2d715dc4d106c868dc3791d584336845e4e53f3452e1df116d6af5d88d7084a0a73ddd8a07b4a7d9e6b29cd3b6b4174f03499f25d8ddd101642b34fabe5c + languageName: node + linkType: hard + "helium-wallet@workspace:.": version: 0.0.0-use.local resolution: "helium-wallet@workspace:." @@ -12366,6 +12737,7 @@ __metadata: "@helium/voter-stake-registry-sdk": 0.9.7 "@helium/wallet-link": 4.11.0 "@jup-ag/api": ^6.0.6 + "@keystonehq/keystone-sdk": ^0.8.0 "@ledgerhq/hw-app-solana": 7.0.13 "@ledgerhq/hw-transport-mocker": 6.27.2 "@ledgerhq/react-native-hid": 6.30.0 @@ -12374,6 +12746,7 @@ __metadata: "@maplibre/maplibre-react-native": ^9.1.0 "@metaplex-foundation/mpl-bubblegum": 0.6.0 "@metaplex-foundation/mpl-token-metadata": 2.10.0 + "@ngraveio/bc-ur": ^1.1.13 "@onsol/tldparser": ^0.5.3 "@react-native-async-storage/async-storage": 1.18.1 "@react-native-community/blur": 4.3.0 @@ -14230,6 +14603,13 @@ __metadata: languageName: node linkType: hard +"jsbi@npm:^3.1.5": + version: 3.2.5 + resolution: "jsbi@npm:3.2.5" + checksum: 642d1bb139ad1c1e96c4907eb159565e980a0d168487626b493d0d0b7b341da0e43001089d3b21703fe17b18a7a6c0f42c92026f71d54471ed0a0d1b3015ec0f + languageName: node + linkType: hard + "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -16010,6 +16390,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^2.0.0": + version: 2.0.2 + resolution: "node-addon-api@npm:2.0.2" + dependencies: + node-gyp: latest + checksum: 31fb22d674648204f8dd94167eb5aac896c841b84a9210d614bf5d97c74ef059cc6326389cf0c54d2086e35312938401d4cc82e5fcd679202503eb8ac84814f8 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -16050,6 +16439,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.2.0": + version: 4.8.2 + resolution: "node-gyp-build@npm:4.8.2" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 1a57bba8c4c193f808bd8ad1484d4ebdd8106dd9f04a3e82554dc716e3a2d87d7e369e9503c145e0e6a7e2c663fec0d8aaf52bd8156342ec7fc388195f37824e + languageName: node + linkType: hard + "node-gyp-build@npm:^4.3.0": version: 4.6.1 resolution: "node-gyp-build@npm:4.6.1" @@ -16583,7 +16983,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^2.0.3": +"pako@npm:^2.0.3, pako@npm:^2.1.0": version: 2.1.0 resolution: "pako@npm:2.1.0" checksum: 71666548644c9a4d056bcaba849ca6fd7242c6cf1af0646d3346f3079a1c7f4a66ffec6f7369ee0dc88f61926c10d6ab05da3e1fca44b83551839e89edd75a3e @@ -18945,7 +19345,7 @@ __metadata: languageName: node linkType: hard -"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.2": version: 2.0.2 resolution: "ripemd160@npm:2.0.2" dependencies: @@ -18955,6 +19355,30 @@ __metadata: languageName: node linkType: hard +"ripple-address-codec@npm:^4.3.1": + version: 4.3.1 + resolution: "ripple-address-codec@npm:4.3.1" + dependencies: + base-x: ^3.0.9 + create-hash: ^1.1.2 + checksum: 2961fa9ffd508137a8fbf52cc75cd34e76245f515d0f0595f3abb3a29a8df0014518c816d2db45fd6dbab433595f345a048781753fedfddeeb4a47f2d5e9c39e + languageName: node + linkType: hard + +"ripple-binary-codec@npm:^1.4.3": + version: 1.11.0 + resolution: "ripple-binary-codec@npm:1.11.0" + dependencies: + assert: ^2.0.0 + big-integer: ^1.6.48 + buffer: 6.0.3 + create-hash: ^1.2.0 + decimal.js: ^10.2.0 + ripple-address-codec: ^4.3.1 + checksum: 901f6da22bb31860e8c149974c55c72ba5a7d50d635b7efa9be81ce35cea6576a3b0c59b480069141829d73c558721ab17f34df801d4d68af8f3ae4ed0bbd42c + languageName: node + linkType: hard + "rn-host-detect@npm:1.2.0": version: 1.2.0 resolution: "rn-host-detect@npm:1.2.0" @@ -19153,6 +19577,18 @@ __metadata: languageName: node linkType: hard +"secp256k1@npm:^4.0.0": + version: 4.0.3 + resolution: "secp256k1@npm:4.0.3" + dependencies: + elliptic: ^6.5.4 + node-addon-api: ^2.0.0 + node-gyp: latest + node-gyp-build: ^4.2.0 + checksum: 21e219adc0024fbd75021001358780a3cc6ac21273c3fcaef46943af73969729709b03f1df7c012a0baab0830fb9a06ccc6b42f8d50050c665cb98078eab477b + languageName: node + linkType: hard + "secure-json-parse@npm:^2.5.0": version: 2.7.0 resolution: "secure-json-parse@npm:2.7.0" @@ -19373,7 +19809,7 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": version: 2.4.11 resolution: "sha.js@npm:2.4.11" dependencies: @@ -20641,6 +21077,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.3.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0"