From c6cb4040230b3713f4e31fc1f9e0d04ccf869495 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:49:47 +0000 Subject: [PATCH 01/17] feat(onboading): identify walletType on import --- apps/mobile/ios/Podfile.lock | 36 +++++- .../src/modules/onboarding/routes/index.tsx | 2 - .../src/modules/onboarding/routes/types.ts | 10 +- .../ImportAccountScreen.tsx | 77 +++---------- .../useImportAccountScreen.ts | 109 ++++++++++++++++++ .../__tests__/ImportInfoScreen.spec.tsx | 15 ++- .../ImportInfoScreen/useImportInfoScreen.ts | 9 +- .../__tests__/OnboardingScreen.spec.tsx | 4 +- .../OnboardingScreen/useOnboardingScreen.ts | 4 +- 9 files changed, 195 insertions(+), 71 deletions(-) create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 9c4732c00..d3a836821 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -2933,6 +2933,34 @@ PODS: - React-perflogger (= 0.83.0) - React-utils (= 0.83.0) - SocketRocket + - ReactNativeBiometrics (0.11.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RNBootSplash (6.3.11): - boost - DoubleConversion @@ -3538,6 +3566,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "ReactNativeBiometrics (from `../node_modules/@sbaiahmed1/react-native-biometrics`)" - RNBootSplash (from `../node_modules/react-native-bootsplash`) - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -3766,6 +3795,8 @@ EXTERNAL SOURCES: :path: build/generated/ios/ReactCodegen ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativeBiometrics: + :path: "../node_modules/@sbaiahmed1/react-native-biometrics" RNBootSplash: :path: "../node_modules/react-native-bootsplash" RNCClipboard: @@ -3831,7 +3862,7 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - hermes-engine: 617e25b15d2a434c37e627dfb6dff32a86fea6c2 + hermes-engine: c3de7a5fb8b174d0371f1dd0bb649d3ac1a4df4e lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 86fa7488b1476563f41071244aa73d5b738faf19 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 @@ -3918,6 +3949,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 37f746e57fa4a5109edd15a2336009ad72e80f8c ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f + ReactNativeBiometrics: 70a5300cf5272c127e700417e3cd4cba4bf05cac RNBootSplash: 98a34471966945e8dacd1da8f0c2f87cbc2e2df7 RNCClipboard: 4b58c780f63676367640f23c8e114e9bd0cf86ac RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c @@ -3942,4 +3974,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f8175fd2f0a1ebad4218ad393842b4d06245a8e2 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 4266ddbe3..5b26133d5 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -84,7 +84,6 @@ export const OnboardingStackNavigator = () => { name='NameAccount' options={{ headerShown: true, - headerTitle: '', }} layout={safeAreaLayout} component={NameAccountScreenWithErrorBoundary} @@ -93,7 +92,6 @@ export const OnboardingStackNavigator = () => { name='ImportAccount' options={{ headerShown: true, - headerTitle: 'Enter your Recovery Passphrase', }} layout={safeAreaLayout} component={ImportAccountScreenWithErrorBoundary} diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index cb46d4eb3..41284b0cd 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -12,6 +12,8 @@ import { WalletAccount } from '@perawallet/wallet-core-accounts' +export type ImportAccountType = 'universal' | 'algo25' + export type OnboardingStackParamList = { OnboardingHome: undefined NameAccount: @@ -19,6 +21,10 @@ export type OnboardingStackParamList = { account?: WalletAccount } | undefined - ImportInfo: undefined - ImportAccount: undefined + ImportInfo: { + accountType: ImportAccountType + } + ImportAccount: { + accountType: ImportAccountType + } } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx index 916548284..f7d7c3e2d 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx @@ -10,78 +10,34 @@ limitations under the License */ -import { ParamListBase, useNavigation } from '@react-navigation/native' -import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useStyles } from './styles' import { useTheme } from '@rneui/themed' import { PWButton, PWInput, PWOverlay, PWText, PWView } from '@components/core' -import { useImportAccount } from '@perawallet/wallet-core-accounts' -import { useState } from 'react' import { ActivityIndicator, KeyboardAvoidingView, Platform, ScrollView, } from 'react-native' -import { useToast } from '@hooks/useToast' -import { useLanguage } from '@hooks/useLanguage' - -const NUM_WORDS = 24 //TODO: we'll add legacy 25 word accounts later +import { useImportAccountScreen } from './useImportAccountScreen' export const ImportAccountScreen = () => { const { theme } = useTheme() - const navigation = useNavigation>() const styles = useStyles() - const importAccount = useImportAccount() - const { showToast } = useToast() - - const [words, setWords] = useState(new Array(NUM_WORDS).fill('')) - const [focused, setFocused] = useState(0) - const [canImport, setCanImport] = useState(false) - const [processing, setProcessing] = useState(false) - const { t } = useLanguage() - - const updateWord = (word: string, index: number) => { - const splitWords = word.split('\n') - - if (splitWords.length === NUM_WORDS) { - setWords(splitWords) - } else { - setWords(prev => { - prev[index] = word.trim() - return [...prev] - }) - } - - if (words.every(w => w.length)) { - setCanImport(true) - } - } - - const handleImportAccount = () => { - setProcessing(true) - setTimeout(async () => { - try { - await importAccount({ mnemonic: words.join(' ') }) - goToHome() - } catch { - showToast({ - title: t('onboarding.import_account.failed_title'), - body: t('onboarding.import_account.failed_body'), - type: 'error', - }) - } finally { - setProcessing(false) - } - }, 0) - } + const { + words, + focused, + setFocused, + canImport, + processing, + updateWord, + handleImportAccount, + mnemonicLength, + t, + } = useImportAccountScreen() - const goToHome = () => { - navigation.replace('TabBar', { - screen: 'Home', - }) - } + const wordsPerColumn = Math.ceil(mnemonicLength / 2) return ( <> @@ -92,14 +48,17 @@ export const ImportAccountScreen = () => { {[0, 1].map(column => { - const columnOffset = 12 * column + const columnOffset = wordsPerColumn * column return ( {words - .slice(columnOffset, columnOffset + 12) + .slice( + columnOffset, + columnOffset + wordsPerColumn, + ) .map((word, index) => { const offsetIndex = index + columnOffset diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts new file mode 100644 index 000000000..6f7649fbe --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -0,0 +1,109 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { useState, useCallback, useMemo } from 'react' +import { RouteProp, useRoute } from '@react-navigation/native' +import { OnboardingStackParamList, ImportAccountType } from '../../routes/types' +import { useImportAccount } from '@perawallet/wallet-core-accounts' +import { useToast } from '@hooks/useToast' +import { useLanguage } from '@hooks/useLanguage' +import { useAppNavigation } from '@hooks/useAppNavigation' + +const MNEMONIC_LENGTH_MAP: Record = { + universal: 24, + algo25: 25, +} + +export type UseImportAccountScreenResult = { + words: string[] + focused: number + setFocused: (index: number) => void + canImport: boolean + processing: boolean + updateWord: (word: string, index: number) => void + handleImportAccount: () => void + mnemonicLength: number + t: (key: string) => string +} + +export function useImportAccountScreen(): UseImportAccountScreenResult { + const { + params: { accountType }, + } = useRoute>() + const navigation = useAppNavigation() + const importAccount = useImportAccount() + const { showToast } = useToast() + const { t } = useLanguage() + + const mnemonicLength = MNEMONIC_LENGTH_MAP[accountType] + + const [words, setWords] = useState( + new Array(mnemonicLength).fill(''), + ) + const [focused, setFocused] = useState(0) + const [processing, setProcessing] = useState(false) + + const canImport = useMemo(() => words.every(w => w.length > 0), [words]) + + const updateWord = useCallback( + (word: string, index: number) => { + const splitWords = word.split('\n') + + if (splitWords.length === mnemonicLength) { + setWords(splitWords) + } else { + setWords(prev => { + const next = [...prev] + next[index] = word.trim() + return next + }) + } + }, + [mnemonicLength], + ) + + const goToHome = useCallback(() => { + navigation.replace('TabBar', { + screen: 'Home', + }) + }, [navigation]) + + const handleImportAccount = useCallback(() => { + setProcessing(true) + setTimeout(async () => { + try { + await importAccount({ mnemonic: words.join(' ') }) + goToHome() + } catch { + showToast({ + title: t('onboarding.import_account.failed_title'), + body: t('onboarding.import_account.failed_body'), + type: 'error', + }) + } finally { + setProcessing(false) + } + }, 0) + }, [importAccount, words, goToHome, showToast, t]) + + return { + words, + focused, + setFocused, + canImport, + processing, + updateWord, + handleImportAccount, + mnemonicLength, + t, + } +} diff --git a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx index 33dc3b423..4ec987db1 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx @@ -81,6 +81,17 @@ vi.mock('@components/core', async () => { } }) +// Mock react-navigation +vi.mock('@react-navigation/native', async () => { + const actual = await vi.importActual('@react-navigation/native') + return { + ...actual, + useRoute: () => ({ + params: { accountType: 'universal' }, + }), + } +}) + describe('ImportInfoScreen', () => { beforeEach(() => { vi.clearAllMocks() @@ -109,7 +120,9 @@ describe('ImportInfoScreen', () => { const recoverButton = screen.getByText('onboarding.import_info.button') fireEvent.click(recoverButton) - expect(mockPush).toHaveBeenCalledWith('ImportAccount') + expect(mockPush).toHaveBeenCalledWith('ImportAccount', { + accountType: 'universal', + }) }) it('handles info button press', () => { diff --git a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/useImportInfoScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/useImportInfoScreen.ts index 0785830cf..3d3935f80 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/useImportInfoScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/useImportInfoScreen.ts @@ -12,17 +12,22 @@ import { useCallback } from 'react' import { useAppNavigation } from '@hooks/useAppNavigation' +import { RouteProp, useRoute } from '@react-navigation/native' +import { OnboardingStackParamList } from '../../routes/types' export const useImportInfoScreen = () => { const navigation = useAppNavigation() + const { + params: { accountType }, + } = useRoute>() const handleBackPress = useCallback(() => { navigation.goBack() }, [navigation]) const handleRecoverPress = useCallback(() => { - navigation.push('ImportAccount') - }, [navigation]) + navigation.push('ImportAccount', { accountType }) + }, [navigation, accountType]) const handleInfoPress = useCallback(() => { // Requirements don't specify what this does, but it's an icon in the right diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx index ee4afcf79..9aef9066a 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx @@ -168,6 +168,8 @@ describe('OnboardingScreen', () => { ) fireEvent.click(universalWalletOption) - expect(mockPush).toHaveBeenCalledWith('ImportInfo') + expect(mockPush).toHaveBeenCalledWith('ImportInfo', { + accountType: 'universal', + }) }) }) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts index be0f57365..d666742c9 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts @@ -45,12 +45,12 @@ export const useOnboardingScreen = () => { const handleUniversalWalletPress = useCallback(() => { closeImportOptions() - navigation.push('ImportInfo') + navigation.push('ImportInfo', { accountType: 'universal' }) }, [closeImportOptions, navigation]) const handleAlgo25Press = useCallback(() => { closeImportOptions() - navigation.push('ImportInfo') + navigation.push('ImportInfo', { accountType: 'algo25' }) }, [closeImportOptions, navigation]) return { From 4bb641ed4671620fea642f41ef150ed10a3daa07 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:17:19 +0000 Subject: [PATCH 02/17] refactor(accounts): extracting wallet from mnemonic as an util --- packages/accounts/src/__tests__/utils.test.ts | 84 +++++++++++++------ .../accounts/src/hooks/useImportAccount.ts | 14 ++-- packages/accounts/src/utils.ts | 21 +++++ 3 files changed, 87 insertions(+), 32 deletions(-) diff --git a/packages/accounts/src/__tests__/utils.test.ts b/packages/accounts/src/__tests__/utils.test.ts index c59f5b920..2c80a57ac 100644 --- a/packages/accounts/src/__tests__/utils.test.ts +++ b/packages/accounts/src/__tests__/utils.test.ts @@ -11,7 +11,6 @@ */ import { describe, test, expect } from 'vitest' -import type { WalletAccount } from '../models' import { canSignWithAccount, getAccountDisplayName, @@ -21,63 +20,65 @@ import { isMultisigAccount, isRekeyedAccount, isWatchAccount, + createUniversalWalletFromMnemonic, + getSeedFromMasterKey, } from '../utils' describe('services/accounts/utils - getAccountDisplayName', () => { test('returns account name when present', () => { - const acc: WalletAccount = { + const acc = { id: '1', type: 'hdWallet', address: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', name: 'Named', canSign: true, - } + } as any expect(getAccountDisplayName(acc)).toEqual('Named') }) test('returns "No Address Found" when address is missing or empty', () => { - const acc: WalletAccount = { + const acc = { id: '2', type: 'hdWallet', address: '', canSign: false, - } + } as any expect(getAccountDisplayName(acc)).toEqual('No Address Found') }) test('returns address unchanged when length <= 11', () => { - const acc1: WalletAccount = { + const acc1 = { id: '3', type: 'hdWallet', address: 'SHORT', canSign: true, - } + } as any expect(getAccountDisplayName(acc1)).toEqual('SHORT') - const acc2: WalletAccount = { + const acc2 = { id: '4', type: 'hdWallet', address: 'ABCDEFGHIJK', canSign: true, - } + } as any expect(getAccountDisplayName(acc2)).toEqual('ABCDEFGHIJK') }) test('truncates long addresses to 5 prefix and suffix characters', () => { - const acc1: WalletAccount = { + const acc1 = { id: '5', type: 'hdWallet', address: 'ABCDEFGHIJKL', canSign: true, - } + } as any expect(getAccountDisplayName(acc1)).toEqual('ABCDE...HIJKL') - const acc2: WalletAccount = { + const acc2 = { id: '6', type: 'hdWallet', address: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', canSign: true, - } + } as any expect(getAccountDisplayName(acc2)).toEqual('ABCDE...VWXYZ') }) @@ -87,12 +88,12 @@ describe('services/accounts/utils - getAccountDisplayName', () => { }) describe('services/accounts/utils - account type checks', () => { - const baseAccount: WalletAccount = { + const baseAccount = { id: '1', type: 'hdWallet', address: 'ADDR1', canSign: true, - } + } as any test('isHDWalletAccount returns true if type is hdWallet', () => { expect(isHDWalletAccount(baseAccount)).toBe(true) @@ -100,7 +101,7 @@ describe('services/accounts/utils - account type checks', () => { isHDWalletAccount({ ...baseAccount, type: 'algo25', - }), + } as any), ).toBe(false) }) @@ -111,14 +112,14 @@ describe('services/accounts/utils - account type checks', () => { ...baseAccount, type: 'hardware', hardwareDetails: { manufacturer: 'ledger' }, - }), + } as any), ).toBe(true) expect( isLedgerAccount({ ...baseAccount, type: 'hardware', hardwareDetails: { manufacturer: 'other' as any }, - }), + } as any), ).toBe(false) }) @@ -128,7 +129,7 @@ describe('services/accounts/utils - account type checks', () => { isRekeyedAccount({ ...baseAccount, rekeyAddress: 'ADDR2', - }), + } as any), ).toBe(true) }) @@ -138,19 +139,19 @@ describe('services/accounts/utils - account type checks', () => { isAlgo25Account({ ...baseAccount, type: 'algo25', - }), + } as any), ).toBe(true) expect( isAlgo25Account({ ...baseAccount, type: 'hdWallet', - }), + } as any), ).toBe(false) expect( isAlgo25Account({ ...baseAccount, type: 'watch', - }), + } as any), ).toBe(false) }) @@ -160,7 +161,7 @@ describe('services/accounts/utils - account type checks', () => { isWatchAccount({ ...baseAccount, type: 'watch', - }), + } as any), ).toBe(true) }) @@ -170,7 +171,7 @@ describe('services/accounts/utils - account type checks', () => { isMultisigAccount({ ...baseAccount, type: 'multisig', - }), + } as any), ).toBe(true) }) @@ -180,7 +181,40 @@ describe('services/accounts/utils - account type checks', () => { canSignWithAccount({ ...baseAccount, canSign: false, - }), + } as any), ).toBe(false) }) }) + +describe('services/accounts/utils - createUniversalWalletFromMnemonic', () => { + test('creates key pair from valid mnemonic', async () => { + const mnemonic = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' + const result = await createUniversalWalletFromMnemonic(mnemonic) + + expect(result.seed).toBeInstanceOf(Buffer) + expect(result.seed.length).toBe(64) // BIP39 seed is 512 bits + expect(result.entropy).toBeDefined() + expect(result.type).toBe('hdwallet-root-key') + }) +}) + +describe('services/accounts/utils - getSeedFromMasterKey', () => { + test('obtains seed from JSON stringified master key data', () => { + const masterKey = { + seed: Buffer.from('test-seed').toString('base64'), + entropy: 'test-entropy', + } + const keyData = new TextEncoder().encode(JSON.stringify(masterKey)) + const seed = getSeedFromMasterKey(keyData) + + expect(seed).toEqual(Buffer.from('test-seed')) + }) + + test('obtains seed from raw master key data', () => { + const keyData = new Uint8Array([1, 2, 3, 4]) + const seed = getSeedFromMasterKey(keyData) + + expect(seed).toEqual(Buffer.from([1, 2, 3, 4])) + }) +}) diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index b2c963631..961eaa66e 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -10,13 +10,12 @@ limitations under the License */ -import { useHDWallet } from './useHDWallet' import { v7 as uuidv7 } from 'uuid' -import { KeyType, useKMS } from '@perawallet/wallet-core-kms' +import { useKMS } from '@perawallet/wallet-core-kms' import { useCreateAccount } from './useCreateAccount' +import { createUniversalWalletFromMnemonic } from '../utils' export const useImportAccount = () => { - const { generateMasterKey } = useHDWallet() const { saveKey } = useKMS() const createAccount = useCreateAccount() @@ -28,17 +27,18 @@ export const useImportAccount = () => { mnemonic: string }) => { const rootWalletId = walletId ?? uuidv7() - const masterKey = await generateMasterKey(mnemonic) + const universalKeyPair = + await createUniversalWalletFromMnemonic(mnemonic) const stringifiedObj = JSON.stringify({ - seed: masterKey.seed.toString('base64'), - entropy: masterKey.entropy, + seed: universalKeyPair.seed.toString('base64'), + entropy: universalKeyPair.entropy, }) const rootKeyPair = { id: rootWalletId, publicKey: '', privateDataStorageKey: rootWalletId, createdAt: new Date(), - type: KeyType.HDWalletRootKey, + type: universalKeyPair.type, } await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts index 79222a3ba..d1b779447 100644 --- a/packages/accounts/src/utils.ts +++ b/packages/accounts/src/utils.ts @@ -20,6 +20,14 @@ import { WatchAccount, type WalletAccount, } from './models' +import { KeyType } from '@perawallet/wallet-core-kms' +import * as bip39 from 'bip39' + +export type UniversalKeyPair = { + seed: Buffer + entropy: string + type: typeof KeyType.HDWalletRootKey +} export const getAccountDisplayName = (account: WalletAccount | null) => { if (!account) return 'No Account' @@ -79,3 +87,16 @@ export const getSeedFromMasterKey = (keyData: Uint8Array) => { return Buffer.from(keyData) } } + +export const createUniversalWalletFromMnemonic = async ( + mnemonic: string, +): Promise => { + const seed = await bip39.mnemonicToSeed(mnemonic) + const entropy = await bip39.mnemonicToEntropy(mnemonic) + + return { + seed, + entropy, + type: KeyType.HDWalletRootKey, + } +} From 8f49f07cfd9682e9b55d04e06b7dae9faf6e9078 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:33:08 +0000 Subject: [PATCH 03/17] feat(accounts): implement account type on the import account hook --- .../src/modules/onboarding/routes/types.ts | 12 +++---- .../useImportAccountScreen.ts | 9 ++++-- .../accounts/src/hooks/useImportAccount.ts | 32 +++++++++++-------- packages/accounts/src/models/accounts.ts | 2 ++ 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index 41284b0cd..c941d6696 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -10,17 +10,15 @@ limitations under the License */ -import { WalletAccount } from '@perawallet/wallet-core-accounts' - -export type ImportAccountType = 'universal' | 'algo25' +import { WalletAccount, ImportAccountType } from '@perawallet/wallet-core-accounts' export type OnboardingStackParamList = { OnboardingHome: undefined NameAccount: - | { - account?: WalletAccount - } - | undefined + | { + account?: WalletAccount + } + | undefined ImportInfo: { accountType: ImportAccountType } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 6f7649fbe..2a013085f 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -12,8 +12,8 @@ import { useState, useCallback, useMemo } from 'react' import { RouteProp, useRoute } from '@react-navigation/native' -import { OnboardingStackParamList, ImportAccountType } from '../../routes/types' -import { useImportAccount } from '@perawallet/wallet-core-accounts' +import { OnboardingStackParamList } from '../../routes/types' +import { useImportAccount, ImportAccountType } from '@perawallet/wallet-core-accounts' import { useToast } from '@hooks/useToast' import { useLanguage } from '@hooks/useLanguage' import { useAppNavigation } from '@hooks/useAppNavigation' @@ -81,7 +81,10 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { setProcessing(true) setTimeout(async () => { try { - await importAccount({ mnemonic: words.join(' ') }) + await importAccount({ + mnemonic: words.join(' '), + type: accountType, + }) goToHome() } catch { showToast({ diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index 961eaa66e..a64669c70 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -13,6 +13,7 @@ import { v7 as uuidv7 } from 'uuid' import { useKMS } from '@perawallet/wallet-core-kms' import { useCreateAccount } from './useCreateAccount' +import { ImportAccountType } from '../models' import { createUniversalWalletFromMnemonic } from '../utils' export const useImportAccount = () => { @@ -22,25 +23,30 @@ export const useImportAccount = () => { return async ({ walletId, mnemonic, + type, }: { walletId?: string mnemonic: string + type: ImportAccountType }) => { const rootWalletId = walletId ?? uuidv7() - const universalKeyPair = - await createUniversalWalletFromMnemonic(mnemonic) - const stringifiedObj = JSON.stringify({ - seed: universalKeyPair.seed.toString('base64'), - entropy: universalKeyPair.entropy, - }) - const rootKeyPair = { - id: rootWalletId, - publicKey: '', - privateDataStorageKey: rootWalletId, - createdAt: new Date(), - type: universalKeyPair.type, + + if (type === 'universal') { + const universalKeyPair = + await createUniversalWalletFromMnemonic(mnemonic) + const stringifiedObj = JSON.stringify({ + seed: universalKeyPair.seed.toString('base64'), + entropy: universalKeyPair.entropy, + }) + const rootKeyPair = { + id: rootWalletId, + publicKey: '', + privateDataStorageKey: rootWalletId, + createdAt: new Date(), + type: universalKeyPair.type, + } + await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) } - await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) //TODO: we currently just create the 0/0 account but we really should scan the blockchain //and look for accounts that might match (see old app logic - we want to scan iteratively diff --git a/packages/accounts/src/models/accounts.ts b/packages/accounts/src/models/accounts.ts index 676fb0a93..e8691056f 100644 --- a/packages/accounts/src/models/accounts.ts +++ b/packages/accounts/src/models/accounts.ts @@ -28,6 +28,8 @@ export const AccountTypes = { export type AccountType = (typeof AccountTypes)[keyof typeof AccountTypes] +export type ImportAccountType = 'universal' | 'algo25' + export type HDWalletDetails = { walletId: string account: number From f20c1e1a7a75dad1e4189b7bb57ea0cf07e328ef Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:09:42 +0000 Subject: [PATCH 04/17] feat(accounts): implementing algo25 wallet import --- packages/accounts/package.json | 2 + .../hooks/__tests__/useImportAccount.test.ts | 169 +++++++++++++++--- .../accounts/src/hooks/useCreateAccount.ts | 34 +++- .../accounts/src/hooks/useImportAccount.ts | 19 +- packages/kms/src/models/keys.ts | 1 + pnpm-lock.yaml | 6 + 6 files changed, 208 insertions(+), 23 deletions(-) diff --git a/packages/accounts/package.json b/packages/accounts/package.json index ba36c7f34..ff8eaeb8f 100644 --- a/packages/accounts/package.json +++ b/packages/accounts/package.json @@ -26,6 +26,8 @@ "@perawallet/wallet-core-shared": "workspace:*", "@perawallet/wallet-core-kms": "workspace:*", "@algorandfoundation/xhd-wallet-api": "catalog:", + "@algorandfoundation/algokit-utils": "catalog:", + "tweetnacl": "^1.0.3", "@tanstack/react-query": "catalog:", "bip39": "catalog:", "decimal.js": "catalog:", diff --git a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts index 338aecce1..4635302b6 100644 --- a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts +++ b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts @@ -50,6 +50,38 @@ const bip39Spies = vi.hoisted(() => ({ })) vi.mock('bip39', () => bip39Spies) +const algo25Spies = vi.hoisted(() => ({ + seedFromMnemonic: vi.fn(() => new Uint8Array(32).fill(3)), +})) +vi.mock('@algorandfoundation/algokit-utils/algo25', () => ({ + seedFromMnemonic: algo25Spies.seedFromMnemonic, +})) + +const naclSpies = vi.hoisted(() => ({ + sign: { + keyPair: { + fromSeed: vi.fn(() => ({ + publicKey: new Uint8Array(32).fill(4), + secretKey: new Uint8Array(64).fill(5), + })), + }, + }, +})) +vi.mock('tweetnacl', () => ({ + default: naclSpies, +})) + +vi.mock('@perawallet/wallet-core-blockchain', async () => { + return { + encodeAlgorandAddress: vi.fn((address: Uint8Array) => + Buffer.from(address).toString('base64'), + ), + useTransactionEncoder: vi.fn(() => ({ + encodeTransaction: vi.fn(), + })), + } +}) + vi.mock('@perawallet/wallet-core-shared', async () => { const actual = await vi.importActual< typeof import('@perawallet/wallet-core-shared') @@ -81,6 +113,43 @@ vi.mock('@perawallet/wallet-core-platform-integration', async () => { } }) +const kmsSpies = vi.hoisted(() => { + const keys = new Map() + return { + saveKey: vi.fn(async (key: any) => { + keys.set(key.id, key) + return key + }), + getKey: vi.fn((id: string) => keys.get(id) || null), + executeWithKey: vi.fn(async (_id: string, _domain: string, handler: any) => { + const dummyData = JSON.stringify({ + seed: Buffer.from('seed').toString('base64'), + entropy: 'entropy', + }) + return handler(new TextEncoder().encode(dummyData)) + }), + } +}) + +vi.mock('@perawallet/wallet-core-kms', async () => { + const actual = await vi.importActual< + typeof import('@perawallet/wallet-core-kms') + >('@perawallet/wallet-core-kms') + return { + ...actual, + useKMS: vi.fn(() => ({ + saveKey: kmsSpies.saveKey, + getKey: kmsSpies.getKey, + deleteKey: vi.fn(), + getPrivateData: vi.fn(), + keys: new Map(), + })), + useWithKey: vi.fn(() => ({ + executeWithKey: kmsSpies.executeWithKey, + })), + } +}) + vi.mock('../../store', async () => { const actual = await vi.importActual('../../store') @@ -133,27 +202,33 @@ describe('useImportAccount', () => { let imported: any await act(async () => { - imported = await result.current({ mnemonic: 'test mnemonic' }) + imported = await result.current({ + mnemonic: 'test mnemonic', + type: 'universal', + }) }) expect(imported.address).toBeTruthy() expect(imported.id).toBeTruthy() - // Verify storage calls - only root key saved - expect(dummySecure.setItem).toHaveBeenCalledTimes(1) - expect(dummySecure.setItem).toHaveBeenNthCalledWith( + // Verify kms calls + expect(kmsSpies.saveKey).toHaveBeenCalledTimes(1) + expect(kmsSpies.saveKey).toHaveBeenNthCalledWith( 1, - 'WALLET1', - expect.anything(), // Root key data (JSON string as Uint8Array) + expect.objectContaining({ + id: 'WALLET1', + type: 'hdwallet-root-key', + }), + expect.anything(), ) expect(useAccountsStore.getState().accounts).toHaveLength(1) }) test('throws error when generateMasterKey fails with invalid mnemonic', async () => { const dummySecure = { - setItem: vi.fn(async () => {}), + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -171,16 +246,16 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'invalid mnemonic' }), + result.current({ mnemonic: 'invalid mnemonic', type: 'universal' }), ).rejects.toThrow('Invalid mnemonic') }) }) test('throws error when secure storage setItem fails for root key', async () => { const dummySecure = { - setItem: vi.fn().mockRejectedValueOnce(new Error('Storage full')), // First call for root key fails + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -190,21 +265,22 @@ describe('useImportAccount', () => { }) uuidSpies.v7.mockImplementationOnce(() => 'WALLET1') + kmsSpies.saveKey.mockRejectedValueOnce(new Error('Storage full')) const { result } = renderHook(() => useImportAccount()) await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic' }), + result.current({ mnemonic: 'test mnemonic', type: 'universal' }), ).rejects.toThrow('Storage full') }) }) test('throws error when mnemonicToEntropy fails', async () => { const dummySecure = { - setItem: vi.fn(async () => {}), + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -222,7 +298,7 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic' }), + result.current({ mnemonic: 'test mnemonic', type: 'universal' }), ).rejects.toThrow('Invalid entropy') }) }) @@ -261,16 +337,19 @@ describe('useImportAccount', () => { imported = await result.current({ walletId: 'CUSTOM_WALLET', mnemonic: 'test mnemonic', + type: 'universal', }) }) expect(imported).toBeTruthy() expect(imported.hdWalletDetails?.walletId).toBe('CUSTOM_WALLET') - // Verify storage calls - only root key saved - expect(dummySecure.setItem).toHaveBeenCalledTimes(1) - expect(dummySecure.setItem).toHaveBeenNthCalledWith( + // Verify kms calls + expect(kmsSpies.saveKey).toHaveBeenCalledTimes(1) + expect(kmsSpies.saveKey).toHaveBeenNthCalledWith( 1, - 'CUSTOM_WALLET', + expect.objectContaining({ + id: 'CUSTOM_WALLET', + }), expect.anything(), ) }) @@ -305,8 +384,58 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic' }), + result.current({ mnemonic: 'test mnemonic', type: 'universal' }), ).rejects.toThrow('Address generation failed') }) }) + + test('imports algo25 account and persists keys', async () => { + const storage = new Map() + const dummySecure = { + setItem: vi.fn(async (key, value) => { + storage.set(key, value) + }), + getItem: vi.fn(async key => storage.get(key) ?? null), + removeItem: vi.fn(async key => { + storage.delete(key) + }), + authenticate: vi.fn(async () => true), + } + + registerTestPlatform({ + keyValueStorage: new MemoryKeyValueStorage() as any, + secureStorage: dummySecure as any, + }) + + uuidSpies.v7 + .mockImplementationOnce(() => 'WALLET1') + .mockImplementationOnce(() => 'ACC1') + + const { result } = renderHook(() => useImportAccount()) + + let imported: any + await act(async () => { + imported = await result.current({ + mnemonic: 'test mnemonic', + type: 'algo25', + }) + }) + + expect(imported.address).toBeTruthy() + expect(imported.id).toBeTruthy() + expect(imported.type).toBe('algo25') + + // Verify kms calls + expect(kmsSpies.saveKey).toHaveBeenCalledTimes(1) + expect(kmsSpies.saveKey).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: 'WALLET1', + type: 'algo25-key', + publicKey: 'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=', // base64(fill(4)) + }), + expect.anything(), + ) + expect(useAccountsStore.getState().accounts).toHaveLength(1) + }) }) diff --git a/packages/accounts/src/hooks/useCreateAccount.ts b/packages/accounts/src/hooks/useCreateAccount.ts index 23532df28..54c8004fe 100644 --- a/packages/accounts/src/hooks/useCreateAccount.ts +++ b/packages/accounts/src/hooks/useCreateAccount.ts @@ -19,7 +19,7 @@ import { import { useAccountsStore } from '../store' import { useHDWallet } from './useHDWallet' import { v7 as uuidv7 } from 'uuid' -import { AccountTypes, WalletAccount } from '../models' +import { AccountTypes, WalletAccount, ImportAccountType } from '../models' import { BIP32DerivationType } from '@algorandfoundation/xhd-wallet-api' import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' import { @@ -46,14 +46,17 @@ export const useCreateAccount = () => { walletId, account, keyIndex, + type = 'universal', }: { walletId?: string account: number keyIndex: number + type?: ImportAccountType }) => { const rootWalletId = walletId ?? uuidv7() //TODO dry this code - maybe create a useHDWalletKey hook and share with useImportAccount let rootKey = getKey(rootWalletId) + if (!rootKey) { const masterKey = await generateMasterKey() const keyData = { @@ -67,7 +70,10 @@ export const useCreateAccount = () => { privateDataStorageKey: '', domain: KEY_DOMAIN, createdAt: new Date(), - type: KeyType.HDWalletRootKey, + type: + type === 'universal' + ? KeyType.HDWalletRootKey + : KeyType.Algo25Key, } as KeyPair rootKey = await saveKey( @@ -77,6 +83,30 @@ export const useCreateAccount = () => { masterKey.seed.fill(0) } + if (rootKey && rootKey.type === KeyType.Algo25Key) { + const newAccount: WalletAccount = { + id: uuidv7(), + address: rootKey.publicKey, + type: AccountTypes.algo25, + canSign: true, + keyPairId: rootKey.id, + } + + accounts.push(newAccount) + setAccounts([...accounts]) + + if (deviceID) { + updateDeviceOnBackend({ + deviceId: deviceID, + data: { + platform: deviceInfo.getDevicePlatform(), + accounts: accounts.map(a => a.address), + }, + }) + } + return newAccount + } + if (!rootKey?.id) { throw new NoHDWalletError(rootWalletId) } diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index a64669c70..51b48560f 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -11,7 +11,10 @@ */ import { v7 as uuidv7 } from 'uuid' -import { useKMS } from '@perawallet/wallet-core-kms' +import { seedFromMnemonic } from '@algorandfoundation/algokit-utils/algo25' +import nacl from 'tweetnacl' +import { useKMS, KeyType } from '@perawallet/wallet-core-kms' +import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' import { useCreateAccount } from './useCreateAccount' import { ImportAccountType } from '../models' import { createUniversalWalletFromMnemonic } from '../utils' @@ -48,6 +51,19 @@ export const useImportAccount = () => { await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) } + if (type === 'algo25') { + const seed = seedFromMnemonic(mnemonic) + const keyPair = nacl.sign.keyPair.fromSeed(seed) + const rootKeyPair = { + id: rootWalletId, + publicKey: encodeAlgorandAddress(keyPair.publicKey), + privateDataStorageKey: rootWalletId, + createdAt: new Date(), + type: KeyType.Algo25Key, + } + await saveKey(rootKeyPair, seed) + } + //TODO: we currently just create the 0/0 account but we really should scan the blockchain //and look for accounts that might match (see old app logic - we want to scan iteratively //until we find 5 empty keyindexes and 5 empty accounts (I think) @@ -55,6 +71,7 @@ export const useImportAccount = () => { walletId: rootWalletId, account: 0, keyIndex: 0, + type, }) return newAccount } diff --git a/packages/kms/src/models/keys.ts b/packages/kms/src/models/keys.ts index 95850870d..b2e52c808 100644 --- a/packages/kms/src/models/keys.ts +++ b/packages/kms/src/models/keys.ts @@ -14,6 +14,7 @@ export const KeyType = { HDWalletRootKey: 'hdwallet-root-key', HDWalletDerivedKey: 'hdwallet-derived-key', DeterministicP256Key: 'deterministic-p256-key', + Algo25Key: 'algo25-key', } export type KeyType = (typeof KeyType)[keyof typeof KeyType] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45d3b35ff..c0bcea1eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -562,6 +562,9 @@ importers: packages/accounts: dependencies: + '@algorandfoundation/algokit-utils': + specifier: 'catalog:' + version: 10.0.0-alpha.29 '@algorandfoundation/xhd-wallet-api': specifier: 'catalog:' version: 2.0.0-canary.1 @@ -601,6 +604,9 @@ importers: react: specifier: 'catalog:' version: 19.2.0 + tweetnacl: + specifier: ^1.0.3 + version: 1.0.3 uuid: specifier: 'catalog:' version: 13.0.0 From edef1ba72e1591724f4dd956baec2cbd1b1785b9 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:42:19 +0000 Subject: [PATCH 05/17] chore(accounts): lint fixes --- .../src/modules/onboarding/routes/types.ts | 13 +++-- .../useImportAccountScreen.ts | 5 +- .../hooks/__tests__/useImportAccount.test.ts | 48 ++++++++++++------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index c941d6696..7776b333b 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -10,15 +10,18 @@ limitations under the License */ -import { WalletAccount, ImportAccountType } from '@perawallet/wallet-core-accounts' +import { + WalletAccount, + ImportAccountType, +} from '@perawallet/wallet-core-accounts' export type OnboardingStackParamList = { OnboardingHome: undefined NameAccount: - | { - account?: WalletAccount - } - | undefined + | { + account?: WalletAccount + } + | undefined ImportInfo: { accountType: ImportAccountType } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 2a013085f..a4e991b18 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -13,7 +13,10 @@ import { useState, useCallback, useMemo } from 'react' import { RouteProp, useRoute } from '@react-navigation/native' import { OnboardingStackParamList } from '../../routes/types' -import { useImportAccount, ImportAccountType } from '@perawallet/wallet-core-accounts' +import { + useImportAccount, + ImportAccountType, +} from '@perawallet/wallet-core-accounts' import { useToast } from '@hooks/useToast' import { useLanguage } from '@hooks/useLanguage' import { useAppNavigation } from '@hooks/useAppNavigation' diff --git a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts index 4635302b6..611b367e1 100644 --- a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts +++ b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts @@ -121,13 +121,15 @@ const kmsSpies = vi.hoisted(() => { return key }), getKey: vi.fn((id: string) => keys.get(id) || null), - executeWithKey: vi.fn(async (_id: string, _domain: string, handler: any) => { - const dummyData = JSON.stringify({ - seed: Buffer.from('seed').toString('base64'), - entropy: 'entropy', - }) - return handler(new TextEncoder().encode(dummyData)) - }), + executeWithKey: vi.fn( + async (_id: string, _domain: string, handler: any) => { + const dummyData = JSON.stringify({ + seed: Buffer.from('seed').toString('base64'), + entropy: 'entropy', + }) + return handler(new TextEncoder().encode(dummyData)) + }, + ), } }) @@ -226,9 +228,9 @@ describe('useImportAccount', () => { test('throws error when generateMasterKey fails with invalid mnemonic', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } @@ -246,16 +248,19 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'invalid mnemonic', type: 'universal' }), + result.current({ + mnemonic: 'invalid mnemonic', + type: 'universal', + }), ).rejects.toThrow('Invalid mnemonic') }) }) test('throws error when secure storage setItem fails for root key', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } @@ -271,16 +276,19 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic', type: 'universal' }), + result.current({ + mnemonic: 'test mnemonic', + type: 'universal', + }), ).rejects.toThrow('Storage full') }) }) test('throws error when mnemonicToEntropy fails', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } @@ -298,7 +306,10 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic', type: 'universal' }), + result.current({ + mnemonic: 'test mnemonic', + type: 'universal', + }), ).rejects.toThrow('Invalid entropy') }) }) @@ -384,7 +395,10 @@ describe('useImportAccount', () => { await act(async () => { await expect( - result.current({ mnemonic: 'test mnemonic', type: 'universal' }), + result.current({ + mnemonic: 'test mnemonic', + type: 'universal', + }), ).rejects.toThrow('Address generation failed') }) }) From dd276eb06dd3628cf46591793674275e30153822 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:54:20 +0000 Subject: [PATCH 06/17] feat(onboarding): adding title to the mnemonic recovery screen --- apps/mobile/src/i18n/locales/en.json | 1 + apps/mobile/src/modules/onboarding/routes/index.tsx | 3 +++ .../screens/ImportAccountScreen/ImportAccountScreen.tsx | 3 +++ .../modules/onboarding/screens/ImportAccountScreen/styles.ts | 1 + 4 files changed, 8 insertions(+) diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 50c88d0e9..e9b2306c1 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -294,6 +294,7 @@ "button": "Recover an Algorand Wallet" }, "import_account": { + "title": "Enter your Recovery Passphrase", "importing": "Importing wallet...", "button": "Import Wallet", "failed_title": "Import failed", diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 5b26133d5..7bd53466c 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -76,6 +76,7 @@ export const OnboardingStackNavigator = () => { name='ImportInfo' options={{ headerShown: false, + title: '', }} layout={safeAreaLayout} component={ImportInfoScreenWithErrorBoundary} @@ -84,6 +85,7 @@ export const OnboardingStackNavigator = () => { name='NameAccount' options={{ headerShown: true, + title: '', }} layout={safeAreaLayout} component={NameAccountScreenWithErrorBoundary} @@ -92,6 +94,7 @@ export const OnboardingStackNavigator = () => { name='ImportAccount' options={{ headerShown: true, + title: '', }} layout={safeAreaLayout} component={ImportAccountScreenWithErrorBoundary} diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx index f7d7c3e2d..5877feca7 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx @@ -46,6 +46,9 @@ export const ImportAccountScreen = () => { behavior={Platform.OS === 'ios' ? 'padding' : 'height'} > + + {t('onboarding.import_account.title')} + {[0, 1].map(column => { const columnOffset = wordsPerColumn * column diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts index 036e10ad6..4898a40f7 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts @@ -50,6 +50,7 @@ export const useStyles = makeStyles(theme => { width: '47%', }, scrollView: { + paddingHorizontal: theme.spacing.xl, paddingBottom: theme.spacing.lg, }, inputContainerRow: { From 31f707d090166c5cd80ed4ac35496a7df82ecdd5 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:35:37 +0000 Subject: [PATCH 07/17] feat(onboarding): adjusting more aspects of the ui --- .../src/modules/onboarding/routes/index.tsx | 3 +- .../ImportAccountScreen.tsx | 38 +++- .../screens/ImportAccountScreen/styles.ts | 189 ++++++++++-------- .../useImportAccountScreen.ts | 26 ++- 4 files changed, 162 insertions(+), 94 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 7bd53466c..6fc0166cb 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -93,8 +93,7 @@ export const OnboardingStackNavigator = () => { { const { theme } = useTheme() - const styles = useStyles() + const insets = useSafeAreaInsets() + const navigation = useAppNavigation() const { words, focused, @@ -35,16 +46,28 @@ export const ImportAccountScreen = () => { handleImportAccount, mnemonicLength, t, + isKeyboardVisible, } = useImportAccountScreen() + const styles = useStyles({ insets, isKeyboardVisible }) const wordsPerColumn = Math.ceil(mnemonicLength / 2) return ( - <> + + + } + /> + {t('onboarding.import_account.title')} @@ -130,15 +153,18 @@ export const ImportAccountScreen = () => { ) })} + + + - + + { color={theme.colors.linkPrimary} /> - + ) } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts index 4898a40f7..04faa84a6 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts @@ -11,89 +11,108 @@ */ import { makeStyles } from '@rneui/themed' +import { EdgeInsets } from 'react-native-safe-area-context' -export const useStyles = makeStyles(theme => { - return { - mainContainer: { - flex: 1, - }, - helperText: { - color: theme.colors.textGray, - paddingBottom: theme.spacing['3xl'], - }, - walletNameContainer: { - backgroundColor: theme.colors.layerGrayLighter, - borderRadius: theme.spacing.sm, - paddingVertical: theme.spacing.md, - flexDirection: 'row', - flexWrap: 'nowrap', - alignItems: 'center', - justifyContent: 'center', - gap: theme.spacing.sm, - }, - nameText: { - color: theme.colors.textGray, - alignSelf: 'center', - }, - finishButton: { - marginHorizontal: theme.spacing.xl, - }, - spacer: { - flexGrow: 1, - }, - wordContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: theme.spacing.lg, - }, - column: { - width: '47%', - }, - scrollView: { - paddingHorizontal: theme.spacing.xl, - paddingBottom: theme.spacing.lg, - }, - inputContainerRow: { - marginTop: theme.spacing.sm, - flexDirection: 'row', - gap: theme.spacing.sm, - alignItems: 'center', - }, - label: { - color: theme.colors.textGray, - }, - focusedLabel: { - color: theme.colors.textMain, - }, - inputOuterContainer: { - flexShrink: 1, - }, - inputContainer: { - backgroundColor: theme.colors.background, - borderBottomWidth: 1, - borderBottomColor: theme.colors.layerGray, - flexShrink: 1, - }, - focusedInputContainer: { - backgroundColor: theme.colors.background, - borderBottomWidth: 1, - borderBottomColor: theme.colors.textMain, - flexShrink: 1, - }, - input: { - flexShrink: 1, - backgroundColor: 'transparent', - }, - overlayBackdrop: { - backgroundColor: 'rgba(52, 52, 52, 0.8)', - }, - overlay: { - padding: theme.spacing.xl, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.layerGray, - borderRadius: theme.spacing.lg, - gap: theme.spacing.lg, - }, - } -}) +type StyleProps = { + insets: EdgeInsets + isKeyboardVisible: boolean +} + +export const useStyles = makeStyles( + (theme, { insets, isKeyboardVisible }: StyleProps) => { + return { + mainContainer: { + flex: 1, + backgroundColor: theme.colors.background, + }, + helperText: { + color: theme.colors.textGray, + paddingBottom: theme.spacing['3xl'], + }, + walletNameContainer: { + backgroundColor: theme.colors.layerGrayLighter, + borderRadius: theme.spacing.sm, + paddingVertical: theme.spacing.md, + flexDirection: 'row', + flexWrap: 'nowrap', + alignItems: 'center', + justifyContent: 'center', + gap: theme.spacing.sm, + }, + nameText: { + color: theme.colors.textGray, + alignSelf: 'center', + }, + finishButton: { + marginHorizontal: 0, // Footers padding handles this + }, + spacer: { + flexGrow: 1, + }, + wordContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: theme.spacing.lg, + }, + column: { + width: '47%', + }, + scrollView: { + paddingHorizontal: theme.spacing.xl, + }, + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + paddingHorizontal: theme.spacing.xl, + paddingBottom: isKeyboardVisible + ? theme.spacing.lg + : Math.max(insets.bottom, theme.spacing.lg), + backgroundColor: 'transparent', + }, + inputContainerRow: { + marginTop: theme.spacing.sm, + flexDirection: 'row', + gap: theme.spacing.sm, + alignItems: 'center', + }, + label: { + color: theme.colors.textGray, + }, + focusedLabel: { + color: theme.colors.textMain, + }, + inputOuterContainer: { + flexShrink: 1, + }, + inputContainer: { + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.layerGray, + flexShrink: 1, + }, + focusedInputContainer: { + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.textMain, + flexShrink: 1, + }, + input: { + flexShrink: 1, + backgroundColor: 'transparent', + }, + overlayBackdrop: { + backgroundColor: 'rgba(52, 52, 52, 0.8)', + }, + overlay: { + padding: theme.spacing.xl, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.colors.layerGray, + borderRadius: theme.spacing.lg, + gap: theme.spacing.lg, + }, + } + }, +) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index a4e991b18..7fb7eade0 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -10,7 +10,8 @@ limitations under the License */ -import { useState, useCallback, useMemo } from 'react' +import { useState, useCallback, useMemo, useEffect } from 'react' +import { Keyboard, Platform } from 'react-native' import { RouteProp, useRoute } from '@react-navigation/native' import { OnboardingStackParamList } from '../../routes/types' import { @@ -36,6 +37,7 @@ export type UseImportAccountScreenResult = { handleImportAccount: () => void mnemonicLength: number t: (key: string) => string + isKeyboardVisible: boolean } export function useImportAccountScreen(): UseImportAccountScreenResult { @@ -47,6 +49,27 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { const { showToast } = useToast() const { t } = useLanguage() + const [isKeyboardVisible, setIsKeyboardVisible] = useState(false) + + useEffect(() => { + const showEvent = + Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow' + const hideEvent = + Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide' + + const showSubscription = Keyboard.addListener(showEvent, () => + setIsKeyboardVisible(true), + ) + const hideSubscription = Keyboard.addListener(hideEvent, () => + setIsKeyboardVisible(false), + ) + + return () => { + showSubscription.remove() + hideSubscription.remove() + } + }, []) + const mnemonicLength = MNEMONIC_LENGTH_MAP[accountType] const [words, setWords] = useState( @@ -111,5 +134,6 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { handleImportAccount, mnemonicLength, t, + isKeyboardVisible, } } From ae007b5c2a5072984edea880c6017d12abc69560 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:08:35 +0000 Subject: [PATCH 08/17] fix(onboarding): fix keyboard positioning --- .../ImportAccountScreen/ImportAccountScreen.tsx | 11 ++++++----- .../screens/ImportAccountScreen/styles.ts | 17 +++++++++-------- .../useImportAccountScreen.ts | 17 +++++++++++------ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx index 4bfcac0e0..29c9cd671 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx @@ -26,7 +26,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' import { ActivityIndicator, KeyboardAvoidingView, - Platform, ScrollView, } from 'react-native' import { useImportAccountScreen } from './useImportAccountScreen' @@ -47,8 +46,9 @@ export const ImportAccountScreen = () => { mnemonicLength, t, isKeyboardVisible, + keyboardHeight, } = useImportAccountScreen() - const styles = useStyles({ insets, isKeyboardVisible }) + const styles = useStyles({ insets, isKeyboardVisible, keyboardHeight }) const wordsPerColumn = Math.ceil(mnemonicLength / 2) @@ -56,8 +56,6 @@ export const ImportAccountScreen = () => { { } /> - + {t('onboarding.import_account.title')} diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts index 04faa84a6..4c8d117af 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/styles.ts @@ -16,10 +16,11 @@ import { EdgeInsets } from 'react-native-safe-area-context' type StyleProps = { insets: EdgeInsets isKeyboardVisible: boolean + keyboardHeight: number } export const useStyles = makeStyles( - (theme, { insets, isKeyboardVisible }: StyleProps) => { + (theme, { insets, isKeyboardVisible, keyboardHeight }: StyleProps) => { return { mainContainer: { flex: 1, @@ -57,19 +58,19 @@ export const useStyles = makeStyles( column: { width: '47%', }, + scrollContainer: { + flex: 1, + }, scrollView: { paddingHorizontal: theme.spacing.xl, }, footer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, + backgroundColor: theme.colors.background, paddingHorizontal: theme.spacing.xl, + paddingTop: theme.spacing.md, paddingBottom: isKeyboardVisible - ? theme.spacing.lg - : Math.max(insets.bottom, theme.spacing.lg), - backgroundColor: 'transparent', + ? keyboardHeight + theme.spacing.lg - insets.bottom + : theme.spacing.lg, }, inputContainerRow: { marginTop: theme.spacing.sm, diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 7fb7eade0..82794144d 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -38,6 +38,7 @@ export type UseImportAccountScreenResult = { mnemonicLength: number t: (key: string) => string isKeyboardVisible: boolean + keyboardHeight: number } export function useImportAccountScreen(): UseImportAccountScreenResult { @@ -50,6 +51,7 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { const { t } = useLanguage() const [isKeyboardVisible, setIsKeyboardVisible] = useState(false) + const [keyboardHeight, setKeyboardHeight] = useState(0) useEffect(() => { const showEvent = @@ -57,12 +59,14 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide' - const showSubscription = Keyboard.addListener(showEvent, () => - setIsKeyboardVisible(true), - ) - const hideSubscription = Keyboard.addListener(hideEvent, () => - setIsKeyboardVisible(false), - ) + const showSubscription = Keyboard.addListener(showEvent, e => { + setIsKeyboardVisible(true) + setKeyboardHeight(e.endCoordinates.height) + }) + const hideSubscription = Keyboard.addListener(hideEvent, () => { + setIsKeyboardVisible(false) + setKeyboardHeight(0) + }) return () => { showSubscription.remove() @@ -135,5 +139,6 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { mnemonicLength, t, isKeyboardVisible, + keyboardHeight, } } From 99f3195ba099661c90febb680bbd45f8e07e3951 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:09:15 +0000 Subject: [PATCH 09/17] chore(onboarding): lint fixes --- .../screens/ImportAccountScreen/ImportAccountScreen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx index 29c9cd671..225a58bce 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/ImportAccountScreen.tsx @@ -54,9 +54,7 @@ export const ImportAccountScreen = () => { return ( - + Date: Thu, 22 Jan 2026 17:15:22 +0000 Subject: [PATCH 10/17] refactor(mobile): universal wallet -> HDWallet --- apps/mobile/src/i18n/locales/en.json | 2 +- .../useImportAccountScreen.ts | 2 +- .../__tests__/ImportInfoScreen.spec.tsx | 4 ++-- .../ImportOptionsBottomSheet.tsx | 12 +++++----- .../ImportOptionsBottomSheet.spec.tsx | 10 ++++---- .../OnboardingScreen/OnboardingScreen.tsx | 4 ++-- .../__tests__/OnboardingScreen.spec.tsx | 2 +- .../OnboardingScreen/useOnboardingScreen.ts | 6 ++--- .../hooks/__tests__/useImportAccount.test.ts | 24 +++++++++---------- .../accounts/src/hooks/useCreateAccount.ts | 4 ++-- .../accounts/src/hooks/useImportAccount.ts | 14 +++++------ packages/accounts/src/models/accounts.ts | 2 +- packages/accounts/src/utils.ts | 6 ++--- 13 files changed, 46 insertions(+), 46 deletions(-) diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index e9b2306c1..cd5b3142a 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -276,7 +276,7 @@ "onboarding": { "import_options": { "title": "Select your Mnemonic type", - "universal_wallet": { + "hd_wallet": { "title": "Universal Wallet", "description": "Wallet that lets you derive new accounts, all using the same mnemonic", "chip": "new" diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 82794144d..068f230a3 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -23,7 +23,7 @@ import { useLanguage } from '@hooks/useLanguage' import { useAppNavigation } from '@hooks/useAppNavigation' const MNEMONIC_LENGTH_MAP: Record = { - universal: 24, + hdWallet: 24, algo25: 25, } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx index 4ec987db1..4f24cc8a4 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportInfoScreen/__tests__/ImportInfoScreen.spec.tsx @@ -87,7 +87,7 @@ vi.mock('@react-navigation/native', async () => { return { ...actual, useRoute: () => ({ - params: { accountType: 'universal' }, + params: { accountType: 'hdWallet' }, }), } }) @@ -121,7 +121,7 @@ describe('ImportInfoScreen', () => { fireEvent.click(recoverButton) expect(mockPush).toHaveBeenCalledWith('ImportAccount', { - accountType: 'universal', + accountType: 'hdWallet', }) }) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/ImportOptionsBottomSheet.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/ImportOptionsBottomSheet.tsx index 2e6629e7f..452e77557 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/ImportOptionsBottomSheet.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/ImportOptionsBottomSheet.tsx @@ -25,14 +25,14 @@ import { useTranslation } from 'react-i18next' export type ImportOptionsBottomSheetProps = { isVisible: boolean onClose: () => void - onUniversalWalletPress: () => void + onHDWalletPress: () => void onAlgo25Press: () => void } export const ImportOptionsBottomSheet = ({ isVisible, onClose, - onUniversalWalletPress, + onHDWalletPress, onAlgo25Press, }: ImportOptionsBottomSheetProps) => { const styles = useStyles() @@ -62,7 +62,7 @@ export const ImportOptionsBottomSheet = ({ @@ -70,12 +70,12 @@ export const ImportOptionsBottomSheet = ({ {t( - 'onboarding.import_options.universal_wallet.title', + 'onboarding.import_options.hd_wallet.title', )} @@ -85,7 +85,7 @@ export const ImportOptionsBottomSheet = ({ style={styles.optionBody} > {t( - 'onboarding.import_options.universal_wallet.description', + 'onboarding.import_options.hd_wallet.description', )} diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx index ef7337d79..d5ef711bf 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx @@ -25,7 +25,7 @@ describe('ImportOptionsBottomSheet', () => { const defaultProps = { isVisible: true, onClose: vi.fn(), - onUniversalWalletPress: vi.fn(), + onHDWalletPress: vi.fn(), onAlgo25Press: vi.fn(), } @@ -78,12 +78,12 @@ describe('ImportOptionsBottomSheet', () => { expect(onClose).toHaveBeenCalledTimes(1) }) - it('calls onUniversalWalletPress when Universal Wallet option is pressed', () => { - const onUniversalWalletPress = vi.fn() + it('calls onHDWalletPress when Universal Wallet option is pressed', () => { + const onHDWalletPress = vi.fn() render( , ) @@ -92,7 +92,7 @@ describe('ImportOptionsBottomSheet', () => { 'onboarding.import_options.universal_wallet.title', ), ) - expect(onUniversalWalletPress).toHaveBeenCalledTimes(1) + expect(onHDWalletPress).toHaveBeenCalledTimes(1) }) it('calls onAlgo25Press when ALGO25 option is pressed', () => { diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/OnboardingScreen.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/OnboardingScreen.tsx index 452f91b96..cc67bb00b 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/OnboardingScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/OnboardingScreen.tsx @@ -32,7 +32,7 @@ export const OnboardingScreen = () => { handleCreateAccount, handleImportAccount, handleCloseImportOptions, - handleUniversalWalletPress, + handleHDWalletPress, handleAlgo25Press, } = useOnboardingScreen() @@ -105,7 +105,7 @@ export const OnboardingScreen = () => { diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx index 9aef9066a..76641bbe0 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx @@ -169,7 +169,7 @@ describe('OnboardingScreen', () => { fireEvent.click(universalWalletOption) expect(mockPush).toHaveBeenCalledWith('ImportInfo', { - accountType: 'universal', + accountType: 'hdWallet', }) }) }) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts index d666742c9..ffbfb2fc5 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts @@ -43,9 +43,9 @@ export const useOnboardingScreen = () => { navigation.push('NameAccount') }, [navigation]) - const handleUniversalWalletPress = useCallback(() => { + const handleHDWalletPress = useCallback(() => { closeImportOptions() - navigation.push('ImportInfo', { accountType: 'universal' }) + navigation.push('ImportInfo', { accountType: 'hdWallet' }) }, [closeImportOptions, navigation]) const handleAlgo25Press = useCallback(() => { @@ -60,7 +60,7 @@ export const useOnboardingScreen = () => { handleCreateAccount, handleImportAccount: openImportOptions, handleCloseImportOptions: closeImportOptions, - handleUniversalWalletPress, + handleHDWalletPress, handleAlgo25Press, } } diff --git a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts index 611b367e1..d5b02c3dd 100644 --- a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts +++ b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts @@ -206,7 +206,7 @@ describe('useImportAccount', () => { await act(async () => { imported = await result.current({ mnemonic: 'test mnemonic', - type: 'universal', + type: 'hdWallet', }) }) @@ -228,9 +228,9 @@ describe('useImportAccount', () => { test('throws error when generateMasterKey fails with invalid mnemonic', async () => { const dummySecure = { - setItem: vi.fn(async () => {}), + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -250,7 +250,7 @@ describe('useImportAccount', () => { await expect( result.current({ mnemonic: 'invalid mnemonic', - type: 'universal', + type: 'hdWallet', }), ).rejects.toThrow('Invalid mnemonic') }) @@ -258,9 +258,9 @@ describe('useImportAccount', () => { test('throws error when secure storage setItem fails for root key', async () => { const dummySecure = { - setItem: vi.fn(async () => {}), + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -278,7 +278,7 @@ describe('useImportAccount', () => { await expect( result.current({ mnemonic: 'test mnemonic', - type: 'universal', + type: 'hdWallet', }), ).rejects.toThrow('Storage full') }) @@ -286,9 +286,9 @@ describe('useImportAccount', () => { test('throws error when mnemonicToEntropy fails', async () => { const dummySecure = { - setItem: vi.fn(async () => {}), + setItem: vi.fn(async () => { }), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => {}), + removeItem: vi.fn(async () => { }), authenticate: vi.fn(async () => true), } @@ -308,7 +308,7 @@ describe('useImportAccount', () => { await expect( result.current({ mnemonic: 'test mnemonic', - type: 'universal', + type: 'hdWallet', }), ).rejects.toThrow('Invalid entropy') }) @@ -348,7 +348,7 @@ describe('useImportAccount', () => { imported = await result.current({ walletId: 'CUSTOM_WALLET', mnemonic: 'test mnemonic', - type: 'universal', + type: 'hdWallet', }) }) @@ -397,7 +397,7 @@ describe('useImportAccount', () => { await expect( result.current({ mnemonic: 'test mnemonic', - type: 'universal', + type: 'hdWallet', }), ).rejects.toThrow('Address generation failed') }) diff --git a/packages/accounts/src/hooks/useCreateAccount.ts b/packages/accounts/src/hooks/useCreateAccount.ts index 54c8004fe..052394d81 100644 --- a/packages/accounts/src/hooks/useCreateAccount.ts +++ b/packages/accounts/src/hooks/useCreateAccount.ts @@ -46,7 +46,7 @@ export const useCreateAccount = () => { walletId, account, keyIndex, - type = 'universal', + type = 'hdWallet', }: { walletId?: string account: number @@ -71,7 +71,7 @@ export const useCreateAccount = () => { domain: KEY_DOMAIN, createdAt: new Date(), type: - type === 'universal' + type === 'hdWallet' ? KeyType.HDWalletRootKey : KeyType.Algo25Key, } as KeyPair diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index 51b48560f..0814ca41e 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -17,7 +17,7 @@ import { useKMS, KeyType } from '@perawallet/wallet-core-kms' import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' import { useCreateAccount } from './useCreateAccount' import { ImportAccountType } from '../models' -import { createUniversalWalletFromMnemonic } from '../utils' +import { createHDWalletFromMnemonic } from '../utils' export const useImportAccount = () => { const { saveKey } = useKMS() @@ -34,19 +34,19 @@ export const useImportAccount = () => { }) => { const rootWalletId = walletId ?? uuidv7() - if (type === 'universal') { - const universalKeyPair = - await createUniversalWalletFromMnemonic(mnemonic) + if (type === 'hdWallet') { + const hdWalletKeyPair = + await createHDWalletFromMnemonic(mnemonic) const stringifiedObj = JSON.stringify({ - seed: universalKeyPair.seed.toString('base64'), - entropy: universalKeyPair.entropy, + seed: hdWalletKeyPair.seed.toString('base64'), + entropy: hdWalletKeyPair.entropy, }) const rootKeyPair = { id: rootWalletId, publicKey: '', privateDataStorageKey: rootWalletId, createdAt: new Date(), - type: universalKeyPair.type, + type: hdWalletKeyPair.type, } await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) } diff --git a/packages/accounts/src/models/accounts.ts b/packages/accounts/src/models/accounts.ts index e8691056f..f05ecfbf8 100644 --- a/packages/accounts/src/models/accounts.ts +++ b/packages/accounts/src/models/accounts.ts @@ -28,7 +28,7 @@ export const AccountTypes = { export type AccountType = (typeof AccountTypes)[keyof typeof AccountTypes] -export type ImportAccountType = 'universal' | 'algo25' +export type ImportAccountType = 'hdWallet' | 'algo25' export type HDWalletDetails = { walletId: string diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts index d1b779447..973a3a65a 100644 --- a/packages/accounts/src/utils.ts +++ b/packages/accounts/src/utils.ts @@ -23,7 +23,7 @@ import { import { KeyType } from '@perawallet/wallet-core-kms' import * as bip39 from 'bip39' -export type UniversalKeyPair = { +export type HDWalletKeyPair = { seed: Buffer entropy: string type: typeof KeyType.HDWalletRootKey @@ -88,9 +88,9 @@ export const getSeedFromMasterKey = (keyData: Uint8Array) => { } } -export const createUniversalWalletFromMnemonic = async ( +export const createHDWalletFromMnemonic = async ( mnemonic: string, -): Promise => { +): Promise => { const seed = await bip39.mnemonicToSeed(mnemonic) const entropy = await bip39.mnemonicToEntropy(mnemonic) From 61e624d868e40fefd9426512aa759dba4e9420bd Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:24:34 +0000 Subject: [PATCH 11/17] refactor(accounts): createHDWalletFromMnemonic -> createHDWalletKeyDataFromMnemonic --- packages/accounts/src/hooks/useImportAccount.ts | 4 ++-- packages/accounts/src/utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index 0814ca41e..703ddbc5a 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -17,7 +17,7 @@ import { useKMS, KeyType } from '@perawallet/wallet-core-kms' import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' import { useCreateAccount } from './useCreateAccount' import { ImportAccountType } from '../models' -import { createHDWalletFromMnemonic } from '../utils' +import { createHDWalletKeyDataFromMnemonic } from '../utils' export const useImportAccount = () => { const { saveKey } = useKMS() @@ -36,7 +36,7 @@ export const useImportAccount = () => { if (type === 'hdWallet') { const hdWalletKeyPair = - await createHDWalletFromMnemonic(mnemonic) + await createHDWalletKeyDataFromMnemonic(mnemonic) const stringifiedObj = JSON.stringify({ seed: hdWalletKeyPair.seed.toString('base64'), entropy: hdWalletKeyPair.entropy, diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts index 973a3a65a..66e71fdf5 100644 --- a/packages/accounts/src/utils.ts +++ b/packages/accounts/src/utils.ts @@ -88,7 +88,7 @@ export const getSeedFromMasterKey = (keyData: Uint8Array) => { } } -export const createHDWalletFromMnemonic = async ( +export const createHDWalletKeyDataFromMnemonic = async ( mnemonic: string, ): Promise => { const seed = await bip39.mnemonicToSeed(mnemonic) From 281b02a46b1b039d96734908c0439f72fdbd5837 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:32:57 +0000 Subject: [PATCH 12/17] refactor(accounts): generalizing mnemonic function outputs --- .../accounts/src/hooks/useImportAccount.ts | 24 ++++++++++------- packages/accounts/src/utils.ts | 26 ++++++++++++++++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index 703ddbc5a..f1788d681 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -11,13 +11,13 @@ */ import { v7 as uuidv7 } from 'uuid' -import { seedFromMnemonic } from '@algorandfoundation/algokit-utils/algo25' -import nacl from 'tweetnacl' -import { useKMS, KeyType } from '@perawallet/wallet-core-kms' -import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' +import { useKMS } from '@perawallet/wallet-core-kms' import { useCreateAccount } from './useCreateAccount' import { ImportAccountType } from '../models' -import { createHDWalletKeyDataFromMnemonic } from '../utils' +import { + createHDWalletKeyDataFromMnemonic, + createAlgo25WalletKeyDataFromMnemonic, +} from '../utils' export const useImportAccount = () => { const { saveKey } = useKMS() @@ -52,16 +52,20 @@ export const useImportAccount = () => { } if (type === 'algo25') { - const seed = seedFromMnemonic(mnemonic) - const keyPair = nacl.sign.keyPair.fromSeed(seed) + const algo25KeyPair = + await createAlgo25WalletKeyDataFromMnemonic(mnemonic) + const stringifiedObj = JSON.stringify({ + seed: algo25KeyPair.seed.toString('base64'), + entropy: algo25KeyPair.entropy, + }) const rootKeyPair = { id: rootWalletId, - publicKey: encodeAlgorandAddress(keyPair.publicKey), + publicKey: algo25KeyPair.publicKey!, privateDataStorageKey: rootWalletId, createdAt: new Date(), - type: KeyType.Algo25Key, + type: algo25KeyPair.type, } - await saveKey(rootKeyPair, seed) + await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) } //TODO: we currently just create the 0/0 account but we really should scan the blockchain diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts index 66e71fdf5..2e1e5cb0e 100644 --- a/packages/accounts/src/utils.ts +++ b/packages/accounts/src/utils.ts @@ -22,11 +22,15 @@ import { } from './models' import { KeyType } from '@perawallet/wallet-core-kms' import * as bip39 from 'bip39' +import { seedFromMnemonic } from '@algorandfoundation/algokit-utils/algo25' +import nacl from 'tweetnacl' +import { encodeAlgorandAddress } from '@perawallet/wallet-core-blockchain' -export type HDWalletKeyPair = { +export type MnemonicKeyData = { seed: Buffer - entropy: string - type: typeof KeyType.HDWalletRootKey + entropy?: string + publicKey?: string + type: KeyType } export const getAccountDisplayName = (account: WalletAccount | null) => { @@ -90,7 +94,7 @@ export const getSeedFromMasterKey = (keyData: Uint8Array) => { export const createHDWalletKeyDataFromMnemonic = async ( mnemonic: string, -): Promise => { +): Promise => { const seed = await bip39.mnemonicToSeed(mnemonic) const entropy = await bip39.mnemonicToEntropy(mnemonic) @@ -100,3 +104,17 @@ export const createHDWalletKeyDataFromMnemonic = async ( type: KeyType.HDWalletRootKey, } } + +export const createAlgo25WalletKeyDataFromMnemonic = async ( + mnemonic: string, +): Promise => { + const seed = seedFromMnemonic(mnemonic) + const keyPair = nacl.sign.keyPair.fromSeed(seed) + + return { + seed: Buffer.from(seed), + entropy: Buffer.from(seed).toString('hex'), + publicKey: encodeAlgorandAddress(keyPair.publicKey), + type: KeyType.Algo25Key, + } +} From 9942139b2f9d1dc7c8091dfa807addf45d98b033 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:35:52 +0000 Subject: [PATCH 13/17] refactor(accounts): deduplication --- .../accounts/src/hooks/useImportAccount.ts | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/accounts/src/hooks/useImportAccount.ts b/packages/accounts/src/hooks/useImportAccount.ts index f1788d681..649945c82 100644 --- a/packages/accounts/src/hooks/useImportAccount.ts +++ b/packages/accounts/src/hooks/useImportAccount.ts @@ -34,40 +34,24 @@ export const useImportAccount = () => { }) => { const rootWalletId = walletId ?? uuidv7() - if (type === 'hdWallet') { - const hdWalletKeyPair = - await createHDWalletKeyDataFromMnemonic(mnemonic) - const stringifiedObj = JSON.stringify({ - seed: hdWalletKeyPair.seed.toString('base64'), - entropy: hdWalletKeyPair.entropy, - }) - const rootKeyPair = { - id: rootWalletId, - publicKey: '', - privateDataStorageKey: rootWalletId, - createdAt: new Date(), - type: hdWalletKeyPair.type, - } - await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) - } + const keyData = await (type === 'hdWallet' + ? createHDWalletKeyDataFromMnemonic(mnemonic) + : createAlgo25WalletKeyDataFromMnemonic(mnemonic)) - if (type === 'algo25') { - const algo25KeyPair = - await createAlgo25WalletKeyDataFromMnemonic(mnemonic) - const stringifiedObj = JSON.stringify({ - seed: algo25KeyPair.seed.toString('base64'), - entropy: algo25KeyPair.entropy, - }) - const rootKeyPair = { - id: rootWalletId, - publicKey: algo25KeyPair.publicKey!, - privateDataStorageKey: rootWalletId, - createdAt: new Date(), - type: algo25KeyPair.type, - } - await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) + const stringifiedObj = JSON.stringify({ + seed: keyData.seed.toString('base64'), + entropy: keyData.entropy, + }) + const rootKeyPair = { + id: rootWalletId, + publicKey: keyData.publicKey ?? '', + privateDataStorageKey: rootWalletId, + createdAt: new Date(), + type: keyData.type, } + await saveKey(rootKeyPair, Buffer.from(stringifiedObj)) + //TODO: we currently just create the 0/0 account but we really should scan the blockchain //and look for accounts that might match (see old app logic - we want to scan iteratively //until we find 5 empty keyindexes and 5 empty accounts (I think) From bcd20a267883c6a993079e3ebb138e63467a1dc9 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:36:28 +0000 Subject: [PATCH 14/17] chore(accounts): lint fixes --- .../src/hooks/__tests__/useImportAccount.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts index d5b02c3dd..9afd653cb 100644 --- a/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts +++ b/packages/accounts/src/hooks/__tests__/useImportAccount.test.ts @@ -228,9 +228,9 @@ describe('useImportAccount', () => { test('throws error when generateMasterKey fails with invalid mnemonic', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } @@ -258,9 +258,9 @@ describe('useImportAccount', () => { test('throws error when secure storage setItem fails for root key', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } @@ -286,9 +286,9 @@ describe('useImportAccount', () => { test('throws error when mnemonicToEntropy fails', async () => { const dummySecure = { - setItem: vi.fn(async () => { }), + setItem: vi.fn(async () => {}), getItem: vi.fn(async () => null), - removeItem: vi.fn(async () => { }), + removeItem: vi.fn(async () => {}), authenticate: vi.fn(async () => true), } From 694bb0a735b288b325ca9b79b0cbbc488ac73fc8 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:46:18 +0000 Subject: [PATCH 15/17] fix(onboarding): fix spacing issue --- .../src/modules/onboarding/routes/index.tsx | 3 +- .../NameAccountScreen/NameAccountScreen.tsx | 93 +++++++++++-------- .../screens/NameAccountScreen/styles.ts | 4 + 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 6fc0166cb..7d1cc066d 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -84,8 +84,7 @@ export const OnboardingStackNavigator = () => { { const styles = useStyles() const { theme } = useTheme() const { t } = useLanguage() + const navigation = useAppNavigation() const { walletDisplay, @@ -42,47 +46,58 @@ export const NameAccountScreen = () => { style={styles.mainContainer} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} > - - - {t('onboarding.name_account.title')} - - - {t('onboarding.name_account.description')} - - - - + + + } + /> + + + + + {t('onboarding.name_account.title')} + + + {t('onboarding.name_account.description')} + + + + + + {t('onboarding.name_account.wallet_label', { + count: numWallets + 1, + })} + + + + + - - {t('onboarding.name_account.wallet_label', { - count: numWallets + 1, - })} - - - - + { return { mainContainer: { + flex: 1, + backgroundColor: theme.colors.background, + }, + content: { flex: 1, paddingHorizontal: theme.spacing.xl, }, From ac14bcfd76ece734ec0ad84550ec5d06a289bf44 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:50:55 +0000 Subject: [PATCH 16/17] test(mobile): fixing broken tests --- .../ImportOptionsBottomSheet.spec.tsx | 6 +-- .../__tests__/OnboardingScreen.spec.tsx | 2 +- packages/accounts/src/__tests__/utils.test.ts | 47 +++++++++++++++++-- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx index d5ef711bf..91cc5c173 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx @@ -35,7 +35,7 @@ describe('ImportOptionsBottomSheet', () => { expect(screen.getByText('onboarding.import_options.title')).toBeTruthy() expect( screen.getByText( - 'onboarding.import_options.universal_wallet.title', + 'onboarding.import_options.hd_wallet.title', ), ).toBeTruthy() expect( @@ -43,7 +43,7 @@ describe('ImportOptionsBottomSheet', () => { ).toBeTruthy() expect( screen.getByText( - 'onboarding.import_options.universal_wallet.description', + 'onboarding.import_options.hd_wallet.description', ), ).toBeTruthy() expect( @@ -89,7 +89,7 @@ describe('ImportOptionsBottomSheet', () => { fireEvent.click( screen.getByText( - 'onboarding.import_options.universal_wallet.title', + 'onboarding.import_options.hd_wallet.title', ), ) expect(onHDWalletPress).toHaveBeenCalledTimes(1) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx index 76641bbe0..999295807 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/__tests__/OnboardingScreen.spec.tsx @@ -164,7 +164,7 @@ describe('OnboardingScreen', () => { // Click one of the options const universalWalletOption = screen.getByText( - 'onboarding.import_options.universal_wallet.title', + 'onboarding.import_options.hd_wallet.title', ) fireEvent.click(universalWalletOption) diff --git a/packages/accounts/src/__tests__/utils.test.ts b/packages/accounts/src/__tests__/utils.test.ts index 2c80a57ac..3834f9166 100644 --- a/packages/accounts/src/__tests__/utils.test.ts +++ b/packages/accounts/src/__tests__/utils.test.ts @@ -20,9 +20,36 @@ import { isMultisigAccount, isRekeyedAccount, isWatchAccount, - createUniversalWalletFromMnemonic, + createHDWalletKeyDataFromMnemonic, + createAlgo25WalletKeyDataFromMnemonic, getSeedFromMasterKey, } from '../utils' +import { vi } from 'vitest' + +vi.mock('@algorandfoundation/algokit-utils/algo25', () => ({ + seedFromMnemonic: vi.fn(() => new Uint8Array(32).fill(1)), +})) + +vi.mock('bip39', () => ({ + mnemonicToSeed: vi.fn(async () => Buffer.from(new Uint8Array(64).fill(2))), + mnemonicToEntropy: vi.fn(async () => 'test-entropy'), +})) + +vi.mock('tweetnacl', () => ({ + default: { + sign: { + keyPair: { + fromSeed: vi.fn(() => ({ + publicKey: new Uint8Array(32).fill(3), + })), + }, + }, + }, +})) + +vi.mock('@perawallet/wallet-core-blockchain', () => ({ + encodeAlgorandAddress: vi.fn(() => 'TEST_ADDRESS'), +})) describe('services/accounts/utils - getAccountDisplayName', () => { test('returns account name when present', () => { @@ -186,11 +213,11 @@ describe('services/accounts/utils - account type checks', () => { }) }) -describe('services/accounts/utils - createUniversalWalletFromMnemonic', () => { +describe('services/accounts/utils - createHDWalletKeyDataFromMnemonic', () => { test('creates key pair from valid mnemonic', async () => { const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' - const result = await createUniversalWalletFromMnemonic(mnemonic) + const result = await createHDWalletKeyDataFromMnemonic(mnemonic) expect(result.seed).toBeInstanceOf(Buffer) expect(result.seed.length).toBe(64) // BIP39 seed is 512 bits @@ -199,6 +226,20 @@ describe('services/accounts/utils - createUniversalWalletFromMnemonic', () => { }) }) +describe('services/accounts/utils - createAlgo25WalletKeyDataFromMnemonic', () => { + test('creates key pair from valid mnemonic', async () => { + const mnemonic = + 'since theory average article fly finger table squirrel music degree arrest shallow unit medal update elevator snap code tip body switch mirror page able total' + const result = await createAlgo25WalletKeyDataFromMnemonic(mnemonic) + + expect(result.seed).toBeInstanceOf(Buffer) + expect(result.seed.length).toBe(32) // Algo25 seed is 256 bits + expect(result.entropy).toBeDefined() + expect(result.publicKey).toBeDefined() + expect(result.type).toBe('algo25-key') + }) +}) + describe('services/accounts/utils - getSeedFromMasterKey', () => { test('obtains seed from JSON stringified master key data', () => { const masterKey = { From b7238504108d58720d9cc5057972e93732bf46e0 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:51:22 +0000 Subject: [PATCH 17/17] chore(onboarding): formatting --- .../__tests__/ImportOptionsBottomSheet.spec.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx index 91cc5c173..c4f86fdcb 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/ImportOptionsBottomSheet/__tests__/ImportOptionsBottomSheet.spec.tsx @@ -34,17 +34,13 @@ describe('ImportOptionsBottomSheet', () => { expect(screen.getByText('onboarding.import_options.title')).toBeTruthy() expect( - screen.getByText( - 'onboarding.import_options.hd_wallet.title', - ), + screen.getByText('onboarding.import_options.hd_wallet.title'), ).toBeTruthy() expect( screen.getByText('onboarding.import_options.algo25.title'), ).toBeTruthy() expect( - screen.getByText( - 'onboarding.import_options.hd_wallet.description', - ), + screen.getByText('onboarding.import_options.hd_wallet.description'), ).toBeTruthy() expect( screen.getByText('onboarding.import_options.algo25.description'), @@ -88,9 +84,7 @@ describe('ImportOptionsBottomSheet', () => { ) fireEvent.click( - screen.getByText( - 'onboarding.import_options.hd_wallet.title', - ), + screen.getByText('onboarding.import_options.hd_wallet.title'), ) expect(onHDWalletPress).toHaveBeenCalledTimes(1) })