Skip to content

Commit c85296b

Browse files
authored
feat(keyring-api): add assertCreateAccountOptionIsSupported helper (for createAccounts) (#464)
Helper to guide TS and narrow down `CreateAccountOptions.type` to a specific set of values. This way, a `createAccounts` implementation can assert this and work with specific `type`s rather than having to deal with a default branch/case for the unsupported `type`s. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Adds a small runtime assertion helper plus tests; no changes to keyring execution paths beyond optional use of the helper. > > **Overview** > Adds a new `assertCreateAccountOptionIsSupported` helper in the v2 create-account API to let `createAccounts` implementations reject unsupported `CreateAccountOptions.type` values and *narrow the option type* in TypeScript. > > Includes comprehensive unit tests covering supported/unsupported type behavior and compile-time narrowing expectations, and documents the addition in the `keyring-api` changelog. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ff8b694. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d01338e commit c85296b

3 files changed

Lines changed: 271 additions & 1 deletion

File tree

packages/keyring-api/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add `EthAddressStrictStruct` struct and `EthAddressStrict` types ([#465](https://github.com/MetaMask/accounts/pull/465))
1313
- This is a stricter variant of `EthAddressStruct` which uses `Hex` instead of `string` for its inferred type.
14+
- Add `assertCreateAccountOptionIsSupported` helper ([#464](https://github.com/MetaMask/accounts/pull/464))
15+
- This helper can be used to implement `createAccounts` and narrow down the `options` to the supported types (based on the keyring capabilities).
1416

1517
### Changed
1618

packages/keyring-api/src/api/v2/create-account/index.test.ts

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { assert, is } from '@metamask/superstruct';
22

3-
import { AccountCreationType, CreateAccountOptionsStruct } from '.';
3+
import {
4+
AccountCreationType,
5+
assertCreateAccountOptionIsSupported,
6+
CreateAccountOptionsStruct,
7+
type CreateAccountBip44DeriveIndexOptions,
8+
type CreateAccountOptions,
9+
} from '.';
410

511
describe('CreateAccountOptionsStruct', () => {
612
describe('valid account creation types', () => {
@@ -195,3 +201,222 @@ describe('CreateAccountOptionsStruct', () => {
195201
});
196202
});
197203
});
204+
205+
describe('assertCreateAccountOptionIsSupported', () => {
206+
describe('when type is supported', () => {
207+
it('does not throw when type is in supportedTypes array', () => {
208+
const options = {
209+
type: AccountCreationType.Bip44DerivePath,
210+
entropySource: 'user-input',
211+
derivationPath: "m/44'/0'/0'/0/0",
212+
} as CreateAccountOptions;
213+
const supportedTypes = [
214+
AccountCreationType.Bip44DerivePath,
215+
AccountCreationType.Bip44DeriveIndex,
216+
];
217+
218+
expect(() =>
219+
assertCreateAccountOptionIsSupported(options, supportedTypes),
220+
).not.toThrow();
221+
});
222+
223+
it('does not throw when type is the only supported type', () => {
224+
const options = {
225+
type: AccountCreationType.PrivateKeyImport,
226+
privateKey: '0x1234567890abcdef',
227+
encoding: 'hexadecimal',
228+
} as CreateAccountOptions;
229+
const supportedTypes = [AccountCreationType.PrivateKeyImport];
230+
231+
expect(() =>
232+
assertCreateAccountOptionIsSupported(options, supportedTypes),
233+
).not.toThrow();
234+
});
235+
236+
it('does not throw when all types are supported', () => {
237+
const options = {
238+
type: AccountCreationType.Custom,
239+
scope: 'scope',
240+
} as CreateAccountOptions;
241+
const supportedTypes = [
242+
AccountCreationType.Bip44DerivePath,
243+
AccountCreationType.Bip44DeriveIndex,
244+
AccountCreationType.Bip44DeriveIndexRange,
245+
AccountCreationType.Bip44Discover,
246+
AccountCreationType.PrivateKeyImport,
247+
AccountCreationType.Custom,
248+
];
249+
250+
expect(() =>
251+
assertCreateAccountOptionIsSupported(options, supportedTypes),
252+
).not.toThrow();
253+
});
254+
255+
it('does not throw for each supported BIP-44 type', () => {
256+
const supportedTypes = [
257+
AccountCreationType.Bip44DerivePath,
258+
AccountCreationType.Bip44DeriveIndex,
259+
AccountCreationType.Bip44DeriveIndexRange,
260+
AccountCreationType.Bip44Discover,
261+
];
262+
263+
const optionsMap: Record<AccountCreationType, CreateAccountOptions> = {
264+
[AccountCreationType.Bip44DerivePath]: {
265+
type: AccountCreationType.Bip44DerivePath,
266+
entropySource: 'user-input',
267+
derivationPath: "m/44'/0'/0'/0/0",
268+
},
269+
[AccountCreationType.Bip44DeriveIndex]: {
270+
type: AccountCreationType.Bip44DeriveIndex,
271+
entropySource: 'user-input',
272+
groupIndex: 0,
273+
},
274+
[AccountCreationType.Bip44DeriveIndexRange]: {
275+
type: AccountCreationType.Bip44DeriveIndexRange,
276+
entropySource: 'user-input',
277+
range: {
278+
from: 0,
279+
to: 1,
280+
},
281+
},
282+
[AccountCreationType.Bip44Discover]: {
283+
type: AccountCreationType.Bip44Discover,
284+
entropySource: 'user-input',
285+
groupIndex: 0,
286+
},
287+
[AccountCreationType.PrivateKeyImport]: {
288+
type: AccountCreationType.PrivateKeyImport,
289+
privateKey: '0x1234567890abcdef',
290+
encoding: 'hexadecimal',
291+
},
292+
[AccountCreationType.Custom]: {
293+
type: AccountCreationType.Custom,
294+
// FIXME: We cannot use custom fields currently with `CreateAccountOptions` type.
295+
// We need to define a struct manually to open up the type for custom options to
296+
// allow additional fields.
297+
},
298+
};
299+
300+
supportedTypes.forEach((type) => {
301+
expect(() =>
302+
assertCreateAccountOptionIsSupported(
303+
optionsMap[type],
304+
supportedTypes,
305+
),
306+
).not.toThrow();
307+
});
308+
});
309+
});
310+
311+
describe('when type is not supported', () => {
312+
it('throws error with correct message when type is not in supportedTypes', () => {
313+
const options = {
314+
type: AccountCreationType.Custom,
315+
scope: 'scope',
316+
} as CreateAccountOptions;
317+
const supportedTypes = [
318+
AccountCreationType.Bip44DerivePath,
319+
AccountCreationType.Bip44DeriveIndex,
320+
];
321+
322+
expect(() =>
323+
assertCreateAccountOptionIsSupported(options, supportedTypes),
324+
).toThrow('Unsupported create account option type: custom');
325+
});
326+
327+
it('throws error when supportedTypes is empty', () => {
328+
const options = {
329+
type: AccountCreationType.Bip44DerivePath,
330+
entropySource: 'user-input',
331+
derivationPath: "m/44'/0'/0'/0/0",
332+
} as CreateAccountOptions;
333+
const supportedTypes: AccountCreationType[] = [];
334+
335+
expect(() =>
336+
assertCreateAccountOptionIsSupported(options, supportedTypes),
337+
).toThrow('Unsupported create account option type: bip44:derive-path');
338+
});
339+
340+
it('throws error for PrivateKeyImport when only BIP-44 types are supported', () => {
341+
const options = {
342+
type: AccountCreationType.PrivateKeyImport,
343+
privateKey: '0x1234567890abcdef',
344+
encoding: 'hexadecimal',
345+
} as CreateAccountOptions;
346+
const supportedTypes = [
347+
AccountCreationType.Bip44DerivePath,
348+
AccountCreationType.Bip44DeriveIndex,
349+
];
350+
351+
expect(() =>
352+
assertCreateAccountOptionIsSupported(options, supportedTypes),
353+
).toThrow('Unsupported create account option type: private-key:import');
354+
});
355+
356+
it('throws error for Bip44Discover when not in supportedTypes', () => {
357+
const options = {
358+
type: AccountCreationType.Bip44Discover,
359+
entropySource: 'user-input',
360+
groupIndex: 0,
361+
} as CreateAccountOptions;
362+
const supportedTypes = [
363+
AccountCreationType.Bip44DerivePath,
364+
AccountCreationType.Custom,
365+
];
366+
367+
expect(() =>
368+
assertCreateAccountOptionIsSupported(options, supportedTypes),
369+
).toThrow('Unsupported create account option type: bip44:discover');
370+
});
371+
372+
it('includes the unsupported type value in error message', () => {
373+
const options = {
374+
type: AccountCreationType.Bip44DeriveIndexRange,
375+
entropySource: 'user-input',
376+
range: {
377+
from: 0,
378+
to: 1,
379+
},
380+
} as CreateAccountOptions;
381+
const supportedTypes = [AccountCreationType.PrivateKeyImport];
382+
383+
expect(() =>
384+
assertCreateAccountOptionIsSupported(options, supportedTypes),
385+
).toThrow(/bip44:derive-index-range/u);
386+
});
387+
});
388+
389+
describe('type narrowing behavior', () => {
390+
it('narrows type correctly after assertion passes', () => {
391+
const options = {
392+
type: AccountCreationType.Bip44DerivePath,
393+
entropySource: 'user-input',
394+
derivationPath: "m/44'/0'/0'/0/0",
395+
} as CreateAccountOptions;
396+
const supportedTypes = [
397+
`${AccountCreationType.Bip44DerivePath}`,
398+
] as const;
399+
400+
assertCreateAccountOptionIsSupported(options, supportedTypes);
401+
402+
// After the assertion, TypeScript should narrow the type.
403+
const narrowedType: (typeof supportedTypes)[number] = options.type; // Compile-time check.
404+
expect(narrowedType).toBe(AccountCreationType.Bip44DerivePath);
405+
});
406+
407+
it('narrows CreateAccountOptions type based on supported type', () => {
408+
const options = {
409+
type: AccountCreationType.Bip44DeriveIndex,
410+
entropySource: 'mock-entropy-source',
411+
groupIndex: 0,
412+
} as CreateAccountOptions;
413+
414+
const supportedTypes = [AccountCreationType.Bip44DeriveIndex] as const;
415+
assertCreateAccountOptionIsSupported(options, supportedTypes);
416+
417+
// After assertion, options should be narrowed to CreateAccountBip44DeriveIndexOptions
418+
const narrowedOptions: CreateAccountBip44DeriveIndexOptions = options; // Compile-time check.
419+
expect(narrowedOptions.type).toBe(AccountCreationType.Bip44DeriveIndex);
420+
});
421+
});
422+
});

packages/keyring-api/src/api/v2/create-account/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,46 @@ export const CreateAccountOptionsStruct = selectiveUnion((value: any) => {
9292
* Represents the available options for creating a new account.
9393
*/
9494
export type CreateAccountOptions = Infer<typeof CreateAccountOptionsStruct>;
95+
96+
/**
97+
* Asserts that a given create account option type is supported by the keyring.
98+
*
99+
* @example
100+
* ```ts
101+
* createAccounts(options: CreateAccountOptions) {
102+
* assertCreateAccountOptionIsSupported(options, [
103+
* ${AccountCreationType.Bip44DeriveIndex},
104+
* ${AccountCreationType.Bip44DeriveIndexRange},
105+
* ] as const);
106+
*
107+
* // At this point, TypeScript knows that options.type is either Bip44DeriveIndex or Bip44DeriveIndexRange.
108+
* if (options.type === AccountCreationType.Bip44DeriveIndex) {
109+
* ... // Handle Bip44DeriveIndex case.
110+
* } else {
111+
* ... // Handle Bip44DeriveIndexRange case.
112+
* }
113+
* ...
114+
* return accounts;
115+
* }
116+
* ```
117+
*
118+
* @param options - The create account option object to check.
119+
* @param supportedTypes - The list of supported create account option types for this keyring.
120+
* @throws Will throw an error if the provided options are not supported.
121+
*/
122+
export function assertCreateAccountOptionIsSupported<
123+
Options extends CreateAccountOptions,
124+
// We use template literal types to enforce string-literal over strict enum values.
125+
Type extends `${CreateAccountOptions['type']}`,
126+
>(
127+
options: Options,
128+
supportedTypes: readonly `${Type}`[],
129+
// Use intersection to avoid widening `type` beyond `Options['type']`.
130+
): asserts options is Options & { type: `${Type}` & `${Options['type']}` } {
131+
const { type } = options;
132+
const types: readonly CreateAccountOptions['type'][] = supportedTypes;
133+
134+
if (!types.includes(type)) {
135+
throw new Error(`Unsupported create account option type: ${type}`);
136+
}
137+
}

0 commit comments

Comments
 (0)