Skip to content

Commit d87b5f3

Browse files
committed
feat: enable adding public accounts via private key
1 parent e010848 commit d87b5f3

File tree

9 files changed

+102
-144
lines changed

9 files changed

+102
-144
lines changed

src/app/pages/ImportAccount.tsx

Lines changed: 9 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import React, { FC, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+
import React, { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
22

3-
import { Controller, useForm } from 'react-hook-form';
3+
import { useForm } from 'react-hook-form';
4+
import { useTranslation } from 'react-i18next';
45

56
import Alert from 'app/atoms/Alert';
67
import FormField from 'app/atoms/FormField';
78
import FormSubmitButton from 'app/atoms/FormSubmitButton';
8-
import NoSpaceField from 'app/atoms/NoSpaceField';
9-
import TabSwitcher from 'app/atoms/TabSwitcher';
109
import PageLayout from 'app/layouts/PageLayout';
11-
import { useTranslation } from 'react-i18next';
1210
import { useMidenContext, useAllAccounts } from 'lib/miden/front';
1311
import { navigate } from 'lib/woozie';
1412

@@ -18,12 +16,6 @@ type ImportAccountProps = {
1816
tabSlug: string | null;
1917
};
2018

21-
interface ImportTabDescriptor {
22-
slug: string;
23-
i18nKey: string;
24-
Form: FC<{}>;
25-
}
26-
2719
const ImportAccount: FC<ImportAccountProps> = ({ tabSlug }) => {
2820
const { t } = useTranslation();
2921
const allAccounts = useAllAccounts();
@@ -39,27 +31,6 @@ const ImportAccount: FC<ImportAccountProps> = ({ tabSlug }) => {
3931
prevAccLengthRef.current = accLength;
4032
}, [allAccounts, updateCurrentAccount]);
4133

42-
const allTabs = useMemo(
43-
() =>
44-
[
45-
{
46-
slug: 'private-key',
47-
i18nKey: 'privateKey',
48-
Form: ByPrivateKeyForm
49-
},
50-
{
51-
slug: 'watch-only',
52-
i18nKey: 'watchOnlyAccount',
53-
Form: WatchOnlyForm
54-
}
55-
].filter((x): x is ImportTabDescriptor => !!x),
56-
[]
57-
);
58-
const { slug, Form } = useMemo(() => {
59-
const tab = tabSlug ? allTabs.find(currentTab => currentTab.slug === tabSlug) : null;
60-
return tab ?? allTabs[0];
61-
}, [allTabs, tabSlug]);
62-
6334
return (
6435
<PageLayout
6536
pageTitle={
@@ -69,9 +40,7 @@ const ImportAccount: FC<ImportAccountProps> = ({ tabSlug }) => {
6940
}
7041
>
7142
<div className="p-4">
72-
<TabSwitcher className="m-4" tabs={allTabs} activeTabSlug={slug} urlPrefix="/import-account" />
73-
74-
<Form />
43+
<ByPrivateKeyForm />
7544
</div>
7645
</PageLayout>
7746
);
@@ -86,7 +55,7 @@ interface ByPrivateKeyFormData {
8655

8756
const ByPrivateKeyForm: FC = () => {
8857
const { t } = useTranslation();
89-
const { importAccount } = useMidenContext();
58+
const { importPublicAccountByPrivateKey } = useMidenContext();
9059

9160
const {
9261
register,
@@ -96,12 +65,12 @@ const ByPrivateKeyForm: FC = () => {
9665
const [error, setError] = useState<ReactNode>(null);
9766

9867
const onSubmit = useCallback(
99-
async ({ privateKey, encPassword }: ByPrivateKeyFormData) => {
68+
async ({ privateKey }: ByPrivateKeyFormData) => {
10069
if (isSubmitting) return;
10170

10271
setError(null);
10372
try {
104-
await importAccount(privateKey.replace(/\s/g, ''), encPassword);
73+
await importPublicAccountByPrivateKey(privateKey);
10574
} catch (err: any) {
10675
console.error(err);
10776

@@ -110,7 +79,7 @@ const ByPrivateKeyForm: FC = () => {
11079
setError(err.message);
11180
}
11281
},
113-
[importAccount, isSubmitting, setError]
82+
[importPublicAccountByPrivateKey, isSubmitting, setError]
11483
);
11584

11685
return (
@@ -129,7 +98,7 @@ const ByPrivateKeyForm: FC = () => {
12998
{t('privateKey')}
13099
</div>
131100
}
132-
placeholder={t('privateKeyInputPlaceholder')}
101+
placeholder={'eg. 3b6a27bccebfb65e3...'}
133102
errorCaption={errors.privateKey?.message}
134103
className="resize-none"
135104
onPaste={() => clearClipboard()}
@@ -154,103 +123,3 @@ const ByPrivateKeyForm: FC = () => {
154123
</form>
155124
);
156125
};
157-
158-
interface WatchOnlyFormData {
159-
viewKey: string;
160-
}
161-
162-
const WatchOnlyForm: FC = () => {
163-
const { t } = useTranslation();
164-
const { importWatchOnlyAccount } = useMidenContext();
165-
166-
const {
167-
handleSubmit,
168-
control,
169-
setValue,
170-
getValues,
171-
trigger,
172-
formState: { errors, isSubmitting }
173-
} = useForm<WatchOnlyFormData>({
174-
mode: 'onChange'
175-
});
176-
const [error, setError] = useState<ReactNode>(null);
177-
178-
const addressFieldRef = useRef<HTMLTextAreaElement>(null);
179-
180-
const cleanViewKeyField = useCallback(() => {
181-
setValue('viewKey', '');
182-
trigger('viewKey');
183-
}, [setValue, trigger]);
184-
185-
const onSubmit = useCallback(
186-
async ({ viewKey }: WatchOnlyFormData) => {
187-
if (isSubmitting) return;
188-
189-
setError(null);
190-
191-
try {
192-
await importWatchOnlyAccount(viewKey);
193-
} catch (err: any) {
194-
console.error(err);
195-
196-
// Human delay
197-
await new Promise(r => setTimeout(r, 300));
198-
setError(err.message);
199-
}
200-
},
201-
[importWatchOnlyAccount, isSubmitting, setError]
202-
);
203-
204-
return (
205-
<form className="w-full max-w-sm mx-auto my-8" onSubmit={handleSubmit(onSubmit)} style={{ minHeight: '325px' }}>
206-
{error && <Alert type="error" title={t('error')} description={error} autoFocus className="mb-6" />}
207-
208-
<Controller
209-
name="viewKey"
210-
control={control}
211-
rules={{
212-
required: true,
213-
validate: (value: any) => true
214-
}}
215-
render={({ field }) => (
216-
<NoSpaceField
217-
{...field}
218-
ref={addressFieldRef}
219-
onFocus={() => addressFieldRef.current?.focus()}
220-
textarea
221-
rows={1}
222-
cleanable={Boolean(getValues().viewKey)}
223-
onClean={cleanViewKeyField}
224-
id="watch-viewKey"
225-
label={
226-
<div className="font-medium -mb-2" style={{ fontSize: '14px', lineHeight: '20px' }}>
227-
{t('viewKeyWatchOnly')}
228-
</div>
229-
}
230-
placeholder={t('viewKeyInputPlaceholder')}
231-
errorCaption={errors.viewKey?.message}
232-
className="resize-none"
233-
/>
234-
)}
235-
/>
236-
<div className="mb-6 text-gray-200" style={{ fontSize: '12px', lineHeight: '16px' }}>
237-
{t('viewKeyInputDescription')}
238-
</div>
239-
240-
<FormSubmitButton
241-
className="capitalize w-full justify-center"
242-
style={{
243-
fontSize: '18px',
244-
lineHeight: '24px',
245-
paddingLeft: '0.5rem',
246-
paddingRight: '0.5rem',
247-
paddingTop: '12px',
248-
paddingBottom: '12px'
249-
}}
250-
loading={isSubmitting}
251-
>
252-
{t('importAccount')}
253-
</FormSubmitButton>
254-
</form>
255-
);
256-
};

src/app/pages/SelectAccount.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const SelectAccount: FC = () => {
2222
navigate('/create-account');
2323
};
2424

25+
const onImportAccountClick = () => {
26+
navigate('/import-account');
27+
};
28+
2529
return (
2630
<PageLayout
2731
pageTitle={
@@ -86,6 +90,12 @@ const SelectAccount: FC = () => {
8690
</div>
8791

8892
<div className="flex flex-col w-full p-6 md:px-8 m-auto">
93+
<Button
94+
title={t('importAccount')}
95+
variant={ButtonVariant.Primary}
96+
onClick={onImportAccountClick}
97+
className="mb-4"
98+
/>
8999
<Button title={t('addAccount')} variant={ButtonVariant.Secondary} onClick={onAddAccountClick} />
90100
</div>
91101
</PageLayout>

src/lib/miden/back/actions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,13 @@ export function editAccount(accPublicKey: string, name: string) {
158158
});
159159
}
160160

161-
export function importAccount(privateKey: string, encPassword?: string) {}
161+
export function importAccount(privateKey: string) {
162+
console.log('importAccount called');
163+
return withUnlocked(async ({ vault }) => {
164+
const accounts = await vault.importAccount(privateKey);
165+
accountsUpdated({ accounts });
166+
});
167+
}
162168

163169
export function importMnemonicAccount(mnemonic: string, password?: string, derivationPath?: string) {}
164170

src/lib/miden/back/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise<W
8787
type: WalletMessageType.EditAccountResponse
8888
};
8989
case WalletMessageType.ImportAccountRequest:
90-
await Actions.importAccount(req.privateKey, req.encPassword);
90+
await Actions.importAccount(req.privateKey);
9191
return {
9292
type: WalletMessageType.ImportAccountResponse
9393
};

src/lib/miden/back/vault.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,42 @@ export class Vault {
365365
});
366366
}
367367

368+
async importAccount(privateKey: string) {
369+
return withError('Failed to import account', async () => {
370+
const allAccounts = await fetchAndDecryptOneWithLegacyFallBack<WalletAccount[]>(accountsStrgKey, this.passKey);
371+
const secretKey = SecretKey.deserialize(new Uint8Array(Buffer.from(privateKey, 'hex')));
372+
const pubKeyWord = secretKey.publicKey().toCommitment();
373+
374+
const pubKeyHex = pubKeyWord.toHex().slice(2); // remove '0x' prefix
375+
console.log({ pubKeyHex });
376+
const publicKey = await withWasmClientLock(async () => {
377+
const midenClient = await getMidenClient();
378+
return await midenClient.importPublicAccountFromPrivateKey(secretKey);
379+
});
380+
console.log({ publicKey });
381+
const newAccount: WalletAccount = {
382+
publicKey,
383+
name: getNewAccountName(allAccounts),
384+
isPublic: true,
385+
type: WalletType.OnChain,
386+
hdIndex: -1 // -1 indicates imported account
387+
};
388+
389+
const newAllAccounts = concatAccount(allAccounts, newAccount);
390+
391+
await encryptAndSaveMany(
392+
[
393+
[accPubKeyStrgKey(pubKeyHex), pubKeyHex],
394+
[accountsStrgKey, newAllAccounts],
395+
[accAuthSecretKeyStrgKey(pubKeyHex), privateKey]
396+
],
397+
this.passKey
398+
);
399+
400+
return newAllAccounts;
401+
});
402+
}
403+
368404
async getCurrentAccount() {
369405
const currAccountPubkey = await getPlain<string>(currentAccPubKeyStrgKey);
370406
const allAccounts = await this.fetchAccounts();

src/lib/miden/front/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { WalletRequest, WalletResponse, WalletSettings, WalletStatus } from 'lib
88
import { useWalletStore } from 'lib/store';
99
import { WalletType } from 'screens/onboarding/types';
1010

11+
import { store } from '../back/store';
1112
import { MidenState } from '../types';
1213
import { AutoSync } from './autoSync';
1314

@@ -64,6 +65,7 @@ export const [MidenContextProvider, useMidenContext] = constate(() => {
6465
const storeGetAllDAppSessions = useWalletStore(s => s.getAllDAppSessions);
6566
const storeRemoveDAppSession = useWalletStore(s => s.removeDAppSession);
6667
const storeResetConfirmation = useWalletStore(s => s.resetConfirmation);
68+
const storeImportAccountByPrivateKey = useWalletStore(s => s.importPublicAccountByPrivateKey);
6769

6870
// Build the state object for backward compatibility
6971
const state: MidenState = useMemo(
@@ -267,7 +269,12 @@ export const [MidenContextProvider, useMidenContext] = constate(() => {
267269
const confirmDAppBulkTransactions = useCallback(async (id: string, confirmed: boolean, delegate: boolean) => {}, []);
268270
const confirmDAppDeploy = useCallback(async (id: string, confirmed: boolean, delegate: boolean) => {}, []);
269271
const getOwnedRecords = useCallback(async (accPublicKey: string) => {}, []);
270-
272+
const importPublicAccountByPrivateKey = useCallback(
273+
async (privateKey: string) => {
274+
await storeImportAccountByPrivateKey(privateKey);
275+
},
276+
[storeImportAccountByPrivateKey]
277+
);
271278
return {
272279
state,
273280
// Aliases
@@ -318,7 +325,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => {
318325
removeDAppSession,
319326
decryptCiphertexts,
320327
getOwnedRecords,
321-
importWalletFromClient
328+
importWalletFromClient,
329+
importPublicAccountByPrivateKey
322330
};
323331
});
324332

src/lib/miden/sdk/miden-client-interface.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,24 @@ export class MidenClientInterface {
148148
return getBech32AddressFromAccountId(account.id());
149149
}
150150

151+
async importPublicAccountFromPrivateKey(privateKey: SecretKey): Promise<string> {
152+
console.log('Importing public account from private key, secret key:', privateKey);
153+
const accountBuilder = new AccountBuilder(new Uint8Array(32).fill(0))
154+
.accountType(AccountType.RegularAccountImmutableCode)
155+
.storageMode(AccountStorageMode.public())
156+
.withAuthComponent(AccountComponent.createAuthComponent(privateKey))
157+
.withBasicWalletComponent();
158+
const wallet = accountBuilder.build().account;
159+
console.log('Importing public account from private key, account id:', wallet.id().toString());
160+
// add the secret key to the web client's keystore
161+
await this.webClient.addAccountSecretKeyToWebStore(privateKey);
162+
// register the new account in the web client
163+
await this.webClient.importAccountById(wallet.id());
164+
const walletId = getBech32AddressFromAccountId(wallet.id());
165+
166+
return walletId;
167+
}
168+
151169
// TODO: is this method even used?
152170
async consumeTransaction(accountId: string, listOfNoteIds: string[], delegateTransaction?: boolean) {
153171
console.log('Consuming transaction...');

src/lib/store/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ export const useWalletStore = create<WalletStore>()(
185185
return res.privateKey;
186186
},
187187

188+
importPublicAccountByPrivateKey: async privateKey => {
189+
console.log(privateKey);
190+
const res = await request({
191+
type: WalletMessageType.ImportAccountRequest,
192+
privateKey
193+
});
194+
console.log(res);
195+
assertResponse(res.type === WalletMessageType.ImportAccountResponse);
196+
},
197+
188198
// Settings actions
189199
updateSettings: async newSettings => {
190200
const { settings } = get();

src/lib/store/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface WalletActions {
8282
editAccountName: (accountPublicKey: string, name: string) => Promise<void>;
8383
revealMnemonic: (password: string) => Promise<string>;
8484
revealPrivateKey: (accountPublicKey: string, password: string) => Promise<string>;
85+
importPublicAccountByPrivateKey: (privateKey: string) => Promise<void>;
8586

8687
// Settings actions
8788
updateSettings: (newSettings: Partial<WalletSettings>) => Promise<void>;

0 commit comments

Comments
 (0)