Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 29 additions & 20 deletions apps/browser-extension-wallet/src/hooks/useWalletManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ export interface UseWalletManager {
reloadWallet: () => Promise<void>;
addAccount: (props: WalletManagerAddAccountProps) => Promise<void>;
getMnemonic: (passphrase: Uint8Array) => Promise<string[]>;
getMnemonicForWallet: (
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>,
passphrase: Uint8Array
) => Promise<string[]>;
getSharedWalletExtendedPublicKey: (passphrase: Uint8Array) => Promise<Wallet.Cardano.Cip1854ExtendedAccountPublicKey>;
enableCustomNode: (network: EnvironmentTypes, value: string) => Promise<void>;
generateSharedWalletKey: GenerateSharedWalletKeyFn;
Expand Down Expand Up @@ -1237,6 +1241,28 @@ export const useWalletManager = (): UseWalletManager => {
}
};

const getMnemonicForWallet = useCallback(
async (wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>, passphrase: Uint8Array) => {
switch (wallet.type) {
case WalletType.InMemory: {
const keyMaterialBytes = await Wallet.KeyManagement.emip3decrypt(
Buffer.from(wallet.encryptedSecrets.keyMaterial, 'hex'),
passphrase
);
const keyMaterialBuffer = Buffer.from(keyMaterialBytes);
const mnemonic = keyMaterialBuffer.toString('utf8').split(' ');
clearBytes(passphrase);
clearBytes(keyMaterialBytes);
return mnemonic;
}
case WalletType.Ledger:
case WalletType.Trezor:
throw new Error('Mnemonic is not available for hardware wallets');
}
},
[]
);

const getMnemonic = useCallback(
async (passphrase: Uint8Array) => {
const { activeBlockchain } = await backgroundService.getBackgroundStorage();
Expand All @@ -1256,27 +1282,9 @@ export const useWalletManager = (): UseWalletManager => {
throw new Error('Wallet not found');
}

switch (wallet.type) {
case WalletType.InMemory: {
const keyMaterialBytes = await Wallet.KeyManagement.emip3decrypt(
Buffer.from(wallet.encryptedSecrets.keyMaterial, 'hex'),
passphrase
);

const keyMaterialBuffer = Buffer.from(keyMaterialBytes);

const mnemonic = keyMaterialBuffer.toString('utf8').split(' ');
clearBytes(passphrase);
clearBytes(keyMaterialBytes);
clearBytes(keyMaterialBuffer);
return mnemonic;
}
case WalletType.Ledger:
case WalletType.Trezor:
throw new Error('Mnemonic is not available for hardware wallets');
}
return getMnemonicForWallet(wallet, passphrase);
},
[backgroundService]
[backgroundService, getMnemonicForWallet]
);

