Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge keychains #1435

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/core/keychain/KeychainManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('[keychain/KeychainManager] :: should be able to export the seed phrase for

test('[keychain/KeychainManager] :: should be able to add a read only wallet using an address', async () => {
await keychainManager.importKeychain({
type: 'ReadOnlyKeychain',
type: KeychainType.ReadOnlyKeychain,
address: '0x70c16D2dB6B00683b29602CBAB72CE0Dcbc243C4',
});
const accounts = await keychainManager.getAccounts();
Expand All @@ -79,7 +79,10 @@ test('[keychain/KeychainManager] :: should be able to remove an account from a R
});

test('[keychain/KeychainManager] :: should be able to import a wallet using a private key', async () => {
await keychainManager.importKeychain({ type: 'KeyPairKeychain', privateKey });
await keychainManager.importKeychain({
type: KeychainType.KeyPairKeychain,
privateKey,
});
const accounts = await keychainManager.getAccounts();
expect(accounts.length).toBe(2);
expect(isAddress(accounts[1])).toBe(true);
Expand Down
86 changes: 50 additions & 36 deletions src/core/keychain/KeychainManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,24 @@ class KeychainManager {
this.state.initialized = true;
}
},
deriveAccounts: async (
opts: SerializedKeypairKeychain | SerializedHdKeychain,
): Promise<Address[]> => {
deriveAccounts: async (opts: SerializedKeychain): Promise<Address[]> => {
let keychain;
switch (opts.type) {
case KeychainType.HdKeychain:
keychain = new HdKeychain();
await keychain.init(opts as SerializedHdKeychain);
await keychain.init(opts);
break;
case KeychainType.KeyPairKeychain:
keychain = new KeyPairKeychain();
await keychain.init(opts as SerializedKeypairKeychain);
await keychain.init(opts);
break;
case KeychainType.ReadOnlyKeychain:
keychain = new ReadOnlyKeychain();
await keychain.init(opts as unknown as SerializedReadOnlyKeychain);
await keychain.init(opts);
break;
case KeychainType.HardwareWalletKeychain:
keychain = new HardwareWalletKeychain();
await keychain.init(
opts as unknown as SerializedHardwareWalletKeychain,
);
await keychain.init(opts);
break;
default:
throw new Error('Keychain type not recognized.');
Expand All @@ -134,24 +130,24 @@ class KeychainManager {
switch (opts.type) {
case KeychainType.HdKeychain:
keychain = new HdKeychain();
await keychain.init(opts as SerializedHdKeychain);
await keychain.init(opts);
break;
case KeychainType.KeyPairKeychain:
keychain = new KeyPairKeychain();
await keychain.init(opts as SerializedKeypairKeychain);
await keychain.init(opts);
break;
case KeychainType.ReadOnlyKeychain:
keychain = new ReadOnlyKeychain();
await keychain.init(opts as SerializedReadOnlyKeychain);
await keychain.init(opts);
break;
case KeychainType.HardwareWalletKeychain:
keychain = new HardwareWalletKeychain();
await keychain.init(opts as SerializedHardwareWalletKeychain);
await keychain.init(opts);
break;
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 @@ -274,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 @@ -330,13 +333,31 @@ class KeychainManager {
return keychain;
}

async importKeychain(
opts:
| SerializedKeypairKeychain
| SerializedHdKeychain
| SerializedReadOnlyKeychain
| SerializedHardwareWalletKeychain,
): Promise<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 @@ -522,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
6 changes: 3 additions & 3 deletions src/core/keychain/keychainTypes/hardwareWalletKeychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface SerializedHardwareWalletKeychain {
hdPath?: string;
deviceId?: string;
accountsEnabled?: number;
type: string;
type: KeychainType.HardwareWalletKeychain;
autodiscover?: boolean;
wallets?: Array<{ address: Address; index: number; hdPath?: string }>;
accountsDeleted?: Array<number>;
Expand All @@ -24,11 +24,11 @@ export interface SerializedHardwareWalletKeychain {
const privates = new WeakMap();

export class HardwareWalletKeychain implements IKeychain {
type: string;
type: KeychainType.HardwareWalletKeychain =
KeychainType.HardwareWalletKeychain;
vendor?: string;

constructor() {
this.type = KeychainType.HardwareWalletKeychain;
this.vendor = undefined;

privates.set(this, {
Expand Down
22 changes: 15 additions & 7 deletions src/core/keychain/keychainTypes/hdKeychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface SerializedHdKeychain {
mnemonic: string;
hdPath?: SupportedHDPath;
accountsEnabled?: number;
type: string;
type: KeychainType.HdKeychain;
imported?: boolean;
autodiscover?: boolean;
accountsDeleted?: Array<number>;
Expand All @@ -47,11 +47,10 @@ const privates = new WeakMap<
>();

export class HdKeychain implements IKeychain {
type: string;
type: KeychainType.HdKeychain = KeychainType.HdKeychain;
imported: boolean;

constructor() {
this.type = KeychainType.HdKeychain;
this.imported = false;

privates.set(this, {
Expand Down Expand Up @@ -86,10 +85,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
5 changes: 2 additions & 3 deletions src/core/keychain/keychainTypes/keyPairKeychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ import { IKeychain, PrivateKey, TWallet } from '../IKeychain';
import { RainbowSigner } from '../RainbowSigner';

export interface SerializedKeypairKeychain {
type: string;
type: KeychainType.KeyPairKeychain;
privateKey: PrivateKey;
}

const privates = new WeakMap();

export class KeyPairKeychain implements IKeychain {
type: string;
type: KeychainType.KeyPairKeychain = KeychainType.KeyPairKeychain;

constructor() {
this.type = KeychainType.KeyPairKeychain;
privates.set(this, {
wallets: [],
});
Expand Down
8 changes: 2 additions & 6 deletions src/core/keychain/keychainTypes/readOnlyKeychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ import { logger } from '~/logger';
import { IKeychain, PrivateKey } from '../IKeychain';

export interface SerializedReadOnlyKeychain {
type: string;
type: KeychainType.ReadOnlyKeychain;
address: Address;
}
export class ReadOnlyKeychain implements IKeychain {
type: string;
type: KeychainType.ReadOnlyKeychain = KeychainType.ReadOnlyKeychain;
address?: Address;

constructor() {
this.type = KeychainType.ReadOnlyKeychain;
}

init(options: SerializedReadOnlyKeychain) {
this.deserialize(options);
}
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 @@ -24,6 +24,7 @@ import {
importHardwareWallet,
importWallet,
isInitialized,
isMnemonicInVault,
isPasswordSet,
isVaultUnlocked,
lockVault,
Expand Down Expand Up @@ -151,6 +152,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