]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blame - src/js/index.js
Words not in list show error with suggestion
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / src / js / index.js
CommitLineData
ebd8d4e8
IC
1(function() {
2
3 var mnemonic = new Mnemonic("english");
3e0ed16a 4 var seed = null
ebd8d4e8
IC
5 var bip32RootKey = null;
6 var bip32ExtendedKey = null;
1759e5e8 7 var network = bitcoin.networks.bitcoin;
ebd8d4e8
IC
8 var addressRowTemplate = $("#address-row-template");
9
700901cd
IC
10 var showIndex = true;
11 var showAddress = true;
12 var showPrivKey = true;
13
ebd8d4e8
IC
14 var phraseChangeTimeoutEvent = null;
15
16 var DOM = {};
d6cedc94
IC
17 DOM.network = $(".network");
18 DOM.phraseNetwork = $("#network-phrase");
ebd8d4e8 19 DOM.phrase = $(".phrase");
1abcc511 20 DOM.passphrase = $(".passphrase");
ebd8d4e8 21 DOM.generate = $(".generate");
3e0ed16a 22 DOM.seed = $(".seed");
ebd8d4e8
IC
23 DOM.rootKey = $(".root-key");
24 DOM.extendedPrivKey = $(".extended-priv-key");
25 DOM.extendedPubKey = $(".extended-pub-key");
d6cedc94
IC
26 DOM.bip32tab = $("#bip32-tab");
27 DOM.bip44tab = $("#bip44-tab");
28 DOM.bip32panel = $("#bip32");
29 DOM.bip44panel = $("#bip44");
ebd8d4e8
IC
30 DOM.bip32path = $("#bip32-path");
31 DOM.bip44path = $("#bip44-path");
32 DOM.bip44purpose = $("#bip44 .purpose");
33 DOM.bip44coin = $("#bip44 .coin");
34 DOM.bip44account = $("#bip44 .account");
35 DOM.bip44change = $("#bip44 .change");
36 DOM.strength = $(".strength");
37 DOM.addresses = $(".addresses");
38 DOM.rowsToAdd = $(".rows-to-add");
39 DOM.more = $(".more");
40 DOM.feedback = $(".feedback");
41 DOM.tab = $(".derivation-type a");
42 DOM.indexToggle = $(".index-toggle");
43 DOM.addressToggle = $(".address-toggle");
44 DOM.privateKeyToggle = $(".private-key-toggle");
45
ebd8d4e8
IC
46 function init() {
47 // Events
d6cedc94 48 DOM.network.on("change", networkChanged);
a19a5498
IC
49 DOM.phrase.on("input", delayedPhraseChanged);
50 DOM.passphrase.on("input", delayedPhraseChanged);
ebd8d4e8
IC
51 DOM.generate.on("click", generateClicked);
52 DOM.more.on("click", showMore);
38523d36
IC
53 DOM.bip32path.on("input", delayedPhraseChanged);
54 DOM.bip44purpose.on("input", delayedPhraseChanged);
55 DOM.bip44coin.on("input", delayedPhraseChanged);
56 DOM.bip44account.on("input", delayedPhraseChanged);
57 DOM.bip44change.on("input", delayedPhraseChanged);
58 DOM.tab.on("click", delayedPhraseChanged);
ebd8d4e8
IC
59 DOM.indexToggle.on("click", toggleIndexes);
60 DOM.addressToggle.on("click", toggleAddresses);
61 DOM.privateKeyToggle.on("click", togglePrivateKeys);
62 disableForms();
63 hidePending();
64 hideValidationError();
7f15cb6e 65 populateNetworkSelect();
ebd8d4e8
IC
66 }
67
68 // Event handlers
69
d6cedc94 70 function networkChanged(e) {
7a995731 71 var network = e.target.value;
7f15cb6e 72 networks[network].onSelect();
d6cedc94
IC
73 delayedPhraseChanged();
74 }
75
ebd8d4e8
IC
76 function delayedPhraseChanged() {
77 hideValidationError();
78 showPending();
79 if (phraseChangeTimeoutEvent != null) {
80 clearTimeout(phraseChangeTimeoutEvent);
81 }
82 phraseChangeTimeoutEvent = setTimeout(phraseChanged, 400);
83 }
84
85 function phraseChanged() {
86 showPending();
87 hideValidationError();
88 // Get the mnemonic phrase
89 var phrase = DOM.phrase.val();
1abcc511 90 var passphrase = DOM.passphrase.val();
ebd8d4e8
IC
91 var errorText = findPhraseErrors(phrase);
92 if (errorText) {
93 showValidationError(errorText);
94 return;
95 }
96 // Get the derivation path
38523d36
IC
97 var derivationPath = getDerivationPath();
98 var errorText = findDerivationPathErrors(derivationPath);
ebd8d4e8
IC
99 if (errorText) {
100 showValidationError(errorText);
101 return;
102 }
103 // Calculate and display
1abcc511 104 calcBip32Seed(phrase, passphrase, derivationPath);
ebd8d4e8
IC
105 displayBip32Info();
106 hidePending();
107 }
108
109 function generateClicked() {
110 clearDisplay();
111 showPending();
112 setTimeout(function() {
113 var phrase = generateRandomPhrase();
114 if (!phrase) {
115 return;
116 }
117 phraseChanged();
118 }, 50);
119 }
120
ebd8d4e8 121 function toggleIndexes() {
700901cd 122 showIndex = !showIndex;
ebd8d4e8
IC
123 $("td.index span").toggleClass("invisible");
124 }
125
126 function toggleAddresses() {
700901cd 127 showAddress = !showAddress;
ebd8d4e8
IC
128 $("td.address span").toggleClass("invisible");
129 }
130
131 function togglePrivateKeys() {
700901cd 132 showPrivKey = !showPrivKey;
ebd8d4e8
IC
133 $("td.privkey span").toggleClass("invisible");
134 }
135
136 // Private methods
137
138 function generateRandomPhrase() {
139 if (!hasStrongRandom()) {
140 var errorText = "This browser does not support strong randomness";
141 showValidationError(errorText);
142 return;
143 }
144 var numWords = parseInt(DOM.strength.val());
ebd8d4e8
IC
145 var strength = numWords / 3 * 32;
146 var words = mnemonic.generate(strength);
147 DOM.phrase.val(words);
148 return words;
149 }
150
1abcc511 151 function calcBip32Seed(phrase, passphrase, path) {
3e0ed16a 152 seed = mnemonic.toSeed(phrase, passphrase);
1759e5e8 153 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
ebd8d4e8
IC
154 bip32ExtendedKey = bip32RootKey;
155 // Derive the key from the path
156 var pathBits = path.split("/");
157 for (var i=0; i<pathBits.length; i++) {
158 var bit = pathBits[i];
159 var index = parseInt(bit);
160 if (isNaN(index)) {
161 continue;
162 }
163 var hardened = bit[bit.length-1] == "'";
164 if (hardened) {
165 bip32ExtendedKey = bip32ExtendedKey.deriveHardened(index);
166 }
167 else {
168 bip32ExtendedKey = bip32ExtendedKey.derive(index);
169 }
170 }
171 }
172
173 function showValidationError(errorText) {
174 DOM.feedback
175 .text(errorText)
176 .show();
177 }
178
179 function hideValidationError() {
180 DOM.feedback
181 .text("")
182 .hide();
183 }
184
185 function findPhraseErrors(phrase) {
186 // TODO make this right
187 // Preprocess the words
783981de 188 phrase = mnemonic.normalizeString(phrase);
ebd8d4e8
IC
189 var parts = phrase.split(" ");
190 var proper = [];
191 for (var i=0; i<parts.length; i++) {
192 var part = parts[i];
193 if (part.length > 0) {
194 // TODO check that lowercasing is always valid to do
195 proper.push(part.toLowerCase());
196 }
197 }
ebd8d4e8 198 var properPhrase = proper.join(' ');
563e401a
IC
199 // Check each word
200 for (var i=0; i<proper.length; i++) {
201 var word = proper[i];
202 if (WORDLISTS["english"].indexOf(word) == -1) {
203 console.log("Finding closest match to " + word);
204 var nearestWord = findNearestWord(word);
205 return word + " not in wordlist, did you mean " + nearestWord + "?";
206 }
207 }
ebd8d4e8
IC
208 // Check the words are valid
209 var isValid = mnemonic.check(properPhrase);
210 if (!isValid) {
211 return "Invalid mnemonic";
212 }
213 return false;
214 }
215
38523d36
IC
216 function getDerivationPath() {
217 if (DOM.bip44tab.hasClass("active")) {
218 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
219 var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
220 var account = parseIntNoNaN(DOM.bip44account.val(), 0);
221 var change = parseIntNoNaN(DOM.bip44change.val(), 0);
222 var path = "m/";
223 path += purpose + "'/";
224 path += coin + "'/";
225 path += account + "'/";
226 path += change;
227 DOM.bip44path.val(path);
228 var derivationPath = DOM.bip44path.val();
229 console.log("Using derivation path from BIP44 tab: " + derivationPath);
230 return derivationPath;
231 }
232 else if (DOM.bip32tab.hasClass("active")) {
233 var derivationPath = DOM.bip32path.val();
234 console.log("Using derivation path from BIP32 tab: " + derivationPath);
235 return derivationPath;
236 }
237 else {
238 console.log("Unknown derivation path");
239 }
240 }
241
ebd8d4e8 242 function findDerivationPathErrors(path) {
30c9e79d
IC
243 // TODO is not perfect but is better than nothing
244 // Inspired by
245 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
246 // and
247 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
248 var maxDepth = 255; // TODO verify this!!
249 var maxIndexValue = Math.pow(2, 31); // TODO verify this!!
250 if (path[0] != "m") {
251 return "First character must be 'm'";
252 }
253 if (path.length > 1) {
254 if (path[1] != "/") {
255 return "Separator must be '/'";
256 }
257 var indexes = path.split("/");
258 if (indexes.length > maxDepth) {
259 return "Derivation depth is " + indexes.length + ", must be less than " + maxDepth;
260 }
261 for (var depth = 1; depth<indexes.length; depth++) {
262 var index = indexes[depth];
263 var invalidChars = index.replace(/^[0-9]+'?$/g, "")
264 if (invalidChars.length > 0) {
265 return "Invalid characters " + invalidChars + " found at depth " + depth;
266 }
267 var indexValue = parseInt(index.replace("'", ""));
268 if (isNaN(depth)) {
269 return "Invalid number at depth " + depth;
270 }
271 if (indexValue > maxIndexValue) {
272 return "Value of " + indexValue + " at depth " + depth + " must be less than " + maxIndexValue;
273 }
274 }
275 }
ebd8d4e8
IC
276 return false;
277 }
278
279 function displayBip32Info() {
280 // Display the key
3e0ed16a 281 DOM.seed.val(seed);
ebd8d4e8
IC
282 var rootKey = bip32RootKey.toBase58();
283 DOM.rootKey.val(rootKey);
284 var extendedPrivKey = bip32ExtendedKey.toBase58();
285 DOM.extendedPrivKey.val(extendedPrivKey);
286 var extendedPubKey = bip32ExtendedKey.toBase58(false);
287 DOM.extendedPubKey.val(extendedPubKey);
288 // Display the addresses and privkeys
289 clearAddressesList();
290 displayAddresses(0, 20);
291 }
292
293 function displayAddresses(start, total) {
294 for (var i=0; i<total; i++) {
a8c45487
IC
295 var index = i + start;
296 new TableRow(index);
ebd8d4e8
IC
297 }
298 }
299
a8c45487
IC
300 function TableRow(index) {
301
302 function init() {
303 calculateValues();
304 }
305
306 function calculateValues() {
307 setTimeout(function() {
308 var key = bip32ExtendedKey.derive(index);
309 var address = key.getAddress().toString();
310 var privkey = key.privKey.toWIF(network);
38523d36
IC
311 var indexText = getDerivationPath() + "/" + index;
312 addAddressToList(indexText, address, privkey);
a8c45487
IC
313 }, 50)
314 }
315
316 init();
317
318 }
319
ebd8d4e8
IC
320 function showMore() {
321 var start = DOM.addresses.children().length;
322 var rowsToAdd = parseInt(DOM.rowsToAdd.val());
323 if (isNaN(rowsToAdd)) {
324 rowsToAdd = 20;
325 DOM.rowsToAdd.val("20");
326 }
327 if (rowsToAdd > 200) {
328 var msg = "Generating " + rowsToAdd + " rows could take a while. ";
329 msg += "Do you want to continue?";
330 if (!confirm(msg)) {
331 return;
332 }
333 }
ebd8d4e8 334 displayAddresses(start, rowsToAdd);
ebd8d4e8
IC
335 }
336
337 function clearDisplay() {
338 clearAddressesList();
339 clearKey();
340 hideValidationError();
341 }
342
343 function clearAddressesList() {
344 DOM.addresses.empty();
345 }
346
347 function clearKey() {
348 DOM.rootKey.val("");
349 DOM.extendedPrivKey.val("");
350 DOM.extendedPubKey.val("");
351 }
352
38523d36 353 function addAddressToList(indexText, address, privkey) {
ebd8d4e8 354 var row = $(addressRowTemplate.html());
700901cd
IC
355 // Elements
356 var indexCell = row.find(".index span");
357 var addressCell = row.find(".address span");
358 var privkeyCell = row.find(".privkey span");
359 // Content
ae30fed8 360 indexCell.text(indexText);
700901cd
IC
361 addressCell.text(address);
362 privkeyCell.text(privkey);
363 // Visibility
364 if (!showIndex) {
365 indexCell.addClass("invisible");
366 }
367 if (!showAddress) {
368 addressCell.addClass("invisible");
369 }
370 if (!showPrivKey) {
6d628db7 371 privkeyCell.addClass("invisible");
700901cd 372 }
ebd8d4e8
IC
373 DOM.addresses.append(row);
374 }
375
376 function hasStrongRandom() {
377 return 'crypto' in window && window['crypto'] !== null;
378 }
379
380 function disableForms() {
381 $("form").on("submit", function(e) {
382 e.preventDefault();
383 });
384 }
385
ebd8d4e8
IC
386 function parseIntNoNaN(val, defaultVal) {
387 var v = parseInt(val);
388 if (isNaN(v)) {
389 return defaultVal;
390 }
391 return v;
392 }
393
394 function showPending() {
395 DOM.feedback
396 .text("Calculating...")
397 .show();
398 }
399
563e401a
IC
400 function findNearestWord(word) {
401 var words = WORDLISTS["english"];
402 var minDistance = 99;
403 var closestWord = words[0];
404 for (var i=0; i<words.length; i++) {
405 var comparedTo = words[i];
406 var distance = Levenshtein.get(word, comparedTo);
407 if (distance < minDistance) {
408 closestWord = comparedTo;
409 minDistance = distance;
410 }
411 }
412 return closestWord;
413 }
414
ebd8d4e8
IC
415 function hidePending() {
416 DOM.feedback
417 .text("")
418 .hide();
419 }
420
7f15cb6e
IC
421 function populateNetworkSelect() {
422 for (var i=0; i<networks.length; i++) {
423 var network = networks[i];
424 var option = $("<option>");
425 option.attr("value", i);
426 option.text(network.name);
427 DOM.phraseNetwork.append(option);
428 }
429 }
430
431 var networks = [
432 {
7a995731
IC
433 name: "Bitcoin",
434 onSelect: function() {
1759e5e8 435 network = bitcoin.networks.bitcoin;
7a995731 436 DOM.bip44coin.val(0);
7a995731
IC
437 },
438 },
7f15cb6e 439 {
7a995731
IC
440 name: "Bitcoin Testnet",
441 onSelect: function() {
1759e5e8 442 network = bitcoin.networks.testnet;
7a995731 443 DOM.bip44coin.val(1);
7a995731
IC
444 },
445 },
7f15cb6e 446 {
7a995731
IC
447 name: "Litecoin",
448 onSelect: function() {
1759e5e8 449 network = bitcoin.networks.litecoin;
7a995731
IC
450 DOM.bip44coin.val(2);
451 },
452 },
7f15cb6e 453 {
7a995731
IC
454 name: "Dogecoin",
455 onSelect: function() {
1759e5e8 456 network = bitcoin.networks.dogecoin;
7a995731
IC
457 DOM.bip44coin.val(3);
458 },
459 },
e3a9508c
IC
460 {
461 name: "ShadowCash",
462 onSelect: function() {
463 network = bitcoin.networks.shadow;
464 DOM.bip44coin.val(35);
465 },
466 },
467 {
468 name: "ShadowCash Testnet",
469 onSelect: function() {
470 network = bitcoin.networks.shadowtn;
471 DOM.bip44coin.val(1);
472 },
473 },
a3baa26e
IC
474 {
475 name: "Viacoin",
476 onSelect: function() {
477 network = bitcoin.networks.viacoin;
478 DOM.bip44coin.val(14);
479 },
480 },
481 {
482 name: "Viacoin Testnet",
483 onSelect: function() {
484 network = bitcoin.networks.viacointestnet;
485 DOM.bip44coin.val(1);
486 },
487 },
488 {
489 name: "Jumbucks",
490 onSelect: function() {
491 network = bitcoin.networks.jumbucks;
492 DOM.bip44coin.val(26);
493 },
494 },
5c434a8a
CM
495 {
496 name: "CLAM",
497 onSelect: function() {
498 network = bitcoin.networks.clam;
499 DOM.bip44coin.val(23);
500 },
501 },
7f15cb6e 502 ]
7a995731 503
ebd8d4e8
IC
504 init();
505
506})();