Skip to content

Commit

Permalink
Merge keychains (#1435)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Sinclair <[email protected]>
  • Loading branch information
greg-schrammel and DanielSinclair authored Aug 16, 2024
1 parent 81c79e3 commit 7d72e35
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 35 deletions.
56 changes: 40 additions & 16 deletions src/core/keychain/KeychainManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class KeychainManager {
default:
throw new Error('Keychain type not recognized.');
}
await this.overrideReadOnlyKeychains(keychain);
await this.mergeKeychains(keychain);
await this.checkForDuplicateInKeychain(keychain);
this.state.keychains.push(keychain as Keychain);
return keychain;
Expand Down Expand Up @@ -270,20 +270,27 @@ class KeychainManager {
return false;
}

async overrideReadOnlyKeychains(incomingKeychain: Keychain) {
async mergeKeychains(incomingKeychain: Keychain) {
if (incomingKeychain.type === KeychainType.ReadOnlyKeychain) return;

const currentAccounts = await this.getAccounts();
const incomingAccounts = await incomingKeychain.getAccounts();
const conflictingAccounts = incomingAccounts.filter((acc) =>
currentAccounts.includes(acc),
);
await Promise.all(
conflictingAccounts.map(async (acc) => {
const wallet = await this.getWallet(acc);
const isReadOnly = wallet.type === KeychainType.ReadOnlyKeychain;
if (isReadOnly) this.removeAccount(acc);
}),
);

for (const account of conflictingAccounts) {
const wallet = await this.getWallet(account);
// the incoming is not readOnly, so if the conflicting is, remove it to leave the one with higher privilages
// if the incoming is a hd wallet that derives an account in which the pk is already in the vault, remove this pk to leave the hd as the main
if (
wallet.type === KeychainType.ReadOnlyKeychain ||
(incomingKeychain.type === KeychainType.HdKeychain &&
wallet.type === KeychainType.KeyPairKeychain)
) {
this.removeAccount(account);
}
}
}

async checkForDuplicateInKeychain(keychain: Keychain) {
Expand Down Expand Up @@ -326,7 +333,31 @@ class KeychainManager {
return keychain;
}

async removeKeychain(keychain: Keychain) {
this.state.keychains = this.state.keychains.filter((k) => k !== keychain);
}

async isMnemonicInVault(mnemonic: string) {
for (const k of this.state.keychains) {
if (k.type != KeychainType.HdKeychain) continue;
if ((await k.exportKeychain()) == mnemonic) return true;
}
return false;
}

async importKeychain(opts: SerializedKeychain): Promise<Keychain> {
if (opts.type === KeychainType.KeyPairKeychain) {
const newAccount = (await this.deriveAccounts(opts))[0];
const existingAccounts = await this.getAccounts();
if (existingAccounts.includes(newAccount)) {
const existingKeychain = await this.getKeychain(newAccount);
// if the account is already in the vault (like in a hd keychain), we don't want to import it again
// UNLESS it's a readOnlyKeychain, which we DO WANT to override it, importing the pk
if (existingKeychain.type != KeychainType.ReadOnlyKeychain)
return existingKeychain;
}
}

const result = await privates.get(this).restoreKeychain({
...opts,
imported: true,
Expand Down Expand Up @@ -512,13 +543,6 @@ class KeychainManager {
throw new Error('No keychain found for account');
}

isAccountInReadOnlyKeychain(address: Address): Keychain | undefined {
for (const keychain of this.state.keychains) {
if (keychain.type !== KeychainType.ReadOnlyKeychain) continue;
if ((keychain as ReadOnlyKeychain).address === address) return keychain;
}
}

async getSigner(address: Address) {
const keychain = await this.getKeychain(address);
return keychain.getSigner(address);
Expand Down
17 changes: 13 additions & 4 deletions src/core/keychain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { addHexPrefix } from '../utils/hex';

import { keychainManager } from './KeychainManager';
import { SerializedKeypairKeychain } from './keychainTypes/keyPairKeychain';

interface TypedDataTypes {
EIP712Domain: MessageTypeProperty[];
Expand Down Expand Up @@ -99,6 +100,10 @@ export const createWallet = async (): Promise<Address> => {
return accounts[0];
};

export const isMnemonicInVault = async (mnemonic: EthereumWalletSeed) => {
return keychainManager.isMnemonicInVault(mnemonic);
};

export const deriveAccountsFromSecret = async (
secret: EthereumWalletSeed,
): Promise<Address[]> => {
Expand Down Expand Up @@ -168,12 +173,16 @@ export const importWallet = async (
return address;
}
case EthereumWalletType.privateKey: {
const keychain = await keychainManager.importKeychain({
const opts: SerializedKeypairKeychain = {
type: KeychainType.KeyPairKeychain,
privateKey: secret,
});
const address = (await keychain.getAccounts())[0];
return address;
};
const newAccount = (await keychainManager.deriveAccounts(opts))[0];

await keychainManager.importKeychain(opts);
// returning the derived address instead of the first from the keychain,
// because this pk could have been elevated to hd while importing
return newAccount;
}
case EthereumWalletType.readOnly: {
const keychain = await keychainManager.importKeychain({
Expand Down
17 changes: 13 additions & 4 deletions src/core/keychain/keychainTypes/hdKeychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,19 @@ export class HdKeychain implements IKeychain {
const _privates = privates.get(this)!;
const derivedWallet = _privates.deriveWallet(index);

// if account already exists in a readonly keychain, remove it
keychainManager
.isAccountInReadOnlyKeychain(derivedWallet.address)
?.removeAccount(derivedWallet.address);
// if account already exists in a another keychain, remove it
keychainManager.state.keychains.forEach(async (keychain) => {
const keychainAccounts = await keychain.getAccounts();
if (keychainAccounts.includes(derivedWallet.address)) {
keychain.removeAccount(derivedWallet.address);
if (
keychain.type == KeychainType.ReadOnlyKeychain ||
keychain.type == KeychainType.KeyPairKeychain
) {
keychainManager.removeKeychain(keychain);
}
}
});

const wallet = new Wallet(
derivedWallet.privateKey as BytesLike,
Expand Down
1 change: 1 addition & 0 deletions src/core/types/walletActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum walletActions {
unlock = 'unlock',
verify_password = 'verify_password',
derive_accounts_from_secret = 'derive_accounts_from_secret',
is_mnemonic_in_vault = 'is_mnemonic_in_vault',
create = 'create',
import = 'import',
add = 'add',
Expand Down
4 changes: 4 additions & 0 deletions src/entries/background/handlers/handleWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
importHardwareWallet,
importWallet,
isInitialized,
isMnemonicInVault,
isPasswordSet,
isVaultUnlocked,
lockVault,
Expand Down Expand Up @@ -152,6 +153,9 @@ export const handleWallets = () =>
payload as EthereumWalletSeed,
);
break;
case 'is_mnemonic_in_vault':
response = await isMnemonicInVault(payload as EthereumWalletSeed);
break;
case 'get_accounts':
response = await getAccounts();
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ export const ImportWalletSelection = ({ onboarding = false }) => {
const onImport = () =>
importSecrets({ secrets }).then(() => {
setCurrentAddress(accountsToImport[0]);
if (onboarding) navigate(ROUTES.CREATE_PASSWORD);
if (onboarding)
navigate(ROUTES.CREATE_PASSWORD, {
state: { backTo: ROUTES.IMPORT__SEED },
});
else navigate(ROUTES.HOME);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ export function ImportWalletSelectionEdit({ onboarding = false }) {
(a) => !accountsIgnored.includes(a),
);
setCurrentAddress(importedAccounts[0]);
if (onboarding) navigate(ROUTES.CREATE_PASSWORD);
else navigate(ROUTES.HOME);
if (onboarding) {
navigate(ROUTES.CREATE_PASSWORD, {
state: { backTo: ROUTES.IMPORT__SEED },
});
} else navigate(ROUTES.HOME);
});

return (
Expand Down
24 changes: 17 additions & 7 deletions src/entries/popup/components/ImportWallet/ImportWalletViaSeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Text,
textStyles,
} from '~/design-system';
import { triggerAlert } from '~/design-system/components/Alert/Alert';
import { accentSelectionStyle } from '~/design-system/components/Input/Input.css';
import {
transformScales,
Expand All @@ -36,6 +37,7 @@ import {
removeImportWalletSecrets,
setImportWalletSecrets,
} from '../../handlers/importWalletSecrets';
import { isMnemonicInVault } from '../../handlers/wallet';
import { useRainbowNavigate } from '../../hooks/useRainbowNavigate';
import { ROUTES } from '../../urls';

Expand Down Expand Up @@ -166,6 +168,9 @@ const secretsReducer = (
return newSecrets;
};

const emptySecrets12 = Array.from({ length: 12 }).map(() => '');
const emptySecrets24 = Array.from({ length: 24 }).map(() => '');

const ImportWalletViaSeed = () => {
const navigate = useRainbowNavigate();
const location = useLocation();
Expand All @@ -174,16 +179,13 @@ const ImportWalletViaSeed = () => {
const [globalError, setGlobalError] = useState(false);
const [invalidWords, setInvalidWords] = useState<number[]>([]);
const [visibleInput, setVisibleInput] = useState<number | null>(null);
const [secrets, setSecrets] = useReducer(
secretsReducer,
Array.from({ length: 12 }).map(() => ''),
);
const [secrets, setSecrets] = useReducer(secretsReducer, emptySecrets12);

const toggleWordLength = useCallback(() => {
if (secrets.length === 12) {
setSecrets(Array.from({ length: 24 }).map(() => ''));
setSecrets(emptySecrets12);
} else {
setSecrets(Array.from({ length: 12 }).map(() => ''));
setSecrets(emptySecrets24);
}
setInvalidWords([]);
setGlobalError(false);
Expand Down Expand Up @@ -228,10 +230,18 @@ const ImportWalletViaSeed = () => {
);

const handleImportWallet = useCallback(async () => {
if (await isMnemonicInVault(secrets.join(' '))) {
triggerAlert({
text: i18n.t('import_wallet_via_seed.duplicate_seed'),
});
setSecrets(emptySecrets12);
return;
}

return navigate(
onboarding ? ROUTES.IMPORT__SELECT : ROUTES.NEW_IMPORT_WALLET_SELECTION,
);
}, [navigate, onboarding]);
}, [navigate, onboarding, secrets]);

const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
Expand Down
3 changes: 3 additions & 0 deletions src/entries/popup/handlers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ export const updatePassword = async (password: string, newPassword: string) => {
export const deriveAccountsFromSecret = async (secret: string) =>
walletAction<Address[]>('derive_accounts_from_secret', secret);

export const isMnemonicInVault = async (secret: string) =>
walletAction<boolean>('is_mnemonic_in_vault', secret);

export const verifyPassword = async (password: string) =>
walletAction<boolean>('verify_password', password);

Expand Down
3 changes: 2 additions & 1 deletion static/json/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,8 @@
"import_wallet_group": "Import wallet group",
"n_words_might_be_wrong": "%{n} words may be misspelled or incorrect.",
"1_word_might_be_wrong": "1 word may be misspelled or incorrect.",
"couldnt_paste": "We couldn’t paste your Secret Recovery Phrase"
"couldnt_paste": "We couldn’t paste your Secret Recovery Phrase",
"duplicate_seed": "This seed is imported already"
},
"import_wallet_via_private_key": {
"title": "Import Your Wallet",
Expand Down

0 comments on commit 7d72e35

Please sign in to comment.