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"