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/public/_locales/ko/messages.json b/public/_locales/ko/messages.json index 518ba687..5a0bd711 100644 --- a/public/_locales/ko/messages.json +++ b/public/_locales/ko/messages.json @@ -2054,7 +2054,7 @@ "englishSource": "Enable Biometric" }, "skipBiometricSetup": { - "message": "지금은 건너뛰기", + "message": "지금은 건너뛰세요", "englishSource": "Skip for now" }, "biometricAuthRequired": { @@ -2150,7 +2150,7 @@ "englishSource": "Tap here to claim it" }, "tokenHistory": { - "message": "$tokenName$ 내역", + "message": "$tokenName$ 기록", "englishSource": "$tokenName$ History", "placeholders": { "tokenname": { diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json index 20afedda..96fe9fd1 100644 --- a/public/_locales/tr/messages.json +++ b/public/_locales/tr/messages.json @@ -255,7 +255,7 @@ "englishSource": "Each word separated with a single space" }, "seedInputNumberOfWords": { - "message": "Benim Seed phrase'ım: $num$ kelimeler", + "message": "Benim Seed phrase'im: $num$ kelimeler", "englishSource": "My Seed phrase is $num$ words", "placeholders": { "num": { @@ -2009,7 +2009,7 @@ "englishSource": "This app wants to connect to your wallet" }, "dappTransactionRequest": { - "message": "Bu uygulama bir işlem istiyor", + "message": "Bu uygulama bir işlem talep ediyor", "englishSource": "This app is requesting a transaction" }, "noAccountSelected": { diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index 5ab0294e..f909101a 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -2146,7 +2146,7 @@ "englishSource": "You have received a note" }, "noteReceivedTapToClaim": { - "message": "Торкніться тут, щоб отримати його", + "message": "Натисніть тут, щоб отримати його", "englishSource": "Tap here to claim it" }, "tokenHistory": { 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/ImportAccount.tsx b/src/app/pages/ImportAccount.tsx index 60b6692d..fac476de 100644 --- a/src/app/pages/ImportAccount.tsx +++ b/src/app/pages/ImportAccount.tsx @@ -1,13 +1,12 @@ import React, { FC, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import Alert from 'app/atoms/Alert'; import FormField from 'app/atoms/FormField'; import FormSubmitButton from 'app/atoms/FormSubmitButton'; -import NoSpaceField from 'app/atoms/NoSpaceField'; -import TabSwitcher from 'app/atoms/TabSwitcher'; +import { ACCOUNT_NAME_PATTERN } from 'app/defaults'; import PageLayout from 'app/layouts/PageLayout'; import { useMidenContext, useAllAccounts } from 'lib/miden/front'; import { navigate } from 'lib/woozie'; @@ -18,12 +17,6 @@ type ImportAccountProps = { tabSlug: string | null; }; -interface ImportTabDescriptor { - slug: string; - i18nKey: string; - Form: FC<{}>; -} - const ImportAccount: FC = ({ tabSlug }) => { const { t } = useTranslation(); const allAccounts = useAllAccounts(); @@ -39,27 +32,6 @@ const ImportAccount: FC = ({ tabSlug }) => { prevAccLengthRef.current = accLength; }, [allAccounts, updateCurrentAccount]); - const allTabs = useMemo( - () => - [ - { - slug: 'private-key', - i18nKey: 'privateKey', - Form: ByPrivateKeyForm - }, - { - slug: 'watch-only', - i18nKey: 'watchOnlyAccount', - Form: WatchOnlyForm - } - ].filter((x): x is ImportTabDescriptor => !!x), - [] - ); - const { slug, Form } = useMemo(() => { - const tab = tabSlug ? allTabs.find(currentTab => currentTab.slug === tabSlug) : null; - return tab ?? allTabs[0]; - }, [allTabs, tabSlug]); - return ( = ({ tabSlug }) => { } > -
- - -
+
+
); @@ -81,27 +51,37 @@ export default ImportAccount; interface ByPrivateKeyFormData { privateKey: string; + name?: string; encPassword?: string; } const ByPrivateKeyForm: FC = () => { const { t } = useTranslation(); - const { importAccount } = useMidenContext(); - + const { importPublicAccountByPrivateKey } = useMidenContext(); + const allAccounts = useAllAccounts(); const { register, handleSubmit, + setValue, formState: { errors, isSubmitting } } = useForm(); const [error, setError] = useState(null); + const computedDefaultName = useMemo(() => { + return `Imported Acc ${allAccounts.length + 1}`; + }, [allAccounts]); + + useEffect(() => { + setValue('name', computedDefaultName); + }, [computedDefaultName, setValue]); + const onSubmit = useCallback( - async ({ privateKey, encPassword }: ByPrivateKeyFormData) => { + async ({ privateKey, name }: ByPrivateKeyFormData) => { if (isSubmitting) return; setError(null); try { - await importAccount(privateKey.replace(/\s/g, ''), encPassword); + await importPublicAccountByPrivateKey(privateKey, name); } catch (err: any) { console.error(err); @@ -110,18 +90,39 @@ const ByPrivateKeyForm: FC = () => { setError(err.message); } }, - [importAccount, isSubmitting, setError] + [importPublicAccountByPrivateKey, isSubmitting, setError] ); return ( + + {t('accountName')} +
+ } + id="create-account-name" + type="text" + placeholder={computedDefaultName} + errorCaption={errors.name?.message} + autoFocus + /> +
+ {t('accountNameInputDescription')} +
{error && } { {t('privateKey')} } - placeholder={t('privateKeyInputPlaceholder')} + placeholder={'eg. 3b6a27bccebfb65e3...'} errorCaption={errors.privateKey?.message} className="resize-none" onPaste={() => clearClipboard()} @@ -154,103 +155,3 @@ const ByPrivateKeyForm: FC = () => { ); }; - -interface WatchOnlyFormData { - viewKey: string; -} - -const WatchOnlyForm: FC = () => { - const { t } = useTranslation(); - const { importWatchOnlyAccount } = useMidenContext(); - - const { - handleSubmit, - control, - setValue, - getValues, - trigger, - formState: { errors, isSubmitting } - } = useForm({ - mode: 'onChange' - }); - const [error, setError] = useState(null); - - const addressFieldRef = useRef(null); - - const cleanViewKeyField = useCallback(() => { - setValue('viewKey', ''); - trigger('viewKey'); - }, [setValue, trigger]); - - const onSubmit = useCallback( - async ({ viewKey }: WatchOnlyFormData) => { - if (isSubmitting) return; - - setError(null); - - try { - await importWatchOnlyAccount(viewKey); - } catch (err: any) { - console.error(err); - - // Human delay - await new Promise(r => setTimeout(r, 300)); - setError(err.message); - } - }, - [importWatchOnlyAccount, isSubmitting, setError] - ); - - return ( -
- {error && } - - true - }} - render={({ field }) => ( - addressFieldRef.current?.focus()} - textarea - rows={1} - cleanable={Boolean(getValues().viewKey)} - onClean={cleanViewKeyField} - id="watch-viewKey" - label={ -
- {t('viewKeyWatchOnly')} -
- } - placeholder={t('viewKeyInputPlaceholder')} - errorCaption={errors.viewKey?.message} - className="resize-none" - /> - )} - /> -
- {t('viewKeyInputDescription')} -
- - - {t('importAccount')} - - - ); -}; diff --git a/src/app/pages/SelectAccount.tsx b/src/app/pages/SelectAccount.tsx index dd9a9b9a..1b66ebb4 100644 --- a/src/app/pages/SelectAccount.tsx +++ b/src/app/pages/SelectAccount.tsx @@ -22,6 +22,10 @@ const SelectAccount: FC = () => { navigate('/create-account'); }; + const onImportAccountClick = () => { + navigate('/import-account'); + }; + return ( {
+
diff --git a/src/app/pages/Settings.selectors.ts b/src/app/pages/Settings.selectors.ts index 59c952cc..063d6f39 100644 --- a/src/app/pages/Settings.selectors.ts +++ b/src/app/pages/Settings.selectors.ts @@ -3,7 +3,6 @@ export enum SettingsSelectors { LanguageButton = 'Settings/LanguageButton', SynchronizationButton = 'Settings/SynchronizationButton', AddressBookButton = 'Settings/AddressBookButton', - RevealViewKeyButton = 'Settings/RevealViewKeyButton', RevealPrivateKeyButton = 'Settings/RevealPrivateKeyButton', RevealSeedPhraseButton = 'Settings/RevealSeedPhraseButton', DAppsButton = 'Settings/DAppsButton', diff --git a/src/app/pages/Settings.tsx b/src/app/pages/Settings.tsx index 8cd0a28b..a66406a1 100644 --- a/src/app/pages/Settings.tsx +++ b/src/app/pages/Settings.tsx @@ -37,8 +37,7 @@ type SettingsProps = { tabSlug?: string | null; }; -// const RevealViewKey: FC = () => ; -// const RevealPrivateKey: FC = () => ; +const RevealPrivateKey: FC = () => ; const RevealSeedPhrase: FC = () => ; type Tab = { @@ -83,26 +82,16 @@ const TABS: Tab[] = [ insertHR: false, iconStyle: { stroke: '#000', strokeWidth: '2px' } }, - // { - // slug: 'reveal-view-key', - // titleI18nKey: 'revealViewKey', - // Icon: KeyIcon, - // Component: RevealViewKey, - // descriptionI18nKey: 'revealViewKeyDescription', - // testID: SettingsSelectors.RevealViewKeyButton, - // insertHR: true, - // iconStyle: { stroke: '#000', strokeWidth: '1px' } - // }, - // { - // slug: 'reveal-private-key', - // titleI18nKey: 'revealPrivateKey', - // Icon: KeyIcon, - // Component: RevealPrivateKey, - // descriptionI18nKey: 'revealPrivateKeyDescription', - // testID: SettingsSelectors.RevealPrivateKeyButton, - // insertHR: false, - // iconStyle: { stroke: '#000', strokeWidth: '1px' } - // }, + { + slug: 'reveal-private-key', + titleI18nKey: 'revealPrivateKey', + Icon: KeyIcon, + Component: RevealPrivateKey, + descriptionI18nKey: 'revealPrivateKeyDescription', + testID: SettingsSelectors.RevealPrivateKeyButton, + insertHR: false, + iconStyle: { stroke: '#000', strokeWidth: '1px' } + }, { slug: 'reveal-seed-phrase', titleI18nKey: 'revealSeedPhrase', diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index 8de8df16..20ab29dd 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -9,7 +9,7 @@ import { authenticate, checkBiometricAvailability, setBiometricEnabled, storeCre import { useMidenContext } from 'lib/miden/front'; import { useMobileBackHandler } from 'lib/mobile/useMobileBackHandler'; import { 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'; @@ -45,6 +45,8 @@ const Welcome: FC = () => { const [enableBiometric, setEnableBiometric] = useState(false); const [importedWithFile, setImportedWithFile] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [importedWalletAccounts, setImportedWalletAccounts] = useState([]); + const [skForImportedAccounts, setSkForImportedAccounts] = useState>({}); const { registerWallet, importWalletFromClient } = useMidenContext(); const { trackEvent } = useAnalytics(); const syncFromBackend = useWalletStore(s => s.syncFromBackend); @@ -65,13 +67,22 @@ const Welcome: FC = () => { } else { try { console.log('importing wallet from client'); - await importWalletFromClient(password, seedPhraseFormatted); + await importWalletFromClient(password, seedPhraseFormatted, importedWalletAccounts, skForImportedAccounts); } catch (e) { console.error(e); } } } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [ + password, + seedPhrase, + importedWithFile, + registerWallet, + onboardingType, + importWalletFromClient, + importedWalletAccounts, + skForImportedAccounts + ]); const onAction = async (action: OnboardingAction) => { let eventCategory = AnalyticsEventCategory.ButtonPress; @@ -93,8 +104,9 @@ const Welcome: FC = () => { break; case 'import-wallet-file-submit': const seedPhrase = action.payload.split(' '); - console.log({ seedPhrase }); setSeedPhrase(seedPhrase); + setImportedWalletAccounts(action.walletAccounts); + setSkForImportedAccounts(action.skForImportedAccounts); setImportedWithFile(true); navigate('/#create-password'); break; diff --git a/src/app/templates/RevealSecret.tsx b/src/app/templates/RevealSecret.tsx index f1bb17be..5336058b 100644 --- a/src/app/templates/RevealSecret.tsx +++ b/src/app/templates/RevealSecret.tsx @@ -7,10 +7,10 @@ import { useTranslation } from 'react-i18next'; import Alert from 'app/atoms/Alert'; import FormField from 'app/atoms/FormField'; import FormSubmitButton from 'app/atoms/FormSubmitButton'; -import { useAccountBadgeTitle } from 'app/defaults'; import { Icon, IconName } from 'app/icons/v2'; import AccountBanner from 'app/templates/AccountBanner'; import { useAccount, useSecretState, useMidenContext } from 'lib/miden/front'; +import { getMidenClient, withWasmClientLock } from 'lib/miden/sdk/miden-client'; import useCopyToClipboard from 'lib/ui/useCopyToClipboard'; const SUBMIT_ERROR_TYPE = 'submit-error'; @@ -20,13 +20,12 @@ type FormData = { }; type RevealSecretProps = { - reveal: 'view-key' | 'private-key' | 'seed-phrase'; + reveal: 'private-key' | 'seed-phrase'; }; const RevealSecret: FC = ({ reveal }) => { const { t } = useTranslation(); - const accountBadgeTitle = useAccountBadgeTitle(); - const { revealMnemonic } = useMidenContext(); + const { revealMnemonic, revealPrivateKey } = useMidenContext(); const account = useAccount(); const { fieldRef: secretFieldRef, copy, copied } = useCopyToClipboard(); @@ -37,7 +36,6 @@ const RevealSecret: FC = ({ reveal }) => { clearErrors, formState: { errors, isSubmitting } } = useForm(); - const [secret, setSecret] = useSecretState(); useEffect(() => { @@ -67,10 +65,20 @@ const RevealSecret: FC = ({ reveal }) => { const onSubmit = useCallback>( async ({ password }) => { if (isSubmitting) return; - + console.log('Revealing secret...'); clearErrors('password'); try { - const secret = await revealMnemonic(password); + let secret; + if (reveal === 'private-key') { + const pkc = await withWasmClientLock(async () => { + const client = await getMidenClient(); + return client.getAccountPkcByPublicKey(account.publicKey); + }); + secret = await revealPrivateKey(pkc, password); + console.log('Revealed private key'); + } else { + secret = await revealMnemonic(password); + } setSecret(secret); } catch (err: any) { console.error(err); @@ -81,33 +89,30 @@ const RevealSecret: FC = ({ reveal }) => { focusPasswordField(); } }, - [isSubmitting, clearErrors, setError, revealMnemonic, setSecret, focusPasswordField] + [ + isSubmitting, + clearErrors, + setError, + revealMnemonic, + revealPrivateKey, + setSecret, + focusPasswordField, + reveal, + account.publicKey + ] ); const texts = useMemo(() => { switch (reveal) { - case 'view-key': - return { - name: t('viewKey'), - accountBanner: ( - - ), - attention: ( -
- - {t('doNotShareViewKey1')}
-
- {t('doNotShareViewKey2')} -
- ), - fieldDesc: t('viewKeyFieldDescription') - }; - case 'private-key': return { name: t('privateKey'), accountBanner: ( - + ), attention: (
@@ -117,30 +122,13 @@ const RevealSecret: FC = ({ reveal }) => { {t('doNotSharePrivateKey2')}
), - fieldDesc: t('privateKeyFieldDescription') + fieldDesc: <>{t('privateKeyFieldDescription')} }; case 'seed-phrase': return { name: t('seedPhrase'), accountBanner: null, - derivationPathBanner: ( -
-

- - {t('derivationPath')} - - - - {t('pathForHDAccounts')} - -

- -
- {t('derivationPathExample')} -
-
- ), attention: (
@@ -156,37 +144,8 @@ const RevealSecret: FC = ({ reveal }) => { ) }; } - }, [reveal, t]); - - const forbidPrivateKeyRevealing = reveal === 'private-key'; + }, [reveal, account, t]); const mainContent = useMemo(() => { - if (forbidPrivateKeyRevealing) { - return ( - - {t('youCannotGetPrivateKeyFromThisAccountType', { - accountType: ( - - {accountBadgeTitle} - - ) - })} -

- } - className="mb-4 bg-blue-200 border-primary-500 rounded-none text-black" - /> - ); - } - if (secret) { return ( <> @@ -255,7 +214,7 @@ const RevealSecret: FC = ({ reveal }) => { = ({ reveal }) => { ); }, [ - forbidPrivateKeyRevealing, errors, handleSubmit, onSubmit, @@ -294,8 +252,7 @@ const RevealSecret: FC = ({ reveal }) => { copy, copied, secretFieldRef, - t, - accountBadgeTitle + t ]); return ( diff --git a/src/lib/intercom/mobile-adapter.test.ts b/src/lib/intercom/mobile-adapter.test.ts index 62ca843b..184e35fd 100644 --- a/src/lib/intercom/mobile-adapter.test.ts +++ b/src/lib/intercom/mobile-adapter.test.ts @@ -94,10 +94,12 @@ describe('MobileIntercomAdapter', () => { const response = await adapter.request({ type: WalletMessageType.ImportFromClientRequest, password: 'test123', - mnemonic: 'word1 word2 word3' - } as any); + mnemonic: 'word1 word2 word3', + walletAccounts: [], + skForImportedAccounts: {} + }); - 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..a3eccdb3 100644 --- a/src/lib/intercom/mobile-adapter.ts +++ b/src/lib/intercom/mobile-adapter.ts @@ -67,11 +67,11 @@ export class MobileIntercomAdapter { }; case WalletMessageType.NewWalletRequest: - await Actions.registerNewWallet((req as any).password, (req as any).mnemonic, (req as any).ownMnemonic); + await Actions.registerNewWallet(req.password, req.mnemonic, req.ownMnemonic); 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, req.skForImportedAccounts); return { type: WalletMessageType.ImportFromClientResponse }; case WalletMessageType.UnlockRequest: @@ -96,6 +96,12 @@ export class MobileIntercomAdapter { type: WalletMessageType.RevealMnemonicResponse, mnemonic }; + case WalletMessageType.RevealPrivateKeyRequest: + const privateKey = await Actions.revealPrivateKey(req.accountPublicKey, req.password); + return { + type: WalletMessageType.RevealPrivateKeyResponse, + privateKey + }; case WalletMessageType.RemoveAccountRequest: await Actions.removeAccount((req as any).accountPublicKey, (req as any).password); diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index d1670339..0d39497b 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'; @@ -77,9 +77,14 @@ export function registerNewWallet(password: string, mnemonic?: string, ownMnemon }); } -export function registerImportedWallet(password: string, mnemonic: string) { +export function registerImportedWallet( + password: string, + mnemonic: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record +) { return withInited(async () => { - await Vault.spawnFromMidenClient(password, mnemonic); + await Vault.spawnFromMidenClient(password, mnemonic, walletAccounts, skForImportedAccounts); await unlock(password); }); } @@ -133,13 +138,13 @@ export function createHDAccount(walletType: WalletType, name?: string) { export function decryptCiphertexts(accPublicKey: string, cipherTexts: string[]) {} -export function revealViewKey(accPublicKey: string, password: string) {} - export function revealMnemonic(password: string) { return withUnlocked(() => Vault.revealMnemonic(password)); } -export function revealPrivateKey(accPublicKey: string, password: string) {} +export function revealPrivateKey(accPublicKey: string, password: string) { + return withUnlocked(() => Vault.revealPrivateKey(accPublicKey, password)); +} export function revealPublicKey(accPublicKey: string) {} @@ -159,7 +164,12 @@ export function editAccount(accPublicKey: string, name: string) { }); } -export function importAccount(privateKey: string, encPassword?: string) {} +export function importAccount(privateKey: string, name?: string) { + return withUnlocked(async ({ vault }) => { + const accounts = await vault.importAccount(privateKey, name); + accountsUpdated({ accounts }); + }); +} export function importMnemonicAccount(mnemonic: string, password?: string, derivationPath?: string) {} diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index c361a209..a9d81455 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 passKey: CryptoKey) {} @@ -76,23 +90,11 @@ export class Vault { // Clear storage before any inserts to avoid wiping newly inserted keys later await clearStorage(); - 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] - ], - passKey - ); - }; const options: MidenClientCreateOptions = { - insertKeyCallback + insertKeyCallback: insertKeyCallbackWrpapper(passKey) }; 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); @@ -132,46 +134,61 @@ export class Vault { }); } - static async spawnFromMidenClient(password: string, mnemonic: string) { + static async spawnFromMidenClient( + password: string, + mnemonic: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record + ) { return withError('Failed to spawn from miden client', async () => { + await clearStorage(false); + + const passKey = await Passworder.generateKey(password); + // insert keys + const options: MidenClientCreateOptions = { + insertKeyCallback: insertKeyCallbackWrpapper(passKey) + }; // Wrap WASM client operations in a lock to prevent concurrent access - const accounts = await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); + await withWasmClientLock(async () => { + const midenClient = await getMidenClient(options); 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); + 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'); + } + if (walletAccount.hdIndex === -1) { + const skHex = skForImportedAccounts[walletAccount.publicKey]; + if (!skHex) { + throw new PublicError('Secret key for imported account not found'); + } + const sk = SecretKey.deserialize(new Uint8Array(Buffer.from(skHex, 'hex'))); + await midenClient.webClient.addAccountSecretKeyToWebStore(sk); + } else { + const walletSeed = deriveClientSeed(walletAccount.type, mnemonic, walletAccount.hdIndex); + const secretKey = SecretKey.rpoFalconWithRNG(walletSeed); + await midenClient.webClient.addAccountSecretKeyToWebStore(secretKey); + } } - 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 - }); - } - } - const passKey = await Passworder.generateKey(password); - - await clearStorage(false); await encryptAndSaveMany( [ [checkStrgKey, generateCheck()], [mnemonicStrgKey, mnemonic ?? ''], - [accountsStrgKey, newAccounts] + [accountsStrgKey, walletAccounts] ], passKey ); - await savePlain(currentAccPubKeyStrgKey, newAccounts[0].publicKey); + await savePlain(currentAccPubKeyStrgKey, walletAccounts[0].publicKey); await savePlain(ownMnemonicStrgKey, true); }); } @@ -203,20 +220,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.passKey - ); - }; const options: MidenClientCreateOptions = { - insertKeyCallback + insertKeyCallback: insertKeyCallbackWrpapper(this.passKey) }; // Wrap WASM client operations in a lock to prevent concurrent access @@ -342,8 +347,6 @@ export class Vault { async decryptCipherTextOrRecord() {} - async revealViewKey(accPublicKey: string) {} - static async revealMnemonic(password: string) { const passKey = await Vault.toValidPassKey(password); return withError('Failed to reveal seed phrase', async () => { @@ -356,6 +359,56 @@ export class Vault { }); } + static async revealPrivateKey(accPublicKey: string, password: string) { + const passKey = await Vault.toValidPassKey(password); + return await withError('Failed to reveal private key', async () => { + try { + const secretKeyHex = await fetchAndDecryptOneWithLegacyFallBack( + accAuthSecretKeyStrgKey(accPublicKey), + passKey + ); + return secretKeyHex; + } catch (e) { + console.error('Error fetching and decrypting private key:', e); + throw e; + } + }); + } + + 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 pubKeyWord = secretKey.publicKey().toCommitment(); + + const pubKeyHex = pubKeyWord.toHex().slice(2); // remove '0x' prefix + const publicKey = await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + return await midenClient.importPublicAccountFromPrivateKey(secretKey); + }); + const newAccount: WalletAccount = { + publicKey, + name: name || `Imported Account ${allAccounts.length + 1}`, + isPublic: true, + type: WalletType.OnChain, + hdIndex: -1 // -1 indicates imported account + }; + + const newAllAccounts = concatAccount(allAccounts, newAccount); + + await encryptAndSaveMany( + [ + [accPubKeyStrgKey(pubKeyHex), pubKeyHex], + [accountsStrgKey, newAllAccounts], + [accAuthSecretKeyStrgKey(pubKeyHex), privateKey] + ], + this.passKey + ); + + return newAllAccounts; + }); + } + async getCurrentAccount() { const currAccountPubkey = await getPlain(currentAccPubKeyStrgKey); const allAccounts = await this.fetchAccounts(); diff --git a/src/lib/miden/front/client.ts b/src/lib/miden/front/client.ts index f22a1d2a..4e8d4143 100644 --- a/src/lib/miden/front/client.ts +++ b/src/lib/miden/front/client.ts @@ -4,10 +4,11 @@ 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'; +import { store } from '../back/store'; import { MidenState } from '../types'; import { AutoSync } from './autoSync'; @@ -48,6 +49,7 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { const storeUpdateCurrentAccount = useWalletStore(s => s.updateCurrentAccount); const storeEditAccountName = useWalletStore(s => s.editAccountName); const storeRevealMnemonic = useWalletStore(s => s.revealMnemonic); + const storeRevealPrivateKey = useWalletStore(s => s.revealPrivateKey); const storeUpdateSettings = useWalletStore(s => s.updateSettings); const storeSignData = useWalletStore(s => s.signData); const storeSignTransaction = useWalletStore(s => s.signTransaction); @@ -63,6 +65,7 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { const storeGetAllDAppSessions = useWalletStore(s => s.getAllDAppSessions); const storeRemoveDAppSession = useWalletStore(s => s.removeDAppSession); const storeResetConfirmation = useWalletStore(s => s.resetConfirmation); + const storeImportAccountByPrivateKey = useWalletStore(s => s.importPublicAccountByPrivateKey); // Build the state object for backward compatibility const state: MidenState = useMemo( @@ -100,8 +103,13 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { ); const importWalletFromClient = useCallback( - async (password: string, mnemonic: string) => { - await storeImportWalletFromClient(password, mnemonic); + async ( + password: string, + mnemonic: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record + ) => { + await storeImportWalletFromClient(password, mnemonic, walletAccounts, skForImportedAccounts); }, [storeImportWalletFromClient] ); @@ -141,6 +149,13 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { [storeRevealMnemonic] ); + const revealPrivateKey = useCallback( + async (accountPublicKey: string, password: string) => { + return storeRevealPrivateKey(accountPublicKey, password); + }, + [storeRevealPrivateKey] + ); + const updateSettings = useCallback( async (newSettings: Partial) => { await storeUpdateSettings(newSettings); @@ -248,8 +263,6 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { // Stub implementations for unimplemented actions const decryptCiphertexts = useCallback(async (accPublicKey: string, ciphertexts: string[]) => {}, []); - const revealViewKey = useCallback(async (accountPublicKey: string, password: string) => {}, []); - const revealPrivateKey = useCallback(async (accountPublicKey: string, password: string) => {}, []); const removeAccount = useCallback(async (accountPublicKey: string, password: string) => {}, []); const importAccount = useCallback(async (privateKey: string, encPassword?: string) => {}, []); const importWatchOnlyAccount = useCallback(async (viewKey: string) => {}, []); @@ -261,7 +274,12 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { const confirmDAppBulkTransactions = useCallback(async (id: string, confirmed: boolean, delegate: boolean) => {}, []); const confirmDAppDeploy = useCallback(async (id: string, confirmed: boolean, delegate: boolean) => {}, []); const getOwnedRecords = useCallback(async (accPublicKey: string) => {}, []); - + const importPublicAccountByPrivateKey = useCallback( + async (privateKey: string, name?: string) => { + await storeImportAccountByPrivateKey(privateKey, name); + }, + [storeImportAccountByPrivateKey] + ); return { state, // Aliases @@ -286,7 +304,6 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { createAccount, updateCurrentAccount, - revealViewKey, revealPrivateKey, revealMnemonic, removeAccount, @@ -313,7 +330,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { removeDAppSession, decryptCiphertexts, getOwnedRecords, - importWalletFromClient + importWalletFromClient, + importPublicAccountByPrivateKey }; }); diff --git a/src/lib/miden/sdk/miden-client-interface.test.ts b/src/lib/miden/sdk/miden-client-interface.test.ts index f79b2124..5446b1da 100644 --- a/src/lib/miden/sdk/miden-client-interface.test.ts +++ b/src/lib/miden/sdk/miden-client-interface.test.ts @@ -71,6 +71,8 @@ describe('MidenClientInterface', () => { { accountId: () => 'id', serialize: () => new Uint8Array([9]) }, { accountId: () => 'other', serialize: () => new Uint8Array([9]) } ]), + addAccountSecretKeyToWebStore: jest.fn(), + newAccount: jest.fn(), terminate: jest.fn() }; @@ -88,7 +90,34 @@ describe('MidenClientInterface', () => { newLocalProver: jest.fn(() => 'local') }, TransactionFilter: { all: jest.fn(() => 'all') }, - MIDEN_NETWORK_NAME: { TESTNET: 'testnet' } + MIDEN_NETWORK_NAME: { TESTNET: 'testnet' }, + SecretKey: { rpoFalconWithRNG: jest.fn() }, + AccountBuilder: class { + constructor(public seed: Uint8Array) {} + storageMode() { + return this; + } + accountType() { + return this; + } + withAuthComponent() { + return this; + } + build() { + return { + account: { id: () => 'account-id' } + }; + } + withBasicWalletComponent() { + return this; + } + }, + AccountType: { + RegularAccountImmutableCode: 'RegularAccountImmutableCode' + }, + AccountComponent: { + createAuthComponent: jest.fn() + } })); jest.doMock('lib/miden-chain/constants', () => ({ MIDEN_NETWORK_ENDPOINTS: new Map([['testnet', 'rpc']]), diff --git a/src/lib/miden/sdk/miden-client-interface.ts b/src/lib/miden/sdk/miden-client-interface.ts index f9b9a99b..e2dc6364 100644 --- a/src/lib/miden/sdk/miden-client-interface.ts +++ b/src/lib/miden/sdk/miden-client-interface.ts @@ -1,7 +1,10 @@ import { Account, + AccountBuilder, + AccountComponent, AccountFile, AccountStorageMode, + AccountType, Address, ConsumableNoteRecord, InputNoteRecord, @@ -10,6 +13,7 @@ import { NoteFile, NoteFilter, NoteType, + SecretKey, TransactionFilter, TransactionProver, TransactionRequest, @@ -114,7 +118,18 @@ export class MidenClientInterface { const accountStorageMode = walletType === WalletType.OnChain ? AccountStorageMode.public() : AccountStorageMode.private(); - const wallet: Account = await this.webClient.newWallet(accountStorageMode, true, 0, seed); + const secretKey = SecretKey.rpoFalconWithRNG(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) + .storageMode(accountStorageMode) + .withAuthComponent(AccountComponent.createAuthComponent(secretKey)) + .withBasicWalletComponent(); + const wallet = accountBuilder.build().account; + // add the secret key to the web client's keystore + await this.webClient.addAccountSecretKeyToWebStore(secretKey); + // register the new account in the web client + await this.webClient.newAccount(wallet, false); const walletId = getBech32AddressFromAccountId(wallet.id()); return walletId; @@ -134,6 +149,22 @@ export class MidenClientInterface { return getBech32AddressFromAccountId(account.id()); } + async importPublicAccountFromPrivateKey(privateKey: SecretKey): Promise { + const accountBuilder = new AccountBuilder(new Uint8Array(32).fill(0)) + .accountType(AccountType.RegularAccountImmutableCode) + .storageMode(AccountStorageMode.public()) + .withAuthComponent(AccountComponent.createAuthComponent(privateKey)) + .withBasicWalletComponent(); + const wallet = accountBuilder.build().account; + // add the secret key to the web client's keystore + await this.webClient.addAccountSecretKeyToWebStore(privateKey); + // register the new account in the web client + await this.webClient.importAccountById(wallet.id()); + const walletId = getBech32AddressFromAccountId(wallet.id()); + + return walletId; + } + // TODO: is this method even used? async consumeTransaction(accountId: string, listOfNoteIds: string[], delegateTransaction?: boolean) { console.log('Consuming transaction...'); @@ -167,6 +198,15 @@ export class MidenClientInterface { return result; } + async getAccountPkcByPublicKey(publicKey: string): Promise { + const acc = await this.webClient.getAccount(accountIdStringToSdk(publicKey)); + if (!acc) { + throw new Error('Account not found'); + } + const result = acc.getPublicKeys()[0].toHex(); + return result.slice(2); // remove 0x prefix + } + async importAccountById(accountId: string) { const result = await this.webClient.importAccountById(accountIdStringToSdk(accountId)); return result; diff --git a/src/lib/shared/types.ts b/src/lib/shared/types.ts index 6ff59eed..b0088495 100644 --- a/src/lib/shared/types.ts +++ b/src/lib/shared/types.ts @@ -34,8 +34,6 @@ export enum WalletMessageType { UpdateCurrentAccountResponse = 'UPDATE_CURRENT_ACCOUNT_RESPONSE', RevealPublicKeyRequest = 'REVEAL_PUBLIC_KEY_REQUEST', RevealPublicKeyResponse = 'REVEAL_PUBLIC_KEY_RESPONSE', - RevealViewKeyRequest = 'REVEAL_VIEW_KEY_REQUEST', - RevealViewKeyResponse = 'REVEAL_VIEW_KEY_RESPONSE', RevealPrivateKeyRequest = 'REVEAL_PRIVATE_KEY_REQUEST', RevealPrivateKeyResponse = 'REVEAL_PRIVATE_KEY_RESPONSE', RevealMnemonicRequest = 'REVEAL_MNEMONIC_REQUEST', @@ -225,17 +223,6 @@ export interface RevealPublicKeyResponse extends WalletMessageBase { publicKey: string; } -export interface RevealViewKeyRequest extends WalletMessageBase { - type: WalletMessageType.RevealViewKeyRequest; - accountPublicKey: string; - password: string; -} - -export interface RevealViewKeyResponse extends WalletMessageBase { - type: WalletMessageType.RevealViewKeyResponse; - viewKey: string; -} - export interface RevealPrivateKeyRequest extends WalletMessageBase { type: WalletMessageType.RevealPrivateKeyRequest; accountPublicKey: string; @@ -280,6 +267,7 @@ export interface EditAccountResponse extends WalletMessageBase { export interface ImportAccountRequest extends WalletMessageBase { type: WalletMessageType.ImportAccountRequest; privateKey: string; + name?: string; encPassword?: string; } @@ -515,6 +503,8 @@ export interface ImportFromClientRequest extends WalletMessageBase { type: WalletMessageType.ImportFromClientRequest; password: string; mnemonic: string; + walletAccounts: WalletAccount[]; + skForImportedAccounts: Record; } export interface ImportFromClientResponse extends WalletMessageBase { @@ -537,7 +527,6 @@ export type WalletRequest = | CreateAccountRequest | UpdateCurrentAccountRequest | RevealPublicKeyRequest - | RevealViewKeyRequest | RevealPrivateKeyRequest | RevealMnemonicRequest | RemoveAccountRequest @@ -579,7 +568,6 @@ export type WalletResponse = | CreateAccountResponse | UpdateCurrentAccountResponse | RevealPublicKeyResponse - | RevealViewKeyResponse | RevealPrivateKeyResponse | RevealMnemonicResponse | RemoveAccountResponse 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 4fcdca51..e941922f 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -124,11 +124,13 @@ export const useWalletStore = create()( // State will be synced via StateUpdated notification }, - importWalletFromClient: async (password, mnemonic) => { + importWalletFromClient: async (password, mnemonic, walletAccounts, skForImportedAccounts) => { const res = await request({ type: WalletMessageType.ImportFromClientRequest, password, - mnemonic + mnemonic, + walletAccounts, + skForImportedAccounts }); assertResponse(res.type === WalletMessageType.ImportFromClientResponse); }, @@ -209,6 +211,25 @@ export const useWalletStore = create()( return res.mnemonic; }, + revealPrivateKey: async (accountPublicKey, password) => { + const res = await request({ + type: WalletMessageType.RevealPrivateKeyRequest, + accountPublicKey, + password + }); + assertResponse(res.type === WalletMessageType.RevealPrivateKeyResponse); + return res.privateKey; + }, + + importPublicAccountByPrivateKey: async (privateKey, name) => { + const res = await request({ + type: WalletMessageType.ImportAccountRequest, + privateKey, + name + }); + assertResponse(res.type === WalletMessageType.ImportAccountResponse); + }, + // Settings actions updateSettings: async newSettings => { const { settings } = get(); diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index 468dda3c..92048cdd 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -95,7 +95,12 @@ export interface WalletActions { // Auth actions registerWallet: (password: string, mnemonic?: string, ownMnemonic?: boolean) => Promise; - importWalletFromClient: (password: string, mnemonic: string) => Promise; + importWalletFromClient: ( + password: string, + mnemonic: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record + ) => Promise; unlock: (password: string) => Promise; // Account actions @@ -103,6 +108,8 @@ export interface WalletActions { updateCurrentAccount: (accountPublicKey: string) => Promise; editAccountName: (accountPublicKey: string, name: string) => Promise; revealMnemonic: (password: string) => Promise; + revealPrivateKey: (accountPublicKey: string, password: string) => Promise; + importPublicAccountByPrivateKey: (privateKey: string, name?: string) => Promise; // Settings actions updateSettings: (newSettings: Partial) => Promise; diff --git a/src/screens/encrypted-file-flow/ExportFileComplete.tsx b/src/screens/encrypted-file-flow/ExportFileComplete.tsx index 9d570676..5353c789 100644 --- a/src/screens/encrypted-file-flow/ExportFileComplete.tsx +++ b/src/screens/encrypted-file-flow/ExportFileComplete.tsx @@ -34,7 +34,7 @@ const ExportFileComplete: React.FC = ({ onGoBack }) => { const { t } = useTranslation(); - const { revealMnemonic } = useMidenContext(); + const { revealMnemonic, accounts, revealPrivateKey } = useMidenContext(); const { fullPage } = useAppEnv(); const getExportFile = useCallback(async () => { @@ -46,13 +46,25 @@ const ExportFileComplete: React.FC = ({ const walletDbDump = await exportDb(); const seedPhrase = await revealMnemonic(walletPassword); + const secretKeysForImportedAccounts: Record = {}; + await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + for (const account of accounts) { + if (account.hdIndex === -1) { + const pubKey = await midenClient.getAccountPkcByPublicKey(account.publicKey); + const sk = await revealPrivateKey(pubKey, walletPassword); + secretKeysForImportedAccounts[account.publicKey] = sk; + } + } + }); const filePayload: DecryptedWalletFile = { seedPhrase, midenClientDbContent: midenClientDbDump, - walletDbContent: walletDbDump + walletDbContent: walletDbDump, + accounts, + secretKeysForImportedAccounts }; - const salt = generateSalt(); const passKey = await generateKey(filePassword); const derivedKey = await deriveKey(passKey, salt); @@ -102,7 +114,7 @@ const ExportFileComplete: React.FC = ({ document.body.removeChild(a); URL.revokeObjectURL(url); } - }, [walletPassword, filePassword, fileName, revealMnemonic, t]); + }, [walletPassword, filePassword, fileName, revealMnemonic, t, accounts, revealPrivateKey]); useEffect(() => { getExportFile(); diff --git a/src/screens/onboarding/create-wallet-flow/VerifySeedPhrase.tsx b/src/screens/onboarding/create-wallet-flow/VerifySeedPhrase.tsx index 24ec4387..97c2b084 100644 --- a/src/screens/onboarding/create-wallet-flow/VerifySeedPhrase.tsx +++ b/src/screens/onboarding/create-wallet-flow/VerifySeedPhrase.tsx @@ -46,8 +46,6 @@ export const VerifySeedPhraseScreen: React.FC = ({ }, [firstSelectedWordIndex, secondSelectedWordIndex] ); - console.log({ firstSelectedWordIndex, secondSelectedWordIndex }); - console.log(seedPhrase); const isCorrectWordSelected = useMemo(() => { if (firstSelectedWordIndex === null || secondSelectedWordIndex === null) { return false; diff --git a/src/screens/onboarding/forgot-password-navigator.tsx b/src/screens/onboarding/forgot-password-navigator.tsx index 5297f553..924ef648 100644 --- a/src/screens/onboarding/forgot-password-navigator.tsx +++ b/src/screens/onboarding/forgot-password-navigator.tsx @@ -1,5 +1,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; +import { WalletAccount } from 'lib/shared/types'; + import { ConfirmationScreen } from './common/Confirmation'; import { CreatePasswordScreen } from './common/CreatePassword'; import OnboardingHeader from './common/OnboardingHeader'; @@ -144,8 +146,8 @@ export const ForgotPasswordFlow: FC = ({ onForwardAction?.({ id: 'confirmation' }); }; - const onImportFileSubmit = (seedPhrase: string) => { - onForwardAction?.({ id: 'import-wallet-file-submit', payload: seedPhrase }); + const onImportFileSubmit = (seedPhrase: string, walletAccounts: WalletAccount[]) => { + onForwardAction?.({ id: 'import-wallet-file-submit', payload: seedPhrase, walletAccounts }); }; const onSelectTransactionTypeSubmit = () => diff --git a/src/screens/onboarding/import-wallet-flow/ImportWalletFile.tsx b/src/screens/onboarding/import-wallet-flow/ImportWalletFile.tsx index ad7ea675..fe9a85fc 100644 --- a/src/screens/onboarding/import-wallet-flow/ImportWalletFile.tsx +++ b/src/screens/onboarding/import-wallet-flow/ImportWalletFile.tsx @@ -10,6 +10,7 @@ import { Icon, IconName } from 'app/icons/v2'; import { decrypt, decryptJson, deriveKey, generateKey } from 'lib/miden/passworder'; import { importDb } from 'lib/miden/repo'; import { getMidenClient, withWasmClientLock } from 'lib/miden/sdk/miden-client'; +import type { WalletAccount } from 'lib/shared/types'; import { DecryptedWalletFile, ENCRYPTED_WALLET_FILE_PASSWORD_CHECK, EncryptedWalletFile } from 'screens/shared'; interface FormData { @@ -18,7 +19,11 @@ interface FormData { export interface ImportWalletFileScreenProps { className?: string; - onSubmit?: (seedPhrase: string) => void; + onSubmit?: ( + seedPhrase: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record + ) => void; } type WalletFile = EncryptedWalletFile & { @@ -76,6 +81,8 @@ export const ImportWalletFileScreen: React.FC = ({ const midenClientDbContent = decryptedWallet.midenClientDbContent; const walletDbContent = decryptedWallet.walletDbContent; const seedPhrase = decryptedWallet.seedPhrase; + const walletAccounts = decryptedWallet.accounts; + const skForImportedAccounts = decryptedWallet.secretKeysForImportedAccounts; // Wrap WASM client operations in a lock to prevent concurrent access await withWasmClientLock(async () => { @@ -84,7 +91,7 @@ export const ImportWalletFileScreen: React.FC = ({ }); await importDb(walletDbContent); - onSubmit(seedPhrase); + onSubmit(seedPhrase, walletAccounts, skForImportedAccounts); } catch (error) { console.error('Decryption failed:', error); setIsWrongPassword(true); // Ensure error appears in case of failure diff --git a/src/screens/onboarding/navigator.tsx b/src/screens/onboarding/navigator.tsx index 5182a0cc..c99f4d03 100644 --- a/src/screens/onboarding/navigator.tsx +++ b/src/screens/onboarding/navigator.tsx @@ -7,6 +7,7 @@ import { Icon, IconName } from 'app/icons/v2'; import { CircleButton } from 'components/CircleButton'; import { ProgressIndicator } from 'components/ProgressIndicator'; import { isMobile } from 'lib/platform'; +import type { WalletAccount } from 'lib/shared/types'; import { BiometricSetupScreen } from './common/BiometricSetup'; import { ConfirmationScreen } from './common/Confirmation'; @@ -159,8 +160,17 @@ export const OnboardingFlow: FC = ({ const onImportSeedPhraseSubmit = (seedPhrase: string) => onForwardAction?.({ id: 'import-seed-phrase-submit', payload: seedPhrase }); - const onImportFileSubmit = (seedPhrase: string) => { - onForwardAction?.({ id: 'import-wallet-file-submit', payload: seedPhrase }); + const onImportFileSubmit = ( + seedPhrase: string, + walletAccounts: WalletAccount[], + skForImportedAccounts: Record + ) => { + onForwardAction?.({ + id: 'import-wallet-file-submit', + payload: seedPhrase, + walletAccounts, + skForImportedAccounts + }); }; switch (step) { diff --git a/src/screens/onboarding/types.ts b/src/screens/onboarding/types.ts index 2f362253..85eefba7 100644 --- a/src/screens/onboarding/types.ts +++ b/src/screens/onboarding/types.ts @@ -1,3 +1,5 @@ +import type { WalletAccount } from 'lib/shared/types'; + export enum OnboardingType { Create = 'create', Import = 'import' @@ -92,6 +94,8 @@ export type BiometricSetupSubmitAction = { export type ImportWalletFileSubmitAction = { id: 'import-wallet-file-submit'; payload: string; + walletAccounts: WalletAccount[]; + skForImportedAccounts: Record; }; export type ImportSeedPhraseSubmitAction = { diff --git a/src/screens/shared.ts b/src/screens/shared.ts index 796f37c6..6671ea0a 100644 --- a/src/screens/shared.ts +++ b/src/screens/shared.ts @@ -1,9 +1,12 @@ import { EncryptedPayload } from 'lib/miden/passworder'; +import type { WalletAccount } from 'lib/shared/types'; export type DecryptedWalletFile = { seedPhrase: string; midenClientDbContent: string; walletDbContent: string; + accounts: WalletAccount[]; + secretKeysForImportedAccounts: Record; }; export type EncryptedWalletFile = {