X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FProjets%2FCryptomonnaies%2FBIP39.git;a=blobdiff_plain;f=src%2Fjs%2Findex.js;h=c5f6c111fa61e4c38695939a1106a8afcb94396d;hp=bbef50d03c4dcdab2aeb45e7705c5626351b64f7;hb=92b46ab44fb7fe7aef1cef3d843d1e3d19c1d7cb;hpb=ea483b4a2e33a48279fc7a740609c7dfd8e200ad diff --git a/src/js/index.js b/src/js/index.js index bbef50d..c5f6c11 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,9 +1,12 @@ (function() { - var mnemonic = new Mnemonic("english"); + // mnemonics is populated as required by getLanguage + var mnemonics = { "english": new Mnemonic("english") }; + var mnemonic = mnemonics["english"]; + var seed = null var bip32RootKey = null; var bip32ExtendedKey = null; - var network = Bitcoin.networks.bitcoin; + var network = bitcoin.networks.bitcoin; var addressRowTemplate = $("#address-row-template"); var showIndex = true; @@ -11,6 +14,7 @@ var showPrivKey = true; var phraseChangeTimeoutEvent = null; + var rootKeyChangedTimeoutEvent = null; var DOM = {}; DOM.network = $(".network"); @@ -18,6 +22,7 @@ DOM.phrase = $(".phrase"); DOM.passphrase = $(".passphrase"); DOM.generate = $(".generate"); + DOM.seed = $(".seed"); DOM.rootKey = $(".root-key"); DOM.extendedPrivKey = $(".extended-priv-key"); DOM.extendedPubKey = $(".extended-pub-key"); @@ -32,6 +37,7 @@ DOM.bip44account = $("#bip44 .account"); DOM.bip44change = $("#bip44 .change"); DOM.strength = $(".strength"); + DOM.hardenedAddresses = $(".hardened-addresses"); DOM.addresses = $(".addresses"); DOM.rowsToAdd = $(".rows-to-add"); DOM.more = $(".more"); @@ -40,9 +46,7 @@ DOM.indexToggle = $(".index-toggle"); DOM.addressToggle = $(".address-toggle"); DOM.privateKeyToggle = $(".private-key-toggle"); - DOM.myceliumPath = $("#mycelium-path"); - - var derivationPath = $(".tab-pane.active .path").val(); + DOM.languages = $(".languages a"); function init() { // Events @@ -51,45 +55,35 @@ DOM.passphrase.on("input", delayedPhraseChanged); DOM.generate.on("click", generateClicked); DOM.more.on("click", showMore); - DOM.bip32path.on("input", bip32Changed); - DOM.bip44purpose.on("input", bip44Changed); - DOM.bip44coin.on("input", bip44Changed); - DOM.bip44account.on("input", bip44Changed); - DOM.bip44change.on("input", bip44Changed); - DOM.tab.on("click", tabClicked); + DOM.rootKey.on("input", delayedRootKeyChanged); + DOM.bip32path.on("input", calcForDerivationPath); + DOM.bip44purpose.on("input", calcForDerivationPath); + DOM.bip44coin.on("input", calcForDerivationPath); + DOM.bip44account.on("input", calcForDerivationPath); + DOM.bip44change.on("input", calcForDerivationPath); + DOM.tab.on("shown.bs.tab", calcForDerivationPath); + DOM.hardenedAddresses.on("change", calcForDerivationPath); DOM.indexToggle.on("click", toggleIndexes); DOM.addressToggle.on("click", toggleAddresses); DOM.privateKeyToggle.on("click", togglePrivateKeys); + DOM.languages.on("click", languageChanged); disableForms(); hidePending(); hideValidationError(); + populateNetworkSelect(); } // Event handlers function networkChanged(e) { - var n = e.target.value; - if (n == "bitcoin") { - network = Bitcoin.networks.bitcoin; - DOM.bip44coin.val(0); - DOM.myceliumPath.val("m/44'/0'/0'/0"); - } - else if (n == "bitcoin-testnet") { - network = Bitcoin.networks.testnet; - DOM.bip44coin.val(1); - DOM.myceliumPath.val("m/44'/1'/0'/0"); - } - else if (n == "litecoin") { - network = Bitcoin.networks.litecoin; - DOM.bip44coin.val(2); + var networkIndex = e.target.value; + networks[networkIndex].onSelect(); + if (seed != null) { + phraseChanged(); } - else if (n == "dogecoin") { - network = Bitcoin.networks.dogecoin; - var UNOFFICIAL_BIP44_COIN = 9999; - DOM.bip44coin.val(UNOFFICIAL_BIP44_COIN); // This coin is not in BIP44 + else { + rootKeyChanged(); } - setBip44DerivationPath(); - delayedPhraseChanged(); } function delayedPhraseChanged() { @@ -104,22 +98,68 @@ function phraseChanged() { showPending(); hideValidationError(); + setMnemonicLanguage(); // Get the mnemonic phrase var phrase = DOM.phrase.val(); - var passphrase = DOM.passphrase.val(); var errorText = findPhraseErrors(phrase); if (errorText) { showValidationError(errorText); return; } - // Get the derivation path - var errorText = findDerivationPathErrors(); + // Calculate and display + var passphrase = DOM.passphrase.val(); + calcBip32RootKeyFromSeed(phrase, passphrase); + calcForDerivationPath(); + hidePending(); + } + + function delayedRootKeyChanged() { + // Warn if there is an existing mnemonic or passphrase. + if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) { + if (!confirm("This will clear existing mnemonic and passphrase")) { + DOM.rootKey.val(bip32RootKey); + return + } + } + hideValidationError(); + showPending(); + // Clear existing mnemonic and passphrase + DOM.phrase.val(""); + DOM.passphrase.val(""); + seed = null; + if (rootKeyChangedTimeoutEvent != null) { + clearTimeout(rootKeyChangedTimeoutEvent); + } + rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400); + } + + function rootKeyChanged() { + showPending(); + hideValidationError(); + // Validate the root key TODO + var rootKeyBase58 = DOM.rootKey.val(); + var errorText = validateRootKey(rootKeyBase58); if (errorText) { showValidationError(errorText); return; } // Calculate and display - calcBip32Seed(phrase, passphrase, derivationPath); + calcBip32RootKeyFromBase58(rootKeyBase58); + calcForDerivationPath(); + hidePending(); + } + + function calcForDerivationPath() { + showPending(); + hideValidationError(); + // Get the derivation path + var derivationPath = getDerivationPath(); + var errorText = findDerivationPathErrors(derivationPath); + if (errorText) { + showValidationError(errorText); + return; + } + calcBip32ExtendedKey(derivationPath); displayBip32Info(); hidePending(); } @@ -128,6 +168,7 @@ clearDisplay(); showPending(); setTimeout(function() { + setMnemonicLanguage(); var phrase = generateRandomPhrase(); if (!phrase) { return; @@ -136,25 +177,18 @@ }, 50); } - function tabClicked(e) { - var activePath = $(e.target.getAttribute("href") + " .path"); - derivationPath = activePath.val(); - derivationChanged(); - } - - function derivationChanged() { - delayedPhraseChanged(); - } - - function bip32Changed() { - derivationPath = DOM.bip32path.val(); - derivationChanged(); - } - - function bip44Changed() { - setBip44DerivationPath(); - derivationPath = DOM.bip44path.val(); - derivationChanged(); + function languageChanged() { + setTimeout(function() { + setMnemonicLanguage(); + if (DOM.phrase.val().length > 0) { + var newPhrase = convertPhraseToNewLanguage(); + DOM.phrase.val(newPhrase); + phraseChanged(); + } + else { + DOM.generate.trigger("click"); + } + }, 50); } function toggleIndexes() { @@ -181,30 +215,22 @@ return; } var numWords = parseInt(DOM.strength.val()); - // Check strength is an integer - if (isNaN(numWords)) { - DOM.strength.val("12"); - numWords = 12; - } - // Check strength is a multiple of 32, if not round it down - if (numWords % 3 != 0) { - numWords = Math.floor(numWords / 3) * 3; - DOM.strength.val(numWords); - } - // Check strength is at least 32 - if (numWords == 0) { - numWords = 3; - DOM.strength.val(numWords); - } var strength = numWords / 3 * 32; var words = mnemonic.generate(strength); DOM.phrase.val(words); return words; } - function calcBip32Seed(phrase, passphrase, path) { - var seed = mnemonic.toSeed(phrase, passphrase); - bip32RootKey = Bitcoin.HDNode.fromSeedHex(seed, network); + function calcBip32RootKeyFromSeed(phrase, passphrase) { + seed = mnemonic.toSeed(phrase, passphrase); + bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network); + } + + function calcBip32RootKeyFromBase58(rootKeyBase58) { + bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58, network); + } + + function calcBip32ExtendedKey(path) { bip32ExtendedKey = bip32RootKey; // Derive the key from the path var pathBits = path.split("/"); @@ -240,18 +266,19 @@ // TODO make this right // Preprocess the words phrase = mnemonic.normalizeString(phrase); - var parts = phrase.split(" "); - var proper = []; - for (var i=0; i 0) { - // TODO check that lowercasing is always valid to do - proper.push(part.toLowerCase()); + var words = phraseToWordArray(phrase); + // Check each word + for (var i=0; i 1) { + if (path[1] != "/") { + return "Separator must be '/'"; + } + var indexes = path.split("/"); + if (indexes.length > maxDepth) { + return "Derivation depth is " + indexes.length + ", must be less than " + maxDepth; + } + for (var depth = 1; depth 0) { + return "Invalid characters " + invalidChars + " found at depth " + depth; + } + var indexValue = parseInt(index.replace("'", "")); + if (isNaN(depth)) { + return "Invalid number at depth " + depth; + } + if (indexValue > maxIndexValue) { + return "Value of " + indexValue + " at depth " + depth + " must be less than " + maxIndexValue; + } + } + } return false; } function displayBip32Info() { // Display the key + DOM.seed.val(seed); var rootKey = bip32RootKey.toBase58(); DOM.rootKey.val(rootKey); var extendedPrivKey = bip32ExtendedKey.toBase58(); @@ -286,16 +382,28 @@ function TableRow(index) { + var useHardenedAddresses = DOM.hardenedAddresses.prop("checked"); + function init() { calculateValues(); } function calculateValues() { setTimeout(function() { - var key = bip32ExtendedKey.derive(index); + var key = ""; + if (useHardenedAddresses) { + key = bip32ExtendedKey.deriveHardened(index); + } + else { + key = bip32ExtendedKey.derive(index); + } var address = key.getAddress().toString(); var privkey = key.privKey.toWIF(network); - addAddressToList(index, address, privkey); + var indexText = getDerivationPath() + "/" + index; + if (useHardenedAddresses) { + indexText = indexText + "'"; + } + addAddressToList(indexText, address, privkey); }, 50) } @@ -336,14 +444,14 @@ DOM.extendedPubKey.val(""); } - function addAddressToList(index, address, privkey) { + function addAddressToList(indexText, address, privkey) { var row = $(addressRowTemplate.html()); // Elements var indexCell = row.find(".index span"); var addressCell = row.find(".address span"); var privkeyCell = row.find(".privkey span"); // Content - indexCell.text(index); + indexCell.text(indexText); addressCell.text(address); privkeyCell.text(privkey); // Visibility @@ -354,7 +462,7 @@ addressCell.addClass("invisible"); } if (!showPrivKey) { - privkeCell.addClass("invisible"); + privkeyCell.addClass("invisible"); } DOM.addresses.append(row); } @@ -369,19 +477,6 @@ }); } - function setBip44DerivationPath() { - var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44); - var coin = parseIntNoNaN(DOM.bip44coin.val(), 0); - var account = parseIntNoNaN(DOM.bip44account.val(), 0); - var change = parseIntNoNaN(DOM.bip44change.val(), 0); - var path = "m/"; - path += purpose + "'/"; - path += coin + "'/"; - path += account + "'/"; - path += change; - DOM.bip44path.val(path); - } - function parseIntNoNaN(val, defaultVal) { var v = parseInt(val); if (isNaN(v)) { @@ -396,12 +491,245 @@ .show(); } + function findNearestWord(word) { + var language = getLanguage(); + var words = WORDLISTS[language]; + var minDistance = 99; + var closestWord = words[0]; + for (var i=0; i"); + option.attr("value", i); + option.text(network.name); + DOM.phraseNetwork.append(option); + } + } + + function getLanguage() { + var defaultLanguage = "english"; + // Try to get from existing phrase + var language = getLanguageFromPhrase(); + // Try to get from url if not from phrase + if (language.length == 0) { + language = getLanguageFromUrl(); + } + // Default to English if no other option + if (language.length == 0) { + language = defaultLanguage; + } + return language; + } + + function getLanguageFromPhrase(phrase) { + // Check if how many words from existing phrase match a language. + var language = ""; + if (!phrase) { + phrase = DOM.phrase.val(); + } + if (phrase.length > 0) { + var words = phraseToWordArray(phrase); + var languageMatches = {}; + for (l in WORDLISTS) { + // Track how many words match in this language + languageMatches[l] = 0; + for (var i=0; i -1; + if (wordInLanguage) { + languageMatches[l]++; + } + } + // Find languages with most word matches. + // This is made difficult due to commonalities between Chinese + // simplified vs traditional. + var mostMatches = 0; + var mostMatchedLanguages = []; + for (var l in languageMatches) { + var numMatches = languageMatches[l]; + if (numMatches > mostMatches) { + mostMatches = numMatches; + mostMatchedLanguages = [l]; + } + else if (numMatches == mostMatches) { + mostMatchedLanguages.push(l); + } + } + } + if (mostMatchedLanguages.length > 0) { + // Use first language and warn if multiple detected + language = mostMatchedLanguages[0]; + if (mostMatchedLanguages.length > 1) { + console.warn("Multiple possible languages"); + console.warn(mostMatchedLanguages); + } + } + } + return language; + } + + function getLanguageFromUrl() { + return window.location.hash.substring(1); + } + + function setMnemonicLanguage() { + var language = getLanguage(); + // Load the bip39 mnemonic generator for this language if required + if (!(language in mnemonics)) { + mnemonics[language] = new Mnemonic(language); + } + mnemonic = mnemonics[language]; + } + + function convertPhraseToNewLanguage() { + var oldLanguage = getLanguageFromPhrase(); + var newLanguage = getLanguageFromUrl(); + var oldPhrase = DOM.phrase.val(); + var oldWords = phraseToWordArray(oldPhrase); + var newWords = []; + for (var i=0; i 0) { + noBlanks.push(word); + } + } + return noBlanks; + } + + // TODO look at jsbip39 - mnemonic.joinWords + function wordArrayToPhrase(words) { + var phrase = words.join(" "); + var language = getLanguageFromPhrase(phrase); + if (language == "japanese") { + phrase = words.join("\u3000"); + } + return phrase; + } + + var networks = [ + { + name: "Bitcoin", + onSelect: function() { + network = bitcoin.networks.bitcoin; + DOM.bip44coin.val(0); + }, + }, + { + name: "Bitcoin Testnet", + onSelect: function() { + network = bitcoin.networks.testnet; + DOM.bip44coin.val(1); + }, + }, + { + name: "Litecoin", + onSelect: function() { + network = bitcoin.networks.litecoin; + DOM.bip44coin.val(2); + }, + }, + { + name: "Dogecoin", + onSelect: function() { + network = bitcoin.networks.dogecoin; + DOM.bip44coin.val(3); + }, + }, + { + name: "ShadowCash", + onSelect: function() { + network = bitcoin.networks.shadow; + DOM.bip44coin.val(35); + }, + }, + { + name: "ShadowCash Testnet", + onSelect: function() { + network = bitcoin.networks.shadowtn; + DOM.bip44coin.val(1); + }, + }, + { + name: "Viacoin", + onSelect: function() { + network = bitcoin.networks.viacoin; + DOM.bip44coin.val(14); + }, + }, + { + name: "Viacoin Testnet", + onSelect: function() { + network = bitcoin.networks.viacointestnet; + DOM.bip44coin.val(1); + }, + }, + { + name: "Jumbucks", + onSelect: function() { + network = bitcoin.networks.jumbucks; + DOM.bip44coin.val(26); + }, + }, + { + name: "CLAM", + onSelect: function() { + network = bitcoin.networks.clam; + DOM.bip44coin.val(23); + }, + }, + { + name: "DASH", + onSelect: function() { + network = bitcoin.networks.dash; + DOM.bip44coin.val(5); + }, + }, + { + name: "Namecoin", + onSelect: function() { + network = bitcoin.networks.namecoin; + DOM.bip44coin.val(7); + }, + }, + { + name: "Peercoin", + onSelect: function() { + network = bitcoin.networks.peercoin; + DOM.bip44coin.val(6); + }, + }, + ] + init(); })();