const getSharedWalletExtendedPublicKey = useCallback(
Expand Down Expand Up @@ -1471,6 +1479,7 @@ export const useWalletManager = (): UseWalletManager => {
bitcoinWalletManager,
bitcoinWallet,
getMnemonic,
getMnemonicForWallet,
enableCustomNode,
generateSharedWalletKey,
createMultiSigAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,25 @@ describe('Multi Wallet Setup/Create Wallet', () => {
</AppSettingsProvider>
);

// Initial state - on SelectBlockchain step
await screen.findByTestId('wallet-setup-step-btn-next');
expect(formDirty).toBe(false);

// Navigate to RecoveryPhraseWriteDown step
const nextButton = getNextButton();
fireEvent.click(nextButton);
await screen.findByTestId('wallet-setup-step-btn-next');
expect(formDirty).toBe(false);

// Navigate to RecoveryPhraseInput step - this marks form as dirty
const nextButton2 = getNextButton();
fireEvent.click(nextButton2);
await waitFor(() => expect(formDirty).toBe(true));

// Go back to RecoveryPhraseWriteDown - form should still be dirty
const backButton = getBackButton();
fireEvent.click(backButton);
await waitFor(() => expect(formDirty).toBe(false));
await screen.findByTestId('wallet-setup-step-btn-next');
expect(formDirty).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ChooseRecoveryMethod } from './steps/ChooseRecoveryMethod';
import { SecurePaperWallet } from './steps/SecurePaperWallet';
import { SavePaperWallet } from './steps/SavePaperWallet';
import { SelectBlockchain } from './steps/SelectBlockchain';
import { ReuseRecoveryPhrase } from './steps/ReuseRecoveryPhrase';
import { EnterWalletPassword } from './steps/EnterWalletPassword';
import { RecoveryPhraseError } from './steps/RecoveryPhraseError';

export const CreateWallet = (): JSX.Element => (
<CreateWalletProvider>
Expand All @@ -23,6 +26,12 @@ export const CreateWallet = (): JSX.Element => (
case WalletCreateStep.RecoveryPhraseWriteDown:
case WalletCreateStep.RecoveryPhraseInput:
return <NewRecoveryPhrase />;
case WalletCreateStep.ReuseRecoveryPhrase:
return <ReuseRecoveryPhrase />;
case WalletCreateStep.EnterWalletPassword:
return <EnterWalletPassword />;
case WalletCreateStep.RecoveryPhraseError:
return <RecoveryPhraseError />;
// Common steps
case WalletCreateStep.Setup:
return <Setup />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable unicorn/no-null, complexity */
import { CreateWalletParams } from '@hooks';
import { CreateWalletParams, useLocalStorage } from '@hooks';
import { Wallet } from '@lace/cardano';
import { walletRoutePaths } from '@routes';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
Expand All @@ -10,7 +10,10 @@ import { WalletCreateStep } from './types';
import { RecoveryMethod } from '../types';
import { usePostHogClientContext } from '@providers/PostHogClientProvider';
import { PublicPgpKeyData } from '@src/types';
import { Blockchain } from '@cardano-sdk/web-extension';
import { Blockchain, AnyWallet, WalletConflictError, WalletType } from '@cardano-sdk/web-extension';
import { useObservable } from '@lace/common';
import { walletRepository } from '@lib/wallet-api-ui';
import { getWalletBlockchain } from './get-wallet-blockchain';

type OnNameChange = (state: { name: string }) => void;
interface PgpValidation {
Expand All @@ -32,6 +35,13 @@ interface State {
setPgpValidation: React.Dispatch<React.SetStateAction<PgpValidation>>;
selectedBlockchain: Blockchain;
setSelectedBlockchain: React.Dispatch<React.SetStateAction<Blockchain>>;
walletToReuse: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> | null;
setWalletToReuse: React.Dispatch<
React.SetStateAction<AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> | null>
>;
showRecoveryPhraseError: () => void;
setMnemonic: (mnemonic: string[]) => void;
nonSelectedBlockchainWallets: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>[] | undefined;
}

interface Props {
Expand Down Expand Up @@ -62,11 +72,17 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
} = useHotWalletCreation({
initialMnemonic: Wallet.KeyManagement.util.generateMnemonicWords()
});
const [, { updateLocalStorage: setShowWalletConflictError }] = useLocalStorage('showWalletConflictError', false);
const [selectedBlockchain, setSelectedBlockchain] = useState<Blockchain>('Cardano');
const [step, setStep] = useState<WalletCreateStep>(WalletCreateStep.SelectBlockchain);
const [walletToReuse, setWalletToReuse] = useState<AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> | null>(
null
);
const [recoveryMethod, setRecoveryMethod] = useState<RecoveryMethod>('mnemonic');
const [pgpInfo, setPgpInfo] = useState<PublicPgpKeyData>(INITIAL_PGP_STATE);
const [pgpValidation, setPgpValidation] = useState<PgpValidation>({ error: null, success: null });
const wallets = useObservable(walletRepository.wallets$);

const generateMnemonic = useCallback(() => {
setCreateWalletData((prevState) => ({ ...prevState, mnemonic: Wallet.KeyManagement.util.generateMnemonicWords() }));
}, [setCreateWalletData]);
Expand All @@ -82,6 +98,26 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
console.error('finalizeBitcoinWalletCreation');
}, []);

const nonSelectedBlockchainWallets = useMemo(
() =>
wallets?.filter(
(wallet) =>
getWalletBlockchain(wallet).toLowerCase() !== selectedBlockchain.toLowerCase() &&
wallet.type === WalletType.InMemory
),
[selectedBlockchain, wallets]
);

const showRecoveryPhraseError = useCallback(() => setStep(WalletCreateStep.RecoveryPhraseError), [setStep]);
const setMnemonic = useCallback(
(mnemonic: string[]) =>
setCreateWalletData((prevState) => ({
...prevState,
mnemonic
})),
[setCreateWalletData]
);

const finalizeWalletCreation = useCallback(
async (params: Partial<CreateWalletParams>) => {
const wallet = await createHotWallet({ ...params, blockchain: selectedBlockchain });
Expand All @@ -96,28 +132,65 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
[createHotWallet, selectedBlockchain, sendPostWalletAddAnalytics, postHogActions.create.WALLET_ADDED, pgpInfo]
);

const handleSetupStep = useCallback(
// eslint-disable-next-line consistent-return
async (state: Partial<CreateWalletParams>) => {
if (recoveryMethod !== 'mnemonic' && recoveryMethod !== 'mnemonic-bitcoin') {
return setStep(WalletCreateStep.SavePaperWallet);
}

try {
const finalizationFn = recoveryMethod === 'mnemonic' ? finalizeWalletCreation : finalizeBitcoinWalletCreation;
await finalizationFn(state);
} catch (error) {
if (error instanceof WalletConflictError) {
setShowWalletConflictError(true);
} else {
throw error;
}
} finally {
history.push(walletRoutePaths.assets);
window.location.reload();
}
},
[recoveryMethod, finalizeWalletCreation, finalizeBitcoinWalletCreation, history, setShowWalletConflictError]
);

const next: State['next'] = useCallback(
// eslint-disable-next-line max-statements
async (state) => {
if (state) {
setCreateWalletData((prevState) => ({ ...prevState, ...state }));
}
switch (step) {
case WalletCreateStep.SelectBlockchain: {
setFormDirty(true);
setStep(
paperWalletEnabled ? WalletCreateStep.ChooseRecoveryMethod : WalletCreateStep.RecoveryPhraseWriteDown
);
break;
}
case WalletCreateStep.ChooseRecoveryMethod: {
if (recoveryMethod === 'mnemonic' || recoveryMethod === 'mnemonic-bitcoin') {
setStep(WalletCreateStep.RecoveryPhraseWriteDown);
const nextStep =
nonSelectedBlockchainWallets.length > 0
? WalletCreateStep.ReuseRecoveryPhrase
: WalletCreateStep.RecoveryPhraseWriteDown;
setStep(nextStep);
break;
}
setStep(WalletCreateStep.SecurePaperWallet);
break;
}
case WalletCreateStep.ReuseRecoveryPhrase: {
setStep(WalletCreateStep.EnterWalletPassword);
break;
}
case WalletCreateStep.EnterWalletPassword:
setStep(WalletCreateStep.Setup);
break;
case WalletCreateStep.RecoveryPhraseError: {
setStep(WalletCreateStep.RecoveryPhraseWriteDown);
break;
}
case WalletCreateStep.RecoveryPhraseWriteDown: {
setFormDirty(true);
setStep(WalletCreateStep.RecoveryPhraseInput);
Expand All @@ -129,21 +202,7 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
break;
}
case WalletCreateStep.Setup: {
if (recoveryMethod === 'mnemonic') {
await finalizeWalletCreation(state);
history.push(walletRoutePaths.assets);
window.location.reload();
break;
}

if (recoveryMethod === 'mnemonic-bitcoin') {
await finalizeBitcoinWalletCreation();
history.push(walletRoutePaths.assets);
window.location.reload();
break;
}

setStep(WalletCreateStep.SavePaperWallet);
await handleSetupStep(state);
break;
}
case WalletCreateStep.SavePaperWallet: {
Expand All @@ -158,12 +217,13 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
[
step,
setCreateWalletData,
setFormDirty,
paperWalletEnabled,
recoveryMethod,
setFormDirty,
nonSelectedBlockchainWallets?.length,
handleSetupStep,
finalizeWalletCreation,
history,
finalizeBitcoinWalletCreation
history
]
);

Expand All @@ -186,10 +246,22 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
: history.push(walletRoutePaths.newWallet.root);
break;
}
case WalletCreateStep.ReuseRecoveryPhrase: {
setStep(WalletCreateStep.RecoveryPhraseWriteDown);
break;
}
case WalletCreateStep.SecurePaperWallet: {
setStep(WalletCreateStep.ChooseRecoveryMethod);
break;
}
case WalletCreateStep.EnterWalletPassword: {
setStep(WalletCreateStep.ReuseRecoveryPhrase);
break;
}
case WalletCreateStep.RecoveryPhraseError: {
setStep(WalletCreateStep.ReuseRecoveryPhrase);
break;
}
case WalletCreateStep.RecoveryPhraseInput: {
setFormDirty(false);
generateMnemonic();
Expand Down Expand Up @@ -225,7 +297,12 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
pgpValidation,
setPgpValidation,
selectedBlockchain,
setSelectedBlockchain
setSelectedBlockchain,
setWalletToReuse,
walletToReuse,
showRecoveryPhraseError,
setMnemonic,
nonSelectedBlockchainWallets
}),
[
back,
Expand All @@ -234,13 +311,13 @@ export const CreateWalletProvider = ({ children }: Props): React.ReactElement =>
onNameChange,
step,
recoveryMethod,
setRecoveryMethod,
pgpInfo,
setPgpInfo,
pgpValidation,
setPgpValidation,
selectedBlockchain,
setSelectedBlockchain
walletToReuse,
showRecoveryPhraseError,
setMnemonic,
nonSelectedBlockchainWallets
]
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AnyWallet, Blockchain } from '@cardano-sdk/web-extension';
import { Wallet } from '@lace/cardano';

type AnyWalletWithBlockchainName = AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> & {
blockchainName: string;
};

const hasBlockchainName = (
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>
): wallet is AnyWalletWithBlockchainName => typeof (wallet as Record<string, unknown>)?.blockchainName === 'string';

export const getWalletBlockchain = (wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>): Blockchain =>
hasBlockchainName(wallet) ? (wallet.blockchainName as Blockchain) : 'Cardano';
Loading
Loading