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
4 changes: 4 additions & 0 deletions packages/keyring-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ export type {
export type * from './types.js';
export * from './errors.js';
export { KeyringControllerErrorMessage } from './constants.js';
export {
selectHdKeyringEntropySourceIds,
selectPrimaryHdKeyringEntropySourceId,
} from './selectors.js';
149 changes: 149 additions & 0 deletions packages/keyring-controller/src/selectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { KeyringTypes } from './KeyringController.js';
import {
selectHdKeyringEntropySourceIds,
selectPrimaryHdKeyringEntropySourceId,
} from './selectors.js';

describe('KeyringController selectors', () => {
describe('selectHdKeyringEntropySourceIds', () => {
it('selects all HD keyring IDs', () => {
const sourceIds = selectHdKeyringEntropySourceIds({
isUnlocked: true,
keyrings: [
{
accounts: [],
type: KeyringTypes.hd.toString(),
metadata: {
id: '1',
name: '1',
},
},
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '2',
name: '2',
},
},
{
accounts: [],
type: KeyringTypes.hd.toString(),
metadata: {
id: '3',
name: '3',
},
},
],
});

expect(sourceIds).toStrictEqual(['1', '3']);
});

it('returns empty array when there are no keyrings', () => {
const sourceIds = selectHdKeyringEntropySourceIds({
isUnlocked: true,
keyrings: [],
});

expect(sourceIds).toStrictEqual([]);
});

it('returns empty array when there are only non-HD keyrings', () => {
const sourceIds = selectHdKeyringEntropySourceIds({
isUnlocked: true,
keyrings: [
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '1',
name: '1',
},
},
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '2',
name: '2',
},
},
],
});

expect(sourceIds).toStrictEqual([]);
});
});

describe('selectPrimaryHdKeyringEntropySourceId', () => {
it('selects the first HD keyring ID', () => {
const primaryId = selectPrimaryHdKeyringEntropySourceId({
isUnlocked: true,
keyrings: [
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '1',
name: '1',
},
},
{
accounts: [],
type: KeyringTypes.hd.toString(),
metadata: {
id: '2',
name: '2',
},
},
{
accounts: [],
type: KeyringTypes.hd.toString(),
metadata: {
id: '3',
name: '3',
},
},
],
});

expect(primaryId).toBe('2');
});

it('returns undefined when there are no keyrings', () => {
const primaryId = selectPrimaryHdKeyringEntropySourceId({
isUnlocked: true,
keyrings: [],
});

expect(primaryId).toBeUndefined();
});

it('returns undefined when there are only non-HD keyrings', () => {
const primaryId = selectPrimaryHdKeyringEntropySourceId({
isUnlocked: true,
keyrings: [
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '1',
name: '1',
},
},
{
accounts: [],
type: 'NOT HD',
metadata: {
id: '2',
name: '2',
},
},
],
});

expect(primaryId).toBeUndefined();
});
});
});
25 changes: 25 additions & 0 deletions packages/keyring-controller/src/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { KeyringControllerState } from './KeyringController.js';
import { KeyringTypes } from './KeyringController.js';

/**
* Select all HD keyring entropy source IDs.
*
* @param state - KeyringController state
* @returns A list of HD keyring entropy source IDs.
*/
export const selectHdKeyringEntropySourceIds = (
state: KeyringControllerState,
): string[] =>
state.keyrings
.filter((keyring) => keyring.type === KeyringTypes.hd.toString())
.map((keyring) => keyring.metadata.id);

/**
* Select the primary HD keyring entropy source ID.
*
* @param state - KeyringController state
* @returns The primary HD keyring entropy source ID.
*/
export const selectPrimaryHdKeyringEntropySourceId = (
state: KeyringControllerState,
): string | undefined => selectHdKeyringEntropySourceIds(state)[0];
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ControllerStateChangeEvent,
StateMetadata,
} from '@metamask/base-controller';
import { selectHdKeyringEntropySourceIds } from '@metamask/keyring-controller';
import type {
KeyringControllerGetStateAction,
KeyringControllerLockEvent,
Expand Down Expand Up @@ -36,10 +37,7 @@ import {
PairConflictError,
} from '../../sdk/index.js';
import type { MetaMetricsAuth } from '../../shared/types/services.js';
import {
getHdKeyringEntropySourceIds,
getPrimaryHdKeyringEntropySourceId,
} from '../../shared/utils/entropy-source.js';
import { getPrimaryHdKeyringEntropySourceId } from '../../shared/utils/entropy-source.js';
import { getHdKeyringSeed } from '../../shared/utils/hd-keyring-seed.js';
import {
getMessageSigningPublicKey,
Expand Down Expand Up @@ -351,8 +349,8 @@ export class AuthenticationController extends BaseController<
* @returns The HD keyring metadata IDs, primary first.
*/
#getHdKeyringEntropySourceIds(): string[] {
const { keyrings } = this.messenger.call('KeyringController:getState');
return getHdKeyringEntropySourceIds(keyrings);
const keyringState = this.messenger.call('KeyringController:getState');
return selectHdKeyringEntropySourceIds(keyringState);
}

/**
Expand All @@ -365,8 +363,8 @@ export class AuthenticationController extends BaseController<
* the wallet is unlocked.
*/
#getPrimaryEntropySourceId(): string {
const { keyrings } = this.messenger.call('KeyringController:getState');
return getPrimaryHdKeyringEntropySourceId(keyrings);
const keyringState = this.messenger.call('KeyringController:getState');
return getPrimaryHdKeyringEntropySourceId(keyringState);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
KeyringControllerUnlockEvent,
KeyringControllerWithKeyringV2UnsafeAction,
} from '@metamask/keyring-controller';
import { selectHdKeyringEntropySourceIds } from '@metamask/keyring-controller';
import type { Messenger } from '@metamask/messenger';

