diff --git a/src/app/pages/CreateAccount.tsx b/src/app/pages/CreateAccount.tsx index 4d8bb466..06835d17 100644 --- a/src/app/pages/CreateAccount.tsx +++ b/src/app/pages/CreateAccount.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'clsx'; +import { AnimatePresence, motion } from 'framer-motion'; import { SubmitHandler, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -9,13 +10,16 @@ import FormSubmitButton from 'app/atoms/FormSubmitButton'; import { ACCOUNT_NAME_PATTERN } from 'app/defaults'; import { ReactComponent as ArrowRightIcon } from 'app/icons/arrow-right.svg'; import PageLayout from 'app/layouts/PageLayout'; +import { Button } from 'components/Button'; import { useMidenContext, useAllAccounts } from 'lib/miden/front'; import { navigate } from 'lib/woozie'; -import { WalletType } from 'screens/onboarding/types'; +import SelectAuthScheme from 'screens/onboarding/create-wallet-flow/SelectAuthScheme'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; type FormData = { name: string; walletType: WalletType; + authScheme: AuthScheme; }; const WalletTypeOptions = [ @@ -31,11 +35,19 @@ const WalletTypeOptions = [ } ]; +enum Step { + SelectAccountType = 1, + SelectAuthScheme = 2 +} + const SUBMIT_ERROR_TYPE = 'submit-error'; const CreateAccount: FC = () => { const { t } = useTranslation(); + const [step, setStep] = useState(Step.SelectAccountType); + const [navigationDirection, setNavigationDirection] = useState<'forward' | 'backward'>('forward'); const [selectedWalletType, setSelectedWalletType] = useState(WalletType.OnChain); + const [selectedAuthScheme, setSelectedAuthScheme] = useState(AuthScheme.Falcon); const { createAccount, updateCurrentAccount } = useMidenContext(); const allAccounts = useAllAccounts(); @@ -79,14 +91,19 @@ const CreateAccount: FC = () => { setSelectedWalletType(type); }; + const handleContinueToAuthScheme = () => { + setNavigationDirection('forward'); + setStep(Step.SelectAuthScheme); + }; + const onSubmit = useCallback>( async ({ name, walletType }) => { if (isSubmitting) return; clearErrors('name'); - + console.log(selectedAuthScheme); try { - await createAccount(selectedWalletType, name); + await createAccount(selectedWalletType, selectedAuthScheme, name); } catch (err: any) { console.error(err); @@ -95,12 +112,12 @@ const CreateAccount: FC = () => { setError('name', { type: SUBMIT_ERROR_TYPE, message: err.message }); } }, - [isSubmitting, clearErrors, setError, createAccount, selectedWalletType] + [isSubmitting, clearErrors, setError, createAccount, selectedWalletType, selectedAuthScheme] ); return ( {t('createAccount')}}> -
+
{ {t('accountNameInputDescription')}
- {/* Wallet Type Selection */} -
-
- {t('chooseYourAccountType')} -
- {WalletTypeOptions.map((option, idx) => ( -
handleWalletTypeSelect(option.id)} - > -
-

{option.title}

- -
-

{option.description}

-
- ))} -
- - - {t('createAccount')} - + + + {step === Step.SelectAccountType ? ( + <> + {/* Wallet Type Selection */} +
+
+ {t('chooseYourAccountType')} +
+ {WalletTypeOptions.map(option => ( +
handleWalletTypeSelect(option.id)} + > +
+

{option.title}

+ +
+

{option.description}

+
+ ))} +
+ +
diff --git a/src/app/pages/Onboarding.selectors.ts b/src/app/pages/Onboarding.selectors.ts index 64679499..03199222 100644 --- a/src/app/pages/Onboarding.selectors.ts +++ b/src/app/pages/Onboarding.selectors.ts @@ -4,5 +4,6 @@ export enum OnboardingSelectors { ImportWalletButton = 'Onboarding/ImportWalletButton', NewSeedPhraseButton = 'Onboarding/NewSeedPhraseButton', VerifySeedPhraseButton = 'Onboarding/VerifySeedPhraseButton', + SelectAuthSchemePage = 'Onboarding/SelectAuthSchemePage', ConfirmExistingSeedPhraseButton = 'Onboarding/ConfirmExistingSeedPhraseButton' } diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index 20ab29dd..a974270b 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -14,7 +14,7 @@ import { useWalletStore } from 'lib/store'; import { fetchStateFromBackend } from 'lib/store/hooks/useIntercomSync'; import { navigate, useLocation } from 'lib/woozie'; import { OnboardingFlow } from 'screens/onboarding/navigator'; -import { ImportType, OnboardingAction, OnboardingStep, OnboardingType } from 'screens/onboarding/types'; +import { AuthScheme, ImportType, OnboardingAction, OnboardingStep, OnboardingType } from 'screens/onboarding/types'; /** * Wait for the wallet state to become Ready after registration. @@ -39,6 +39,7 @@ const Welcome: FC = () => { const { hash } = useLocation(); const [step, setStep] = useState(OnboardingStep.Welcome); const [seedPhrase, setSeedPhrase] = useState(null); + const [authScheme, setAuthScheme] = useState(AuthScheme.Falcon); const [onboardingType, setOnboardingType] = useState(null); const [importType, setImportType] = useState(null); const [password, setPassword] = useState(null); @@ -58,6 +59,7 @@ const Welcome: FC = () => { try { await registerWallet( password, + authScheme, seedPhraseFormatted, onboardingType === OnboardingType.Import // might be able to leverage ownMnemonic to determine whther to attempt imports in general ); @@ -75,6 +77,7 @@ const Welcome: FC = () => { } }, [ password, + authScheme, seedPhrase, importedWithFile, registerWallet, @@ -125,6 +128,10 @@ const Welcome: FC = () => { case 'verify-seed-phrase': navigate('/#verify-seed-phrase'); break; + case 'select-auth-scheme': + setAuthScheme(action.payload); + navigate('/#select-auth-scheme'); + break; case 'create-password': navigate('/#create-password'); break; @@ -196,7 +203,7 @@ const Welcome: FC = () => { navigate('/#backup-seed-phrase'); } else if (step === OnboardingStep.CreatePassword) { if (onboardingType === OnboardingType.Create) { - navigate('/#verify-seed-phrase'); + navigate('/#select-auth-scheme'); } else { if (importType === ImportType.WalletFile) { navigate('/#import-from-file'); @@ -206,6 +213,8 @@ const Welcome: FC = () => { } } else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) { navigate('/#select-import-type'); + } else if (step === OnboardingStep.SelectAuthScheme) { + navigate('/#verify-seed-phrase'); } break; default: @@ -244,6 +253,9 @@ const Welcome: FC = () => { case '#create-password': setStep(OnboardingStep.CreatePassword); break; + case '#select-auth-scheme': + setStep(OnboardingStep.SelectAuthScheme); + break; case '#confirmation': if (!password) { navigate('/'); @@ -280,6 +292,8 @@ const Welcome: FC = () => { password={password} isLoading={isLoading} onAction={onAction} + authScheme={authScheme} + setAuthScheme={setAuthScheme} /> ); }; diff --git a/src/components/ProgressIndicator.tsx b/src/components/ProgressIndicator.tsx index 94dc3c35..27694a0e 100644 --- a/src/components/ProgressIndicator.tsx +++ b/src/components/ProgressIndicator.tsx @@ -8,7 +8,7 @@ export interface ProgressIndicatorProps extends React.HTMLAttributes = ({ className, steps, currentStep, ...props }) => { return ( -
+
{Array.from({ length: steps }).map((_, index) => (
{ it('handles NewWalletRequest', async () => { const response = await adapter.request({ type: WalletMessageType.NewWalletRequest, + authScheme: AuthScheme.Falcon, password: 'test123', mnemonic: 'word1 word2 word3', ownMnemonic: false - } as any); + }); - expect(Actions.registerNewWallet).toHaveBeenCalledWith('test123', 'word1 word2 word3', false); + expect(Actions.registerNewWallet).toHaveBeenCalledWith('test123', AuthScheme.Falcon, 'word1 word2 word3', false); expect(response).toEqual({ type: WalletMessageType.NewWalletResponse }); }); @@ -123,11 +125,12 @@ describe('MobileIntercomAdapter', () => { it('handles CreateAccountRequest', async () => { const response = await adapter.request({ type: WalletMessageType.CreateAccountRequest, - walletType: 'public', + authScheme: AuthScheme.Falcon, + walletType: WalletType.OnChain, name: 'Test Account' - } as any); + }); - expect(Actions.createHDAccount).toHaveBeenCalledWith('public', 'Test Account'); + expect(Actions.createHDAccount).toHaveBeenCalledWith(WalletType.OnChain, AuthScheme.Falcon, 'Test Account'); expect(response).toEqual({ type: WalletMessageType.CreateAccountResponse }); }); diff --git a/src/lib/intercom/mobile-adapter.ts b/src/lib/intercom/mobile-adapter.ts index a3eccdb3..03cd9c22 100644 --- a/src/lib/intercom/mobile-adapter.ts +++ b/src/lib/intercom/mobile-adapter.ts @@ -67,7 +67,7 @@ export class MobileIntercomAdapter { }; case WalletMessageType.NewWalletRequest: - await Actions.registerNewWallet(req.password, req.mnemonic, req.ownMnemonic); + await Actions.registerNewWallet(req.password, req.authScheme, req.mnemonic, req.ownMnemonic); return { type: WalletMessageType.NewWalletResponse }; case WalletMessageType.ImportFromClientRequest: @@ -83,7 +83,7 @@ export class MobileIntercomAdapter { return { type: WalletMessageType.LockResponse }; case WalletMessageType.CreateAccountRequest: - await Actions.createHDAccount((req as any).walletType, (req as any).name); + await Actions.createHDAccount(req.walletType, req.authScheme, req.name); return { type: WalletMessageType.CreateAccountResponse }; case WalletMessageType.UpdateCurrentAccountRequest: diff --git a/src/lib/miden/back/actions.test.ts b/src/lib/miden/back/actions.test.ts index d6d3d319..26c4dc76 100644 --- a/src/lib/miden/back/actions.test.ts +++ b/src/lib/miden/back/actions.test.ts @@ -1,6 +1,6 @@ import { MidenDAppMessageType } from 'lib/adapter/types'; import { WalletStatus } from 'lib/shared/types'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { getFrontState, @@ -308,9 +308,9 @@ describe('actions', () => { const accounts = [{ publicKey: 'pk1', name: 'Account 1' }]; mockVault.createHDAccount.mockResolvedValueOnce(accounts); - await createHDAccount(WalletType.OnChain); + await createHDAccount(WalletType.OnChain, AuthScheme.Falcon); - expect(mockVault.createHDAccount).toHaveBeenCalledWith(WalletType.OnChain, undefined); + expect(mockVault.createHDAccount).toHaveBeenCalledWith(WalletType.OnChain, AuthScheme.Falcon, undefined); expect(mockAccountsUpdated).toHaveBeenCalledWith({ accounts }); }); @@ -318,16 +318,16 @@ describe('actions', () => { const accounts = [{ publicKey: 'pk1', name: 'MyWallet' }]; mockVault.createHDAccount.mockResolvedValueOnce(accounts); - await createHDAccount(WalletType.OnChain, ' MyWallet '); + await createHDAccount(WalletType.OnChain, AuthScheme.Falcon, ' MyWallet '); - expect(mockVault.createHDAccount).toHaveBeenCalledWith(WalletType.OnChain, 'MyWallet'); + expect(mockVault.createHDAccount).toHaveBeenCalledWith(WalletType.OnChain, AuthScheme.Falcon, 'MyWallet'); expect(mockAccountsUpdated).toHaveBeenCalledWith({ accounts }); }); it('throws for name longer than 16 characters', async () => { const longName = 'a'.repeat(17); - await expect(createHDAccount(WalletType.OnChain, longName)).rejects.toThrow('Invalid name'); + await expect(createHDAccount(WalletType.OnChain, AuthScheme.Falcon, longName)).rejects.toThrow('Invalid name'); }); }); diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index 0d39497b..be763847 100644 --- a/src/lib/miden/back/actions.ts +++ b/src/lib/miden/back/actions.ts @@ -16,7 +16,8 @@ import { import { Vault } from 'lib/miden/back/vault'; import { getStorageProvider } from 'lib/platform/storage-adapter'; import { WalletAccount, WalletSettings, WalletState } from 'lib/shared/types'; -import { WalletType } from 'screens/onboarding/types'; +import type { WalletType } from 'screens/onboarding/types'; +import { AuthScheme } from 'screens/onboarding/types'; import { MidenSharedStorageKey } from '../types'; import { @@ -70,9 +71,9 @@ export async function isDAppEnabled() { return bools.every(Boolean); } -export function registerNewWallet(password: string, mnemonic?: string, ownMnemonic?: boolean) { +export function registerNewWallet(password: string, authScheme: AuthScheme, mnemonic?: string, ownMnemonic?: boolean) { return withInited(async () => { - await Vault.spawn(password, mnemonic, ownMnemonic); + await Vault.spawn(password, authScheme, mnemonic, ownMnemonic); await unlock(password); }); } @@ -122,7 +123,7 @@ export function getCurrentAccount() { }); } -export function createHDAccount(walletType: WalletType, name?: string) { +export function createHDAccount(walletType: WalletType, authScheme: AuthScheme, name?: string) { return withUnlocked(async ({ vault }) => { if (name) { name = name.trim(); @@ -131,7 +132,7 @@ export function createHDAccount(walletType: WalletType, name?: string) { } } - const accounts = await vault.createHDAccount(walletType, name); + const accounts = await vault.createHDAccount(walletType, authScheme, name); accountsUpdated({ accounts }); }); } diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index a9d81455..30e6c1b0 100644 --- a/src/lib/miden/back/main.ts +++ b/src/lib/miden/back/main.ts @@ -36,7 +36,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise { const passKey = await Passworder.generateKey(password); @@ -104,10 +104,10 @@ export class Vault { } catch (e) { // TODO: Need some way to propagate this up. Should we fail the entire process or just log it? console.error('Failed to import wallet from seed in spawn, creating new wallet instead', e); - return await midenClient.createMidenWallet(WalletType.OnChain, walletSeed); + return await midenClient.createMidenWallet(WalletType.OnChain, authScheme, walletSeed); } } else { - return await midenClient.createMidenWallet(WalletType.OnChain, walletSeed); + return await midenClient.createMidenWallet(WalletType.OnChain, authScheme, walletSeed); } }); @@ -116,7 +116,8 @@ export class Vault { name: 'Miden Account 1', isPublic: true, type: WalletType.OnChain, - hdIndex: hdAccIndex + hdIndex: hdAccIndex, + authScheme }; const newAccounts = [initialAccount]; @@ -174,7 +175,10 @@ export class Vault { await midenClient.webClient.addAccountSecretKeyToWebStore(sk); } else { const walletSeed = deriveClientSeed(walletAccount.type, mnemonic, walletAccount.hdIndex); - const secretKey = SecretKey.rpoFalconWithRNG(walletSeed); + const secretKey = + walletAccount.authScheme === AuthScheme.Falcon + ? SecretKey.rpoFalconWithRNG(walletSeed) + : SecretKey.ecdsaWithRNG(walletSeed); await midenClient.webClient.addAccountSecretKeyToWebStore(secretKey); } } @@ -201,7 +205,7 @@ export class Vault { return DEFAULT_SETTINGS; } - async createHDAccount(walletType: WalletType, name?: string): Promise { + async createHDAccount(walletType: WalletType, authScheme: AuthScheme, name?: string): Promise { return withError('Failed to create account', async () => { const [mnemonic, allAccounts] = await Promise.all([ fetchAndDecryptOneWithLegacyFallBack(mnemonicStrgKey, this.passKey), @@ -232,10 +236,10 @@ export class Vault { return await midenClient.importPublicMidenWalletFromSeed(walletSeed); } catch (e) { console.warn('Failed to import wallet from seed, creating new wallet instead', e); - return await midenClient.createMidenWallet(walletType, walletSeed); + return await midenClient.createMidenWallet(walletType, authScheme, walletSeed); } } else { - return await midenClient.createMidenWallet(walletType, walletSeed); + return await midenClient.createMidenWallet(walletType, authScheme, walletSeed); } }); @@ -246,7 +250,8 @@ export class Vault { name: accName, publicKey: walletId, isPublic: walletType === WalletType.OnChain, - hdIndex: hdAccIndex + hdIndex: hdAccIndex, + authScheme }; const newAllAcounts = concatAccount(allAccounts, newAccount); @@ -378,7 +383,11 @@ export class Vault { async importAccount(privateKey: string, name?: string): Promise { return withError('Failed to import account', async () => { const allAccounts = await fetchAndDecryptOneWithLegacyFallBack(accountsStrgKey, this.passKey); - const secretKey = SecretKey.deserialize(new Uint8Array(Buffer.from(privateKey, 'hex'))); + const buff = Buffer.from(privateKey, 'hex'); + if (buff[0] !== 0 && buff[0] !== 1) { + throw new PublicError('Invalid private key format'); + } + const secretKey = SecretKey.deserialize(new Uint8Array(buff)); const pubKeyWord = secretKey.publicKey().toCommitment(); const pubKeyHex = pubKeyWord.toHex().slice(2); // remove '0x' prefix @@ -391,6 +400,7 @@ export class Vault { name: name || `Imported Account ${allAccounts.length + 1}`, isPublic: true, type: WalletType.OnChain, + authScheme: buff[0] === 0 ? AuthScheme.Falcon : AuthScheme.ECDSA, hdIndex: -1 // -1 indicates imported account }; diff --git a/src/lib/miden/front/client.ts b/src/lib/miden/front/client.ts index 4e8d4143..c000260f 100644 --- a/src/lib/miden/front/client.ts +++ b/src/lib/miden/front/client.ts @@ -6,7 +6,7 @@ import constate from 'constate'; import { createIntercomClient, IIntercomClient } from 'lib/intercom/client'; import { WalletAccount, WalletRequest, WalletResponse, WalletSettings, WalletStatus } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { store } from '../back/store'; import { MidenState } from '../types'; @@ -96,8 +96,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { // Wrap store actions in useCallback for stable references const registerWallet = useCallback( - async (password: string, mnemonic?: string, ownMnemonic?: boolean) => { - await storeRegisterWallet(password, mnemonic, ownMnemonic); + async (password: string, authScheme: AuthScheme, mnemonic?: string, ownMnemonic?: boolean) => { + await storeRegisterWallet(password, authScheme, mnemonic, ownMnemonic); }, [storeRegisterWallet] ); @@ -122,8 +122,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { ); const createAccount = useCallback( - async (walletType: WalletType, name?: string) => { - await storeCreateAccount(walletType, name); + async (walletType: WalletType, authScheme: AuthScheme, name?: string) => { + await storeCreateAccount(walletType, authScheme, name); }, [storeCreateAccount] ); diff --git a/src/lib/miden/sdk/miden-client-interface.test.ts b/src/lib/miden/sdk/miden-client-interface.test.ts index 5446b1da..f96f3ed4 100644 --- a/src/lib/miden/sdk/miden-client-interface.test.ts +++ b/src/lib/miden/sdk/miden-client-interface.test.ts @@ -1,3 +1,5 @@ +import { AuthScheme, WalletType } from 'screens/onboarding/types'; + describe('MidenClientInterface', () => { afterEach(() => { jest.resetModules(); @@ -91,7 +93,7 @@ describe('MidenClientInterface', () => { }, TransactionFilter: { all: jest.fn(() => 'all') }, MIDEN_NETWORK_NAME: { TESTNET: 'testnet' }, - SecretKey: { rpoFalconWithRNG: jest.fn() }, + SecretKey: { rpoFalconWithRNG: jest.fn(), ecdsaWithRNG: jest.fn() }, AccountBuilder: class { constructor(public seed: Uint8Array) {} storageMode() { @@ -136,9 +138,6 @@ describe('MidenClientInterface', () => { ConsumeTransaction: class {}, SendTransaction: class {} })); - jest.doMock('screens/onboarding/types', () => ({ - WalletType: { OnChain: 'on-chain', OffChain: 'off-chain' } - })); const { MidenClientInterface } = await import('./miden-client-interface'); const insertKeyCallback = jest.fn(); @@ -159,7 +158,7 @@ describe('MidenClientInterface', () => { client.free(); expect(client.webClient.terminate).toBeDefined(); // smoke a few methods to raise coverage - await client.createMidenWallet('on-chain' as any, new Uint8Array([4])); + await client.createMidenWallet(WalletType.OnChain, AuthScheme.Falcon, new Uint8Array([4])); await client.importPublicMidenWalletFromSeed(new Uint8Array([5])); await client.importNoteBytes(new Uint8Array([1, 2])); await client.consumeNoteId({ accountId: 'id', noteId: 'note', faucetId: 'f', type: 'public' } as any); diff --git a/src/lib/miden/sdk/miden-client-interface.ts b/src/lib/miden/sdk/miden-client-interface.ts index e2dc6364..c766149d 100644 --- a/src/lib/miden/sdk/miden-client-interface.ts +++ b/src/lib/miden/sdk/miden-client-interface.ts @@ -29,7 +29,7 @@ import { MIDEN_TRANSPORT_LAYER_NAME } from 'lib/miden-chain/constants'; import { isMobile } from 'lib/platform'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { ConsumeTransaction, SendTransaction } from '../db/types'; import { toNoteType } from '../helpers'; @@ -113,12 +113,12 @@ export class MidenClientInterface { this.webClient.terminate(); } - async createMidenWallet(walletType: WalletType, seed?: Uint8Array): Promise { + async createMidenWallet(walletType: WalletType, authScheme: AuthScheme, seed?: Uint8Array): Promise { // Create a new wallet const accountStorageMode = walletType === WalletType.OnChain ? AccountStorageMode.public() : AccountStorageMode.private(); - - const secretKey = SecretKey.rpoFalconWithRNG(seed); + const secretKey = + authScheme === AuthScheme.Falcon ? SecretKey.rpoFalconWithRNG(seed) : SecretKey.ecdsaWithRNG(seed); // create a new account with 0 seed so we can recreate it later from the secret key const accountBuilder = new AccountBuilder(new Uint8Array(32).fill(0)) .accountType(AccountType.RegularAccountImmutableCode) diff --git a/src/lib/shared/types.ts b/src/lib/shared/types.ts index b0088495..126ee371 100644 --- a/src/lib/shared/types.ts +++ b/src/lib/shared/types.ts @@ -1,5 +1,5 @@ import { MidenMessageType, MidenRequest, MidenResponse } from 'lib/miden/types'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { SendPageEventRequest, @@ -153,6 +153,7 @@ export interface WalletAccount { isPublic: boolean; type: WalletType; hdIndex: number; + authScheme: AuthScheme; } export interface WalletNetwork { @@ -169,6 +170,7 @@ export interface LoadingResponse extends WalletMessageBase { export interface NewWalletRequest extends WalletMessageBase { type: WalletMessageType.NewWalletRequest; password: string; + authScheme: AuthScheme; mnemonic?: string; ownMnemonic?: boolean; } @@ -197,6 +199,7 @@ export interface LockResponse extends WalletMessageBase { export interface CreateAccountRequest extends WalletMessageBase { type: WalletMessageType.CreateAccountRequest; walletType: WalletType; + authScheme: AuthScheme; name?: string; } diff --git a/src/lib/store/index.test.ts b/src/lib/store/index.test.ts index aade7a27..e80cfaae 100644 --- a/src/lib/store/index.test.ts +++ b/src/lib/store/index.test.ts @@ -2,7 +2,7 @@ import '../../../test/jest-mocks'; import { MidenMessageType } from 'lib/miden/types'; import { WalletMessageType, WalletStatus } from 'lib/shared/types'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { useWalletStore, selectIsReady, selectIsLocked, selectIsIdle, getIntercom } from './index'; @@ -57,8 +57,24 @@ describe('useWalletStore', () => { syncFromBackend({ status: WalletStatus.Ready, - accounts: [{ publicKey: 'pk1', name: 'Account 1', isPublic: true, type: WalletType.OnChain, hdIndex: 0 }], - currentAccount: { publicKey: 'pk1', name: 'Account 1', isPublic: true, type: WalletType.OnChain, hdIndex: 0 }, + accounts: [ + { + publicKey: 'pk1', + name: 'Account 1', + isPublic: true, + type: WalletType.OnChain, + hdIndex: 0, + authScheme: AuthScheme.Falcon + } + ], + currentAccount: { + publicKey: 'pk1', + name: 'Account 1', + isPublic: true, + type: WalletType.OnChain, + hdIndex: 0, + authScheme: AuthScheme.Falcon + }, networks: [], settings: { contacts: [] }, ownMnemonic: true @@ -75,8 +91,22 @@ describe('useWalletStore', () => { describe('editAccountName', () => { const mockAccounts = [ - { publicKey: 'pk1', name: 'Account 1', isPublic: true, type: WalletType.OnChain, hdIndex: 0 }, - { publicKey: 'pk2', name: 'Account 2', isPublic: false, type: WalletType.OnChain, hdIndex: 1 } + { + publicKey: 'pk1', + name: 'Account 1', + isPublic: true, + type: WalletType.OnChain, + hdIndex: 0, + authScheme: AuthScheme.Falcon + }, + { + publicKey: 'pk2', + name: 'Account 2', + isPublic: false, + type: WalletType.OnChain, + hdIndex: 1, + authScheme: AuthScheme.Falcon + } ]; beforeEach(() => { @@ -140,8 +170,22 @@ describe('useWalletStore', () => { describe('updateCurrentAccount', () => { const mockAccounts = [ - { publicKey: 'pk1', name: 'Account 1', isPublic: true, type: WalletType.OnChain, hdIndex: 0 }, - { publicKey: 'pk2', name: 'Account 2', isPublic: false, type: WalletType.OnChain, hdIndex: 1 } + { + publicKey: 'pk1', + name: 'Account 1', + isPublic: true, + type: WalletType.OnChain, + hdIndex: 0, + authScheme: AuthScheme.Falcon + }, + { + publicKey: 'pk2', + name: 'Account 2', + isPublic: false, + type: WalletType.OnChain, + hdIndex: 1, + authScheme: AuthScheme.Falcon + } ]; beforeEach(() => { @@ -287,10 +331,11 @@ describe('useWalletStore', () => { mockRequest.mockResolvedValueOnce({ type: WalletMessageType.NewWalletResponse }); const { registerWallet } = useWalletStore.getState(); - await registerWallet('password123', 'mnemonic words', true); + await registerWallet('password123', AuthScheme.Falcon, 'mnemonic words', true); expect(mockRequest).toHaveBeenCalledWith({ type: WalletMessageType.NewWalletRequest, + authScheme: AuthScheme.Falcon, password: 'password123', mnemonic: 'mnemonic words', ownMnemonic: true @@ -301,13 +346,14 @@ describe('useWalletStore', () => { mockRequest.mockResolvedValueOnce({ type: WalletMessageType.ImportFromClientResponse }); const { importWalletFromClient } = useWalletStore.getState(); - await importWalletFromClient('password123', 'mnemonic words', []); + await importWalletFromClient('password123', 'mnemonic words', [], {}); expect(mockRequest).toHaveBeenCalledWith({ type: WalletMessageType.ImportFromClientRequest, password: 'password123', mnemonic: 'mnemonic words', - walletAccounts: [] + walletAccounts: [], + skForImportedAccounts: {} }); }); @@ -336,11 +382,12 @@ describe('useWalletStore', () => { mockRequest.mockResolvedValueOnce({ type: WalletMessageType.CreateAccountResponse }); const { createAccount } = useWalletStore.getState(); - await createAccount(WalletType.OnChain, 'My Account'); + await createAccount(WalletType.OnChain, AuthScheme.Falcon, 'My Account'); expect(mockRequest).toHaveBeenCalledWith({ type: WalletMessageType.CreateAccountRequest, walletType: WalletType.OnChain, + authScheme: AuthScheme.Falcon, name: 'My Account' }); }); diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index e941922f..7ef0ea1a 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -113,12 +113,13 @@ export const useWalletStore = create()( }, // Auth actions - registerWallet: async (password, mnemonic, ownMnemonic) => { + registerWallet: async (password, authScheme, mnemonic, ownMnemonic) => { const res = await request({ type: WalletMessageType.NewWalletRequest, password, mnemonic, - ownMnemonic + ownMnemonic, + authScheme }); assertResponse(res.type === WalletMessageType.NewWalletResponse); // State will be synced via StateUpdated notification @@ -144,10 +145,11 @@ export const useWalletStore = create()( }, // Account actions - createAccount: async (walletType, name) => { + createAccount: async (walletType, authScheme, name) => { const res = await request({ type: WalletMessageType.CreateAccountRequest, walletType, + authScheme, name }); assertResponse(res.type === WalletMessageType.CreateAccountResponse); diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index 92048cdd..01a50957 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -4,7 +4,7 @@ import { ExchangeRateRecord, FiatCurrencyOption } from 'lib/fiat-curency'; import { AssetMetadata } from 'lib/miden/metadata'; import { MidenDAppSessions, MidenNetwork, MidenState } from 'lib/miden/types'; import { WalletAccount, WalletSettings, WalletStatus } from 'lib/shared/types'; -import { WalletType } from 'screens/onboarding/types'; +import { AuthScheme, WalletType } from 'screens/onboarding/types'; import { TokenBalanceData } from '../miden/front/balance'; @@ -94,7 +94,7 @@ export interface WalletActions { syncFromBackend: (state: MidenState) => void; // Auth actions - registerWallet: (password: string, mnemonic?: string, ownMnemonic?: boolean) => Promise; + registerWallet: (password: string, authScheme: AuthScheme, mnemonic?: string, ownMnemonic?: boolean) => Promise; importWalletFromClient: ( password: string, mnemonic: string, @@ -104,7 +104,7 @@ export interface WalletActions { unlock: (password: string) => Promise; // Account actions - createAccount: (walletType: WalletType, name?: string) => Promise; + createAccount: (walletType: WalletType, authScheme: AuthScheme, name?: string) => Promise; updateCurrentAccount: (accountPublicKey: string) => Promise; editAccountName: (accountPublicKey: string, name: string) => Promise; revealMnemonic: (password: string) => Promise; diff --git a/src/screens/encrypted-file-flow/ExportFileComplete.tsx b/src/screens/encrypted-file-flow/ExportFileComplete.tsx index 5353c789..58e6f61e 100644 --- a/src/screens/encrypted-file-flow/ExportFileComplete.tsx +++ b/src/screens/encrypted-file-flow/ExportFileComplete.tsx @@ -14,6 +14,7 @@ import { deriveKey, encrypt, encryptJson, generateKey, generateSalt } from 'lib/ import { exportDb } from 'lib/miden/repo'; import { getMidenClient, withWasmClientLock } from 'lib/miden/sdk/miden-client'; import { isMobile } from 'lib/platform'; +import { AuthScheme } from 'screens/onboarding/types'; import { EncryptedWalletFile, ENCRYPTED_WALLET_FILE_PASSWORD_CHECK, DecryptedWalletFile } from 'screens/shared'; export interface ExportFileCompleteProps { @@ -45,6 +46,11 @@ const ExportFileComplete: React.FC = ({ }); const walletDbDump = await exportDb(); + const accs = accounts.map(acc => ({ + ...acc, + authScheme: acc.authScheme ?? AuthScheme.Falcon + })); + const seedPhrase = await revealMnemonic(walletPassword); const secretKeysForImportedAccounts: Record = {}; await withWasmClientLock(async () => { diff --git a/src/screens/onboarding/create-wallet-flow/SelectAuthScheme.tsx b/src/screens/onboarding/create-wallet-flow/SelectAuthScheme.tsx new file mode 100644 index 00000000..4b8bfbe7 --- /dev/null +++ b/src/screens/onboarding/create-wallet-flow/SelectAuthScheme.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import classNames from 'clsx'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as ArrowRightIcon } from 'app/icons/arrow-right.svg'; +import { Button } from 'components/Button'; +import { AuthScheme } from 'screens/onboarding/types'; + +const AuthSchemeOptions = [ + { + id: AuthScheme.Falcon, + title: 'Falcon', + description: + 'Provides security against future quantum attacks, ensuring long-term protection of your assets. Longer keys and less widely supported with longer proving times.' + }, + { + id: AuthScheme.ECDSA, + title: 'ECDSA(secp256k1)', + description: + 'Does not provide security against future quantum attacks. Widely adopted and supported, ECDSA offers shorter keys and faster proving times.' + } +]; + +export interface SelectAuthSchemeScreenProps { + onSubmit?: () => void; + authScheme: AuthScheme; + setAuthScheme: (authScheme: AuthScheme) => void; + onCreateAccountScreen?: boolean; +} + +const SelectAuthScheme = ({ + onSubmit, + authScheme, + setAuthScheme, + onCreateAccountScreen = false +}: SelectAuthSchemeScreenProps) => { + const { t } = useTranslation(); + const handleWalletTypeSelect = (type: AuthScheme) => { + setAuthScheme(type); + }; + + return ( +
+ {/* Wallet Type Selection */} +
+
+ Choose your preferred authentication scheme, which have trade-offs between security and performance: +
+ {AuthSchemeOptions.map((option, idx) => ( +
handleWalletTypeSelect(option.id)} + > +
+

{option.title}

+ +
+

{option.description}

+
+ ))} +
+ {!onCreateAccountScreen && ( +
+
+ )} +
+ ); +}; + +export default SelectAuthScheme; diff --git a/src/screens/onboarding/navigator.tsx b/src/screens/onboarding/navigator.tsx index c99f4d03..a7d8c6fe 100644 --- a/src/screens/onboarding/navigator.tsx +++ b/src/screens/onboarding/navigator.tsx @@ -14,12 +14,13 @@ import { ConfirmationScreen } from './common/Confirmation'; import { CreatePasswordScreen } from './common/CreatePassword'; import { WelcomeScreen } from './common/Welcome'; import { BackUpSeedPhraseScreen } from './create-wallet-flow/BackUpSeedPhrase'; +import SelectAuthScheme from './create-wallet-flow/SelectAuthScheme'; import { SelectTransactionTypeScreen } from './create-wallet-flow/SelectTransactionType'; import { VerifySeedPhraseScreen } from './create-wallet-flow/VerifySeedPhrase'; import { ImportSeedPhraseScreen } from './import-wallet-flow/ImportSeedPhrase'; import { ImportWalletFileScreen } from './import-wallet-flow/ImportWalletFile'; import { SelectImportTypeScreen } from './import-wallet-flow/SelectImportType'; -import { ImportType, OnboardingAction, OnboardingStep, OnboardingType, WalletType } from './types'; +import { AuthScheme, ImportType, OnboardingAction, OnboardingStep, OnboardingType, WalletType } from './types'; export interface OnboardingFlowProps { wordslist: string[]; @@ -29,13 +30,15 @@ export interface OnboardingFlowProps { password?: string | null; isLoading?: boolean; onAction?: (action: OnboardingAction) => void; + authScheme: AuthScheme; + setAuthScheme: (authScheme: AuthScheme) => void; } const Header: React.FC<{ onBack: () => void; step: OnboardingStep; onboardingType?: 'import' | 'create' | null; -}> = ({ step, onBack }) => { +}> = ({ step, onBack, onboardingType }) => { // Hide header on full-screen steps if ( step === OnboardingStep.Confirmation || @@ -46,16 +49,18 @@ const Header: React.FC<{ } const shouldRenderBackButton = step !== OnboardingStep.Welcome; - let currentStep: number | null = step === OnboardingStep.Welcome ? null : 3; + let currentStep: number | null = step === OnboardingStep.Welcome ? null : 4; if (step === OnboardingStep.BackupSeedPhrase) { currentStep = 1; } else if (step === OnboardingStep.VerifySeedPhrase) { currentStep = 2; + } else if (step === OnboardingStep.SelectAuthScheme) { + currentStep = 3; } else if (step === OnboardingStep.SelectImportType) { currentStep = 1; } else if (step === OnboardingStep.CreatePassword) { - currentStep = 3; + currentStep = 4; } else if (step === OnboardingStep.ImportFromSeed || step === OnboardingStep.ImportFromFile) { currentStep = 2; } @@ -76,7 +81,11 @@ const Header: React.FC<{ }} /> - +
); }; @@ -88,10 +97,11 @@ export const OnboardingFlow: FC = ({ step, password, isLoading, + authScheme, + setAuthScheme, onAction }) => { const [navigationDirection, setNavigationDirection] = useState<'forward' | 'backward'>('forward'); - const onForwardAction = useCallback( (onboardingAction: OnboardingAction) => { setNavigationDirection('forward'); @@ -141,6 +151,12 @@ export const OnboardingFlow: FC = ({ }); const onVerifySeedPhraseSubmit = () => + onForwardAction?.({ + id: 'select-auth-scheme', + payload: authScheme + }); + + const onSelectAuthSchemeSubmit = () => onForwardAction?.({ id: 'create-password', payload: WalletType.OnChain @@ -180,6 +196,10 @@ export const OnboardingFlow: FC = ({ return ; case OnboardingStep.VerifySeedPhrase: return ; + case OnboardingStep.SelectAuthScheme: + return ( + + ); case OnboardingStep.SelectImportType: return ; case OnboardingStep.ImportFromSeed: @@ -200,7 +220,7 @@ export const OnboardingFlow: FC = ({ default: return <>; } - }, [step, isLoading, onForwardAction, seedPhrase, wordslist, password]); + }, [step, isLoading, onForwardAction, seedPhrase, wordslist, authScheme, setAuthScheme, password]); const onBack = () => { setNavigationDirection('backward'); diff --git a/src/screens/onboarding/types.ts b/src/screens/onboarding/types.ts index 85eefba7..acb33fd6 100644 --- a/src/screens/onboarding/types.ts +++ b/src/screens/onboarding/types.ts @@ -15,11 +15,17 @@ export enum ImportType { WalletFile = 'wallet-file' } +export enum AuthScheme { + Falcon = 0, + ECDSA = 1 +} + export enum OnboardingStep { Welcome = 'welcome', SelectWalletType = 'select-wallet-type', BackupSeedPhrase = 'backup-seed-phrase', VerifySeedPhrase = 'verify-seed-phrase', + SelectAuthScheme = 'select-auth-scheme', SelectImportType = 'select-import-type', ImportFromSeed = 'import-from-seed', ImportFromFile = 'import-from-file', @@ -107,6 +113,11 @@ export type BackAction = { id: 'back'; }; +export type SelectAuthSchemeAction = { + id: 'select-auth-scheme'; + payload: AuthScheme; +}; + export type OnboardingAction = | CreateWalletAction | BackupSeedPhraseAction @@ -121,7 +132,8 @@ export type OnboardingAction = | BackAction | ImportFromFileAction | ImportFromSeedAction - | ImportWalletFileSubmitAction; + | ImportWalletFileSubmitAction + | SelectAuthSchemeAction; // TODO: Potentially make this into what the onboarding flows use to render the // steps rather than hardcode the path in onboarding flow