-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
James Calfee
committed
May 19, 2017
0 parents
commit 91df898
Showing
10 changed files
with
6,210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
coverage | ||
test | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
language: node_js | ||
node_js: | ||
- "6" | ||
after_success: npm run coveralls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
[![Build Status](https://travis-ci.org/jcalfee/bip39-checker.svg?branch=master)](https://travis-ci.org/jcalfee/bip39-checker) | ||
[![Coverage Status](https://coveralls.io/repos/github/jcalfee/bip39-checker/badge.svg?branch=master)](https://coveralls.io/github/jcalfee/bip39-checker?branch=master) | ||
|
||
# BIP-0039 Checker | ||
|
||
Normalize and spell check BIP-0039 brain seed phrases. | ||
|
||
## API | ||
|
||
```javascript | ||
const assert = require('assert') | ||
const {normalize, checkWords, suggest} = require('bip39-checker') | ||
|
||
// Normalizes | ||
assert.equal('double spaces', normalize('double spaces'), 'removes extra spaces') | ||
assert.equal('lowercase', normalize('Lowercase'), 'lowercase') | ||
assert.equal('trim', normalize(' trim '), 'trim') | ||
|
||
// Checks each word in the seed | ||
assert(checkWords('lazy dog', 'english')) | ||
assert(!checkWords('lazy ogday', 'english')) | ||
|
||
// Auto-correct suggestions | ||
assert(suggest('quality') === true) | ||
assert(suggest('ágil', {language: 'spanish'}) === true) | ||
assert.equal(suggest('quality1')[0], 'quality') | ||
|
||
// BIP-0039 dictionaries are exported | ||
const {languages, wordlist} = require('bip39-checker') | ||
|
||
console.log(languages) | ||
assert(wordlist(languages[0])) | ||
|
||
// Word list array for a given language | ||
assert(wordlist('english').length, 2048) | ||
assert(wordlist('spanish').length, 2048) | ||
// etc.. (all languages must be 2048 words) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
const LevensteinDistance = require('./src/levenstein_distance') | ||
|
||
module.exports = { | ||
suggest, | ||
languages: [ | ||
'chinese_simplified', | ||
'chinese_traditional', | ||
'japanese', | ||
'english', | ||
'italian', | ||
'french', | ||
'spanish' | ||
], | ||
wordlist: language => language ? wordlists[language] : wordlists, | ||
normalize, | ||
checkWords, | ||
validWordlist, | ||
bip39: require('bip39') | ||
} | ||
|
||
const wordlists = { | ||
chinese_simplified: require('./src/wordlist_chinese_simplified'), | ||
chinese_traditional: require('./src/wordlist_chinese_traditional'), | ||
english: require('bip39/wordlists/english.json'), | ||
french: require('bip39/wordlists/french.json'), | ||
italian: require('bip39/wordlists/italian.json'), | ||
japanese: require('bip39/wordlists/japanese.json'), | ||
spanish: require('bip39/wordlists/spanish.json') | ||
} | ||
|
||
/** | ||
@summary Character cleansing: printable characters, all lowercase, trim. | ||
@description Filter and remove invalid characters or extraneous spaces from BIP-0039 word phrases. Future implementation can assume that this method will not change any word in the language files (@see index.test.js). | ||
@retrun {string} normalized seed | ||
*/ | ||
function normalize (seed) { | ||
if (typeof seed !== 'string') { | ||
throw new TypeError('seed string required') | ||
} | ||
|
||
// TODO? use unorm module until String.prototype.normalize gets better browser support | ||
seed = seed.normalize('NFKD')// Normalization Form: Compatibility Decomposition | ||
seed = seed.replace(/\s+/g, ' ') // Remove multiple spaces in a row | ||
seed = seed.toLowerCase() | ||
seed = seed.trim() | ||
return seed | ||
} | ||
|
||
/** | ||
Find the best matching word or words in the list. | ||
@return {Array|boolean} 0 or more suggestions, true when perfect match | ||
*/ | ||
function suggest (word = '', {maxSuggestions = 15, language = 'english'} = {}) { | ||
word = word.trim().toLowerCase() | ||
const nword = normalize(word) | ||
const wordlist = validWordlist(language) | ||
|
||
if (word === '') { return [] } | ||
|
||
// Words that begin the same, also handles perfect match | ||
let match = false | ||
const matches = wordlist.reduce((arr, w) => { | ||
if (w === word || match) { | ||
match = true | ||
return | ||
} | ||
if (w.indexOf(nword) === 0 && arr.length < 10) { arr.push(w) } | ||
|
||
return arr | ||
}, []) | ||
if (match) { | ||
return true | ||
} | ||
|
||
// Levenshtein distance | ||
if (!/chinese/.test(language)) { | ||
const levenstein = LevensteinDistance(wordlist) | ||
const lwords = levenstein(nword, {threshold: 0.5, language}) | ||
lwords.forEach(w => { matches.push(w) }) | ||
} | ||
|
||
if (language === 'english') { | ||
// Vowels are almost useless | ||
const nvword = novowels(nword) | ||
if (nvword !== '') { | ||
wordlist.reduce((arr, w) => { | ||
const score = novowels(w).indexOf(nvword) | ||
if (score !== -1) { arr.push([score, w]) } | ||
return arr | ||
}, []) | ||
.sort((a, b) => Math.sign(a[0], b[0])) | ||
.map(a => a[1]) | ||
.forEach(w => { matches.push(w) }) | ||
} | ||
} | ||
|
||
const dedupe = {} | ||
const finalMatches = matches.filter(item => | ||
dedupe[item] ? false : dedupe[item] = true | ||
) | ||
|
||
// console.log('suggest finalMatches', word, finalMatches) | ||
return finalMatches.slice(0, maxSuggestions) | ||
} | ||
|
||
/** | ||
@arg {string} seed - single word or combination of words from the wordlist | ||
@arg {string} [language = 'english'] - Language dictionary to test seed against | ||
@return {boolean} true if seed contains no words or all valid words | ||
@throws {Error} 'Missing wordlist for ${language}' | ||
*/ | ||
function checkWords (seed = '', language = 'english') { | ||
const words = seed.split(' ') | ||
const wordlist = validWordlist(language) | ||
let word | ||
while ((word = words.pop()) != null) { | ||
const idx = wordlist.findIndex(w => w === word) | ||
if (idx === -1) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
// private follows | ||
|
||
/** | ||
@throws {Error} 'Missing wordlist for ${language}' | ||
*/ | ||
function validWordlist (language) { | ||
const wordlist = wordlists[language] | ||
if (!wordlist) { | ||
throw new Error(`Missing wordlist for language ${language}`) | ||
} | ||
return wordlist | ||
} | ||
|
||
const vowelRe = /[aeiou]/g | ||
const novowels = word => word.replace(vowelRe, '') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* eslint-env mocha */ | ||
const assert = require('assert') | ||
const LevensteinDistance = require('./src/levenstein_distance') | ||
const {suggest, wordlist, languages, checkWords, validWordlist, normalize} = require('.') | ||
|
||
describe('Seed', () => { | ||
it('Normalize', () => { | ||
throws(() => normalize(), /^seed string required$/) | ||
// Update README if these change: | ||
assert.equal('double spaces', normalize('double spaces'), 'removes extra spaces') | ||
assert.equal('lowercase', normalize('Lowercase'), 'lowercase') | ||
assert.equal('trim', normalize(' trim '), 'trim') | ||
}) | ||
|
||
it('Suggests', () => { | ||
assert(suggest('quality') === true) | ||
assert(suggest('ágil', {language: 'spanish'}) === true) | ||
assert.equal(suggest('quality1')[0], 'quality') | ||
assert(suggest('').length === 0) | ||
assert(suggest('qua').length > 0) | ||
assert(suggest('seeder').length > 0) | ||
assert(suggest('aeiou').length === 0) | ||
assert(suggest('qlty').length === 1) | ||
LevensteinDistance.distance('', '') | ||
}) | ||
|
||
it('Length', () => { | ||
for (let lang of languages) { | ||
assertLen(wordlist(lang)) | ||
} | ||
}) | ||
|
||
it('Check Words', () => { | ||
assert(checkWords('lazy dog', 'english')) | ||
assert(!checkWords('lazy dogma', 'english')) | ||
throws(() => validWordlist('pig_latin'), /^Missing wordlist for language pig_latin$/) | ||
for (let lang of languages) { | ||
assertNormalized(lang) | ||
} | ||
}) | ||
}) | ||
|
||
const assertNormalized = lang => { | ||
for (let word of wordlist(lang)) { | ||
assert(word === normalize(word), `word ${word} in wordlist ${lang} did not normalize`) | ||
} | ||
} | ||
const assertLen = wordlist => { | ||
assert.equal(2048, wordlist.length, `Expecting 2048 words, got ${wordlist.length}`) | ||
} | ||
|
||
/* istanbul ignore next */ | ||
function throws (fn, match) { | ||
try { | ||
fn() | ||
assert(false, 'Expecting error') | ||
} catch (error) { | ||
if (!match.test(error.message)) { | ||
error.message = `Error did not match ${match}\n${error.message}` | ||
throw error | ||
} | ||
} | ||
} | ||
try { | ||
throws(() => { throw '1' }, /2/) | ||
} catch (err) { | ||
// for code-coverage | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "bip39-checker", | ||
"description": "The BIP-0039 wordlist spell check module.", | ||
"version": "1.0.1", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "mocha index.test.js", | ||
"coverage": "istanbul cover _mocha -- -R spec index.test.js", | ||
"coveralls": "npm run coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/jcalfee/bip39-checker.git" | ||
}, | ||
"dependencies": { | ||
"bip39": "^2.3.0" | ||
}, | ||
"devDependencies": { | ||
"coveralls": "^2.13.1", | ||
"istanbul": "^0.4.5", | ||
"mocha": "^3.4.1", | ||
"standard": "^10.0.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
module.exports = suggestDictionary | ||
|
||
// Modified npm 'suggest-it' package to return an array (instead of single words) | ||
|
||
function suggestDictionary (dict, opts) { | ||
opts = opts || {} | ||
var threshold = opts.threshold || 0.5 | ||
return function suggest (word) { | ||
var length = word.length | ||
return dict.reduce(function (result, dictEntry) { | ||
var score = distance(dictEntry, word) | ||
if (score / length < threshold) { | ||
// console.log('suggestDictionary', word, (score / length).toFixed(3), dictEntry) | ||
result.push([score, dictEntry]) | ||
} | ||
return result | ||
}, []) | ||
.sort((a, b) => Math.sign(a[0], b[0])) | ||
.map(a => a[1]) | ||
} | ||
} | ||
|
||
function distance (a, b) { | ||
var table = [], diag = 0, left, top | ||
if (a.length === 0 || b.length === 0) return Math.max(a.length, b.length) | ||
for (var ii = 0, ilen = a.length + 1; ii !== ilen; ++ii) { | ||
for (var jj = 0, jlen = b.length + 1; jj !== jlen; ++jj) { | ||
if (ii === 0 || jj === 0) table[jj] = Math.max(ii, jj) | ||
else { | ||
diag += Number(a[ii - 1] !== b[jj - 1]) | ||
left = table[jj - 1] + 1 | ||
top = table[jj] + 1 | ||
table[jj] = Math.min(left, top, diag) | ||
diag = top - 1 | ||
} | ||
} | ||
} | ||
return table[b.length] | ||
} | ||
|
||
suggestDictionary.distance = distance |
Oops, something went wrong.