Skip to content

Commit

Permalink
Merge pull request #105 from bitcoinjs/tsAllowExclude
Browse files Browse the repository at this point in the history
Allow excluding wordlists when building for browserify
  • Loading branch information
junderw committed Apr 2, 2019
2 parents 6dedef0 + cce4bd0 commit 68d0bb4
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 63 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Expand Up @@ -6,6 +6,10 @@ node_js:
- "lts/*"
- "9"
- "10"
matrix:
include:
- node_js: "lts/*"
env: TEST_SUITE=gitdiff:ci
env:
- TEST_SUITE=test
script: npm run-script $TEST_SUITE
56 changes: 56 additions & 0 deletions README.md
Expand Up @@ -16,6 +16,62 @@ When a checksum is invalid, warn the user that the phrase is not something gener

However, there should be other checks in place, such as checking to make sure the user is inputting 12 words or more separated by a space. ie. `phrase.trim().split(/\s+/g).length >= 12`

## Removing wordlists from webpack/browserify

Browserify/Webpack bundles can get very large if you include all the wordlists, so you can now exclude wordlists to make your bundle lighter.

For example, if we want to exclude all wordlists besides chinese_simplified, you could build using the browserify command below.

```bash
$ browserify -r bip39 -s bip39 \
--exclude=./wordlists/english.json \
--exclude=./wordlists/japanese.json \
--exclude=./wordlists/spanish.json \
--exclude=./wordlists/italian.json \
--exclude=./wordlists/french.json \
--exclude=./wordlists/korean.json \
--exclude=./wordlists/chinese_traditional.json \
> bip39.browser.js
```

This will create a bundle that only contains the chinese_simplified wordlist, and it will be the default wordlist for all calls without explicit wordlists.

This is how it will look in the browser console.

```javascript
> bip39.entropyToMnemonic('00000000000000000000000000000000')
"的 的 的 的 的 的 的 的 的 的 的 在"
> bip39.wordlists.chinese_simplified
Array(2048) [ "", "", "", "", "", "", "", "", "", "", … ]
> bip39.wordlists.english
undefined
> bip39.wordlists.japanese
undefined
> bip39.wordlists.spanish
undefined
> bip39.wordlists.italian
undefined
> bip39.wordlists.french
undefined
> bip39.wordlists.korean
undefined
> bip39.wordlists.chinese_traditional
undefined
```

For a list of supported wordlists check the wordlists folder. The name of the json file (minus the extension) is the name of the key to access the wordlist.

You can also change the default wordlist at runtime if you dislike the wordlist you were given as default.

```javascript
> bip39.entropyToMnemonic('00000000000000000000000000000fff')
"あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あまい ろんり"
> bip39.setDefaultWordlist('italian')
undefined
> bip39.entropyToMnemonic('00000000000000000000000000000fff')
"abaco abaco abaco abaco abaco abaco abaco abaco abaco abaco aforisma zibetto"
```

