diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index bd2bfeea..f3543f32 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ /* Begin PBXFileReference section */ 1A6B2865F76F213517CFCCD1 /* AppViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = ""; }; 21A34DAD709C71D553F88951 /* LocalBiometricPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalBiometricPlugin.swift; sourceTree = ""; }; - C7D4E92B3F8B1C5D00A2B9E2 /* BarcodeScannerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarcodeScannerPlugin.swift; sourceTree = ""; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -36,6 +35,7 @@ 873F0344C8952CB5585102E0 /* App.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; BFB20C26958B0AB36D108D0E /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + C7D4E92B3F8B1C5D00A2B9E2 /* BarcodeScannerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarcodeScannerPlugin.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,6 +56,7 @@ 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, 504EC3061FED79650016851F /* App */, 504EC3051FED79650016851F /* Products */, + 7BF04FDF2F1F799A005E306E /* Recovered References */, ); sourceTree = ""; }; @@ -86,6 +87,14 @@ path = App; sourceTree = ""; }; + 7BF04FDF2F1F799A005E306E /* Recovered References */ = { + isa = PBXGroup; + children = ( + BFB20C26958B0AB36D108D0E /* Pods-App.release.xcconfig */, + ); + name = "Recovered References"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -313,6 +322,7 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = YQ9XQQJ5ZM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -337,6 +347,7 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = YQ9XQQJ5ZM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/src/app/pages/ForgotPassword/ForgotPassword.tsx b/src/app/pages/ForgotPassword/ForgotPassword.tsx index 34003992..1a87ac28 100644 --- a/src/app/pages/ForgotPassword/ForgotPassword.tsx +++ b/src/app/pages/ForgotPassword/ForgotPassword.tsx @@ -7,6 +7,7 @@ import { formatMnemonic } from 'app/defaults'; import { useMidenContext } from 'lib/miden/front'; import { clearClientStorage } from 'lib/miden/reset'; import { useMobileBackHandler } from 'lib/mobile/useMobileBackHandler'; +import type { WalletAccount } from 'lib/shared/types'; import { navigate } from 'lib/woozie'; import { ForgotPasswordFlow } from 'screens/onboarding/forgot-password-navigator'; import { ForgotPasswordAction, ForgotPasswordStep, OnboardingType } from 'screens/onboarding/types'; @@ -18,6 +19,7 @@ const ForgotPassword: FC = () => { const [password, setPassword] = useState(null); const [importedWithFile, setImportedWithFile] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [importedWalletAccounts, setImportedWalletAccounts] = useState([]); const { registerWallet, importWalletFromClient } = useMidenContext(); @@ -39,13 +41,21 @@ const ForgotPassword: FC = () => { } else { try { console.log('importing wallet from client'); - await importWalletFromClient(password, seedPhraseFormatted); + await importWalletFromClient(password, seedPhraseFormatted, importedWalletAccounts); } catch (e) { console.error(e); } } } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [ + password, + seedPhrase, + importedWithFile, + registerWallet, + onboardingType, + importWalletFromClient, + importedWalletAccounts + ]); const onAction = useCallback( async (action: ForgotPasswordAction) => { @@ -64,8 +74,8 @@ const ForgotPassword: FC = () => { break; case 'import-wallet-file-submit': const seedPhrase = action.payload.split(' '); - console.log({ seedPhrase }); setSeedPhrase(seedPhrase); + setImportedWalletAccounts(action.walletAccounts); setImportedWithFile(true); setStep(ForgotPasswordStep.CreatePassword); break; diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index c30b7259..012feb42 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -8,7 +8,7 @@ import { AnalyticsEventCategory, useAnalytics } from 'lib/analytics'; import { useMidenContext } from 'lib/miden/front'; import { useMobileBackHandler } from 'lib/mobile/useMobileBackHandler'; import { isDesktop, isMobile } from 'lib/platform'; -import { WalletStatus } from 'lib/shared/types'; +import { WalletStatus, WalletAccount } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { fetchStateFromBackend } from 'lib/store/hooks/useIntercomSync'; import { navigate, useLocation } from 'lib/woozie'; @@ -77,6 +77,7 @@ const Welcome: FC = () => { const [isHardwareSecurityAvailable, setIsHardwareSecurityAvailable] = useState(false); const [biometricAttempts, setBiometricAttempts] = useState(0); const [biometricError, setBiometricError] = useState(null); + const [importedWalletAccounts, setImportedWalletAccounts] = useState([]); const { registerWallet, importWalletFromClient } = useMidenContext(); const { trackEvent } = useAnalytics(); const syncFromBackend = useWalletStore(s => s.syncFromBackend); @@ -96,12 +97,25 @@ const Welcome: FC = () => { if (!importedWithFile) { await registerWallet(actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import); } else { - await importWalletFromClient(actualPassword, seedPhraseFormatted); + try { + console.log('importing wallet from client'); + await importWalletFromClient(actualPassword, seedPhraseFormatted, importedWalletAccounts); + } catch (e) { + console.error(e); + } } } else { throw new Error('Missing password or seed phrase'); } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [ + password, + seedPhrase, + importedWithFile, + registerWallet, + onboardingType, + importWalletFromClient, + importedWalletAccounts + ]); const onAction = async (action: OnboardingAction) => { let eventCategory = AnalyticsEventCategory.ButtonPress; @@ -123,8 +137,8 @@ const Welcome: FC = () => { break; case 'import-wallet-file-submit': const seedPhrase = action.payload.split(' '); - console.log({ seedPhrase }); setSeedPhrase(seedPhrase); + setImportedWalletAccounts(action.walletAccounts); setImportedWithFile(true); // Check if hardware security is available - if so, skip password step { diff --git a/src/lib/intercom/desktop-adapter.ts b/src/lib/intercom/desktop-adapter.ts index 2bd4a74c..862d3284 100644 --- a/src/lib/intercom/desktop-adapter.ts +++ b/src/lib/intercom/desktop-adapter.ts @@ -86,7 +86,7 @@ export class DesktopIntercomAdapter { return { type: WalletMessageType.NewWalletResponse }; case WalletMessageType.ImportFromClientRequest: - await Actions.registerImportedWallet((req as any).password, (req as any).mnemonic); + await Actions.registerImportedWallet(req.password, req.mnemonic, req.walletAccounts); return { type: WalletMessageType.ImportFromClientResponse }; case WalletMessageType.UnlockRequest: diff --git a/src/lib/intercom/mobile-adapter.test.ts b/src/lib/intercom/mobile-adapter.test.ts index 62ca843b..2a98144e 100644 --- a/src/lib/intercom/mobile-adapter.test.ts +++ b/src/lib/intercom/mobile-adapter.test.ts @@ -94,10 +94,11 @@ describe('MobileIntercomAdapter', () => { const response = await adapter.request({ type: WalletMessageType.ImportFromClientRequest, password: 'test123', - mnemonic: 'word1 word2 word3' - } as any); + mnemonic: 'word1 word2 word3', + walletAccounts: [] + }); - expect(Actions.registerImportedWallet).toHaveBeenCalledWith('test123', 'word1 word2 word3'); + expect(Actions.registerImportedWallet).toHaveBeenCalledWith('test123', 'word1 word2 word3', []); expect(response).toEqual({ type: WalletMessageType.ImportFromClientResponse }); }); diff --git a/src/lib/intercom/mobile-adapter.ts b/src/lib/intercom/mobile-adapter.ts index f49cec92..027bfec0 100644 --- a/src/lib/intercom/mobile-adapter.ts +++ b/src/lib/intercom/mobile-adapter.ts @@ -71,7 +71,7 @@ export class MobileIntercomAdapter { return { type: WalletMessageType.NewWalletResponse }; case WalletMessageType.ImportFromClientRequest: - await Actions.registerImportedWallet((req as any).password, (req as any).mnemonic); + await Actions.registerImportedWallet(req.password, req.mnemonic, req.walletAccounts); return { type: WalletMessageType.ImportFromClientResponse }; case WalletMessageType.UnlockRequest: diff --git a/src/lib/miden/back/actions.test.ts b/src/lib/miden/back/actions.test.ts index 8a3eecbc..d97ce4c7 100644 --- a/src/lib/miden/back/actions.test.ts +++ b/src/lib/miden/back/actions.test.ts @@ -245,11 +245,12 @@ describe('actions', () => { }; Vault.spawnFromMidenClient.mockResolvedValueOnce(mockVaultInstance); - await registerImportedWallet('password123', 'mnemonic words'); + await registerImportedWallet('password123', 'mnemonic words', []); - expect(Vault.spawnFromMidenClient).toHaveBeenCalledWith('password123', 'mnemonic words'); expect(mockVaultInstance.fetchAccounts).toHaveBeenCalled(); expect(mockUnlocked).toHaveBeenCalled(); + expect(Vault.spawnFromMidenClient).toHaveBeenCalledWith('password123', 'mnemonic words', []); + expect(Vault.setup).toHaveBeenCalledWith('password123'); }); }); diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index 8249dca6..0ead828b 100644 --- a/src/lib/miden/back/actions.ts +++ b/src/lib/miden/back/actions.ts @@ -15,7 +15,7 @@ import { } from 'lib/miden/back/store'; import { Vault } from 'lib/miden/back/vault'; import { getStorageProvider } from 'lib/platform/storage-adapter'; -import { WalletSettings, WalletState } from 'lib/shared/types'; +import { WalletAccount, WalletSettings, WalletState } from 'lib/shared/types'; import { WalletType } from 'screens/onboarding/types'; import { MidenSharedStorageKey } from '../types'; @@ -90,11 +90,15 @@ export function registerNewWallet(password?: string, mnemonic?: string, ownMnemo }); } -export function registerImportedWallet(password?: string, mnemonic?: string) { +export function registerImportedWallet( + password: string | undefined, + mnemonic: string, + walletAccounts: WalletAccount[] +) { return withInited(async () => { // Password may be undefined for hardware-only wallets // spawnFromMidenClient() returns the vault directly, avoiding a second biometric prompt - const vault = await Vault.spawnFromMidenClient(password ?? '', mnemonic ?? ''); + const vault = await Vault.spawnFromMidenClient(password ?? '', mnemonic, walletAccounts); const accounts = await vault.fetchAccounts(); const settings = await vault.fetchSettings(); const currentAccount = await vault.getCurrentAccount(); @@ -154,7 +158,7 @@ export function decryptCiphertexts(accPublicKey: string, cipherTexts: string[]) export function revealViewKey(accPublicKey: string, password: string) {} -export function revealMnemonic(password: string) { +export function revealMnemonic(password: string | undefined) { return withUnlocked(() => Vault.revealMnemonic(password)); } diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index c361a209..40449cfc 100644 --- a/src/lib/miden/back/main.ts +++ b/src/lib/miden/back/main.ts @@ -39,7 +39,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise { + return async (key: Uint8Array, secretKey: Uint8Array) => { + const pubKeyHex = Buffer.from(key).toString('hex'); + const secretKeyHex = Buffer.from(secretKey).toString('hex'); + await encryptAndSaveMany( + [ + [accAuthPubKeyStrgKey(pubKeyHex), pubKeyHex], + [accAuthSecretKeyStrgKey(pubKeyHex), secretKeyHex] + ], + passKey + ); + }; +}; export class Vault { constructor(private vaultKey: CryptoKey) {} @@ -176,6 +190,19 @@ export class Vault { } } + private static async getVaultKey(password?: string): Promise { + // If password is not provided, try hardware unlock first + if (!password) { + const vault = await Vault.tryHardwareUnlock(); + if (vault) { + return vault.vaultKey; + } + throw new PublicError('Password required'); + } + // Password-based unlock + return Vault.unlockWithPassword(password); + } + /** * Legacy password unlock for wallets created before vault key model * This maintains backward compatibility with existing wallets @@ -225,23 +252,11 @@ export class Vault { await savePlain(VAULT_KEY_PASSWORD_STORAGE_KEY, passwordProtectedVaultKey); } - const insertKeyCallback = async (key: Uint8Array, secretKey: Uint8Array) => { - const pubKeyHex = Buffer.from(key).toString('hex'); - const secretKeyHex = Buffer.from(secretKey).toString('hex'); - await encryptAndSaveMany( - [ - [accAuthPubKeyStrgKey(pubKeyHex), pubKeyHex], - [accAuthSecretKeyStrgKey(pubKeyHex), secretKeyHex] - ], - vaultKey - ); - }; const options: MidenClientCreateOptions = { - insertKeyCallback + insertKeyCallback: insertKeyCallbackWrapper(vaultKey) }; const hdAccIndex = 0; const walletSeed = deriveClientSeed(WalletType.OnChain, mnemonic, 0); - // Wrap WASM client operations in a lock to prevent concurrent access const accPublicKey = await withWasmClientLock(async () => { const midenClient = await getMidenClient(options); @@ -283,35 +298,12 @@ export class Vault { }); } - static async spawnFromMidenClient(password: string, mnemonic: string): Promise { + static async spawnFromMidenClient( + password: string, + mnemonic: string, + walletAccounts: WalletAccount[] + ): Promise { return withError('Failed to spawn from miden client', async (): Promise => { - // Wrap WASM client operations in a lock to prevent concurrent access - const accounts = await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); - const accountHeaders = await midenClient.getAccounts(); - const accts = []; - - // Have to do this sequentially else the wasm fails - for (const accountHeader of accountHeaders) { - const account = await midenClient.getAccount(getBech32AddressFromAccountId(accountHeader.id())); - accts.push(account); - } - return accts; - }); - - const newAccounts = []; - for (let i = 0; i < accounts.length; i++) { - const acc = accounts[i]; - if (acc) { - newAccounts.push({ - publicKey: getBech32AddressFromAccountId(acc.id()), - name: 'Miden Account ' + (i + 1), - isPublic: acc.isPublic(), - type: WalletType.OnChain - }); - } - } - // Generate random vault key (256-bit) const vaultKeyBytes = Passworder.generateVaultKey(); const vaultKey = await Passworder.importVaultKey(vaultKeyBytes); @@ -333,20 +325,51 @@ export class Vault { throw new PublicError('Hardware security setup failed. Please try again.'); } } else { + if (!password) { + throw new PublicError('Password is required for password-based vault protection'); + } // Password-based protection (user opted out of biometrics or hardware not available) const passwordProtectedVaultKey = await Passworder.encryptVaultKeyWithPassword(vaultKeyBytes, password); await savePlain(VAULT_KEY_PASSWORD_STORAGE_KEY, passwordProtectedVaultKey); } + // insert keys + const options: MidenClientCreateOptions = { + insertKeyCallback: insertKeyCallbackWrapper(vaultKey) + }; + + // Wrap WASM client operations in a lock to prevent concurrent access + await withWasmClientLock(async () => { + const midenClient = await getMidenClient(options); + const accountHeaders = await midenClient.getAccounts(); + + // Have to do this sequentially else the wasm fails + for (const accountHeader of accountHeaders) { + const account = await midenClient.getAccount(getBech32AddressFromAccountId(accountHeader.id())); + if (!account || account.isFaucet() || account.isNetwork()) { + continue; + } + const walletAccount = walletAccounts.find(wa => + compareAccountIds(wa.publicKey, getBech32AddressFromAccountId(account.id())) + ); + if (!walletAccount) { + throw new PublicError('Account from Miden Client not found in provided wallet accounts'); + } + const walletSeed = deriveClientSeed(walletAccount.type, mnemonic, walletAccount.hdIndex); + const secretKey = SecretKey.rpoFalconWithRNG(walletSeed); + await midenClient.webClient.addAccountSecretKeyToWebStore(secretKey); + } + }); + await encryptAndSaveMany( [ [checkStrgKey, generateCheck()], [mnemonicStrgKey, mnemonic ?? ''], - [accountsStrgKey, newAccounts] + [accountsStrgKey, walletAccounts] ], vaultKey ); - await savePlain(currentAccPubKeyStrgKey, newAccounts[0].publicKey); + await savePlain(currentAccPubKeyStrgKey, walletAccounts[0].publicKey); await savePlain(ownMnemonicStrgKey, true); // Return the vault instance so caller doesn't need to call unlock() separately @@ -381,20 +404,8 @@ export class Vault { hdAccIndex = accounts.length; const walletSeed = deriveClientSeed(walletType, mnemonic, hdAccIndex); - - const insertKeyCallback = async (key: Uint8Array, secretKey: Uint8Array) => { - const pubKeyHex = Buffer.from(key).toString('hex'); - const secretKeyHex = Buffer.from(secretKey).toString('hex'); - await encryptAndSaveMany( - [ - [accAuthPubKeyStrgKey(pubKeyHex), pubKeyHex], - [accAuthSecretKeyStrgKey(pubKeyHex), secretKeyHex] - ], - this.vaultKey - ); - }; const options: MidenClientCreateOptions = { - insertKeyCallback + insertKeyCallback: insertKeyCallbackWrapper(this.vaultKey) }; // Wrap WASM client operations in a lock to prevent concurrent access @@ -498,15 +509,20 @@ export class Vault { } async signTransaction(publicKey: string, signingInputs: string): Promise { - const secretKey = await fetchAndDecryptOneWithLegacyFallBack( - accAuthSecretKeyStrgKey(publicKey), - this.vaultKey - ); - let secretKeyBytes = new Uint8Array(Buffer.from(secretKey, 'hex')); - const wasmSigningInputs = SigningInputs.deserialize(new Uint8Array(Buffer.from(signingInputs, 'hex'))); - const wasmSecretKey = SecretKey.deserialize(secretKeyBytes); - const signature = wasmSecretKey.signData(wasmSigningInputs); - return Buffer.from(signature.serialize()).toString('hex'); + try { + const secretKey = await fetchAndDecryptOneWithLegacyFallBack( + accAuthSecretKeyStrgKey(publicKey), + this.vaultKey + ); + let secretKeyBytes = new Uint8Array(Buffer.from(secretKey, 'hex')); + const wasmSigningInputs = SigningInputs.deserialize(new Uint8Array(Buffer.from(signingInputs, 'hex'))); + const wasmSecretKey = SecretKey.deserialize(secretKeyBytes); + const signature = wasmSecretKey.signData(wasmSigningInputs); + return Buffer.from(signature.serialize()).toString('hex'); + } catch (e) { + console.error('Error signing transaction in vault', e); + throw e; + } } async getAuthSecretKey(key: string) { @@ -522,8 +538,8 @@ export class Vault { async revealViewKey(accPublicKey: string) {} - static async revealMnemonic(password: string) { - const vaultKey = await Vault.unlockWithPassword(password); + static async revealMnemonic(password: string | undefined) { + const vaultKey = await Vault.getVaultKey(password); return withError('Failed to reveal seed phrase', async () => { const mnemonic = await fetchAndDecryptOneWithLegacyFallBack(mnemonicStrgKey, vaultKey); const mnemonicPattern = /^(\b\w+\b\s?){12}$/; diff --git a/src/lib/miden/front/client.ts b/src/lib/miden/front/client.ts index f1215302..535a2b68 100644 --- a/src/lib/miden/front/client.ts +++ b/src/lib/miden/front/client.ts @@ -4,7 +4,7 @@ import { AllowedPrivateData, PrivateDataPermission } from '@demox-labs/miden-wal import constate from 'constate'; import { createIntercomClient, IIntercomClient } from 'lib/intercom/client'; -import { WalletRequest, WalletResponse, WalletSettings, WalletStatus } from 'lib/shared/types'; +import { WalletAccount, WalletRequest, WalletResponse, WalletSettings, WalletStatus } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { WalletType } from 'screens/onboarding/types'; @@ -100,8 +100,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { ); const importWalletFromClient = useCallback( - async (password: string | undefined, mnemonic: string) => { - await storeImportWalletFromClient(password, mnemonic); + async (password: string | undefined, mnemonic: string, walletAccounts: WalletAccount[]) => { + await storeImportWalletFromClient(password, mnemonic, walletAccounts); }, [storeImportWalletFromClient] ); @@ -135,7 +135,7 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { ); const revealMnemonic = useCallback( - async (password: string) => { + async (password: string | undefined) => { return storeRevealMnemonic(password); }, [storeRevealMnemonic] diff --git a/src/lib/shared/types.ts b/src/lib/shared/types.ts index 1cfc1699..82274d0c 100644 --- a/src/lib/shared/types.ts +++ b/src/lib/shared/types.ts @@ -249,7 +249,7 @@ export interface RevealPrivateKeyResponse extends WalletMessageBase { export interface RevealMnemonicRequest extends WalletMessageBase { type: WalletMessageType.RevealMnemonicRequest; - password: string; + password?: string; } export interface RevealMnemonicResponse extends WalletMessageBase { @@ -515,6 +515,7 @@ export interface ImportFromClientRequest extends WalletMessageBase { type: WalletMessageType.ImportFromClientRequest; password?: string; // Optional for hardware-only wallets (mobile/desktop with Secure Enclave) mnemonic: string; + walletAccounts: WalletAccount[]; } export interface ImportFromClientResponse extends WalletMessageBase { diff --git a/src/lib/store/index.test.ts b/src/lib/store/index.test.ts index f4b3df06..aade7a27 100644 --- a/src/lib/store/index.test.ts +++ b/src/lib/store/index.test.ts @@ -301,12 +301,13 @@ 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' + mnemonic: 'mnemonic words', + walletAccounts: [] }); }); diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index 8647dae1..26447088 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -125,11 +125,12 @@ export const useWalletStore = create()( // State will be synced via StateUpdated notification }, - importWalletFromClient: async (password, mnemonic) => { + importWalletFromClient: async (password, mnemonic, walletAccounts) => { const res = await request({ type: WalletMessageType.ImportFromClientRequest, password, - mnemonic + mnemonic, + walletAccounts }); assertResponse(res.type === WalletMessageType.ImportFromClientResponse); }, diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index dc51fd0d..704b8239 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -97,14 +97,18 @@ export interface WalletActions { // Auth actions registerWallet: (password: string | undefined, mnemonic?: string, ownMnemonic?: boolean) => Promise; - importWalletFromClient: (password: string | undefined, mnemonic: string) => Promise; + importWalletFromClient: ( + password: string | undefined, + mnemonic: string, + walletAccounts: WalletAccount[] + ) => Promise; unlock: (password?: string) => Promise; // Account actions createAccount: (walletType: WalletType, name?: string) => Promise; updateCurrentAccount: (accountPublicKey: string) => Promise; editAccountName: (accountPublicKey: string, name: string) => Promise; - revealMnemonic: (password: string) => Promise; + revealMnemonic: (password: string | undefined) => Promise; // Settings actions updateSettings: (newSettings: Partial) => Promise; diff --git a/src/screens/encrypted-file-flow/EncryptedWalletFileWalletPassword.tsx b/src/screens/encrypted-file-flow/EncryptedWalletFileWalletPassword.tsx index 71d56864..752b020e 100644 --- a/src/screens/encrypted-file-flow/EncryptedWalletFileWalletPassword.tsx +++ b/src/screens/encrypted-file-flow/EncryptedWalletFileWalletPassword.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -58,6 +58,22 @@ const EncryptedWalletFileWalletPassword: React.FC { setIsPasswordVisible(prev => !prev); }, []); + const [loading, setLoading] = useState(true); + const [showPassword, setShowPassword] = useState(false); + + useEffect(() => { + const checkIfPasswordShouldBeShown = async () => { + try { + const { Vault } = await import('lib/miden/back/vault'); + setShowPassword(await Vault.hasPasswordProtector()); + } catch (e) { + console.error('Error checking vault password protector:', e); + } finally { + setLoading(false); + } + }; + checkIfPasswordShouldBeShown(); + }, []); const [timeleft, setTimeleft] = useState(getTimeLeft(timelock, lockLevel)); @@ -69,7 +85,7 @@ const EncryptedWalletFileWalletPassword: React.FC LAST_ATTEMPT) await new Promise(res => setTimeout(res, Math.random() * 2000 + 1000)); - await unlock(walletPassword!); + await unlock(walletPassword); setAttempt(1); onGoNext(); @@ -100,49 +116,59 @@ const EncryptedWalletFileWalletPassword: React.FC
-
-

{t('encryptedWalletFileDescription')}

-
- - - - } - onChange={onPasswordChange} - onKeyDown={handleEnterKey} - autoFocus - /> - {errors.password &&

{errors.password.message}

} + {loading ? ( +
+
-
- + ) : ( +
+

{t('encryptedWalletFileDescription')}

+
+ {showPassword && ( + <> + + + + } + onChange={onPasswordChange} + onKeyDown={handleEnterKey} + autoFocus + /> + {errors.password &&

{errors.password.message}

} + + )} +
+
+ +
+ {isDisabled && ( + + )}
- {isDisabled && ( - - )} -
+ )}