From 9c31e5c12cdf6518185a4a337d577082094cff17 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 17 Sep 2026 13:05:55 -0230 Subject: [PATCH] chore: add KeyringController entropy ID selectors Add selectors to the `keyring-controller` package for getting entropy source IDs. This is a refactor, these selectors were previously written as part of the `profile-sync-controller` package, but they belong with the keyring package. --- packages/keyring-controller/src/index.ts | 4 + .../keyring-controller/src/selectors.test.ts | 149 ++++++++++++++++++ packages/keyring-controller/src/selectors.ts | 25 +++ .../AuthenticationController.ts | 14 +- .../user-storage/UserStorageController.ts | 14 +- .../src/shared/utils/entropy-source.test.ts | 41 +---- .../src/shared/utils/entropy-source.ts | 27 +--- 7 files changed, 204 insertions(+), 70 deletions(-) create mode 100644 packages/keyring-controller/src/selectors.test.ts create mode 100644 packages/keyring-controller/src/selectors.ts diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts index f1394113e80..8be3d3548d0 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -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'; diff --git a/packages/keyring-controller/src/selectors.test.ts b/packages/keyring-controller/src/selectors.test.ts new file mode 100644 index 00000000000..40360dc1f94 --- /dev/null +++ b/packages/keyring-controller/src/selectors.test.ts @@ -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(); + }); + }); +}); diff --git a/packages/keyring-controller/src/selectors.ts b/packages/keyring-controller/src/selectors.ts new file mode 100644 index 00000000000..0159c034183 --- /dev/null +++ b/packages/keyring-controller/src/selectors.ts @@ -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]; diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts index 30f033c8dbf..17b9b4f3ed4 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts @@ -4,6 +4,7 @@ import type { ControllerStateChangeEvent, StateMetadata, } from '@metamask/base-controller'; +import { selectHdKeyringEntropySourceIds } from '@metamask/keyring-controller'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -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, @@ -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); } /** @@ -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); } /** diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 2a3d960e87e..c4ee9c65be8 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -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 { @@ -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'; @@ -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); } /** @@ -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); } /** diff --git a/packages/profile-sync-controller/src/shared/utils/entropy-source.test.ts b/packages/profile-sync-controller/src/shared/utils/entropy-source.test.ts index 9e8069c611a..46fa0c6efa3 100644 --- a/packages/profile-sync-controller/src/shared/utils/entropy-source.test.ts +++ b/packages/profile-sync-controller/src/shared/utils/entropy-source.test.ts @@ -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, @@ -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'); }); }); }); diff --git a/packages/profile-sync-controller/src/shared/utils/entropy-source.ts b/packages/profile-sync-controller/src/shared/utils/entropy-source.ts index 9de8c974e5f..4ed005873ec 100644 --- a/packages/profile-sync-controller/src/shared/utils/entropy-source.ts +++ b/packages/profile-sync-controller/src/shared/utils/entropy-source.ts @@ -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',