## Installation
``` bash
npm install bip39
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -10,6 +10,7 @@
"coverage": "nyc --branches 100 --functions 100 --check-coverage npm run unit",
"format": "npm run prettier -- --write",
"format:ci": "npm run prettier -- --check",
"gitdiff:ci": "npm run build && git diff --exit-code",
"lint": "tslint -p tsconfig.json -c tslint.json",
"prettier": "prettier 'ts_src/**/*.ts' --ignore-path ./.prettierignore",
"test": "npm run build && npm run format:ci && npm run lint && npm run unit",
Expand Down
30 changes: 30 additions & 0 deletions src/_wordlists.js
@@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// browserify by default only pulls in files that are hard coded in requires
// In order of last to first in this file, the default wordlist will be chosen
// based on what is present. (Bundles may remove wordlists they don't need)
const wordlistFilenames = [
'chinese_simplified',
'chinese_traditional',
'korean',
'french',
'italian',
'spanish',
'japanese',
'english',
];
const wordlists = {};
exports.wordlists = wordlists;
let _default;
exports._default = _default;
wordlistFilenames.forEach(lang => {
try {
exports._default = _default = require('./wordlists/' + lang + '.json');
wordlists[lang] = _default;
if (lang === 'japanese')
wordlists.JA = _default;
if (lang === 'english')
wordlists.EN = _default;
}
catch (err) { }
});
53 changes: 31 additions & 22 deletions src/index.js
Expand Up @@ -5,18 +5,13 @@ const pbkdf2_1 = require("pbkdf2");
const randomBytes = require("randombytes");
// use unorm until String.prototype.normalize gets better browser support
const unorm = require("unorm");
const CHINESE_SIMPLIFIED_WORDLIST = require("./wordlists/chinese_simplified.json");
const CHINESE_TRADITIONAL_WORDLIST = require("./wordlists/chinese_traditional.json");
const ENGLISH_WORDLIST = require("./wordlists/english.json");
const FRENCH_WORDLIST = require("./wordlists/french.json");
const ITALIAN_WORDLIST = require("./wordlists/italian.json");
const JAPANESE_WORDLIST = require("./wordlists/japanese.json");
const KOREAN_WORDLIST = require("./wordlists/korean.json");
const SPANISH_WORDLIST = require("./wordlists/spanish.json");
const DEFAULT_WORDLIST = ENGLISH_WORDLIST;
const _wordlists_1 = require("./_wordlists");
let DEFAULT_WORDLIST = _wordlists_1._default;
const INVALID_MNEMONIC = 'Invalid mnemonic';
const INVALID_ENTROPY = 'Invalid entropy';
const INVALID_CHECKSUM = 'Invalid mnemonic checksum';
const WORDLIST_REQUIRED = 'A wordlist is required but a default could not be found.\n' +
'Please explicitly pass a 2048 word array explicitly.';
function lpad(str, padString, length) {
while (str.length < length)
str = padString + str;
Expand Down Expand Up @@ -74,6 +69,9 @@ async function mnemonicToSeedHexAsync(mnemonic, password) {
exports.mnemonicToSeedHexAsync = mnemonicToSeedHexAsync;
function mnemonicToEntropy(mnemonic, wordlist) {
wordlist = wordlist || DEFAULT_WORDLIST;
if (!wordlist) {
throw new Error(WORDLIST_REQUIRED);
}
const words = unorm.nfkd(mnemonic).split(' ');
if (words.length % 3 !== 0)
throw new Error(INVALID_MNEMONIC);
Expand Down Expand Up @@ -109,6 +107,9 @@ function entropyToMnemonic(entropy, wordlist) {
if (!Buffer.isBuffer(entropy))
entropy = Buffer.from(entropy, 'hex');
wordlist = wordlist || DEFAULT_WORDLIST;
if (!wordlist) {
throw new Error(WORDLIST_REQUIRED);
}
// 128 <= ENT <= 256
if (entropy.length < 16)
throw new TypeError(INVALID_ENTROPY);
Expand All @@ -124,7 +125,7 @@ function entropyToMnemonic(entropy, wordlist) {
const index = binaryToByte(binary);
return wordlist[index];
});
return wordlist === JAPANESE_WORDLIST
return wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093' // Japanese wordlist
? words.join('\u3000')
: words.join(' ');
}
Expand All @@ -147,15 +148,23 @@ function validateMnemonic(mnemonic, wordlist) {
return true;
}
exports.validateMnemonic = validateMnemonic;
exports.wordlists = {
EN: ENGLISH_WORDLIST,
JA: JAPANESE_WORDLIST,
chinese_simplified: CHINESE_SIMPLIFIED_WORDLIST,
chinese_traditional: CHINESE_TRADITIONAL_WORDLIST,
english: ENGLISH_WORDLIST,
french: FRENCH_WORDLIST,
italian: ITALIAN_WORDLIST,
japanese: JAPANESE_WORDLIST,
korean: KOREAN_WORDLIST,
spanish: SPANISH_WORDLIST,
};
function setDefaultWordlist(language) {
const result = _wordlists_1.wordlists[language];
if (result)
DEFAULT_WORDLIST = result;
else
throw new Error('Could not find wordlist for language "' + language + '"');
}
exports.setDefaultWordlist = setDefaultWordlist;
function getDefaultWordlist() {
if (!DEFAULT_WORDLIST)
throw new Error('No Default Wordlist set');
return Object.keys(_wordlists_1.wordlists).filter(lang => {
if (lang === 'JA' || lang === 'EN')
return false;
return _wordlists_1.wordlists[lang].every((word, index) => word === DEFAULT_WORDLIST[index]);
})[0];
}
exports.getDefaultWordlist = getDefaultWordlist;
var _wordlists_2 = require("./_wordlists");
exports.wordlists = _wordlists_2.wordlists;
40 changes: 40 additions & 0 deletions test/index.js
Expand Up @@ -34,6 +34,46 @@ vectors.english.forEach(function (v, i) { testVector('English', undefined, 'TREZ
vectors.japanese.forEach(function (v, i) { testVector('Japanese', WORDLISTS.japanese, '㍍ガバヴァぱばぐゞちぢ十人十色', v, i) })
vectors.custom.forEach(function (v, i) { testVector('Custom', WORDLISTS.custom, undefined, v, i) })

test('getDefaultWordlist returns "english"', function (t) {
t.plan(1)
const english = bip39.getDefaultWordlist()
t.equal(english, 'english')
// TODO: Test that Error throws when called if no wordlists are compiled with bip39
})

test('setDefaultWordlist changes default wordlist', function (t) {
t.plan(4)
const english = bip39.getDefaultWordlist()
t.equal(english, 'english')

bip39.setDefaultWordlist('italian')

const italian = bip39.getDefaultWordlist()
t.equal(italian, 'italian')

const phraseItalian = bip39.entropyToMnemonic('00000000000000000000000000000000')
t.equal(phraseItalian.slice(0, 5), 'abaco')

bip39.setDefaultWordlist('english')

const phraseEnglish = bip39.entropyToMnemonic('00000000000000000000000000000000')
t.equal(phraseEnglish.slice(0, 7), 'abandon')
})

test('setDefaultWordlist throws on unknown wordlist', function (t) {
t.plan(2)
const english = bip39.getDefaultWordlist()
t.equal(english, 'english')

try {
bip39.setDefaultWordlist('abcdefghijklmnop')
} catch (error) {
t.equal(error.message, 'Could not find wordlist for language "abcdefghijklmnop"')
return
}
t.assert(false)
})

test('invalid entropy', function (t) {
t.plan(3)

Expand Down
26 changes: 26 additions & 0 deletions ts_src/_wordlists.ts
@@ -0,0 +1,26 @@
// browserify by default only pulls in files that are hard coded in requires
// In order of last to first in this file, the default wordlist will be chosen
// based on what is present. (Bundles may remove wordlists they don't need)
const wordlistFilenames: string[] = [
'chinese_simplified',
'chinese_traditional',
'korean',
'french',
'italian',
'spanish',
'japanese',
'english', // Last language available in list will be the default.
];
const wordlists: { [index: string]: string[] } = {};
let _default: string[] | undefined;
wordlistFilenames.forEach(lang => {
try {
_default = require('./wordlists/' + lang + '.json');
wordlists[lang] = _default as string[];
if (lang === 'japanese') wordlists.JA = _default as string[];
if (lang === 'english') wordlists.EN = _default as string[];
} catch (err) {}
});

// Last one to overwrite wordlist gets to be default.
export { wordlists, _default };
62 changes: 34 additions & 28 deletions ts_src/index.ts
@@ -1,23 +1,18 @@
import createHash = require('create-hash');
import * as createHash from 'create-hash';
import { pbkdf2 as pbkdf2Async, pbkdf2Sync as pbkdf2 } from 'pbkdf2';
import randomBytes = require('randombytes');

import * as randomBytes from 'randombytes';
// use unorm until String.prototype.normalize gets better browser support
import unorm = require('unorm');

import CHINESE_SIMPLIFIED_WORDLIST = require('./wordlists/chinese_simplified.json');
import CHINESE_TRADITIONAL_WORDLIST = require('./wordlists/chinese_traditional.json');
import ENGLISH_WORDLIST = require('./wordlists/english.json');
import FRENCH_WORDLIST = require('./wordlists/french.json');
import ITALIAN_WORDLIST = require('./wordlists/italian.json');
import JAPANESE_WORDLIST = require('./wordlists/japanese.json');
import KOREAN_WORDLIST = require('./wordlists/korean.json');
import SPANISH_WORDLIST = require('./wordlists/spanish.json');
const DEFAULT_WORDLIST = ENGLISH_WORDLIST;
import * as unorm from 'unorm';
import { _default as _DEFAULT_WORDLIST, wordlists } from './_wordlists';

let DEFAULT_WORDLIST: string[] | undefined = _DEFAULT_WORDLIST;

const INVALID_MNEMONIC = 'Invalid mnemonic';
const INVALID_ENTROPY = 'Invalid entropy';
const INVALID_CHECKSUM = 'Invalid mnemonic checksum';
const WORDLIST_REQUIRED =
'A wordlist is required but a default could not be found.\n' +
'Please explicitly pass a 2048 word array explicitly.';

function lpad(str: string, padString: string, length: number): string {
while (str.length < length) str = padString + str;
Expand Down Expand Up @@ -97,6 +92,9 @@ export function mnemonicToEntropy(
wordlist?: string[],
): string {
wordlist = wordlist || DEFAULT_WORDLIST;
if (!wordlist) {
throw new Error(WORDLIST_REQUIRED);
}

const words = unorm.nfkd(mnemonic).split(' ');
if (words.length % 3 !== 0) throw new Error(INVALID_MNEMONIC);
Expand Down Expand Up @@ -135,6 +133,9 @@ export function entropyToMnemonic(
): string {
if (!Buffer.isBuffer(entropy)) entropy = Buffer.from(entropy, 'hex');
wordlist = wordlist || DEFAULT_WORDLIST;
if (!wordlist) {
throw new Error(WORDLIST_REQUIRED);
}

// 128 <= ENT <= 256
if (entropy.length < 16) throw new TypeError(INVALID_ENTROPY);
Expand All @@ -151,7 +152,7 @@ export function entropyToMnemonic(
return wordlist![index];
});

return wordlist === JAPANESE_WORDLIST
return wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093' // Japanese wordlist
? words.join('\u3000')
: words.join(' ');
}
Expand Down Expand Up @@ -181,16 +182,21 @@ export function validateMnemonic(
return true;
}

export const wordlists = {
EN: ENGLISH_WORDLIST,
JA: JAPANESE_WORDLIST,

chinese_simplified: CHINESE_SIMPLIFIED_WORDLIST,
chinese_traditional: CHINESE_TRADITIONAL_WORDLIST,
english: ENGLISH_WORDLIST,
french: FRENCH_WORDLIST,
italian: ITALIAN_WORDLIST,
japanese: JAPANESE_WORDLIST,
korean: KOREAN_WORDLIST,
spanish: SPANISH_WORDLIST,
};
export function setDefaultWordlist(language: string): void {
const result = wordlists[language];
if (result) DEFAULT_WORDLIST = result;
else
throw new Error('Could not find wordlist for language "' + language + '"');
}

export function getDefaultWordlist(): string {
if (!DEFAULT_WORDLIST) throw new Error('No Default Wordlist set');
return Object.keys(wordlists).filter(lang => {
if (lang === 'JA' || lang === 'EN') return false;
return wordlists[lang].every(
(word, index) => word === DEFAULT_WORDLIST![index],
);
})[0];
}

export { wordlists } from './_wordlists';
3 changes: 2 additions & 1 deletion tsconfig.json
Expand Up @@ -24,7 +24,8 @@
"resolveJsonModule": true
},
"include": [
"ts_src/**/*.ts"
"ts_src/**/*.ts",
"ts_src/**/*.json"
],
"exclude": [
"**/*.spec.ts",
Expand Down
5 changes: 5 additions & 0 deletions types/_wordlists.d.ts
@@ -0,0 +1,5 @@
declare const wordlists: {
[index: string]: string[];
};
declare let _default: string[] | undefined;
export { wordlists, _default };

0 comments on commit 68d0bb4

Please sign in to comment.