diff --git a/packages/core-mobile/app/assets/icons/keystone.svg b/packages/core-mobile/app/assets/icons/keystone.svg new file mode 100644 index 0000000000..630f9475d4 --- /dev/null +++ b/packages/core-mobile/app/assets/icons/keystone.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/core-mobile/app/assets/icons/keystone_logo_dark.svg b/packages/core-mobile/app/assets/icons/keystone_logo_dark.svg new file mode 100644 index 0000000000..327f33a89f --- /dev/null +++ b/packages/core-mobile/app/assets/icons/keystone_logo_dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/core-mobile/app/assets/icons/keystone_logo_light.svg b/packages/core-mobile/app/assets/icons/keystone_logo_light.svg new file mode 100644 index 0000000000..5066b1532e --- /dev/null +++ b/packages/core-mobile/app/assets/icons/keystone_logo_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core-mobile/app/assets/icons/qrcode_dark.svg b/packages/core-mobile/app/assets/icons/qrcode_dark.svg new file mode 100644 index 0000000000..5b0bfae405 --- /dev/null +++ b/packages/core-mobile/app/assets/icons/qrcode_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core-mobile/app/assets/icons/qrcode_light.svg b/packages/core-mobile/app/assets/icons/qrcode_light.svg new file mode 100644 index 0000000000..77c7f7ba09 --- /dev/null +++ b/packages/core-mobile/app/assets/icons/qrcode_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core-mobile/app/hardware/services/KeystoneService.ts b/packages/core-mobile/app/hardware/services/KeystoneService.ts new file mode 100644 index 0000000000..d346a41b04 --- /dev/null +++ b/packages/core-mobile/app/hardware/services/KeystoneService.ts @@ -0,0 +1,39 @@ +import { UR } from '@ngraveio/bc-ur' +import KeystoneSDK from '@keystonehq/keystone-sdk' +import { fromPublicKey } from 'bip32' +import { KeystoneDataStorage } from 'features/keystone/storage/KeystoneDataStorage' + +class KeystoneService { + private walletInfo = { + evm: '', + xp: '', + mfp: '' + } + + init(ur: UR): void { + const sdk = new KeystoneSDK() + const accounts = sdk.parseMultiAccounts(ur) + const mfp = accounts.masterFingerprint + const ethAccount = accounts.keys.find(key => key.chain === 'ETH') + const avaxAccount = accounts.keys.find(key => key.chain === 'AVAX') + if (!ethAccount || !avaxAccount) { + throw new Error('No ETH or AVAX account found') + } + + this.walletInfo.evm = fromPublicKey( + Buffer.from(ethAccount.publicKey, 'hex'), + Buffer.from(ethAccount.chainCode, 'hex') + ).toBase58() + this.walletInfo.xp = fromPublicKey( + Buffer.from(avaxAccount.publicKey, 'hex'), + Buffer.from(avaxAccount.chainCode, 'hex') + ).toBase58() + this.walletInfo.mfp = mfp + } + + async save(): Promise { + await KeystoneDataStorage.save(this.walletInfo) + } +} + +export default new KeystoneService() diff --git a/packages/core-mobile/app/hardware/wallet/keystoneSigner.ts b/packages/core-mobile/app/hardware/wallet/keystoneSigner.ts new file mode 100644 index 0000000000..f9a5a8ca16 --- /dev/null +++ b/packages/core-mobile/app/hardware/wallet/keystoneSigner.ts @@ -0,0 +1,21 @@ +import { UR } from '@ngraveio/bc-ur' +import { requestKeystoneSigner } from 'features/keystone/utils' + +export const signer = async ( + request: UR, + responseURTypes: string[], + handleResult: (cbor: Buffer) => Promise +): Promise => { + return new Promise((resolve, reject) => { + requestKeystoneSigner({ + request, + responseURTypes, + onReject: (message?: string) => { + reject(message ?? 'User rejected') + }, + onApprove: (cbor: Buffer) => { + return handleResult(cbor).then(resolve).catch(reject) + } + }) + }) +} diff --git a/packages/core-mobile/app/new/common/components/KeystoneQrScanner.tsx b/packages/core-mobile/app/new/common/components/KeystoneQrScanner.tsx new file mode 100644 index 0000000000..2c22a5fee0 --- /dev/null +++ b/packages/core-mobile/app/new/common/components/KeystoneQrScanner.tsx @@ -0,0 +1,131 @@ +import React, { useState, useCallback, useEffect } from 'react' +import { UR, URDecoder } from '@ngraveio/bc-ur' +import * as Progress from 'react-native-progress' +import { View, Text, SCREEN_WIDTH, useTheme } from '@avalabs/k2-alpine' +import { showKeystoneTroubleshooting } from 'features/keystone/utils' +import { QrCodeScanner } from './QrCodeScanner' +import { Space } from './Space' + +const SCANNER_WIDTH = SCREEN_WIDTH - 64 + +interface Props { + urTypes: string[] + onSuccess: (ur: UR) => void + onError?: () => void + info?: string +} + +export const KeystoneQrScanner: (props: Props) => JSX.Element = ({ + info, + urTypes, + onSuccess, + onError +}) => { + const [urDecoder, setUrDecoder] = useState(new URDecoder()) + const [progress, setProgress] = useState(0) + const [showTroubleshooting, setShowTroubleshooting] = useState(false) + const { theme } = useTheme() + + const progressColor = theme.isDark ? theme.colors.$white : theme.colors.$black + const handleError = useCallback(() => { + setUrDecoder(new URDecoder()) + setShowTroubleshooting(true) + if (onError) { + onError() + } + }, [onError]) + + const showErrorSheet = useCallback(() => { + showKeystoneTroubleshooting({ + errorCode: -1, + retry: () => { + setShowTroubleshooting(false) + setProgress(0) + } + }) + }, []) + + useEffect(() => { + if (showTroubleshooting && !onError) { + showErrorSheet() + } + }, [showTroubleshooting, showErrorSheet, onError]) + + const handleScan = useCallback( + (code: string) => { + if (showTroubleshooting) { + return + } + try { + urDecoder.receivePart(code) + if (!urDecoder.isComplete()) { + setProgress(urDecoder.estimatedPercentComplete()) + return + } + + if (urDecoder.isError()) { + handleError() + } + + if (urDecoder.isSuccess()) { + const ur = urDecoder.resultUR() + + if (urTypes.includes(ur.type)) { + setProgress(1) + + onSuccess(ur) + } else { + throw new Error('Invalid qr code') + } + } + } catch (error) { + handleError() + } + }, + [ + setProgress, + onSuccess, + urDecoder, + urTypes, + handleError, + showTroubleshooting + ] + ) + + return ( + + + + + + {info && ( + <> + + {info} + + )} + + + ) +} diff --git a/packages/core-mobile/app/new/common/components/NavigationRedirect.tsx b/packages/core-mobile/app/new/common/components/NavigationRedirect.tsx index 5c643ee77f..f873b65c44 100644 --- a/packages/core-mobile/app/new/common/components/NavigationRedirect.tsx +++ b/packages/core-mobile/app/new/common/components/NavigationRedirect.tsx @@ -68,6 +68,7 @@ export const NavigationRedirect = (): null => { } else if ( pathName === '/onboarding/mnemonic/confirmation' || pathName === '/onboarding/seedless/confirmation' || + pathName === '/onboarding/keystone/confirmation' || (pathName === '/loginWithPinOrBiometry' && !isSignedIn) ) { // must call dismissAll() here diff --git a/packages/core-mobile/app/new/common/components/QrCodeScanner.tsx b/packages/core-mobile/app/new/common/components/QrCodeScanner.tsx index 44374f4263..9e43d4f578 100644 --- a/packages/core-mobile/app/new/common/components/QrCodeScanner.tsx +++ b/packages/core-mobile/app/new/common/components/QrCodeScanner.tsx @@ -24,12 +24,14 @@ type Props = { onSuccess: (data: string) => void vibrate?: boolean sx?: SxProp + paused?: boolean } export const QrCodeScanner = ({ onSuccess, vibrate = false, - sx + sx, + paused = false }: Props): React.JSX.Element | undefined => { const { theme: { colors } @@ -40,20 +42,27 @@ export const QrCodeScanner = ({ ) const [data, setData] = useState() - const handleSuccess = (scanningResult: BarcodeScanningResult): void => { - // expo-camera's onBarcodeScanned callback is not debounced, so we need to debounce it ourselves - setData(scanningResult.data) - } + useEffect(() => { + if (paused) { + setData(undefined) + } + }, [paused, setData]) useEffect(() => { - if (data) { + if (data && !paused) { onSuccess(data) if (vibrate) { notificationAsync(NotificationFeedbackType.Success) } } - }, [data, onSuccess, vibrate]) + }, [data, onSuccess, vibrate, paused]) + + const handleSuccess = (scanningResult: BarcodeScanningResult): void => { + if (paused) return + // expo-camera's onBarcodeScanned callback is not debounced, so we need to debounce it ourselves + setData(scanningResult.data) + } const checkIosPermission = useCallback(async () => { if ( diff --git a/packages/core-mobile/app/new/features/keystone/screens/KeystoneSignerScreen/index.tsx b/packages/core-mobile/app/new/features/keystone/screens/KeystoneSignerScreen/index.tsx new file mode 100644 index 0000000000..da976c6fbc --- /dev/null +++ b/packages/core-mobile/app/new/features/keystone/screens/KeystoneSignerScreen/index.tsx @@ -0,0 +1,199 @@ +import { Button, Text, useTheme, View } from '@avalabs/k2-alpine' +import React, { FC, useCallback, useEffect, useState } from 'react' +import { withWalletConnectCache } from 'common/components/withWalletConnectCache' +import { KeystoneSignerParams } from 'services/walletconnectv2/walletConnectCache/types' +import { useNavigation } from 'expo-router' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { UREncoder } from '@ngraveio/bc-ur' +import { Space } from 'common/components/Space' +import QRCode from 'react-native-qrcode-svg' +import { Dimensions, BackHandler } from 'react-native' +import { KeystoneQrScanner } from 'common/components/KeystoneQrScanner' +import KeystoneLogoLight from 'assets/icons/keystone_logo_light.svg' +import KeystoneLogoDark from 'assets/icons/keystone_logo_dark.svg' +import { useSelector } from 'react-redux' +import { selectIsKeystoneBlocked } from 'store/posthog' + +enum KeystoneSignerStep { + QR, + Scanner +} + +const KeystoneSignerScreen = ({ + params +}: { + params: KeystoneSignerParams +}): JSX.Element => { + const navigation = useNavigation() + const { request, responseURTypes, onApprove, onReject } = params + const [currentStep, setCurrentStep] = useState(KeystoneSignerStep.QR) + const [signningUr, setSigningUr] = useState('(null)') + const isKeystoneBlocked = useSelector(selectIsKeystoneBlocked) + + useEffect(() => { + const urEncoder = new UREncoder(request, 150) + const timer = setInterval(() => { + setSigningUr(urEncoder.nextPart()) + }, 200) + return () => { + clearInterval(timer) + } + }, [request]) + + const rejectAndClose = useCallback( + (message?: string) => { + onReject(message) + navigation.goBack() + }, + [navigation, onReject] + ) + + useEffect(() => { + if (isKeystoneBlocked) { + rejectAndClose() + } + }, [isKeystoneBlocked, rejectAndClose]) + + useEffect(() => { + const onBackPress = (): boolean => { + // modal is being dismissed via physical back button + rejectAndClose() + return false + } + + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress + ) + + return () => backHandler.remove() + }, [rejectAndClose]) + + useEffect(() => { + return navigation.addListener('beforeRemove', e => { + if ( + e.data.action.type === 'POP' // gesture dismissed + ) { + // modal is being dismissed via gesture or back button + rejectAndClose() + } + }) + }, [navigation, rejectAndClose]) + + return ( + + + {currentStep === KeystoneSignerStep.QR && ( + <> + + + + )} + {currentStep === KeystoneSignerStep.Scanner && ( + <> +
Scan the QR Code
+ + + )} +
+
+ ) +} + +const { width: screenWidth } = Dimensions.get('window') + +const Header: FC<{ + children: React.ReactNode +}> = ({ children }) => { + const { theme } = useTheme() + + return ( + <> + {theme.isDark ? : } + + {children} + + + ) +} + +const QRRenderer: FC<{ + data: string +}> = ({ data }) => { + const { theme } = useTheme() + const borderWidth = 16 + const containerSize = screenWidth * 0.7 + const qrCodeSize = containerSize - borderWidth * 2 + + return ( + <> +
Scan the QR Code
+ Scan the QR code via your Keystone device + + + + + Click on the 'Get Signature' button after signing the transaction with + your Keystone device. + + + ) +} + +const QRScanner: FC> = ({ + onApprove, + responseURTypes +}) => { + const navigation = useNavigation() + + return ( + + onApprove(ur.cbor).finally(navigation.goBack)} + info={ + 'Place the QR code from your Keystone device in front of the camera.' + } + /> + + ) +} + +export default withWalletConnectCache('keystoneSignerParams')( + KeystoneSignerScreen +) diff --git a/packages/core-mobile/app/new/features/keystone/screens/keystoneTroubleshooting/index.tsx b/packages/core-mobile/app/new/features/keystone/screens/keystoneTroubleshooting/index.tsx new file mode 100644 index 0000000000..96aba207e0 --- /dev/null +++ b/packages/core-mobile/app/new/features/keystone/screens/keystoneTroubleshooting/index.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect } from 'react' +import { withWalletConnectCache } from 'common/components/withWalletConnectCache' +import { KeystoneTroubleshootingParams } from 'services/walletconnectv2/walletConnectCache/types' +import { useNavigation, Link } from 'expo-router' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { BackHandler } from 'react-native' +import { View, Text, SCREEN_WIDTH, Button, useTheme } from '@avalabs/k2-alpine' +import { Space } from 'common/components/Space' +import { Steps } from 'features/onboarding/components/KeystoneTroubleshooting' + +const KeystoneTroubleshootingScreen = ({ + params +}: { + params: KeystoneTroubleshootingParams +}): JSX.Element => { + const { theme } = useTheme() + const navigation = useNavigation() + const { retry } = params + + const closeAndRetry = useCallback(() => { + retry() + navigation.goBack() + }, [navigation, retry]) + + useEffect(() => { + const onBackPress = (): boolean => { + // modal is being dismissed via physical back button + closeAndRetry() + return false + } + + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress + ) + + return () => backHandler.remove() + }, [closeAndRetry]) + + useEffect(() => { + return navigation.addListener('beforeRemove', e => { + if ( + e.data.action.type === 'POP' // gesture dismissed + ) { + // modal is being dismissed via gesture or back button + closeAndRetry() + } + }) + }, [navigation, closeAndRetry]) + + return ( + + + + Invalid QR Code + + Please ensure you have selected a valid QR code from your Keystone + device. + + + + + + + + + Keystone Support + + + + + + ) +} + +export default withWalletConnectCache('keystoneTroubleshootingParams')( + KeystoneTroubleshootingScreen +) diff --git a/packages/core-mobile/app/new/features/keystone/storage/KeystoneDataStorage.ts b/packages/core-mobile/app/new/features/keystone/storage/KeystoneDataStorage.ts new file mode 100644 index 0000000000..524a6ac0f8 --- /dev/null +++ b/packages/core-mobile/app/new/features/keystone/storage/KeystoneDataStorage.ts @@ -0,0 +1,37 @@ +import SecureStorageService, { KeySlot } from 'security/SecureStorageService' +import { assertNotUndefined } from 'utils/assertions' + +export type KeystoneDataStorageType = { + evm: string + xp: string + mfp: string +} + +export class KeystoneDataStorage { + private static cache: KeystoneDataStorageType | undefined = undefined + + static async save(keystoneData: KeystoneDataStorageType): Promise { + await SecureStorageService.store(KeySlot.KeystoneData, keystoneData) + + this.cache = keystoneData + } + + static async retrieve(): Promise { + if (this.cache?.mfp && this.cache?.xp && this.cache?.evm) { + return this.cache + } + + const walletInfo = await SecureStorageService.load( + KeySlot.KeystoneData + ) + assertNotUndefined(walletInfo.mfp, 'no mfp found') + assertNotUndefined(walletInfo.xp, 'no xp found') + assertNotUndefined(walletInfo.evm, 'no evm found') + + return walletInfo + } + + static clearCache(): void { + this.cache = undefined + } +} diff --git a/packages/core-mobile/app/new/features/keystone/utils/index.ts b/packages/core-mobile/app/new/features/keystone/utils/index.ts new file mode 100644 index 0000000000..acf1aaef02 --- /dev/null +++ b/packages/core-mobile/app/new/features/keystone/utils/index.ts @@ -0,0 +1,24 @@ +import { router } from 'expo-router' +import { KeystoneTroubleshootingParams } from 'services/walletconnectv2/walletConnectCache/types' +import { walletConnectCache } from 'services/walletconnectv2/walletConnectCache/walletConnectCache' +import { KeystoneSignerParams } from 'services/walletconnectv2/walletConnectCache/types' + +export const showKeystoneTroubleshooting = ( + params: KeystoneTroubleshootingParams +): void => { + walletConnectCache.keystoneTroubleshootingParams.set(params) + + router.navigate({ + // @ts-ignore + pathname: '/keystoneTroubleshooting' + }) +} + +export const requestKeystoneSigner = (params: KeystoneSignerParams): void => { + walletConnectCache.keystoneSignerParams.set(params) + + router.navigate({ + // @ts-ignore + pathname: '/keystoneSigner' + }) +} diff --git a/packages/core-mobile/app/new/features/onboarding/components/KeystoneTroubleshooting.tsx b/packages/core-mobile/app/new/features/onboarding/components/KeystoneTroubleshooting.tsx new file mode 100644 index 0000000000..26a8d79673 --- /dev/null +++ b/packages/core-mobile/app/new/features/onboarding/components/KeystoneTroubleshooting.tsx @@ -0,0 +1,102 @@ +import { Button, Text, useTheme, View } from '@avalabs/k2-alpine' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { Link } from 'expo-router' +import React, { FC, useCallback } from 'react' + +export const KeystoneTroubleshooting = ({ + retry +}: { + retry: () => void +}): JSX.Element => { + const renderFooter = useCallback(() => { + return ( + + + + + + Keystone Support + + + + + + + ) + }, [retry]) + + return ( + + + + Please ensure you have selected a valid QR code from your Keystone + device. + + + + + ) +} + +export const Steps: FC<{ steps: string[] }> = ({ steps }) => { + const { theme } = useTheme() + + return ( + + {steps.map((step, index) => ( + + {`Step ${index + 1}`} + + {step} + + + ))} + + ) +} diff --git a/packages/core-mobile/app/new/features/onboarding/components/RecoveryUsingKeystone.tsx b/packages/core-mobile/app/new/features/onboarding/components/RecoveryUsingKeystone.tsx new file mode 100644 index 0000000000..eb555affd0 --- /dev/null +++ b/packages/core-mobile/app/new/features/onboarding/components/RecoveryUsingKeystone.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { View } from '@avalabs/k2-alpine' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { UR, URType } from '@keystonehq/keystone-sdk' +import { KeystoneQrScanner } from 'common/components/KeystoneQrScanner' + +export const RecoveryUsingKeystone = ({ + onSuccess, + onError +}: { + onSuccess: (ur: UR) => void + onError: () => void +}): JSX.Element => { + return ( + + + + + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/_layout.tsx new file mode 100644 index 0000000000..4a96eda371 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/_layout.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { Stack } from 'common/components/Stack' +import { useModalScreenOptions } from 'common/hooks/useModalScreenOptions' +export default function KeystoneSignerLayout(): JSX.Element { + const { modalStackNavigatorScreenOptions, modalFirstScreenOptions } = + useModalScreenOptions() + return ( + + + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/index.tsx new file mode 100644 index 0000000000..111a09e101 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneSigner/index.tsx @@ -0,0 +1 @@ +export { default } from 'features/keystone/screens/KeystoneSignerScreen' diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/_layout.tsx new file mode 100644 index 0000000000..4a96eda371 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/_layout.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { Stack } from 'common/components/Stack' +import { useModalScreenOptions } from 'common/hooks/useModalScreenOptions' +export default function KeystoneSignerLayout(): JSX.Element { + const { modalStackNavigatorScreenOptions, modalFirstScreenOptions } = + useModalScreenOptions() + return ( + + + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/index.tsx new file mode 100644 index 0000000000..3c962c62c0 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/keystoneTroubleshooting/index.tsx @@ -0,0 +1 @@ +export { default } from 'features/keystone/screens/keystoneTroubleshooting' diff --git a/packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx index a72831b9c0..52537d33c0 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx @@ -43,6 +43,14 @@ export default function WalletLayout(): JSX.Element { return modalScreensOptions }} /> + + { const { theme } = useTheme() const { navigate } = useRouter() + const isKeystoneBlocked = useSelector(selectIsKeystoneBlocked) - const handleEnterRecoveryPhrase = (): void => { + const handleEnterRecoveryPhrase = useCallback((): void => { navigate({ // @ts-ignore TODO: make routes typesafe pathname: '/onboarding/mnemonic/', params: { recovering: 'true' } }) - } + }, [navigate]) - const handleCreateMnemonicWallet = (): void => { + const handleEnterKeystone = useCallback((): void => { + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/termsAndConditions/' + }) + }, [navigate]) + + const handleCreateMnemonicWallet = useCallback((): void => { navigate({ // @ts-ignore TODO: make routes typesafe pathname: '/onboarding/mnemonic/' }) - } + }, [navigate]) + + const data = useMemo(() => { + const res = [] + res.push({ + title: 'Type in a recovery phrase', + leftIcon: , + onPress: handleEnterRecoveryPhrase + }) + if (!isKeystoneBlocked) { + res.push({ + title: 'Add using Keystone', + leftIcon: , + onPress: handleEnterKeystone + }) + } + res.push({ + title: 'Create a new wallet', + leftIcon: , + onPress: handleCreateMnemonicWallet + }) + return res + }, [ + handleCreateMnemonicWallet, + handleEnterKeystone, + handleEnterRecoveryPhrase, + isKeystoneBlocked, + theme.colors + ]) return ( { style={{ marginTop: 24 }}> - , - onPress: handleEnterRecoveryPhrase - }, - { - title: 'Create a new wallet', - leftIcon: , - onPress: handleCreateMnemonicWallet - } - ]} - itemHeight={60} - /> + ) diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/_layout.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/_layout.tsx new file mode 100644 index 0000000000..c8d1a6095c --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/_layout.tsx @@ -0,0 +1,49 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { Stack } from 'common/components/Stack' +import { PageControl } from '@avalabs/k2-alpine' +import { stackNavigatorScreenOptions } from 'common/consts/screenOptions' +import { useRootNavigationState } from 'expo-router' +import { NavigationState } from '@react-navigation/native' + +export default function KeystoneOnboardingLayout(): JSX.Element { + const [currentPage, setCurrentPage] = useState(0) + const rootState: NavigationState = useRootNavigationState() + + const screens = useMemo(() => KEYSTONE_ONBOARDING_SCREENS, []) + + useEffect(() => { + const keystoneOnboardingRoute = rootState.routes + .find(route => route.name === 'onboarding') + ?.state?.routes.find(route => route.name === 'keystone') + if (keystoneOnboardingRoute?.state?.index !== undefined) { + setCurrentPage(keystoneOnboardingRoute.state.index) + } + }, [rootState]) + + const renderPageControl = (): React.ReactNode => ( + + ) + + return ( + + {screens.map(screen => { + return + })} + + ) +} + +const KEYSTONE_ONBOARDING_SCREENS = [ + 'termsAndConditions', + 'analyticsConsent', + 'recoveryUsingKeystone', + 'keystoneTroubleshooting', + 'createPin', + 'setWalletName', + 'selectAvatar', + 'confirmation' +] diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/analyticsConsent.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/analyticsConsent.tsx new file mode 100644 index 0000000000..9278f1a5ee --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/analyticsConsent.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useAnalyticsConsent } from 'hooks/useAnalyticsConsent' +import { useRouter } from 'expo-router' +import { AnalyticsConsent as Component } from 'features/onboarding/components/AnalyticsConsent' + +export default function AnalyticsConsent(): JSX.Element { + const { navigate } = useRouter() + const { accept, reject } = useAnalyticsConsent() + + const nextPathname = './recoveryUsingKeystone' + + function handleAcceptAnalytics(): void { + accept() + navigate(nextPathname) + } + + function handleRejectAnalytics(): void { + reject() + navigate(nextPathname) + } + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/confirmation.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/confirmation.tsx new file mode 100644 index 0000000000..771001240f --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/confirmation.tsx @@ -0,0 +1,15 @@ +import { Confirmation as Component } from 'features/onboarding/components/Confirmation' +import { useWallet } from 'hooks/useWallet' +import React from 'react' +import { WalletType } from 'services/wallet/types' +import Logger from 'utils/Logger' + +export default function Confirmation(): JSX.Element { + const { login } = useWallet() + + const handleNext = (): void => { + login(WalletType.KEYSTONE).catch(Logger.error) + } + + return +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/createPin.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/createPin.tsx new file mode 100644 index 0000000000..58b61dac70 --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/createPin.tsx @@ -0,0 +1,55 @@ +import { useStoredBiometrics } from 'common/hooks/useStoredBiometrics' +import { useRouter } from 'expo-router' +import { CreatePin as Component } from 'features/onboarding/components/CreatePin' +import { useWallet } from 'hooks/useWallet' +import React, { useCallback } from 'react' +import { useSelector } from 'react-redux' +import AnalyticsService from 'services/analytics/AnalyticsService' +import { WalletType } from 'services/wallet/types' +import { selectActiveWalletId } from 'store/wallet/slice' +import BiometricsSDK from 'utils/BiometricsSDK' +import Logger from 'utils/Logger' +import { uuid } from 'utils/uuid' + +export default function CreatePin(): JSX.Element { + const { navigate } = useRouter() + const { onPinCreated } = useWallet() + const { isBiometricAvailable, useBiometrics, setUseBiometrics } = + useStoredBiometrics() + const activeWalletId = useSelector(selectActiveWalletId) + + const handleEnteredValidPin = useCallback( + (pin: string): void => { + AnalyticsService.capture('OnboardingPasswordSet') + onPinCreated({ + walletId: activeWalletId ?? uuid(), + mnemonic: uuid(), + pin, + walletType: WalletType.KEYSTONE + }) + .then(() => { + if (useBiometrics) { + BiometricsSDK.enableBiometry().catch(Logger.error) + } + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/setWalletName' + }) + }) + .catch(Logger.error) + }, + [navigate, onPinCreated, useBiometrics] + ) + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/keystoneTroubleshooting.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/keystoneTroubleshooting.tsx new file mode 100644 index 0000000000..5c031b80bf --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/keystoneTroubleshooting.tsx @@ -0,0 +1,16 @@ +import React, { useCallback } from 'react' +import { useRouter } from 'expo-router' +import { KeystoneTroubleshooting as Component } from 'features/onboarding/components/KeystoneTroubleshooting' + +export default function KeystoneTroubleshooting(): JSX.Element { + const { replace } = useRouter() + + const retry = useCallback(() => { + replace({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/recoveryUsingKeystone' + }) + }, [replace]) + + return +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/recoveryUsingKeystone.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/recoveryUsingKeystone.tsx new file mode 100644 index 0000000000..8913a415b1 --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/recoveryUsingKeystone.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { useRouter } from 'expo-router' +import { UR } from '@ngraveio/bc-ur' +import { RecoveryUsingKeystone as Component } from 'features/onboarding/components/RecoveryUsingKeystone' +import Logger from 'utils/Logger' +import KeystoneService from 'hardware/services/KeystoneService' + +export default function RecoveryUsingKeystone(): JSX.Element { + const { navigate, replace } = useRouter() + + function handleNext(ur: UR): void { + try { + KeystoneService.init(ur) + + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/createPin' + }) + } catch (error: any) { + Logger.error(error.message) + throw new Error('Failed to parse UR') + } + } + + function handleError(): void { + replace({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/keystoneTroubleshooting' + }) + } + + return +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/selectAvatar.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/selectAvatar.tsx new file mode 100644 index 0000000000..47015545c2 --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/selectAvatar.tsx @@ -0,0 +1,40 @@ +import { SelectAvatar as Component } from 'common/components/SelectAvatar' +import { useAvatar } from 'common/hooks/useAvatar' +import { useRouter } from 'expo-router' +import { useRandomAvatar } from 'features/onboarding/hooks/useRandomAvatar' +import { useRandomizedAvatars } from 'features/onboarding/hooks/useRandomizedAvatars' +import React, { useState } from 'react' + +export default function SelectAvatar(): JSX.Element { + const { navigate } = useRouter() + const { saveLocalAvatar } = useAvatar() + + const randomizedAvatars = useRandomizedAvatars() + const randomAvatar = useRandomAvatar(randomizedAvatars) + + const [selectedAvatar, setSelectedAvatar] = useState(randomAvatar) + + const onSubmit = (): void => { + if (selectedAvatar) { + saveLocalAvatar(selectedAvatar.id) + } + + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/confirmation', + params: { selectedAvatarId: selectedAvatar?.id } + }) + } + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/setWalletName.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/setWalletName.tsx new file mode 100644 index 0000000000..9e68e06ac0 --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/setWalletName.tsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react' +import AnalyticsService from 'services/analytics/AnalyticsService' +import { useDispatch } from 'react-redux' +import { useRouter } from 'expo-router' +import { SetWalletName as Component } from 'features/onboarding/components/SetWalletName' +import { setWalletName } from 'store/wallet/slice' +import { useActiveWallet } from 'common/hooks/useActiveWallet' + +export default function SetWalletName(): JSX.Element { + const [name, setName] = useState('Wallet 1') + const dispatch = useDispatch() + const { navigate } = useRouter() + const activeWallet = useActiveWallet() + + const handleNext = (): void => { + AnalyticsService.capture('Onboard:WalletNameSet') + dispatch(setWalletName({ walletId: activeWallet.id, name })) + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/onboarding/keystone/selectAvatar' + }) + } + + return +} diff --git a/packages/core-mobile/app/new/routes/onboarding/keystone/termsAndConditions.tsx b/packages/core-mobile/app/new/routes/onboarding/keystone/termsAndConditions.tsx new file mode 100644 index 0000000000..490bdbefad --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/keystone/termsAndConditions.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { TermsAndConditions as Component } from 'features/onboarding/components/TermsAndConditions' +import { useRouter } from 'expo-router' + +export default function TermsAndConditions(): JSX.Element { + const { navigate } = useRouter() + + const handleAgreeAndContinue = (): void => { + // @ts-ignore TODO: make routes typesafe + navigate('/onboarding/keystone/analyticsConsent') + } + + return +} diff --git a/packages/core-mobile/app/security/SecureStorageService.ts b/packages/core-mobile/app/security/SecureStorageService.ts index 704ae78690..6c7f32de25 100644 --- a/packages/core-mobile/app/security/SecureStorageService.ts +++ b/packages/core-mobile/app/security/SecureStorageService.ts @@ -9,7 +9,8 @@ export enum KeySlot { SignerSessionData = 'SignerSessionData', SeedlessPubKeys = 'SeedlessPubKeysV2', OidcProvider = 'OidcProvider', - OidcUserId = 'OidcUserId' + OidcUserId = 'OidcUserId', + KeystoneData = 'KeystoneData' } /** diff --git a/packages/core-mobile/app/services/posthog/types.ts b/packages/core-mobile/app/services/posthog/types.ts index 630db1454c..c62f234b0c 100644 --- a/packages/core-mobile/app/services/posthog/types.ts +++ b/packages/core-mobile/app/services/posthog/types.ts @@ -40,6 +40,7 @@ export enum FeatureGates { MELD_ONRAMP = 'meld-onramp', MELD_OFFRAMP = 'meld-offramp', SWAP_USE_MARKR = 'swap-use-markr', + KEYSTONE = 'keystone', MELD_INTEGRATION = 'meld-integration', SWAP_FEES_JUPITER = 'swap-fees-jupiter', SWAP_SOLANA = 'swap-solana' diff --git a/packages/core-mobile/app/services/wallet/KeystoneWallet.test.ts b/packages/core-mobile/app/services/wallet/KeystoneWallet.test.ts new file mode 100644 index 0000000000..4b81b55117 --- /dev/null +++ b/packages/core-mobile/app/services/wallet/KeystoneWallet.test.ts @@ -0,0 +1,67 @@ +import { KeystoneDataStorageType } from 'features/keystone/storage/KeystoneDataStorage' +import { Curve } from 'utils/publicKeys' +import { BitcoinProvider } from '@avalabs/core-wallets-sdk' +import { signer } from 'hardware/wallet/keystoneSigner' +import KeystoneWallet from './KeystoneWallet' + +const MockedKeystoneData: KeystoneDataStorageType = { + evm: 'xpub661MyMwAqRbcGSmFWVZk2h773zMrcPFqDUWi7cFRpgPhfn7y9HEPzPsBDEXYxAWfAoGo7E7ijjYfB3xAY86MYzfvGLDHmcy2epZKNeDd4uQ', + xp: 'xpub661MyMwAqRbcFFDMuFiGQmA1EqWxxgDLdtNvxxiucf9qkfoVrvwgnYyshxWoewWtkZ1aLhKoVDrpeDvn1YRqxX2szhGKi3UiSEv1hYRMF8q', + mfp: '1250b6bc' +} + +jest.mock('./keystoneSigner.ts', () => ({ + signer: jest.fn().mockImplementation(async () => '0xmockedsignature') +})) + +describe('KeystoneWallet', () => { + let wallet: KeystoneWallet + + beforeEach(() => { + wallet = new KeystoneWallet(MockedKeystoneData) + }) + + it('should have returned the evm xpub', async () => { + expect(wallet.xpub).toEqual(MockedKeystoneData.evm) + }) + + it('should have returned the xp xpub', async () => { + expect(wallet.xpubXP).toEqual(MockedKeystoneData.xp) + }) + + it('should have returned the mfp', async () => { + expect(wallet.mfp).toEqual(MockedKeystoneData.mfp) + }) + + it('should have returned the correct public key', async () => { + const evmPublicKey = await wallet.getPublicKeyFor({ + derivationPath: `m/44'/60'/0'/0/1`, + curve: Curve.SECP256K1 + }) + expect(evmPublicKey).toEqual( + '0341f20093c553b2aa95dd57449532b85480de93a9aaa225a391dcfe8679e33f50' + ) + const xpPublicKey = await wallet.getPublicKeyFor({ + derivationPath: `m/44'/9000'/0'/0/1`, + curve: Curve.SECP256K1 + }) + expect(xpPublicKey).toEqual( + '034814b89f62338b37881a71ffe40cdd29752241560b861a7086ac711fa7a8fe79' + ) + }) + + describe('getSigner', () => { + it('should sign BTC transaction successfully', async () => { + const result = await wallet.signBtcTransaction({ + accountIndex: 0, + transaction: { inputs: [], outputs: [] }, + network: { vmName: 'BITCOIN' } as any, + provider: new BitcoinProvider() + }) + + expect(typeof result).toBe('string') + expect(signer).toHaveBeenCalled() + expect(result).toBe('0xmockedsignature') + }) + }) +}) diff --git a/packages/core-mobile/app/services/wallet/KeystoneWallet.ts b/packages/core-mobile/app/services/wallet/KeystoneWallet.ts new file mode 100644 index 0000000000..1f522e9f85 --- /dev/null +++ b/packages/core-mobile/app/services/wallet/KeystoneWallet.ts @@ -0,0 +1,437 @@ +import { + AvalancheTransactionRequest, + BtcTransactionRequest, + Wallet +} from 'services/wallet/types' +import { + TypedDataV1, + TypedData, + MessageTypes, + RpcMethod +} from '@avalabs/vm-module-types' +import { Curve } from 'utils/publicKeys' +import { assertNotUndefined } from 'utils/assertions' +import { + Avalanche, + BitcoinProvider, + createPsbt, + DerivationPath, + getAddressDerivationPath, + getAddressPublicKeyFromXPub, + JsonRpcBatchInternal +} from '@avalabs/core-wallets-sdk' +import { + CryptoPSBT, + RegistryTypes, + DataType, + ETHSignature, + EthSignRequest +} from '@keystonehq/bc-ur-registry-eth' +import { Common, Hardfork } from '@ethereumjs/common' +import { + AvalancheSignRequest, + AvalancheSignature +} from '@keystonehq/bc-ur-registry-avalanche' +import { + FeeMarketEIP1559Transaction, + FeeMarketEIP1559TxData, + LegacyTransaction +} from '@ethereumjs/tx' +import { UR } from '@ngraveio/bc-ur' +import { rlp } from 'ethereumjs-util' +import { KeystoneDataStorageType } from 'features/keystone/storage/KeystoneDataStorage' +import { Network } from '@avalabs/core-chains-sdk' +import { Psbt } from 'bitcoinjs-lib' +import { v4 } from 'uuid' +import { hexlify, Signature, TransactionRequest } from 'ethers' +import { URType } from '@keystonehq/animated-qr' +import { BytesLike, AddressLike } from '@ethereumjs/util' +import { BN } from 'bn.js' +import { isTypedData } from '@avalabs/evm-module' +import { convertTxData, makeBigIntLike } from 'services/wallet/utils' +import { signer } from 'hardware/wallet/keystoneSigner' + +export const EVM_DERIVATION_PATH = `m/44'/60'/0'` +export const AVAX_DERIVATION_PATH = `m/44'/9000'/0'` + +export default class KeystoneWallet implements Wallet { + #mfp: string + #xpub: string + #xpubXP: string + + constructor(keystoneData: KeystoneDataStorageType) { + this.#mfp = keystoneData.mfp + this.#xpub = keystoneData.evm + this.#xpubXP = keystoneData.xp + } + + public get xpub(): string { + assertNotUndefined(this.#xpub, 'no public key (xpub) available') + return this.#xpub + } + + public get xpubXP(): string { + assertNotUndefined(this.#xpubXP, 'no public key (xpubXP) available') + return this.#xpubXP + } + + public get mfp(): string { + assertNotUndefined(this.#mfp, 'no master fingerprint available') + return this.#mfp + } + + public async signSvmTransaction(): Promise { + throw new Error('signSvmTransaction not implemented') + } + + private async deriveEthSignature(cbor: Buffer): Promise<{ + r: string + s: string + v: number + }> { + const signature: any = ETHSignature.fromCBOR(cbor).getSignature() + const r = hexlify(new Uint8Array(signature.slice(0, 32))) + const s = hexlify(new Uint8Array(signature.slice(32, 64))) + const v = new BN(signature.slice(64)).toNumber() + return { r, s, v } + } + + public async signMessage({ + rpcMethod, + data, + accountIndex + }: { + rpcMethod: RpcMethod + data: string | TypedDataV1 | TypedData + accountIndex: number + network: Network + provider: JsonRpcBatchInternal + }): Promise { + switch (rpcMethod) { + case RpcMethod.AVALANCHE_SIGN_MESSAGE: { + throw new Error( + '[KeystoneWallet-signMessage] AVALANCHE_SIGN_MESSAGE not implemented.' + ) + } + case RpcMethod.ETH_SIGN: + case RpcMethod.PERSONAL_SIGN: { + if (typeof data !== 'string') + throw new Error(`Invalid message type ${typeof data}`) + + const ur = EthSignRequest.constructETHRequest( + Buffer.from(data.replace('0x', ''), 'hex'), + DataType.personalMessage, + `${EVM_DERIVATION_PATH}/0/${accountIndex}`, + this.mfp, + crypto.randomUUID() + ).toUR() + + return await signer( + ur, + [URType.ETH_SIGNATURE, URType.EVM_SIGNATURE], + async cbor => { + const sig = await this.deriveEthSignature(cbor) + + return Signature.from(sig).serialized + } + ) + } + case RpcMethod.SIGN_TYPED_DATA: + case RpcMethod.SIGN_TYPED_DATA_V1: { + throw new Error( + '[KeystoneWallet-signMessage] SIGN_TYPED_DATA/SIGN_TYPED_DATA_V1 not implemented.' + ) + } + case RpcMethod.SIGN_TYPED_DATA_V3: + case RpcMethod.SIGN_TYPED_DATA_V4: { + if (!isTypedData(data)) throw new Error('Invalid typed data') + + const ur = EthSignRequest.constructETHRequest( + Buffer.from(JSON.stringify(data), 'utf-8'), + DataType.typedData, + `${EVM_DERIVATION_PATH}/0/${accountIndex}`, + this.mfp, + crypto.randomUUID() + ).toUR() + + return await signer( + ur, + [URType.ETH_SIGNATURE, URType.EVM_SIGNATURE], + async cbor => { + const sig = await this.deriveEthSignature(cbor) + + return Signature.from(sig).serialized + } + ) + } + default: + throw new Error('unknown method') + } + } + + public async signBtcTransaction({ + accountIndex, + transaction, + provider + }: { + accountIndex: number + transaction: BtcTransactionRequest + network: Network + provider: BitcoinProvider + }): Promise { + const { inputs, outputs } = transaction + const psbt = createPsbt(inputs, outputs, provider.getNetwork()) + + inputs.forEach((_, index) => { + psbt.updateInput(index, { + bip32Derivation: [ + { + masterFingerprint: Buffer.from(this.mfp, 'hex'), + pubkey: getAddressPublicKeyFromXPub(this.xpub, accountIndex), + path: getAddressDerivationPath( + accountIndex, + DerivationPath.BIP44, + 'EVM' + ) + } + ] + }) + }) + + const cryptoPSBT = new CryptoPSBT(psbt.toBuffer()) + const ur = cryptoPSBT.toUR() + + return await signer(ur, [RegistryTypes.CRYPTO_PSBT.getType()], cbor => { + const signedTx = CryptoPSBT.fromCBOR(cbor).getPSBT() + return Promise.resolve( + Psbt.fromBuffer(signedTx) + .finalizeAllInputs() + .extractTransaction() + .toHex() + ) + }) + } + + public async signAvalancheTransaction({ + accountIndex, + transaction + }: { + accountIndex: number + transaction: AvalancheTransactionRequest + network: Network + provider: Avalanche.JsonRpcProvider + }): Promise { + const tx = transaction.tx + const isEvmChain = tx.getVM() === 'EVM' + + const requestUR = AvalancheSignRequest.constructAvalancheRequest( + Buffer.from(tx.toBytes()), + this.mfp, + isEvmChain ? this.xpub : this.xpubXP, + accountIndex + ).toUR() + + return await signer(requestUR, ['avax-signature'], cbor => { + const response = AvalancheSignature.fromCBOR(cbor) + const sig = response.getSignature() + tx.addSignature(sig as any) + return Promise.resolve(JSON.stringify(tx.toJSON())) + }) + } + + private txRequestToFeeMarketTxData( + txRequest: TransactionRequest + ): FeeMarketEIP1559TxData { + const { + to, + nonce, + gasLimit, + value, + data, + type, + maxFeePerGas, + maxPriorityFeePerGas + } = txRequest + + return { + to: (to?.toString() || undefined) as AddressLike, + nonce: makeBigIntLike(nonce), + maxFeePerGas: makeBigIntLike(maxFeePerGas), + maxPriorityFeePerGas: makeBigIntLike(maxPriorityFeePerGas), + gasLimit: makeBigIntLike(gasLimit), + value: makeBigIntLike(value), + data: data as BytesLike, + type: type || undefined + } + } + + private async getTxFromTransactionRequest( + txRequest: TransactionRequest, + signature?: { r: string; s: string; v: number } + ): Promise { + const _signature = signature + ? { + r: makeBigIntLike(signature.r), + s: makeBigIntLike(signature.s), + v: makeBigIntLike(signature.v) + } + : {} + return typeof txRequest.gasPrice !== 'undefined' + ? LegacyTransaction.fromTxData( + { + ...convertTxData(txRequest), + ..._signature + }, + { + common: Common.custom({ + chainId: Number(txRequest.chainId) + }) + } + ) + : FeeMarketEIP1559Transaction.fromTxData( + { ...this.txRequestToFeeMarketTxData(txRequest), ..._signature }, + { + common: Common.custom( + { chainId: Number(txRequest.chainId) }, + { + // "London" hardfork introduced EIP-1559 proposal. Setting it here allows us + // to use the new TX props (maxFeePerGas and maxPriorityFeePerGas) in combination + // with the custom chainId. + hardfork: Hardfork.London + } + ) + } + ) + } + + private async buildSignatureUR( + txRequest: TransactionRequest, + fingerprint: string, + activeAccountIndex: number + ): Promise { + const chainId = txRequest.chainId + const isLegacyTx = typeof txRequest.gasPrice !== 'undefined' + + const tx = await this.getTxFromTransactionRequest(txRequest) + + const message = + tx instanceof FeeMarketEIP1559Transaction + ? tx.getMessageToSign() + : rlp.encode(tx.getMessageToSign()) // Legacy transactions are not RLP-encoded + + const dataType = isLegacyTx + ? DataType.transaction + : DataType.typedTransaction + + // The keyPath below will depend on how the user onboards and should come from WalletService probably, + // based on activeAccount.index, or fetched based on the address passed in params.from. + // This here is BIP44 for the first account (index 0). 2nd account should be M/44'/60'/0'/0/1, etc.. + const keyPath = `${EVM_DERIVATION_PATH}/0/${activeAccountIndex}` + const ethSignRequest = EthSignRequest.constructETHRequest( + Buffer.from(message as any), + dataType, + keyPath, + fingerprint, + v4(), + Number(chainId) + ) + + return ethSignRequest.toUR() + } + + public async signEvmTransaction({ + accountIndex, + transaction + }: { + accountIndex: number + transaction: TransactionRequest + network: Network + provider: JsonRpcBatchInternal + }): Promise { + const ur = await this.buildSignatureUR(transaction, this.mfp, accountIndex) + + return await signer( + ur, + [URType.ETH_SIGNATURE, URType.EVM_SIGNATURE], + async cbor => { + const sig = await this.deriveEthSignature(cbor) + + const signedTx = await this.getTxFromTransactionRequest( + transaction, + sig + ) + + return '0x' + Buffer.from(signedTx.serialize()).toString('hex') + } + ) + } + + private async getAvaSigner( + accountIndex: number, + provider: Avalanche.JsonRpcProvider + ): Promise { + const evmPub = getAddressPublicKeyFromXPub(this.xpub, accountIndex) + const xpPub = Avalanche.getAddressPublicKeyFromXpub( + this.xpubXP, + accountIndex + ) + return Avalanche.StaticSigner.fromPublicKey(xpPub, evmPub, provider) + } + + public async getReadOnlyAvaSigner({ + accountIndex, + provXP + }: { + accountIndex: number + provXP: Avalanche.JsonRpcProvider + }): Promise { + return (await this.getAvaSigner( + accountIndex, + provXP + )) as Avalanche.StaticSigner + } + + private getPublicKey(path: string): Buffer { + const accountIndex = this.getAccountIndex(path) + + if (path.startsWith(EVM_DERIVATION_PATH)) { + return getAddressPublicKeyFromXPub(this.xpub, accountIndex) + } + if (path.startsWith(AVAX_DERIVATION_PATH)) { + return Avalanche.getAddressPublicKeyFromXpub(this.xpubXP, accountIndex) + } + throw new Error(`Unknown path: ${path}`) + } + + private getAccountIndex(path: string): number { + const accountIndex = path.split('/').pop() + if (!accountIndex) { + throw new Error(`Invalid path: ${path}`) + } + return Number(accountIndex) + } + + public async getPublicKeyFor({ + derivationPath, + curve + }: { + derivationPath?: string + curve: Curve + }): Promise { + if (curve === Curve.ED25519) { + throw new Error(`ED25519 not supported for path: ${derivationPath}`) + } + if (!derivationPath) { + throw new Error(`Path is required for curve: ${curve}`) + } + const publicKey = this.getPublicKey(derivationPath).toString('hex') + + if (!publicKey) { + throw new Error( + `Public key not found for path: ${derivationPath} and curve: ${curve}` + ) + } + + return publicKey + } +} diff --git a/packages/core-mobile/app/services/wallet/WalletFactory.ts b/packages/core-mobile/app/services/wallet/WalletFactory.ts index fa48fef2a4..3861863c07 100644 --- a/packages/core-mobile/app/services/wallet/WalletFactory.ts +++ b/packages/core-mobile/app/services/wallet/WalletFactory.ts @@ -3,6 +3,8 @@ import SeedlessService from 'seedless/services/SeedlessService' import BiometricsSDK from 'utils/BiometricsSDK' import { PrivateKeyWallet } from 'services/wallet/PrivateKeyWallet' import { SeedlessPubKeysStorage } from 'seedless/services/storage/SeedlessPubKeysStorage' +import { KeystoneDataStorage } from 'features/keystone/storage/KeystoneDataStorage' +import KeystoneWallet from './KeystoneWallet' import { Wallet, WalletType } from './types' import { MnemonicWallet } from './MnemonicWallet' @@ -38,6 +40,15 @@ class WalletFactory { } return new MnemonicWallet(walletSecret.value) } + case WalletType.KEYSTONE: { + const keystoneData = await KeystoneDataStorage.retrieve() + + if (!keystoneData) { + throw new Error('Keystone data not available') + } + + return new KeystoneWallet(keystoneData) + } case WalletType.PRIVATE_KEY: { const walletSecret = await BiometricsSDK.loadWalletSecret(walletId) if (!walletSecret.success) { diff --git a/packages/core-mobile/app/services/wallet/WalletService.tsx b/packages/core-mobile/app/services/wallet/WalletService.tsx index fa95131cec..112b419865 100644 --- a/packages/core-mobile/app/services/wallet/WalletService.tsx +++ b/packages/core-mobile/app/services/wallet/WalletService.tsx @@ -333,7 +333,7 @@ class WalletService { return [] } - if (walletType === WalletType.MNEMONIC) { + if ([WalletType.MNEMONIC, WalletType.KEYSTONE].includes(walletType)) { const provXP = await NetworkService.getAvalancheProviderXP(isTestnet) const publicKeys = await this.getPublicKey(walletId, walletType, account) const xpubXP = publicKeys.xp diff --git a/packages/core-mobile/app/services/wallet/types.ts b/packages/core-mobile/app/services/wallet/types.ts index 0c0ffb70d3..2e69d4103a 100644 --- a/packages/core-mobile/app/services/wallet/types.ts +++ b/packages/core-mobile/app/services/wallet/types.ts @@ -122,7 +122,8 @@ export enum WalletType { UNSET = 'UNSET', SEEDLESS = 'SEEDLESS', MNEMONIC = 'MNEMONIC', - PRIVATE_KEY = 'PRIVATE_KEY' + PRIVATE_KEY = 'PRIVATE_KEY', + KEYSTONE = 'KEYSTONE' } /** diff --git a/packages/core-mobile/app/services/wallet/utils.ts b/packages/core-mobile/app/services/wallet/utils.ts index 7701b4e50b..3f338f952f 100644 --- a/packages/core-mobile/app/services/wallet/utils.ts +++ b/packages/core-mobile/app/services/wallet/utils.ts @@ -4,6 +4,10 @@ import { TokenUnit } from '@avalabs/core-utils-sdk' import { cChainToken } from 'utils/units/knownTokens' import { DerivationPathType, NetworkVMType } from '@avalabs/vm-module-types' import ModuleManager from 'vmModule/ModuleManager' +import { BigNumberish, TransactionRequest } from 'ethers' +import { BigIntLike, BytesLike, AddressLike } from '@ethereumjs/util' +import isString from 'lodash.isstring' +import { LegacyTxData } from '@ethereumjs/tx' import { AvalancheTransactionRequest, BtcTransactionRequest, @@ -103,3 +107,30 @@ export const getAddressDerivationPath = ({ return derivationPath } + +const convertToHexString = (n: string): string => { + if (n.startsWith('0x')) return n + return `0x${n}` +} + +export function makeBigIntLike( + n: BigNumberish | undefined | null +): BigIntLike | undefined { + if (n == null) return undefined + if (isString(n)) { + n = convertToHexString(n) + } + return ('0x' + BigInt(n).toString(16)) as BigIntLike +} + +export function convertTxData(txData: TransactionRequest): LegacyTxData { + return { + to: txData.to?.toString() as AddressLike, + nonce: makeBigIntLike(txData.nonce), + gasPrice: makeBigIntLike(txData.gasPrice), + gasLimit: makeBigIntLike(txData.gasLimit), + value: makeBigIntLike(txData.value), + data: txData.data as BytesLike, + type: makeBigIntLike(txData.type) + } +} diff --git a/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/types.ts b/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/types.ts index 130b78d4fe..85df48eb35 100644 --- a/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/types.ts +++ b/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/types.ts @@ -12,6 +12,7 @@ import { Contact } from 'store/addressBook/types' import { WalletAddEthereumChainRpcRequest } from 'store/rpc/handlers/chain/wallet_addEthereumChain/wallet_addEthereumChain' import { Account } from 'store/account' import { WalletType } from 'services/wallet/types' +import { UR } from '@ngraveio/bc-ur' export type SessionProposalParams = { request: WCSessionProposal @@ -43,6 +44,18 @@ export type ApprovalParams = { onReject: (message?: string) => void } +export type KeystoneSignerParams = { + request: UR + responseURTypes: string[] + onApprove: (cbor: Buffer) => Promise + onReject: (message?: string) => void +} + +export type KeystoneTroubleshootingParams = { + errorCode: number + retry: () => void +} + export type SetDeveloperModeParams = { request: AvalancheSetDeveloperModeRpcRequest data: AvalancheSetDeveloperModeApproveData diff --git a/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/walletConnectCache.ts b/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/walletConnectCache.ts index e7874773e9..4c79408b65 100644 --- a/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/walletConnectCache.ts +++ b/packages/core-mobile/app/services/walletconnectv2/walletConnectCache/walletConnectCache.ts @@ -3,7 +3,9 @@ import { SetDeveloperModeParams, SessionProposalParams, EditContactParams, - AddEthereumChainParams + AddEthereumChainParams, + KeystoneSignerParams, + KeystoneTroubleshootingParams } from './types' // a simple in-memory cache (no reactivity or persistence support) @@ -15,7 +17,11 @@ export const walletConnectCache = { createCache('set developer mode'), editContactParams: createCache('edit contact'), addEthereumChainParams: - createCache('add ethereum chain') + createCache('add ethereum chain'), + keystoneSignerParams: createCache('keystone signer'), + keystoneTroubleshootingParams: createCache( + 'keystone troubleshooting' + ) } function createCache(key: string): { diff --git a/packages/core-mobile/app/store/account/listeners.ts b/packages/core-mobile/app/store/account/listeners.ts index 3116078dec..aec4b760c5 100644 --- a/packages/core-mobile/app/store/account/listeners.ts +++ b/packages/core-mobile/app/store/account/listeners.ts @@ -25,6 +25,7 @@ import WalletFactory from 'services/wallet/WalletFactory' import SeedlessWallet from 'seedless/services/wallet/SeedlessWallet' import { transactionSnackbar } from 'common/utils/toast' import Logger from 'utils/Logger' +import KeystoneService from 'hardware/services/KeystoneService' import { pendingSeedlessWalletNameStore } from 'features/onboarding/store' import { selectAccounts, @@ -65,6 +66,14 @@ const initAccounts = async ( } } + if (activeWallet.type === WalletType.KEYSTONE) { + try { + await KeystoneService.save() + } catch (error) { + Logger.error('Failed to save public keys for Keystone wallet', error) + } + } + const acc = await accountService.createNextAccount({ index: 0, walletType: activeWallet.type, @@ -124,7 +133,8 @@ const initAccounts = async ( } } else if ( activeWallet.type === WalletType.MNEMONIC || - activeWallet.type === WalletType.PRIVATE_KEY + activeWallet.type === WalletType.PRIVATE_KEY || + activeWallet.type === WalletType.KEYSTONE ) { accounts[acc.id] = acc diff --git a/packages/core-mobile/app/store/posthog/slice.ts b/packages/core-mobile/app/store/posthog/slice.ts index 8f76f79bb7..6592cef3da 100644 --- a/packages/core-mobile/app/store/posthog/slice.ts +++ b/packages/core-mobile/app/store/posthog/slice.ts @@ -368,6 +368,14 @@ export const selectIsMeldOfframpBlocked = (state: RootState): boolean => { ) } +export const selectIsKeystoneBlocked = (state: RootState): boolean => { + const { featureFlags } = state.posthog + return ( + !featureFlags[FeatureGates.KEYSTONE] || + !featureFlags[FeatureGates.EVERYTHING] + ) +} + export const selectIsSwapUseMarkrBlocked = (state: RootState): boolean => { const { featureFlags } = state.posthog return ( diff --git a/packages/core-mobile/e2e/locators/onboarding.loc.ts b/packages/core-mobile/e2e/locators/onboarding.loc.ts index 0ff0c006dc..4483e38d30 100644 --- a/packages/core-mobile/e2e/locators/onboarding.loc.ts +++ b/packages/core-mobile/e2e/locators/onboarding.loc.ts @@ -14,6 +14,7 @@ export default { "Add a display avatar for your wallet. You can change it at any time in the app's settings", selectedAvatar: 'selected_avatar', chooseWalletTitle: 'How would you like to access your existing wallet?', + addWalletUsingKeystone: 'Add using Keystone', typeRecoverPhase: 'Type in a recovery phrase', createNewWalletBtn: 'Create a new wallet', noThanksBtn: 'No thanks', diff --git a/packages/core-mobile/jest.config.js b/packages/core-mobile/jest.config.js index cef365fd75..e206b496ab 100644 --- a/packages/core-mobile/jest.config.js +++ b/packages/core-mobile/jest.config.js @@ -31,7 +31,12 @@ module.exports = { 'map-obj', 'camelcase', 'quick-lru', - 'react-redux' + 'react-redux', + 'uuid', + '@keystonehq/animated-qr', + '@keystonehq/keystone-sdk', + '@keystonehq/bc-ur-registry-eth', + '@keystonehq/bc-ur-registry-avalanche' ].join('|') + ')' ] diff --git a/packages/core-mobile/package.json b/packages/core-mobile/package.json index 15ecee647d..76e31f7479 100644 --- a/packages/core-mobile/package.json +++ b/packages/core-mobile/package.json @@ -49,15 +49,19 @@ "@date-fns/utc": "2.1.0", "@ethereumjs/common": "4.4.0", "@ethereumjs/tx": "5.4.0", + "@ethereumjs/util": "9.1.0", "@formatjs/intl-locale": "4.2.11", "@formatjs/intl-numberformat": "8.15.4", "@formatjs/intl-pluralrules": "5.4.4", "@gorhom/bottom-sheet": "4.6.4", "@hookform/resolvers": "3.9.0", "@invertase/react-native-apple-authentication": "2.4.0", + "@keystonehq/animated-qr": "0.10.0", + "@keystonehq/keystone-sdk": "0.11.3", "@lavamoat/preinstall-always-fail": "2.1.0", "@metamask/eth-sig-util": "7.0.3", "@metamask/rpc-errors": "6.3.0", + "@ngraveio/bc-ur": "1.1.13", "@noble/secp256k1": "2.1.0", "@notifee/react-native": "9.1.8", "@openzeppelin/contracts": "5.0.2", @@ -180,6 +184,7 @@ "react-native-permissions": "4.1.5", "react-native-popable": "0.4.3", "react-native-popover-view": "6.1.0", + "react-native-progress": "5.0.1", "react-native-qrcode-svg": "6.3.2", "react-native-quick-base64": "2.1.2", "react-native-quick-crypto": "0.7.11", diff --git a/yarn.lock b/yarn.lock index 14a2e939c0..4c58c9372c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,16 +225,20 @@ __metadata: "@dlenroc/testrail": 1.9.1 "@ethereumjs/common": 4.4.0 "@ethereumjs/tx": 5.4.0 + "@ethereumjs/util": 9.1.0 "@formatjs/intl-locale": 4.2.11 "@formatjs/intl-numberformat": 8.15.4 "@formatjs/intl-pluralrules": 5.4.4 "@gorhom/bottom-sheet": 4.6.4 "@hookform/resolvers": 3.9.0 "@invertase/react-native-apple-authentication": 2.4.0 + "@keystonehq/animated-qr": 0.10.0 + "@keystonehq/keystone-sdk": 0.11.3 "@lavamoat/allow-scripts": 3.2.1 "@lavamoat/preinstall-always-fail": 2.1.0 "@metamask/eth-sig-util": 7.0.3 "@metamask/rpc-errors": 6.3.0 + "@ngraveio/bc-ur": 1.1.13 "@noble/secp256k1": 2.1.0 "@notifee/react-native": 9.1.8 "@openzeppelin/contracts": 5.0.2 @@ -399,6 +403,7 @@ __metadata: react-native-permissions: 4.1.5 react-native-popable: 0.4.3 react-native-popover-view: 6.1.0 + react-native-progress: 5.0.1 react-native-qrcode-svg: 6.3.2 react-native-quick-base64: 2.1.2 react-native-quick-crypto: 0.7.11 @@ -4157,6 +4162,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^1.2.0": + version: 1.10.1 + resolution: "@bufbuild/protobuf@npm:1.10.1" + checksum: 403838ad278d504e33e72ec0f64ce1bac9f5025ee6396253382e821a1fe0c371a58ffe45a0e8f23306205b6890c5f83e85828168a35dae118915cd4c3d091177 + languageName: node + linkType: hard + "@coinbase/cbpay-js@npm:2.2.1": version: 2.2.1 resolution: "@coinbase/cbpay-js@npm:2.2.1" @@ -4592,6 +4604,16 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/util@npm:9.1.0, @ethereumjs/util@npm:^9.0.3, @ethereumjs/util@npm:^9.1.0": + 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 + "@ethereumjs/util@npm:^8.1.0": version: 8.1.0 resolution: "@ethereumjs/util@npm:8.1.0" @@ -4603,16 +4625,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/util@npm:^9.1.0": - 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/abi@npm:^5.5.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" @@ -7113,6 +7125,250 @@ __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/animated-qr-base@npm:^0.0.1": + version: 0.0.1 + resolution: "@keystonehq/animated-qr-base@npm:0.0.1" + dependencies: + "@ngraveio/bc-ur": ^1.1.13 + checksum: 5058f7e21b12f3c429e5c0c37c9c9adf718d7c397a13bd75c7bc4f81820754d6a01ca12a8f955472189b3d9fc452f117846db45dcc045108b9211abcfb2cc89c + languageName: node + linkType: hard + +"@keystonehq/animated-qr@npm:0.10.0": + version: 0.10.0 + resolution: "@keystonehq/animated-qr@npm:0.10.0" + dependencies: + "@keystonehq/animated-qr-base": ^0.0.1 + "@ngraveio/bc-ur": ^1.1.6 + "@zxing/browser": ^0.1.1 + "@zxing/library": ^0.19.1 + qrcode.react: ^3.1.0 + peerDependencies: + react: ">= 16.8" + react-dom: ">= 16.8" + checksum: f0657d3c600ea4bc3d0c76dc78c1e6242bf43f5a2ecb34bc3e1de3428dd00f8f8a2c0728a63fc2606294c51a75af50d99d8f0fe4443761cd14fd74db8bf80e76 + 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-avalanche@npm:^0.0.4": + version: 0.0.4 + resolution: "@keystonehq/bc-ur-registry-avalanche@npm:0.0.4" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + buffer: ^6.0.3 + uuid: ^8.3.2 + checksum: c8bff304d1bf2430572d07408e8fdbcacf5f769fc885fc094c248c7c4a7a771cde427f385dd4bb1a38d864361829f6ba2f4f0104e46ce37c251720dfd0ec2336 + 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.5.0": + version: 0.5.0 + resolution: "@keystonehq/bc-ur-registry-cardano@npm:0.5.0" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^8.3.2 + checksum: b8c72bd44a086b41763f8fa3ddbe3cc7227b5e1c42fe22f76ee35abec6acc1df106d74453c928ffcc6afed75abf045cc74db82c66803d141d98be5e52d7532bf + 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.22.0": + version: 0.22.1 + resolution: "@keystonehq/bc-ur-registry-eth@npm:0.22.1" + dependencies: + "@ethereumjs/util": ^9.0.3 + "@keystonehq/bc-ur-registry": ^0.7.0 + hdkey: ^2.0.1 + uuid: ^8.3.2 + checksum: d8effcca1443464c8cd7e2247dd7a49cc8221cb9f34377400a0bba4363a02cd818ed8125f9106a95146174d170959e917ddbe1eabb1376f07f90d71148b1aea5 + 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-iota@npm:^0.1.4": + version: 0.1.4 + resolution: "@keystonehq/bc-ur-registry-iota@npm:0.1.4" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^9.0.0 + checksum: c3a3ee3573e9acb03acf8c7679af8c61c5345469ef0e2dc9f3c18c9ff921ad9e681347401af0894939dd0625b7948346dee67df8deb2e490dc3743319be174df + 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.5 + resolution: "@keystonehq/bc-ur-registry-sol@npm:0.9.5" + dependencies: + "@keystonehq/bc-ur-registry": ^0.7.0 + bs58check: ^2.1.2 + uuid: ^8.3.2 + checksum: f8002e74b4fcad5fe3d67b3d2d462d8a6e6a80ed6dc2cb14815da827ccde18772f0b6595da8e79311a3b81bf273891ab524c981d7c705b24ec8dc977508259a5 + 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.4.0-alpha.0": + version: 0.4.0-alpha.0 + resolution: "@keystonehq/bc-ur-registry-sui@npm:0.4.0-alpha.0" + dependencies: + "@keystonehq/bc-ur-registry": ^0.6.4 + uuid: ^9.0.0 + checksum: 193385f60751fbc261299233f37e01ecc130d67c63f805b811043e03409f06e6ecc03c6d8effab17728a4174cc3eb210c3fbeeeacac058bef2bc301e47be2a1c + 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.11.3": + version: 0.11.3 + resolution: "@keystonehq/keystone-sdk@npm:0.11.3" + 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-avalanche": ^0.0.4 + "@keystonehq/bc-ur-registry-btc": ^0.1.1 + "@keystonehq/bc-ur-registry-cardano": ^0.5.0 + "@keystonehq/bc-ur-registry-cosmos": ^0.5.3 + "@keystonehq/bc-ur-registry-eth": ^0.22.0 + "@keystonehq/bc-ur-registry-evm": ^0.5.3 + "@keystonehq/bc-ur-registry-iota": ^0.1.4 + "@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.4.0-alpha.0 + "@keystonehq/bc-ur-registry-ton": ^0.1.2 + "@ngraveio/bc-ur": ^1.1.6 + "@noble/hashes": ^1.5.0 + bs58check: ^3.0.1 + buffer: ^6.0.3 + pako: ^2.1.0 + ripple-binary-codec: ^1.4.3 + uuid: ^9.0.0 + checksum: c4acc2e14853ba70d8ab53a79ac4426bccb6c1a6b391f428cc2942ab8c0fbbfb34dde38ebe691ccf0c76a6b7ac5cd3fac4f170a59371813e94bcea749f466287 + languageName: node + linkType: hard + "@lavamoat/aa@npm:^4.3.0": version: 4.3.0 resolution: "@lavamoat/aa@npm:4.3.0" @@ -7782,6 +8038,21 @@ __metadata: languageName: node linkType: hard +"@ngraveio/bc-ur@npm:1.1.13, @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" @@ -13753,7 +14024,34 @@ __metadata: languageName: node linkType: hard -"@zxing/text-encoding@npm:0.9.0": +"@zxing/browser@npm:^0.1.1": + version: 0.1.5 + resolution: "@zxing/browser@npm:0.1.5" + dependencies: + "@zxing/text-encoding": ^0.9.0 + peerDependencies: + "@zxing/library": ^0.21.0 + dependenciesMeta: + "@zxing/text-encoding": + optional: true + checksum: 27bfddd707e8e643624b432666a956f1e7e283e20963243f7050c781fd80daad9f54408acdf46d809c64586983f7b02a4981c1d3b0251519e0566c165f6b3924 + languageName: node + linkType: hard + +"@zxing/library@npm:^0.19.1": + version: 0.19.3 + resolution: "@zxing/library@npm:0.19.3" + dependencies: + "@zxing/text-encoding": ~0.9.0 + ts-custom-error: ^3.2.1 + dependenciesMeta: + "@zxing/text-encoding": + optional: true + checksum: 2a3adaccbde0e075ee4c3c73ab7fa9306be979dafeff6d373204470ea3cddab88608c6eca5e891c7d5e693c5df0f0664e14ea0a74d38e0658fc7464f5c986474 + languageName: node + linkType: hard + +"@zxing/text-encoding@npm:0.9.0, @zxing/text-encoding@npm:^0.9.0, @zxing/text-encoding@npm:~0.9.0": version: 0.9.0 resolution: "@zxing/text-encoding@npm:0.9.0" checksum: c23b12aee7639382e4949961304a1294776afaffa40f579e09ffecd0e5e68cf26ef3edd75009de46da8a536e571448755ca68b3e2ea707d53793c0edb2e2c34a @@ -14390,7 +14688,7 @@ __metadata: languageName: node linkType: hard -"assert@npm:2.1.0, assert@npm:^2.1.0": +"assert@npm:2.1.0, assert@npm:^2.0.0, assert@npm:^2.1.0": version: 2.1.0 resolution: "assert@npm:2.1.0" dependencies: @@ -14839,6 +15137,15 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^3.0.9": + version: 3.0.11 + resolution: "base-x@npm:3.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: c2e3c443fd07cb9b9d3e179a9e9c581daa31881005841fe8d6a834e534505890fedf03465ccf14512da60e3f7be00fe66167806b159ba076d2c03952ae7460c4 + languageName: node + linkType: hard + "base-x@npm:^4.0.0": version: 4.0.0 resolution: "base-x@npm:4.0.0" @@ -14883,7 +15190,7 @@ __metadata: languageName: node linkType: hard -"big-integer@npm:1.6.x, big-integer@npm:^1.6.52": +"big-integer@npm:1.6.x, big-integer@npm:^1.6.48, big-integer@npm:^1.6.52": version: 1.6.52 resolution: "big-integer@npm:1.6.52" checksum: 6e86885787a20fed96521958ae9086960e4e4b5e74d04f3ef7513d4d0ad631a9f3bde2730fc8aaa4b00419fc865f6ec573e5320234531ef37505da7da192c40b @@ -14904,6 +15211,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.1": + version: 9.3.0 + resolution: "bignumber.js@npm:9.3.0" + checksum: 580d783d60246e758e527fa879ae0d282d8f250f555dd0fcee1227d680186ceba49ed7964c6d14e2e8d8eac7a2f4dd6ef1b7925dc52f5fc28a5a87639dd2dbd1 + languageName: node + linkType: hard + "bignumber.js@npm:^9.1.1, bignumber.js@npm:^9.1.2": version: 9.1.2 resolution: "bignumber.js@npm:9.1.2" @@ -15401,7 +15715,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.4.3, buffer@npm:^5.5.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: @@ -15689,6 +16003,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.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -16409,6 +16730,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-hash@npm:1.2.0, create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": version: 1.2.0 resolution: "create-hash@npm:1.2.0" @@ -17087,13 +17417,20 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.3": +"decimal.js@npm:^10.2.0": version: 10.5.0 resolution: "decimal.js@npm:10.5.0" checksum: 91c6b53b5dd2f39a05535349ced6840f591d1f914e3c025c6dcec6ffada6e3cfc8dc3f560d304b716be9a9aece3567a7f80f6aff8f38d11ab6f78541c3a91a01 languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 9302b990cd6f4da1c7602200002e40e15d15660374432963421d3cd6d81cc6e27e0a488356b030fee64650947e32e78bdbea245d596dadfeeeb02e146d485999 + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.2": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -20829,6 +21166,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 + "he@npm:1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" @@ -22715,6 +23064,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" @@ -25727,6 +26083,13 @@ __metadata: languageName: node linkType: hard +"pako@npm:^2.1.0": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 71666548644c9a4d056bcaba849ca6fd7242c6cf1af0646d3346f3079a1c7f4a66ffec6f7369ee0dc88f61926c10d6ab05da3e1fca44b83551839e89edd75a3e + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -26696,6 +27059,15 @@ __metadata: languageName: node linkType: hard +"qrcode.react@npm:^3.1.0": + version: 3.2.0 + resolution: "qrcode.react@npm:3.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 55d020ca482d57e8d73ee9e2e18f152184fd3d7d2d0742ae54ec58c5a3bab08b242a648585178d7fc91877fc75d6fbad7a35fb51bc4bddd4374e1de450ca78e7 + languageName: node + linkType: hard + "qrcode@npm:^1.5.1": version: 1.5.4 resolution: "qrcode@npm:1.5.4" @@ -27477,6 +27849,17 @@ __metadata: languageName: node linkType: hard +"react-native-progress@npm:5.0.1": + version: 5.0.1 + resolution: "react-native-progress@npm:5.0.1" + dependencies: + prop-types: ^15.7.2 + peerDependencies: + react-native-svg: "*" + checksum: fc9b68f1ca381b011859f8900c89595d62461cfa5b4faf65527639e8d8247d494cc75d5eb86e6dd393bca8dec18996b451775670d7ef2ae70247fa4079c9a8cf + languageName: node + linkType: hard + "react-native-qrcode-svg@npm:6.3.2": version: 6.3.2 resolution: "react-native-qrcode-svg@npm:6.3.2" @@ -27765,7 +28148,7 @@ react-native-webview@ava-labs/react-native-webview: peerDependencies: react: "*" react-native: "*" - checksum: 77324747a8b5df0a5558bb99a9a0804a5575d328e84f480e462c56417af97213f38a9930e4582fb749bec374dc4d1a8910a45b006e77af9b14a8e64057b932bf + checksum: 6f005151d0b9f4210ad58a55706334c950077e45bb9358a6e2d992386d4079c6f1eac04911a925ffbb839cb7ab847a0cff1177af0fc61cf3172e0a0bb018ba37 languageName: node linkType: hard @@ -28738,7 +29121,7 @@ react-native-webview@ava-labs/react-native-webview: languageName: node linkType: hard -"ripemd160@npm:2, ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": +"ripemd160@npm:2, 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: @@ -28748,6 +29131,30 @@ react-native-webview@ava-labs/react-native-webview: 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 + "rlp@npm:^2.2.4": version: 2.2.7 resolution: "rlp@npm:2.2.7" @@ -29257,7 +29664,7 @@ react-native-webview@ava-labs/react-native-webview: languageName: node linkType: hard -"sha.js@npm:2, sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": +"sha.js@npm:2, 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: @@ -30698,6 +31105,13 @@ react-native-webview@ava-labs/react-native-webview: languageName: node linkType: hard +"ts-custom-error@npm:^3.2.1": + version: 3.3.1 + resolution: "ts-custom-error@npm:3.3.1" + checksum: 50a1e825fced68d70049bd8d282379a635e43aa023a370fa8e736b12a6edba7f18a2d731fa194ac35303a8b625be56e121bdb31d8a0318250d1a8b277059fce3 + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -30848,7 +31262,7 @@ react-native-webview@ava-labs/react-native-webview: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.8.0": +"tslib@npm:2.8.1, tslib@npm:^2.3.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a