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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,13 @@ 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`.
- Add `@metamask/scure-bip39` as a dependency ([#300](https://github.com/MetaMask/utils/pull/300))

## [11.11.0]

### Added
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,7 @@ describe('index', () => {
"calculateNumberSize",
"calculateStringSize",
"concatBytes",
"convertMnemonicToWordlistIndices",
"createBigInt",
"createBytes",
"createDataView",
Expand DownExpand Up@@ -162,6 +163,7 @@ describe('index', () => {
"toCaipAssetType",
"toCaipChainId",
"toWei",
"uint8ArrayToMnemonic",
"unitMap",
"valueToBytes",
"wrapError",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
91 changes: 91 additions & 0 deletions src/mnemonic.test.ts
Original file line numberDiff line numberDiff line change
@@ -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);
});
});
31 changes: 31 additions & 0 deletions src/mnemonic.ts
Original file line numberDiff line numberDiff line change
@@ -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);
}
Comment thread
cursor[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,7 @@ describe('node', () => {
"calculateNumberSize",
"calculateStringSize",
"concatBytes",
"convertMnemonicToWordlistIndices",
"createBigInt",
"createBytes",
"createDataView",
Expand DownExpand Up@@ -169,6 +170,7 @@ describe('node', () => {
"toCaipAssetType",
"toCaipChainId",
"toWei",
"uint8ArrayToMnemonic",
"unitMap",
"valueToBytes",
"wrapError",
Expand Down
28 changes: 23 additions & 5 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Expand All@@ -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"
Expand DownExpand Up@@ -1140,13 +1151,20 @@ __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.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
languageName: node
linkType: hard

"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
Expand DownExpand Up@@ -1297,10 +1315,10 @@ __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
"@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
languageName: node
linkType: hard

Expand Down
Loading