From ce170e33665b868f85411d09260ac80106422d02 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 14 Aug 2026 01:04:42 +0800 Subject: [PATCH 1/5] feat: added two new utils for mnemonic/english-word-indices conversion --- package.json | 1 + src/mnemonic.test.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++ src/mnemonic.ts | 31 +++++++++++++++ yarn.lock | 25 ++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 src/mnemonic.test.ts create mode 100644 src/mnemonic.ts diff --git a/package.json b/package.json index 34392dad..15028861 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "dependencies": { "@ethereumjs/tx": "^4.2.0", + "@metamask/scure-bip39": "^2.0.3", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", diff --git a/src/mnemonic.test.ts b/src/mnemonic.test.ts new file mode 100644 index 00000000..e42413aa --- /dev/null +++ b/src/mnemonic.test.ts @@ -0,0 +1,91 @@ +import { + convertMnemonicToWordlistIndices, + uint8ArrayToMnemonic, +} from './mnemonic'; + +const TWELVE_WORD_MNEMONIC = + 'bulk riot robust reward museum path chunk health rate soon zone wagon'; +const TWELVE_WORD_INDICES = [ + 240, 1489, 1498, 1477, 1165, 1287, 325, 849, 1425, 1658, 2046, 1970, +]; + +const TWENTY_FOUR_WORD_MNEMONIC = + 'abuse boss fly battle rubber wasp afraid hamster guide essence vibrant task banana pencil owner cube social job emotion member joy sting dash trouble'; +const TWENTY_FOUR_WORD_INDICES = [ + 9, 209, 719, 154, 1510, 1980, 36, 837, 828, 618, 1947, 1776, 145, 1301, 1265, + 427, 1647, 960, 582, 1110, 964, 1711, 445, 1864, +]; + +/** + * Encode BIP-39 word indices as little-endian Uint16 bytes. + * + * @param indices - Wordlist indices to encode. + * @returns The encoded bytes. + */ +function indicesToBytes(indices: number[]): Uint8Array { + return new Uint8Array(new Uint16Array(indices).buffer); +} + +describe('uint8ArrayToMnemonic', () => { + it('throws for an empty array', () => { + expect(() => uint8ArrayToMnemonic(new Uint8Array())).toThrow( + 'The method uint8ArrayToMnemonic expects a non-empty array', + ); + }); + + it('returns a 12-word mnemonic', () => { + expect(uint8ArrayToMnemonic(indicesToBytes(TWELVE_WORD_INDICES))).toBe( + TWELVE_WORD_MNEMONIC, + ); + }); + + it('returns a 24-word mnemonic', () => { + expect(uint8ArrayToMnemonic(indicesToBytes(TWENTY_FOUR_WORD_INDICES))).toBe( + TWENTY_FOUR_WORD_MNEMONIC, + ); + }); + + it('interprets bytes as little-endian word indices', () => { + expect( + uint8ArrayToMnemonic( + new Uint8Array([ + 240, 0, 209, 5, 218, 5, 197, 5, 141, 4, 7, 5, 69, 1, 81, 3, 145, 5, + 122, 6, 254, 7, 178, 7, + ]), + ), + ).toBe(TWELVE_WORD_MNEMONIC); + }); +}); + +describe('convertMnemonicToWordlistIndices', () => { + it('converts a 12-word mnemonic', () => { + expect( + convertMnemonicToWordlistIndices(TWELVE_WORD_MNEMONIC), + ).toStrictEqual(indicesToBytes(TWELVE_WORD_INDICES)); + }); + + it('converts a 24-word mnemonic', () => { + expect( + convertMnemonicToWordlistIndices(TWENTY_FOUR_WORD_MNEMONIC), + ).toStrictEqual(indicesToBytes(TWENTY_FOUR_WORD_INDICES)); + }); + + it('encodes an unknown word as the Uint16 representation of -1', () => { + const mnemonicWithInvalidWord = + 'bulk riot robust reward notaword path chunk health rate soon zone wagon'; + const result = convertMnemonicToWordlistIndices(mnemonicWithInvalidWord); + const indices = new Uint16Array(result.buffer); + + expect(Array.from(indices)).toStrictEqual([ + 240, 1489, 1498, 1477, 65535, 1287, 325, 849, 1425, 1658, 2046, 1970, + ]); + }); + + it('round-trips with uint8ArrayToMnemonic', () => { + expect( + uint8ArrayToMnemonic( + convertMnemonicToWordlistIndices(TWELVE_WORD_MNEMONIC), + ), + ).toBe(TWELVE_WORD_MNEMONIC); + }); +}); diff --git a/src/mnemonic.ts b/src/mnemonic.ts new file mode 100644 index 00000000..16815344 --- /dev/null +++ b/src/mnemonic.ts @@ -0,0 +1,31 @@ +import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; + +/** + * Transform a typed array containing mnemonic data to the seed phrase. + * @param uint8Array - Typed array containing mnemonic data. + * @returns The seed phrase. + */ +export function uint8ArrayToMnemonic(uint8Array: Uint8Array): string { + if (uint8Array.length === 0) { + throw new Error( + 'The method uint8ArrayToMnemonic expects a non-empty array', + ); + } + + const recoveredIndices = Array.from( + new Uint16Array(new Uint8Array(uint8Array).buffer), + ); + + return recoveredIndices.map((i) => wordlist[i]).join(' '); +} + +/** + * Encodes a BIP-39 mnemonic as the indices of words in the English BIP-39 wordlist. + * + * @param mnemonic - The BIP-39 mnemonic. + * @returns The Unicode code points for the seed phrase formed from the words in the wordlist. + */ +export function convertMnemonicToWordlistIndices(mnemonic: string): Uint8Array { + const indices = mnemonic.split(' ').map((word) => wordlist.indexOf(word)); + return new Uint8Array(new Uint16Array(indices).buffer); +} diff --git a/yarn.lock b/yarn.lock index b6667f51..a5093345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1070,6 +1070,16 @@ __metadata: languageName: node linkType: hard +"@metamask/scure-bip39@npm:^2.0.3": + version: 2.1.1 + resolution: "@metamask/scure-bip39@npm:2.1.1" + dependencies: + "@noble/hashes": "npm:~1.3.2" + "@scure/base": "npm:~1.1.3" + checksum: 10/785d75e52f85103af7556c25d3dbba6da52ee9e61c31e95786bfbab0e18b9899a367bf714c71dd311c04161c45ba14354bd6fb4fbeb42cb26b7bd2f35c582f73 + languageName: node + linkType: hard + "@metamask/superstruct@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/superstruct@npm:3.1.0" @@ -1089,6 +1099,7 @@ __metadata: "@metamask/eslint-config-jest": "npm:^12.0.0" "@metamask/eslint-config-nodejs": "npm:^12.0.0" "@metamask/eslint-config-typescript": "npm:^12.0.0" + "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/superstruct": "npm:^3.1.0" "@noble/hashes": "npm:^1.3.1" "@scure/base": "npm:^1.1.3" @@ -1147,6 +1158,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:~1.3.2": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1304,6 +1322,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.3": + version: 1.1.9 + resolution: "@scure/base@npm:1.1.9" + checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb + languageName: node + linkType: hard + "@scure/bip32@npm:1.3.1": version: 1.3.1 resolution: "@scure/bip32@npm:1.3.1" From 91292ff058937594cc295f81bc6dd67866e2f8b3 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 14 Aug 2026 01:16:14 +0800 Subject: [PATCH 2/5] chore: updated CHANGELOG --- CHANGELOG.md | 10 ++++++++++ src/index.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f4ffe3..94085406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `uint8ArrayToMnemonic` and `convertMnemonicToWordlistIndices` utilities ([#300](https://github.com/MetaMask/utils/pull/300)) + - `uint8ArrayToMnemonic` converts little-endian `Uint16` English BIP-39 wordlist indices into a mnemonic string. + - `convertMnemonicToWordlistIndices` converts a mnemonic string into little-endian `Uint16` wordlist indices encoded as a `Uint8Array`. + +### Changed + +- Add `@metamask/scure-bip39` as a dependency + ## [11.11.0] ### Added diff --git a/src/index.ts b/src/index.ts index 88ff8e79..689aad08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ export * from './json'; export * from './keyring'; export * from './logging'; export * from './misc'; +export * from './mnemonic'; export * from './number'; export * from './opaque'; export * from './promise'; From 82b739817016b4353e26d88b0a4cc0f086a322a9 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 14 Aug 2026 01:29:17 +0800 Subject: [PATCH 3/5] fix: fixed lint and test --- src/index.test.ts | 2 ++ src/node.test.ts | 2 ++ yarn.lock | 13 +++---------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 6204787f..f4e41bf3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -81,6 +81,7 @@ describe('index', () => { "calculateNumberSize", "calculateStringSize", "concatBytes", + "convertMnemonicToWordlistIndices", "createBigInt", "createBytes", "createDataView", @@ -162,6 +163,7 @@ describe('index', () => { "toCaipAssetType", "toCaipChainId", "toWei", + "uint8ArrayToMnemonic", "unitMap", "valueToBytes", "wrapError", diff --git a/src/node.test.ts b/src/node.test.ts index c3b927fd..8926d905 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -81,6 +81,7 @@ describe('node', () => { "calculateNumberSize", "calculateStringSize", "concatBytes", + "convertMnemonicToWordlistIndices", "createBigInt", "createBytes", "createDataView", @@ -169,6 +170,7 @@ describe('node', () => { "toCaipAssetType", "toCaipChainId", "toWei", + "uint8ArrayToMnemonic", "unitMap", "valueToBytes", "wrapError", diff --git a/yarn.lock b/yarn.lock index a5093345..621cc84c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,14 +1151,14 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.1, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": +"@noble/hashes@npm:1.3.1": version: 1.3.1 resolution: "@noble/hashes@npm:1.3.1" checksum: 10/39474bab7e7813dbbfd8750476f48046d3004984e161fcd4333e40ca823f07b069010b35a20246e5b4ac20858e29913172a4d69720fd1e93620f7bedb70f9b72 languageName: node linkType: hard -"@noble/hashes@npm:~1.3.2": +"@noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d @@ -1315,14 +1315,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0": - version: 1.1.3 - resolution: "@scure/base@npm:1.1.3" - checksum: 10/cb715fa8cdb043c4d96b6ba0666791d4eb4d033f7b5285a853aba25e0ba94914f05ff5d956029ad060005f9bdd02dab0caef9a0a63f07ed096a2c2a0c0cf9c36 - languageName: node - linkType: hard - -"@scure/base@npm:~1.1.3": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.3": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb From 02e47447b488280298bfa3a7980df015a868e95a Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 14 Aug 2026 01:42:11 +0800 Subject: [PATCH 4/5] fix: fixed CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94085406..dc9da0a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Add `@metamask/scure-bip39` as a dependency +- Add `@metamask/scure-bip39` as a dependency ([#300](https://github.com/MetaMask/utils/pull/300)) ## [11.11.0] From e6ce3dff03418a83835689887eec70eb3bb6c079 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 14 Aug 2026 01:53:42 +0800 Subject: [PATCH 5/5] chore: updated CHANGELOG --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9da0a2..8b5bbb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `uint8ArrayToMnemonic` and `convertMnemonicToWordlistIndices` utilities ([#300](https://github.com/MetaMask/utils/pull/300)) - `uint8ArrayToMnemonic` converts little-endian `Uint16` English BIP-39 wordlist indices into a mnemonic string. - `convertMnemonicToWordlistIndices` converts a mnemonic string into little-endian `Uint16` wordlist indices encoded as a `Uint8Array`. - -### Changed - - Add `@metamask/scure-bip39` as a dependency ([#300](https://github.com/MetaMask/utils/pull/300)) ## [11.11.0]