import type {
Expand All @@ -32,10 +33,7 @@ import type {
} from '../../sdk/index.js';
import { Env, UserStorage } from '../../sdk/index.js';
import type { NativeScrypt } from '../../shared/types/encryption.js';
import {
getHdKeyringEntropySourceIds,
getPrimaryHdKeyringEntropySourceId,
} from '../../shared/utils/entropy-source.js';
import { getPrimaryHdKeyringEntropySourceId } from '../../shared/utils/entropy-source.js';
import { EventQueue } from '../../shared/utils/event-queue.js';
import { getHdKeyringSeed } from '../../shared/utils/hd-keyring-seed.js';
import { signMessageWithMessageSigningKey } from '../../shared/utils/message-signing.js';
Expand Down Expand Up @@ -549,8 +547,8 @@ export class UserStorageController extends BaseController<
* @returns The HD keyring metadata IDs, primary first.
*/
#getHdKeyringEntropySourceIds(): string[] {
const { keyrings } = this.messenger.call('KeyringController:getState');
return getHdKeyringEntropySourceIds(keyrings);
const keyringState = this.messenger.call('KeyringController:getState');
return selectHdKeyringEntropySourceIds(keyringState);
}

/**
Expand All @@ -564,8 +562,8 @@ export class UserStorageController extends BaseController<
* while the wallet is unlocked.
*/
#getPrimaryEntropySourceId(): string {
const { keyrings } = this.messenger.call('KeyringController:getState');
return getPrimaryHdKeyringEntropySourceId(keyrings);
const keyringState = this.messenger.call('KeyringController:getState');
return getPrimaryHdKeyringEntropySourceId(keyringState);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { KeyringTypes } from '@metamask/keyring-controller';
import type { KeyringObject } from '@metamask/keyring-controller';

import {
getHdKeyringEntropySourceIds,
getPrimaryHdKeyringEntropySourceId,
} from './entropy-source.js';
import { getPrimaryHdKeyringEntropySourceId } from './entropy-source.js';

const hd = (id: string): KeyringObject => ({
type: KeyringTypes.hd,
Expand All @@ -13,42 +10,20 @@ const hd = (id: string): KeyringObject => ({
});

describe('entropy-source utils', () => {
describe('getHdKeyringEntropySourceIds', () => {
it('returns HD keyring metadata IDs, primary first', () => {
const keyrings: KeyringObject[] = [
hd('primary'),
{
type: 'Simple Key Pair',
accounts: [],
metadata: { id: 'simple', name: '' },
},
hd('secondary'),
];

expect(getHdKeyringEntropySourceIds(keyrings)).toStrictEqual([
'primary',
'secondary',
]);
});

it('returns an empty array when keyrings are missing or empty', () => {
expect(getHdKeyringEntropySourceIds(undefined)).toStrictEqual([]);
expect(getHdKeyringEntropySourceIds(null)).toStrictEqual([]);
expect(getHdKeyringEntropySourceIds([])).toStrictEqual([]);
});
});

describe('getPrimaryHdKeyringEntropySourceId', () => {
it('returns the first HD keyring metadata ID', () => {
expect(
getPrimaryHdKeyringEntropySourceId([hd('primary'), hd('secondary')]),
getPrimaryHdKeyringEntropySourceId({
isUnlocked: true,
keyrings: [hd('primary'), hd('secondary')],
}),
).toBe('primary');
});

it('throws when no HD keyring is available', () => {
expect(() => getPrimaryHdKeyringEntropySourceId([])).toThrow(
'no HD keyring available',
);
expect(() =>
getPrimaryHdKeyringEntropySourceId({ isUnlocked: false, keyrings: [] }),
).toThrow('no HD keyring available');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
import { KeyringTypes } from '@metamask/keyring-controller';
import type { KeyringObject } from '@metamask/keyring-controller';

/**
* Reads HD keyring entropy source IDs (metadata IDs) from KeyringController
* keyrings, primary first. Returns an empty array when none are available
* (e.g. the wallet is locked, where `keyrings` is cleared).
*
* @param keyrings - Keyrings from `KeyringController:getState`.
* @returns The HD keyring metadata IDs, primary first.
*/
export function getHdKeyringEntropySourceIds(
keyrings: KeyringObject[] | null | undefined,
): string[] {
return (keyrings ?? [])
.filter((keyring) => keyring.type === KeyringTypes.hd.toString())
.map((keyring) => keyring.metadata.id);
}
import { selectPrimaryHdKeyringEntropySourceId } from '@metamask/keyring-controller';
import type { KeyringControllerState } from '@metamask/keyring-controller';

/**
* Resolves the primary SRP's entropy source ID (the first HD keyring's
* metadata ID). The ID is randomly regenerated whenever the vault is
* recreated (e.g. on restore).
*
* @param keyrings - Keyrings from `KeyringController:getState`.
* @param keyringState - KeyringController state.
* @returns The primary HD keyring metadata ID.
* @throws If no HD keyring is available; callers must only resolve while
* the wallet is unlocked.
*/
export function getPrimaryHdKeyringEntropySourceId(
keyrings: KeyringObject[] | null | undefined,
keyringState: KeyringControllerState,
): string {
const [primaryEntropySourceId] = getHdKeyringEntropySourceIds(keyrings);
const primaryEntropySourceId =
selectPrimaryHdKeyringEntropySourceId(keyringState);
if (!primaryEntropySourceId) {
throw new Error(
'getPrimaryHdKeyringEntropySourceId - no HD keyring available',
Expand Down